web-dev-qa-db-ja.com

Entity Frameworkを使用して、ストアドプロシージャの結果を別の名前のパラメーターを持つエンティティにマップする方法

Entity Frameworkを使用して、SQL Serverストアドプロシージャの出力をC#のエンティティにマッピングする基本的な例を作成しようとしていますが、エンティティには、わかりにくい名前とは異なる(わかりやすい)名前パラメーターがあります。私はこれをFluent(つまり、非edmx)構文でも実行しようとしています。


何が機能するか...

ストアドプロシージャは、UT_ID、UT_LONG_NM、UT_STR_AD、UT_CITY_AD、UT_ST_AD、UT_Zip_CD_AD、UT_CTと呼ばれる値を返します。

このようなオブジェクトを作成すると...

public class DBUnitEntity
{
    public Int16 UT_ID { get; set; }
    public string UT_LONG_NM { get; set; }
    public string UT_STR_AD { get; set; }
    public string UT_CITY_AD { get; set; }
    public string UT_ST_AD { get; set; }
    public Int32 UT_Zip_CD_AD { get; set; }
    public string UT_CT { get; set; } 
}

そして、このようなEntityTypeConfiguration ...

public class DbUnitMapping: EntityTypeConfiguration<DBUnitEntity>
{
        public DbUnitMapping()
        {
            HasKey(t => t.UT_ID);
        }
}

...これをDbContextのOnModelCreatingに追加すると、これを使用して、データベースからエンティティを正常に取得できます。

var allUnits = _context.Database.SqlQuery<DBUnitEntity>(StoredProcedureHelper.GetAllUnitsProc);

しかし、何が機能しないか

このようなエンティティが必要な場合は、わかりやすい名前を付けます。

public class UnitEntity : IUnit
{
    public Int16 UnitId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public Int32 Zip { get; set; }
    public string Category { get; set; }
}

そして、このようなEntityTypeConfiguration ...

    public UnitMapping()
    {
        HasKey(t => t.UnitId);

        Property(t => t.UnitId).HasColumnName("UT_ID");
        Property(t => t.Name).HasColumnName("UT_LONG_NM");
        Property(t => t.Address).HasColumnName("UT_STR_AD");
        Property(t => t.City).HasColumnName("UT_CITY_AD");
        Property(t => t.State).HasColumnName("UT_ST_AD");
        Property(t => t.Zip).HasColumnName("UT_Zip_CD_AD");
        Property(t => t.Category).HasColumnName("UT_CT");
    }

データを取得しようとすると、System.Data.EntityCommandExecutionExceptionというメッセージが表示されます....

「データリーダーは、指定された 'DataAccess.EFCodeFirstSample.UnitEntity'と互換性がありません。タイプ 'UnitId'のメンバーには、同じ名前の対応する列がデータリーダーにありません。

「ストアドプロシージャ名」プロパティをエンティティに追加すると、次の「不明な」プロパティについて不満が出てきます。

この「HasColumnName」は、EFのこのコードファーストストアドプロシージャの流暢なスタイルで期待したとおりに機能しませんか?


更新:

DataAnnotations(ComponentModelからのキー、およびEntityFrameworkからの列)を使用して試しました... ala

public class UnitEntity : IUnit
{
    [Key]
    [Column("UT_ID")]
    public Int16 UnitId { get; set; }
    public string Name { get; set; }

これにより、データベースと同じ名前のDBUnitEntity(つまり、[Key]属性を追加するだけ)のEntityTypeConfigurationはまったく不要になりましたが、プロパティ名がデータベースに一致しないエンティティには何もしませんでした(同じエラー)従来通り)。

私はモデルでComponentModelアノテーションを使用してもかまいませんが、私がそれを支援できる場合は、モデルでEntityFrameworkアノテーションを使用したくありません(モデルを特定のデータアクセスフレームワークに関連付けたくない)。

23
babernethy

From Entity Framework Code First 本(155ページ):

SQLQueryメソッドは常に、プロパティ名に基づいて列とプロパティのマッチングを試みます...列とプロパティの名前のマッチングでは、マッピングは考慮されません。たとえば、DestinationIdプロパティをDestinationテーブルのIdという列にマップした場合、SqlQueryメソッドはこのマッピングを使用しません。

したがって、ストアドプロシージャを呼び出すときにマッピングを使用することはできません。回避策の1つは、オブジェクトのプロパティの名前と一致する各列のエイリアスを含む結果を返すようにストアドプロシージャを変更することです。

Select UT_STR_AD as Address From SomeTableなど

21
Sergey

これはEntity Frameworkを使用していませんが、dbcontextに由来しています。私は何時間もインターネットを調査し、何もせずにドットピークを使用しました。 ColumnAttributeがSqlQueryRawで無視される箇所をいくつか読みました。しかし、私はリフレクション、ジェネリックス、SQLデータリーダー、およびアクティベーターで何かを作り上げました。他のいくつかのプロシージャでテストします。他にエラーチェックが必要な場合は、コメントしてください。

