Как использовать Entity Framework для сопоставления результатов хранимой процедуры с сущностью с параметрами с разными именами - PullRequest
23 голосов
/ 29 марта 2012

Я пытаюсь создать базовый пример, используя 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);
        }
}

... который я добавляю в OnModelCreating DbContext, тогда я могу просто отлично получить сущности из базы данных, что неплохо, используя это ....

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; }

Это вообще устраняло необходимость в какой-либо EntityTypeConfiguration для DBUnitEntity с идентификатором, идентичным базе данных (т.е. просто добавляя атрибут [Key]), но ничего не делало для объекта с именами свойств, которые не соответствуют базе данных (та же ошибка, что и раньше).

Я не против использовать аннотации ComponentModel в модели, но я действительно не хочу использовать аннотации EntityFramework в модели, если я могу помочь (не хочу связывать модель с каким-либо конкретным доступом к данным рамки)

Ответы [ 3 ]

21 голосов
/ 03 апреля 2012

С Entity Framework Code First книга (стр. 155):

Метод SQLQuery всегда пытается сопоставить столбец с свойством на основе имени свойства ... Ни один, что сопоставление имени столбца к свойству не принимает во внимание никакое сопоставление. Например, если вы сопоставили свойство DestinationId со столбцом с именем Id в таблице Destination, метод SqlQuery не будет использовать это сопоставление.

Таким образом, вы не можете использовать сопоставления при вызове хранимой процедуры. Одним из обходных путей является изменение вашей хранимой процедуры, чтобы она возвращала результат с псевдонимами для каждого столбца, которые будут соответствовать именам свойств вашего объекта.

Select UT_STR_AD as Address From SomeTable и т. Д.

3 голосов
/ 10 мая 2015

Это не использует Entity Framework, но это вытекает из dbcontext.Я часами часами рыщу в интернете и пользуюсь точечным взглядом.Я читал кое-где, что ColumnAttribute игнорируется для SqlQueryRaw.Но я создал что-то с помощью отражения, обобщений, sql datareader и Activator.Я собираюсь протестировать его на нескольких других процессах.Если есть какая-либо другая проверка ошибок, которая должна войти, прокомментируйте.

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),,,,,,,);
1 голос
/ 29 октября 2017

Пример, опубликованный "DeadlyChambers", великолепен, но я хотел бы расширить пример, включив в него атрибут ColumnAttribute, который можно использовать с EF для добавления к свойствам для сопоставления поля SQL со свойством Class.

Ex.

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

Вот модифицированный код.
Этот код также включает параметр, позволяющий настраивать сопоставления при необходимости, передавая словарь.
Вам понадобится конвертер типов, отличный от Convert.ChangeType, для таких вещей, как типы, допускающие значения NULL.
Ex. Если у вас есть поле, которое является битовым в базе данных и имеет логическое значение NULL в .NET, вы получите проблему с преобразованием типов.

/// <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;
}
...