C # Entity-Framework: Как я могу объединить .Find и .Include для модельного объекта?

146

Я занимаюсь практическим руководством по mvcmusicstore. Я кое-что заметил при создании скаффолда для менеджера альбомов (добавить, удалить, изменить).

Я хочу писать код элегантно, поэтому я ищу чистый способ написать это.

К вашему сведению, я делаю магазин более общим:

Альбомы = Предметы

Жанры = Категории

Исполнитель = Бренд

Вот как получается индекс (сгенерированный MVC):

var items = db.Items.Include(i => i.Category).Include(i => i.Brand);

Вот как извлекается элемент для удаления:

Item item = db.Items.Find(id);

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

Как я могу написать второй, чтобы сделать поиск и заполнить что внутри (желательно в 1 строку) ... теоретически - что-то вроде:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
Ральф Н
источник
Если кому-то нужно сделать это в основном в .net-core, посмотрите мой ответ
Джонни 5

Ответы:

162

Include()Сначала нужно использовать , а затем извлечь один объект из полученного запроса:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .SingleOrDefault(x => x.ItemId == id);
Деннис Трауб
источник
24
Я действительно рекомендовал бы использовать последнее (SingleOrDefault), ToList сначала получит все записи, а затем выберет одну
Sander Rijken
5
Это ломается, если у нас есть составной первичный ключ и мы используем соответствующую перегрузку поиска.
Джаппольдт
78
Это будет работать, но есть разница между использованием «Find» и «SingleOrDefault». Метод «Find» возвращает объект из локального отслеживаемого хранилища, если он существует, избегая кругового обращения к базе данных, где использование «SingleOrDefault» в любом случае принудительно вызывает запрос к базе данных.
Ираванчи
3
@ Ираванчи правильный. Возможно, это сработало для пользователя, но, насколько я знаю, операция и ее побочные эффекты не эквивалентны Find.
Мвилсон
3
На самом деле не отвечает на вопрос ops, поскольку он не использует. Найти
Пол Свец
73

Деннис ответ использует Includeи SingleOrDefault. Последний идет в обход базы данных.

Альтернативой является использование Find, в сочетании с Load, для явной загрузки связанных объектов ...

Ниже пример MSDN :

using (var context = new BloggingContext()) 
{ 
  var post = context.Posts.Find(2); 

  // Load the blog related to a given post 
  context.Entry(post).Reference(p => p.Blog).Load(); 

  // Load the blog related to a given post using a string  
  context.Entry(post).Reference("Blog").Load(); 

  var blog = context.Blogs.Find(1); 

  // Load the posts related to a given blog 
  context.Entry(blog).Collection(p => p.Posts).Load(); 

  // Load the posts related to a given blog  
  // using a string to specify the relationship 
  context.Entry(blog).Collection("Posts").Load(); 
}

Конечно, Findнемедленно возвращается без запроса магазина, если этот объект уже загружен контекстом.

ученик
источник
30
Этот метод использует, Findпоэтому, если сущность присутствует, для самой сущности нет обратной передачи в БД. НО, у вас будет круговая поездка для каждого отношения, в котором вы находитесь Load, тогда как SingleOrDefaultкомбинация с Includeзагрузкой всего за один раз.
Ираванчи
Когда я сравнил 2 в профилировщике SQL, Find / Load был лучше для моего случая (у меня было соотношение 1: 1). @Iravanchi: ты хочешь сказать, что если бы я имел отношение 1: m, он бы назвал m раз магазином? ... потому что это не имело бы особого смысла.
Учащийся
3
Не 1: м отношение, но несколько отношений. Каждый раз, когда вы вызываете Loadфункцию, отношение должно заполняться при возврате вызова. Так что, если вы звоните Loadнесколько раз для нескольких отношений, каждый раз будет двусторонняя поездка. Даже для одного отношения, если Findметод не находит сущность в памяти, он выполняет два цикла: один для, Findа второй для Load. Но Include. SingleOrDefaultнасколько я знаю, подход извлекает сущность и отношения за один раз (но я не уверен)
Иреванчи
1
Было бы неплохо, если бы кто-то следовал дизайну «Включить» каким-то образом, а не обрабатывал бы коллекции и ссылки по-другому. Это затрудняет создание фасада GetById (), который просто принимает необязательную коллекцию Expression <Func <T, object >> (например, _repo.GetById (id, x => x.MyCollection))
Дерек Грир
4
Не забудьте упомянуть ссылку на ваш пост: msdn.microsoft.com/en-us/data/jj574232.aspx#explicit
Хоссейн
1

