C # Entity-Framework: Jak mogę połączyć .Find i .Include na obiekcie modelu?

145

Robię samouczek praktyczny mvcmusicstore. Zauważyłem coś podczas tworzenia rusztowania dla menedżera albumów (dodaj usuń edycję).

Chcę pisać kod elegancko, więc szukam przejrzystego sposobu, aby to napisać.

FYI, sprawiam, że sklep jest bardziej ogólny:

Albumy = elementy

Gatunki = Kategorie

Artysta = marka

Oto jak indeks jest pobierany (generowany przez MVC):

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

Oto jak jest pobierany element do usunięcia:

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

Pierwsza z nich przywraca wszystkie elementy i zapełnia modele kategorii i marki w modelu przedmiotu. Drugi nie wypełnia kategorii i marki.

Jak mogę napisać drugą, aby znaleźć i zapełnić to, co jest w środku (najlepiej w 1 linii) ... teoretycznie - coś takiego:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);
Ralph N
źródło
Jeśli ktoś potrzebuje zrobić to ogólnie w.net-core, zobacz moją odpowiedź
johnny 5

Odpowiedzi:

162

Musisz Include()najpierw użyć , a następnie pobrać pojedynczy obiekt z wynikowego zapytania:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .SingleOrDefault(x => x.ItemId == id);
Dennis Traub
źródło
24
Naprawdę poleciłbym użycie tego drugiego (SingleOrDefault), ToList najpierw pobierze wszystkie wpisy, a następnie wybierze jeden
Sander Rijken
5
To się psuje, jeśli mamy złożony klucz podstawowy i używamy odpowiedniego przeciążenia wyszukiwania.
jhappoldt
78
To by zadziałało, ale istnieje różnica między używaniem „Find” a „SingleOrDefault”. Metoda "Find" zwraca obiekt z lokalnego śledzonego magazynu, jeśli istnieje, unikając podróży w obie strony do bazy danych, gdzie użycie "SingleOrDefault" i tak wymusi zapytanie do bazy danych.
Iravanchi
3
@Iravanchi ma rację. Może to zadziałało dla użytkownika, ale o ile wiem, operacja i jej skutki uboczne nie są równoważne funkcji Find.
mwilson
3
Właściwie nie odpowiada na pytanie ops, ponieważ nie używa
.Find
73

Odpowiedź Dennisa to użycie Includei SingleOrDefault. Ten ostatni przechodzi w obie strony do bazy danych.

Alternatywą jest użycie Find, w połączeniu z Load, do jawnego ładowania powiązanych jednostek ...

Poniżej przykład 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(); 
}

Oczywiście Findwraca natychmiast bez składania zapytania do sklepu, jeśli ten podmiot jest już załadowany przez kontekst.

Uczeń
źródło
30
Ta metoda używa Findwięc, jeśli jednostka jest obecna, nie ma rundy do bazy danych dla samej jednostki. ALE, będziesz mieć podróż w obie strony dla każdego związku, z którym się łączysz Load, podczas gdy SingleOrDefaultkombinacja Includeładuje wszystko za jednym razem.
Iravanchi
Kiedy porównałem 2 w profilerze SQL, Find / Load był lepszy w moim przypadku (miałem relację 1: 1). @Iravanchi: czy chcesz powiedzieć, że gdybym miał relację 1: m, nazwałaby to m razy więcej niż sklep? ... ponieważ nie miałoby to większego sensu.
Learner
3
Nie relacja 1: m, ale relacje wielokrotne. Za każdym razem, gdy wywołujesz Loadfunkcję, relacja powinna zostać wypełniona po powrocie wywołania. Więc jeśli dzwonisz Loadwiele razy dla wielu relacji, za każdym razem nastąpi podróż w obie strony. Nawet dla pojedynczej relacji, jeśli Findmetoda nie znajdzie jednostki w pamięci, wykonuje dwie rundy: jedną for Findi drugą for Load. Ale Include. SingleOrDefaultpodejście wyszukuje byt i relację za jednym razem, o ile wiem (ale nie jestem pewien)
Iravanchi
1
Byłoby miło, gdyby udało się jakoś podążać za projektem Include, zamiast inaczej traktować kolekcje i odniesienia. To utrudnia utworzenie fasady GetById (), która po prostu przyjmuje opcjonalną kolekcję Expression <Func <T, object >> (np. _Repo.GetById (id, x => x.MyCollection))
Derek Greer
4
Warto wspomnieć o numerze
Hossein,
1

Musisz rzucić IQueryable na DbSet

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

return dbSet.Find(id);

Rafael R. Souza
źródło
W zestawie dbSet nie ma plików .Find ani .FindAsync. Czy to jest EF Core?
Thierry
jest ef 6 również na rdzeniu ef
Rafael R. Souza
Miałem nadzieję, a potem „
InvalidCastException
0

Nie działa dla mnie. Ale rozwiązałem to w ten sposób.

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

Nie wiem, czy to dobre rozwiązanie. Ale drugi, podany przez Dennisa, dał mi błąd bool .SingleOrDefault(x => x.ItemId = id);

Johan
źródło
4
Rozwiązanie Dennisa też musi działać. Być może masz ten błąd SingleOrDefault(x => x.ItemId = id)tylko z powodu złego pojedynczego =zamiast podwójnego ==?
Slauma,
6
tak, wygląda na to, że użyłeś = nie ==. Błąd składniowy;)
Ralph N,
Wypróbowałem je oba == i = nadal dał mi błąd w .SingleOrDefault (x => x.ItemId = id); = / W moim kodzie musi być coś innego, co jest nie tak. Ale sposób, w jaki to zrobiłem, jest zły? Może nie rozumiem, co masz na myśli, Dennis ma również singel = w swoim kodzie.
Johan,
0

Nie ma naprawdę łatwego sposobu na filtrowanie za pomocą znalezienia. Ale wymyśliłem bliski sposób na odtworzenie funkcjonalności, ale proszę zwrócić uwagę na kilka rzeczy dotyczących mojego rozwiązania.

To rozwiązanie umożliwia filtrowanie ogólne bez znajomości klucza podstawowego w .net-core

  1. Funkcja Find różni się zasadniczo, ponieważ uzyskuje jednostkę, jeśli jest ona obecna w śledzeniu przed wysłaniem zapytania do bazy danych.

  2. Dodatkowo może filtrować według obiektu, dzięki czemu użytkownik nie musi znać klucza podstawowego.

  3. To rozwiązanie jest przeznaczone dla EntityFramework Core.

  4. Wymaga to dostępu do kontekstu

Oto kilka metod rozszerzenia do dodania, które pomogą Ci filtrować według klucza podstawowego

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

Gdy masz już te metody rozszerzenia, możesz filtrować w ten sposób:

query.FilterByPrimaryKey(this._context, id);
johnny 5
źródło