czwartek, 13 grudnia 2012

[SQL|ORM] Entity Framework : Loading Entities

Entity Framework udostępnia szereg mechanizmów umożliwiających sterowanie sposobem ładowania encji powiązanych w zapytaniach.Domyślnie ładowane są jedynie encje bezpośrednio wołane w zapytaniu. Czasami jednak potrzebujemy zredukować ilość zapytań do bazy danych i pobrać zarówno bezpośrednio interesujące nas encje, jak i encje powiązane za pomocą jednego zapytania. Wiąże się to jednak ze znacznym zwiększeniem grafu obiektów w pamięci. Nie ma jednoznacznej odpowiedzi na to, jak powinno się konstruować zapytania, wiele zależy od ilości danych i częstości korzystania z danych powiązanych.

Ładowanie encji powiązanych

W tym celu należy skorzystać z funkcji Include(), gdzie przez string podaje się nazwę navigation property..

var customers = context.Customers.Include("CustomerType").
                    Include("CustomerEmails");
foreach (var customer in customers)
{
    Console.WriteLine("Name : {0}, Email : {1}", customer.Name, 
        customer.CustomerType.Description);
    foreach (var customerEmail in customer.CustomerEmails)
    {
        Console.WriteLine(customerEmail.Email);
    }
}

Wykorzystując funkcję Include powyższy kod przetransformuje się do jednego dużego zapytania do bazy danych. Jeśli usuniemy Include, dostejemy po dwa dodatkowe zapytania do bazy danych dla każdej encji z Customers.

Pobieranie całego grafu encji

W bardziej skomplikowanym przypadku, przedstawionym na poniższym diagramie, poprzez odpowiedni dobór kolejności wywołań funkcji Include(), również możemy w jednym zapytaniu załadować do pamięci pełny graf.



var graph = context.Courses.Include("Sections.Students")
                    .Include("Sections.Instructor");

A zatem korzystając z funkcji Include możemy podawać nazwy encji z całego grafu poprzez ścieżkę do takiej nazwy budowaną przy użyciu navigation properties.

W przypadku dziedziczenia encji, gdy tworzymy zapytanie o encje dziedziczące, możemy bez problemu korzystać z navigation properties encji bazowych.

Korzystanie z Include w dowolnych zapytaniach LINQ

Jeżeli chcemy wykorzystać metodę Include() w przypadku dowolnych zapytań, np. joinów, group by czy where, warto pamiętać o kilku faktach:
  • Niezmaterializowane wyrażenie LINQ musimy rzutować do typu ObjectQuery<T>, aby móc skorzystać z metody Include()
  • Include() jest stosowane jedynie do końcowych rezultatów zapytania, w podzapytaniach będzie ignorowane
  • Include() będzie ignorowane, gdy kolekcja zawiera cokolwiek innego niż encje
W poniższym przykładzie zmienna events przechowuje zapytanie typu IQueryable<T> , dopiero podczas materializacji możemy skorzystać z Include().

var events = from ev in context.Events
             where ev.Club.City == "New York"
             group ev by ev.Club
             into g
             select g.FirstOrDefault(e1 => e1.EventDate == g
                 .Min(s => s.EventDate));
var e = ((ObjectQuery<Event>) events).Include("Club").First();

Opóźnione ładowanie encji powiązanych

Sytuacja przedstawia się następująco: mamy jeden obiekt w pamięci i chcemy pobrać dla niego dane z wielu encji powiązanych. Nie chcemy drugi raz pobierać instancji obiektu zmaterializowanego. Możemy skorzystać z metody CreateSourceQuery(), a na zwróconym przez nią obiekcie wywołać dopiero Include(). Tak zbudowane zapytanie należy dołożyć do kontekstu za pomocą funkcji Attach(). Jest to rozwiązanie bardzo wydajne, ponieważ nie musimy po raz drugi pozyskiwać kolumn z encji, którą mamy już w pamięci.

var jill = context.Employees.Where(e => e.Name == "Jill Carpenter").First();
var moreResults = jill.DepartmentReference.CreateSourceQuery()
    .Include("Company").First();
context.Attach(moreResults);
Console.WriteLine("{0} works in {1}", jill.Name, jill.Department.Company.Name);

CreateSourceQuery() zwraca obiekt zapytania, który w momencie wykonania zwraca ten sam zestaw obiektów, który istnieje w obecnej kolekcji.
Attach() dołącza obiekt lub graf obiektów do kontekstu.

Filtrowanie i sortowanie encji powiązanych

Mamy encję w pamięci i chcemy przefiltrować oraz posortować encje z nią powiązane. Kolejny raz skorzystać można z metody CreateSourceQuery(), która pozwoli uzyskać dostęp do zapytania używanego przy pobieraniu kolekcji przez navigation property. Funkcję Include() musimy wywołać w momecie, gdy mamy do czynienia z odpowiednim typem (ObjectQuery<T>). Metoda Attach() łączy przefiltrowane encje z pierwszą encją w pamięci.

var hotel = context.Hotels.First();
var rooms = hotel.Rooms.CreateSourceQuery()
            .Include("Reservations")
            .Where(r => r is ExecutiveSuite && r.Reservations.Any())
            .OrderBy(r => r.Rate);

hotel.Rooms.Attach(rooms);

Sprawdzenie, czy referencja została już załadowana do pamięci 

Chcemy sprawdzić, czy dana referencja do encji powiązanej (lub powiązanej kolekcji) została już załadowana do kontekstu. EF udostępnie property IsLoaded. Różnica polega na tym, że dla kolekcji sprawdzamy IsLoaded bezpośrednio na navigation property, natomiast dla pojedynczych referencji przez nazwa_propertyReference.

var project = context.Projects.Include("Manager").First();
if(project.ManagerReference.IsLoaded)
    Console.WriteLine("Manager reference is loaded");
else
    Console.WriteLine("Manager reference is NOT loaded");
if(project.Contractors.IsLoaded)
    Console.WriteLine("Contractors are loaded");

Aby doczytać kolekcje w sposób jawny, można skorzystać z funkcji Load(), przed jej wywołaniem warto sprawdzić, czy referencja jest już w pamięci za pomocą IsLoaded. Metoda Load jest przeładowana tak, że jako parametr przyjmuje flagę MergeOption, dostępne opcje:
  • AppendOnly dołącza instancje, których nie ma obecnie w kontekście
  • OverwriteChanges przywraca do kontekstu stan z bazy danych
  • NoTracking wyłącza śledzenie stanu
  • PreserveChanges przeciwieństwo dla OverwriteChanges

Brak komentarzy:

Prześlij komentarz