Entity Framework: повторный поиск объектов, недавно добавленных в контекст

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

string theId = "someId";

private void Test()
{
  using(MyEntities entities = new MyEntities())
  {
    EntityObject o = new EntityObject();
    o.Id = theId;
    entities.AddToEntityObject(o);
    CallSomeOtherMethod(entities);
  }
}

void CallSomeOtherMethod(MyEntities ents)
{
  EntityObject search = ents.EntityObject.FirstOrDefault(o => o.Id == theId);
  if(search == null) 
  {
    Console.WriteLine("wha happened???");
  }
}

(нет гарантии, что код работает, кстати - это все из моей головы)

Почему запрос не "находит" объект EntityObject, который был только что создан?

Если я вызываю SaveChanges() после AddToEntityObject, он работает (что меня не удивляет), но почему он не извлекается из кэша должным образом?

Я все еще зелен в этом, так что я надеюсь, что есть кое-что действительно простое, что я просто упускаю из виду...

Спасибо

6 ответов

Решение

Это происходит потому, что ents.EntityObject.WhatEver всегда запрашивает источник данных. Это дизайнерское решение. Они делают это таким образом, потому что иначе им придется выполнить запрос к источнику данных, к локальному кешу, а затем объединить результаты. Как отметил один из разработчиков в блоге (не могу вспомнить, где именно), они не смогли справиться с этим последовательно.

Как вы можете себе представить, есть много угловых случаев, с которыми вы должны обращаться правильно. Вы можете просто найти идентификатор, который вы создали локально, созданный кем-то еще в базе данных. Это заставит вас быть готовым к обработке конфликтов (почти) на каждый запрос. Возможно, они могли бы создать методы для запроса локального кэша и методы для запроса источника данных, но это не слишком умно.

Вы можете взглянуть на прозрачную ленивую загрузку для Entity Framework. Это заменяет обычный генератор кода, и вы получаете сущности, которые автоматически заполняют их связанные коллекции сущностей и ссылки на сущности при доступе. Это позволяет избежать всего

if (!Entity.ReleatedEntities.IsLoaded)
{
   Entity.RelatedEntities.Load();
}

фрагменты кода. И вы можете запросить коллекции, потому что они всегда загружаются неявно. Но и это решение не идеально. Есть некоторые проблемы. Например, если вы создаете новую сущность и получаете доступ к коллекции связанных сущностей, вы получите исключение, потому что код не может извлечь связанные сущности из базы данных. Существует также проблема, связанная с привязкой данных, и, возможно, я еще не знаю об этом.

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

Недавно добавленный объект находится в локальном источнике данных, так как он еще не сохранен в базе данных, так что вы можете сказать, EntityObject search = ents.EntityObject.FirstOrDefault(o => o.Id == theId) ?? ents.EntityObject.Local.FirstOrDefault(o => o.Id == theId);

Я был в такой же ситуации. Я написал этот метод расширения, который, по крайней мере, для меня решает проблему (у меня нет проблем с т. Е. Конфликтами в моем контексте...)

    public static IEnumerable<T> WhereInclAdded<T>(this ObjectSet<T> set, Expression<Func<T, bool>> predicate)  where T : class
    {
        var dbResult = set.Where(predicate);

        var offlineResult = set.Context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Select(entry => entry.Entity).OfType<T>().Where(predicate.Compile());

        return offlineResult.Union(dbResult);
    }

Ниже приведен метод расширения для DbSet<>

public static T TryAttach<T>(this DbSet<T> dbSet, T entity, Expression<Func<T, bool>> predicate) where T : class
{
     T found = dbSet.Local.SingleOrDefault(predicate.Compile());
     if (found == null) dbSet.Attach(entity);
     return found ?? entity;
}

Как пользоваться:

contextInstance.MyEntity.TryAttach(entityInstance, e => e.ID == entityInstance.ID);

кстати: я люблю дженерики!

Я недавно боролся с этим же вопросом. Я публикую этот ответ через 2 года после того, как вопрос был задан в надежде, что этот фрагмент кода может помочь кому-то в поиске ответа.

Я в основном реализовал метод расширения (как предложено Алексом Джеймсом) под названием "Find", который работает так же, как "Where", но "Find" также проверяет ObjectContext, чтобы увидеть, есть ли какие-либо добавленные объекты, которые удовлетворяют заданному сказуемое. Это позволяет найти объект, даже если он еще не был сохранен в базе данных.

Find возвращает IQueryable(из T), так что вы можете использовать его, как и любой другой оператор LINQ.

<Extension()>
Public Function Find(Of T As Class)(ByVal OSet As ObjectSet(Of T), _
       ByVal predicate As Expression(Of Func(Of T, Boolean))) _
       As System.Linq.IQueryable(Of T)

    'Check the object context for Added objects first.
    Dim AddedContextObjects = OSet.Context.ObjectStateManager _
                        .GetObjectStateEntries(EntityState.Added) _
                        .Select(Function(entity) entity.Entity).OfType(Of T)()


    Dim Cpredicate = predicate.Compile
    Dim MatchingObjects As New List(Of T)

    For Each TObj As T In AddedContextObjects
        If Cpredicate.Invoke(TObj) Then
            MatchingObjects.Add(TObj)
        End If
    Next

    'Now include a query to retrieve objects from the DB.
    Dim DBObjects = OSet.Where(predicate)

    If MatchingObjects.Count > 0 Then
        'We found some added objects in the context.
        'We want to return these objects as well as any Objects in DB
        'that satisfy the predicate.
        Return MatchingObjects.Union(DBObjects).AsQueryable
    Else
        'We didn't find any added objects in the context,
        'so we just return the DB query.
        Return DBObjects
    End If

End Function

Entity Framework 6

Согласно EF Docs Dbset всегда запрашивает базу данных.

Обратите внимание, что DbSet и IDbSet всегда создают запросы к базе данных и всегда включают в себя обратную передачу в базу данных, даже если возвращаемые объекты уже существуют в контексте. Запрос выполняется к базе данных, когда:

Он перечисляется оператором foreach (C#) или For Each (Visual Basic). Он перечисляется операцией коллекции, такой как ToArray, ToDictionary или ToList. Операторы LINQ, такие как First или Any, указываются в самой внешней части запроса. Вызываются следующие методы: метод расширения Load для DbSet, DbEntityEntry.Reload и Database.ExecuteSqlCommand. Когда результаты возвращаются из базы данных, объекты, которые не существуют в контексте, присоединяются к контексту. Если объект уже находится в контексте, возвращается существующий объект (текущие и исходные значения свойств объекта в записи не перезаписываются значениями базы данных).

При выполнении запроса сущности, которые были добавлены в контекст, но еще не сохранены в базе данных, не возвращаются как часть набора результатов. Чтобы получить данные в контексте, см. Локальные данные.

Если запрос не возвращает строк из базы данных, результатом будет пустая коллекция, а не ноль.

Ниже приведен простой фрагмент с локальными данными:

await dbContext.Entity
      .Where(e => e.Title.Contains("Text"))
      .LoadAsync();

var locaEntities = dbContext.Entity.Local;

dbContext.Entity.Add(new Entity {});

// call save post atomic operation is finished.
await dbContext.SaveChangesAsync();

У вас есть несколько вариантов. Вы могли бы продлить ObjectContext с другим частичным классом, чтобы сделать свой собственный механизм для получения недавно добавленной информации.

Или вы можете просто добавить метод расширения на ObjectContext который смотрит через ObjectContext.ObjectStateManager ищу "добавлено" ObjectStateEntries, а затем используйте LINQ to Objects, чтобы найти то, что вы ищете.

Другие вопросы по тегам