Entity Framework Получить сущность по первичному ключу

Некоторое время назад я хотел реализовать метод, который мог бы определить, выполнять ли вставку или обновление данного объекта, поэтому мне не нужно было раскрывать методы «Вставка» и «Обновление», а просто простой «InsertOrUpdate». ".

Часть кода, которая выясняет, является ли сущность новой или нет, такова:

    public virtual T GetEntityByPrimaryKey<T>(T entity) where T : class
    {
        var entityType = entity.GetType();
        var objectSet = ((IObjectContextAdapter)this.DatabaseContext).ObjectContext.CreateObjectSet<T>();
        var keyNames = objectSet.EntitySet.ElementType.KeyMembers.Select(edmMember => edmMember.Name);
        var keyValues = keyNames.Select(name => entityType.GetProperty(name).GetValue(entity, null)).ToArray();

        return this.DatabaseContext.Set<T>().Find(keyValues);
    }

И метод InsertOrUpdate таков:

    public virtual T InsertOrUpdate<T>(T entity) where T : class
    {
        var databaseEntity = this.GetEntityByPrimaryKey(entity);

        if (databaseEntity == null)
        {
            var entry = this.DatabaseContext.Entry(entity);

            entry.State = EntityState.Added;

            databaseEntity = entry.Entity;
        }
        else
        {
            this.DatabaseContext.Entry(databaseEntity).CurrentValues.SetValues(entity);
        }

        return databaseEntity;
    }

Теперь этот подход творит чудеса, пока «первичный ключ» объекта определяется кодом. Допустимыми примерами являются GUID, алгоритмы HI-LO, естественные ключи и т. д.

Это, однако, ужасно нарушено для сценария «идентификация, сгенерированная базой данных», и причина довольно проста: поскольку мой «Id» в коде будет равен 0 для всех объектов, которые я собираюсь вставить, метод будет учитывать их одинаковый. Если я добавлю 10 объектов, первый будет "новым", а следующие девять - "уже существующими". Это связано с тем, что метод «Найти» EF считывает данные из объектного контекста и только в том случае, если он отсутствует, он переходит в базу данных для выполнения запроса.

После первого объекта будет отслеживаться объект данного типа с Id 0. Последовательные вызовы приведут к «обновлению», и это просто неправильно.

Теперь я знаю, что идентификаторы, сгенерированные базой данных, являются злом и абсолютно не подходят для любого ORM, но я застрял с ними, и мне нужно либо исправить этот метод, либо полностью удалить его и вернуться к отдельным методам «Вставить» и «Обновить». и делегируйте вызывающей стороне задачу, чтобы определить, что делать. Поскольку у нас есть сильно развязанное решение, я бы предпочел этого не делать.

Если кто-нибудь может помочь и найти способ исправить метод GetEntityByPrimaryKey, это было бы здорово.

Спасибо.


person Matteo Mosca    schedule 21.02.2013    source источник
comment
Я инициализирую все свои новые объекты отрицательным целым числом (все PK являются целыми числами), чтобы они были разными   -  person Jehof    schedule 21.02.2013
comment
Любой, кто голосует против, объяснит, почему .. спасибо.   -  person Matteo Mosca    schedule 21.02.2013
comment
Это не пойдет. Наш администратор базы данных решил использовать полный диапазон целых чисел для некоторых таблиц, начиная с наименьшего отрицательного значения в качестве начального значения.   -  person Matteo Mosca    schedule 21.02.2013


Ответы (3)


У меня есть следующие предложения:

1:
я бы добавил к объектам что-то вроде свойства IsTransient. Он возвращает true, если PK равен 0, иначе возвращает false.
Вы можете использовать это свойство, чтобы изменить свой метод следующим образом:

  1. IsTransient == верно? -> Вставить
  2. IsTransient == ложь? -> Ваш существующий код с проверкой базы данных

Сделайте это свойство виртуальным, и вы даже сможете поддерживать объекты со «странным» PK, переопределив IsTransient.

2:
Если вам не нравится добавлять это к своим объектам, вы все равно можете создать метод расширения, который инкапсулирует эту логику. Или даже добавьте этот чек прямо в свой InsertOrUpdate.

Поскольку у вас нет общего базового класса для ваших сущностей, эти предложения станут немного утомительными. В основном вам нужно будет иметь один метод расширения для каждой сущности.

3:
Если у вас есть соглашение для PK, вы можете использовать dynamic для доступа к свойству ID:

dynamic dynamicEntity = entity;
if(dynamicEntity.Id == 0)
{
    // Insert
}
else
{
    // Current code.
}

4:
Поскольку добавление временного объекта в контекст нарушает работу всех последующих временных элементов, было бы неплохо добавить временные элементы в список, а не в контекст.
Добавляйте их в контекст только тогда, когда он будет зафиксирован. Я уверен, что для этого есть крючок, который вы можете использовать:

List<object> _newEntities;

private override OnCommit()
{
    foreach(var newEntity in newEntities)
        DatabaseContext.Entry(newEntity).State = EntityState.Added;
}