Вы должны привести IQueryable к DbSet

var dbSet = (DbSet<Item>) db.Set<Item>().Include("");

return dbSet.Find(id);

Рафаэль Р. Соуза
источник
В dbSet нет .Find или .FindAsync. Это EF Core?
Тьерри
есть также 6 ef на ядре
Рафаэль Р. Соуза
Я был полон надежд, а затем "InvalidCastException"
ZX9
0

У меня не сработало. Но я решил это, сделав так.

var item = db.Items
             .Include(i => i.Category)
             .Include(i => i.Brand)
             .Where(x => x.ItemId == id)
             .First();

Не знаю, нормально ли это решение. Но другой, который дал Деннис, дал мне ошибку .SingleOrDefault(x => x.ItemId = id);

Johan
источник
4
Решение Денниса тоже должно сработать. Возможно, у вас есть эта ошибка SingleOrDefault(x => x.ItemId = id)только из-за неправильного сингла =вместо двойного ==?
Слаума
6
да, похоже, что вы использовали = не ==. Синтаксическая ошибка;)
Ральф Н
Я попробовал их оба == и = все еще дал мне ошибку в .SingleOrDefault (x => x.ItemId = id); = / Должно быть что-то еще в моем коде, это неправильно. Но то, как я поступил, это плохо? Может быть, я не понимаю, что вы имеете в виду, что у Денниса в коде тоже есть сингел =.
Йохан
0

Там нет действительно простой способ отфильтровать с помощью поиска. Но я придумал близкий способ повторить функциональность, но, пожалуйста, обратите внимание на несколько вещей для моего решения.

Это решение позволяет вам выполнять общую фильтрацию, не зная первичного ключа в .net-core

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

  2. Кроме того, он может фильтровать по объекту, поэтому пользователю не нужно знать первичный ключ.

  3. Это решение для EntityFramework Core.

  4. Это требует доступа к контексту

Вот несколько методов расширения, которые помогут вам фильтровать по первичному ключу, поэтому

    public static IReadOnlyList<IProperty> GetPrimaryKeyProperties<T>(this DbContext dbContext)
    {
        return dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
    }

    //TODO Precompile expression so this doesn't happen everytime
    public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id)
    {
        var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var body = keyProperties
            // e => e.PK[i] == id[i]
            .Select((p, i) => Expression.Equal(
                Expression.Property(parameter, p.Name),
                Expression.Convert(
                    Expression.PropertyOrField(Expression.Constant(new { id = id[i] }), "id"),
                    p.ClrType)))
            .Aggregate(Expression.AndAlso);
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }

    public static Expression<Func<T, object[]>> GetPrimaryKeyExpression<T>(this DbContext context)
    {
        var keyProperties = context.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var keyPropertyAccessExpression = keyProperties.Select((p, i) => Expression.Convert(Expression.Property(parameter, p.Name), typeof(object))).ToArray();
        var selectPrimaryKeyExpressionBody = Expression.NewArrayInit(typeof(object), keyPropertyAccessExpression);

        return Expression.Lambda<Func<T, object[]>>(selectPrimaryKeyExpressionBody, parameter);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this DbSet<TEntity> dbSet, DbContext context, object[] id)
        where TEntity : class
    {
        return FilterByPrimaryKey(dbSet.AsQueryable(), context, id);
    }

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this IQueryable<TEntity> queryable, DbContext context, object[] id)
        where TEntity : class
    {
        return queryable.Where(context.FilterByPrimaryKeyPredicate<TEntity>(id));
    }

Когда у вас есть эти методы расширения, вы можете фильтровать так:

query.FilterByPrimaryKey(this._context, id);
Джонни 5
источник