public static List<T> SqlQuery<T>( DbContext db, string sql, params object[] parameters)
    {

        List<T> Rows = new List<T>();
        using (SqlConnection con = new SqlConnection(db.Database.Connection.ConnectionString))
        {
            using (SqlCommand cmd = new SqlCommand(sql, con))
            {
                cmd.CommandType = CommandType.StoredProcedure;
                foreach (var param in parameters)
                    cmd.Parameters.Add(param);
                con.Open();
                using (SqlDataReader dr = cmd.ExecuteReader())
                {
                    if (dr.HasRows)
                    {
                        var dictionary = typeof(T).GetProperties().ToDictionary(
                   field => CamelCaseToUnderscore(field.Name), field => field.Name);
                        while (dr.Read())
                        {
                            T tempObj = (T)Activator.CreateInstance(typeof(T));
                            foreach (var key in dictionary.Keys)
                            {
                                PropertyInfo propertyInfo = tempObj.GetType().GetProperty(dictionary[key], BindingFlags.Public | BindingFlags.Instance);
                                if (null != propertyInfo && propertyInfo.CanWrite)
                                    propertyInfo.SetValue(tempObj, Convert.ChangeType(dr[key], propertyInfo.PropertyType), null);
                            }
                            Rows.Add(tempObj);
                        }
                    }
                    dr.Close();
                }
            }
        }
        return Rows;
    }

    private static string CamelCaseToUnderscore(string str)
    {
        return Regex.Replace(str, @"(?<!_)([A-Z])", "_$1").TrimStart('_').ToLower();
    }

また、知っておくべきことは、すべてのストアドプロシージャが小文字のアンダースコアで区切られていることです。 CamelCaseToUnderscoreは、そのために特別に構築されています。

BigDealがbig_dealにマッピングできるようになりました

あなたはそのようにそれを呼び出すことができるはずです

Namespace.SqlQuery<YourObj>(db, "name_of_stored_proc", new SqlParameter("@param",value),,,,,,,);
3
DeadlyChambers

「DeadlyChambers」によって投稿された例は素晴らしいですが、例を拡張して、EFで使用できるColumnAttributeをプロパティに追加して、SQLフィールドをClassプロパティにマップします。

例.

[Column("sqlFieldName")]
public string AdjustedName { get; set; }

これが変更されたコードです。
このコードには、辞書を渡すことにより、必要に応じてカスタムマッピングを可能にするパラメーターも含まれています。
null許容型などには、Convert.ChangeType以外の型コンバーターが必要です。
例データベースにあるフィールドと.NETのnull許容ブール値がある場合、型変換の問題が発生します。

/// <summary>
/// WARNING: EF does not use the ColumnAttribute when mapping from SqlQuery. So this is a "fix" that uses "lots" of REFLECTION
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="database"></param>
/// <param name="sqlCommandString"></param>
/// <param name="modelPropertyName_sqlPropertyName">Model Property Name and SQL Property Name</param>
/// <param name="sqlParameters">SQL Parameters</param>
/// <returns></returns>
public static List<T> SqlQueryMapped<T>(this System.Data.Entity.Database database, 
    string sqlCommandString, 
    Dictionary<string,string> modelPropertyName_sqlPropertyName, 
    params System.Data.SqlClient.SqlParameter[] sqlParameters)
{
    List<T> listOfT = new List<T>();

    using (var cmd = database.Connection.CreateCommand())
    {
        cmd.CommandText = sqlCommandString;
        if (cmd.Connection.State != System.Data.ConnectionState.Open)
        {
            cmd.Connection.Open();
        }

        cmd.Parameters.AddRange(sqlParameters);

        using (var dataReader = cmd.ExecuteReader())
        {
            if (dataReader.HasRows)
            {
                // HACK: you can't use extension methods without a type at design time. So this is a way to call an extension method through reflection.
                var convertTo = typeof(GenericExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(mi => mi.Name == "ConvertTo").Where(m => m.GetParameters().Count() == 1).FirstOrDefault();

                // now build a new list of the SQL properties to map
                // NOTE: this method is used because GetOrdinal can throw an exception if column is not found by name
                Dictionary<string, int> sqlPropertiesAttributes = new Dictionary<string, int>();
                for (int index = 0; index < dataReader.FieldCount; index++)
                {
                    sqlPropertiesAttributes.Add(dataReader.GetName(index), index);
                }

                while (dataReader.Read())
                {
                    // create a new instance of T
                    T newT = (T)Activator.CreateInstance(typeof(T));

                    // get a list of the model properties
                    var modelProperties = newT.GetType().GetProperties();

                    // now map the SQL property to the EF property
                    foreach (var propertyInfo in modelProperties)
                    {
                        if (propertyInfo != null && propertyInfo.CanWrite)
                        {
                            // determine if the given model property has a different map then the one based on the column attribute
                            string sqlPropertyToMap = (propertyInfo.GetCustomAttribute<ColumnAttribute>()?.Name ?? propertyInfo.Name);
                            string sqlPropertyName;
                            if (modelPropertyName_sqlPropertyName!= null && modelPropertyName_sqlPropertyName.TryGetValue(propertyInfo.Name, out sqlPropertyName))
                            {
                                sqlPropertyToMap = sqlPropertyName;
                            }

                            // find the SQL value based on the column name or the property name
                            int columnIndex;
                            if (sqlPropertiesAttributes.TryGetValue(sqlPropertyToMap, out columnIndex))
                            {
                                var sqlValue = dataReader.GetValue(columnIndex);

                                // ignore this property if it is DBNull
                                if (Convert.IsDBNull(sqlValue))
                                {
                                    continue;
                                }

                                // HACK: you can't use extension methods without a type at design time. So this is a way to call an extension method through reflection.
                                var newValue = convertTo.MakeGenericMethod(propertyInfo.PropertyType).Invoke(null, new object[] { sqlValue });

                                propertyInfo.SetValue(newT, newValue);
                            }
                        }
                    }

                    listOfT.Add(newT);
                }
            }
        }
    }

    return listOfT;
}
2
goroth