public virtual T InsertOrUpdate<T>(T entity) where T : class
{
    var databaseEntity = this.GetEntityByPrimaryKey(entity);

    if (databaseEntity == null)
        _newEntities.Add(entity);
    else
        this.DatabaseContext.Entry(databaseEntity).CurrentValues.SetValues(entity);

    return databaseEntity;
}
person Daniel Hilgarth    schedule 21.02.2013
comment
Мы используем POCO, игнорируя настойчивость. Мы не собираемся загрязнять наши объекты вещами, связанными с методом персистентности, просто это совершенно неправильно для нашего несвязанного дизайна. Нам нужно решение, ограниченное возможностями EF. - person Matteo Mosca; 21.02.2013
comment
@MatteoMosca: ваши классы PI POCO уже содержат материалы, связанные с сохраняемостью. Идентификатор например. И я почти уверен, что у вас есть какой-то базовый класс сущности, чтобы не повторять свойство ID везде. - person Daniel Hilgarth; 21.02.2013
comment
@MatteoMosca: В любом случае, я изменил свой ответ, добавив некоторые дополнительные соображения. - person Daniel Hilgarth; 21.02.2013
comment
Нет, у нас нет базового класса для наших POCO. И я не думаю, что это будет оценено по достоинству, хотя я мог бы попытаться предложить это. В любом случае, исправление моего метода все равно было бы предпочтительнее. - person Matteo Mosca; 21.02.2013
comment
@MatteoMosca: А как насчет абзацев, которые я добавил к своему ответу? Это что-то, что может вам помочь? - person Daniel Hilgarth; 21.02.2013
comment
Наши существа очень разные. Некоторые имеют идентификатор tinyint, некоторые — int. У некоторых есть естественные ключи. Проблема только для tinyints, smallints, ints и т. д., которые имеют приращение идентичности, а не для всех сущностей. Это одна из причин, по которой у нас нет базового класса. Как бы я реализовал эту логику в своем методе, не имея этого дискриминатора в POCO? - person Matteo Mosca; 21.02.2013
comment
@MatteoMosca: Хорошо, в таком случае вам нужно сделать шаг назад и четко определить свои требования. Посмотрите на каждую сущность отдельно и спросите себя: как определить, является ли эта сущность временной? Возможно ли вообще использовать его ПК? - person Daniel Hilgarth; 21.02.2013
comment
@MatteoMosca: я не думаю, что вы действительно можете использовать EF для этого, если вы не фиксируете контекст базы данных, чтобы получить вновь созданные идентификаторы для вновь добавленных объектов. - person Daniel Hilgarth; 21.02.2013
comment
Я рассматриваю ваши последние дополнения. Можно ввести соглашение об идентификаторах, поскольку мы можем называть наши свойства как угодно и просто сопоставлять их с нужными столбцами базы данных. Что касается временного рассмотрения... ну, это непонятно для объектов, у которых есть ключ, не допускающий значения NULL, и где ключ не может быть назначен из кода. Вот почему я думаю, что лучше всего использовать GUID или HI-LO. Мне придется обсудить ваши идеи с парой коллег. Я дам Вам знать. - person Matteo Mosca; 21.02.2013
comment
@MatteoMosca: Пожалуйста, ознакомьтесь с моим последним предложением, прежде чем начинать обсуждение. Возможно, это самый простой способ решить эту проблему. - person Daniel Hilgarth; 21.02.2013
comment
Ваш пункт 4 кажется правильным. У меня уже есть метод SaveChanges, который сохраняет все, что находится на рассмотрении в объектном контексте. Я могу добавить этот временный список и изменить метод savechanges, чтобы выбрать содержимое списка и добавить его в контекст непосредственно перед фиксацией. Я собираюсь попробовать это, спасибо. - person Matteo Mosca; 21.02.2013

так как мой "Id" в коде будет равен 0 для всех объектов, которые я собираюсь вставить

Кажется, вы ожидаете уникальности ключей, когда не предоставляете их. Можно ли инициализировать их уникальным отрицательным числом? (что-то, что не является допустимым значением для фактических записей БД)

У меня была аналогичная проблема (это было с объектами самоотслеживания, которые могли определить, являются ли два еще не вставленных дочерних объекта одинаковыми по ключу или нет...), и это решило ее.

person TDaver    schedule 21.02.2013
comment
ОП уже сказал, что это не вариант. Смотрите последний комментарий к его вопросу. - person Daniel Hilgarth; 21.02.2013
comment
Да, как я уже говорил, во многих таблицах используется полный диапазон допустимых целых чисел, от отрицательных до положительных, так что это не вариант. И в любом случае, это делегировало бы работу по предоставлению поддельных идентификаторов вызывающей стороне, чего я хочу избежать, потому что, если вызывающая сторона этого не сделает, у него будет не исключение, а одна вставка со значениями последнего объекта при условии, что будет работать и быть неправильным. - person Matteo Mosca; 21.02.2013

Учитывая, что ваши POCO свободны от грязи БД, вы должны использовать Fluent API для объявления информации о сгенерированном ключе БД. Так что, возможно, DbSets в CONTEXT содержат этот флаг DB Generated.

Я использую расширение, чтобы получить все POCO, используемые в контексте. Возможно, при достаточном размышлении вы сможете найти свойство или атрибут, который будет полезен в качестве флага, сгенерированного БД. Остальное тогда уже понятно.

Возможно, это полезная отправная точка:

 public static List<string> GetModelNames(this DbContext context ) {
      var model = new List<string>();
      var propList = context.GetType().GetProperties();
      foreach (var propertyInfo in propList)
      {
      if (propertyInfo.PropertyType.GetTypeInfo().Name.StartsWith("DbSet"))
      {
          model.Add(propertyInfo.Name);
          var innerProps = propertyInfo.GetType().GetProperties(); // added to snoop around in debug mode , can your find anything useful?
      }
      }


      return model;
  }
 public static List<string> GetModelTypes(this DbContext context)
 {
     var model = new List<string>();
     var propList = context.GetType().GetProperties();
     foreach (var propertyInfo in propList)
     {
         if (propertyInfo.PropertyType.GetTypeInfo().Name.StartsWith("DbSet"   ))
         {
             model.Add(propertyInfo.PropertyType.GenericTypeArguments[0].Name);
         }
     }


     return model;
 }
}   
person phil soady    schedule 21.02.2013