sens lazy loading w aplikacjach webowych

0

Zastanawiam się nad sensem lazy loading w aplikacjach webowych. Bo w aplikacjach okienkowych gdzie mamy wykonywanie akcji na żywo w odpowiedzi na działanie użytkownika to faktycznie ma sens, natomiast w aplikacjach webowych gdzie zwykle wykonanie akcji oznacza załadowanie nowej strony (pomijam ajaxa) to mija się chyba z celem. Oczywiście, np. jak mamy stronicowanie to lazy loading niesie korzyści, ale już przykładowo jak mamy notatki, a każda notatka może mieć wiele komentarzy to wyświetlenie notatek w ten sposób biorąc pod uwagę lazy loading:

 
public IQueryable<Notes> GetAllNotes()
{
    return (from x in context.Notes
            select x);
}

oraz

 
@foreach (var note in Notes) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => note.Title)

            @foreach (var comment in note.Comments)
            {
                @Html.DisplayFor(modelItem => comment.Content)
            }
        </td>
    </tr>
}

spowoduje, że jak mamy np. 5. notatek to zostanie wykonanych w sumie 5. zapytań do bazy danych pobierających komentarze dla notatek wyświetlanych po kolei w pętli. Oczywiście należałoby zastosować tu Include() dla eagerly loading, ale przecież w aplikacjach webowych tego typu sytuacje są znacznie częściej spotykane niż potrzeba użycia lazy loading - skąd więc to promowanie w ORMach dla javy i c# lazy loadingu w aplikacjach webowych??

0

Co za różnica, czy aplikacja webowa, desktopowa czy jakaś inna?
Jeśli nie użyjesz lazy loadingu, to pobierając jeden obiekt z bazy pobierzesz cały powiązany z nim graf obiektów, w ekstremalnym przypadku pobierzesz całą bazę danych.

0

Przeglądając sić zauważyłem, że ktoś ma podobny problem i podobne przemyślenia:
http://stackoverflow.com/questions/13999779/what-are-the-benefits-of-orm-lazy-loading

0

Jaki problem i jakie przemyślenia?
Jeśli trzeba pobrać encję z dziećmi, to robi się to zachłannie, żeby zminimalizować liczbę zapytań do bazy.
Ale domyślnie wszystkie relacje muszą być leniwe, z przyczyn o których napisałem w pierwszym poście.

Czy pobierając post z komentarzami, chcesz też pobrać autorów tych komentarzy ze wszystkimi ich postami oraz komentarzami do nich?

1

Zalozmy ze, masz cos takiego:

jeden na 10000 obiektow ma wlasciwosc dupa=true.

dbojects = repository.GetObjects();
for(int i=0;i<10000;i++){
 if (dbobjects[i].dupa) {
      Przetworz(lazy_loaded_objects[i].lazyLoadedChildren);
 }
 else {
  ....
 }
}

Wniosek? Nie ma sensu w tym przypadku zrezygnować z lazy loading. (po co pobierać dla 10000 obiektów cały graf, skoro tylko w jednym na 10000 przypadków będziesz robił dostęp do children-właściwości).
Oczywiscie sa przypadki gdy lazy loading nie ma sensu i wtedy stosuje sie eager loading. Np. przedstawiamy stronicowany grid z klientami, w ktorym chcemy rowniez przedstawic powiazany z klientem adres + inne powiazane tabele. Wtedy, zakladajac, ze strona grida ma 8 pozycji, gdybysmy zastosowali lazy loading, zrobilismy dodatkowe 8 odpytan. Wtedy faktycznie warto jawnie pobrac dodatkowe (tylko wymagane przez nas) dane - nie caly graf obiektow ;).

Druga rzecz, czasami, przy prostych odpytaniach, te kilka milisekund straconych na zapytanie nie zaszkodzi wydajnosci aplikacji, za to kod będzie krótszy, przyjemniejszy, czytelniejszy i wygodniejszy.

0

Podam podobny przykład do powyższego. Podobny, a nie taki sam bo akurat mam taką bazę danych jaką mam :)
Załóżmy, że mamy dwie tabele: Orders i Customers powiązane ze sobą kolumną CustomerID.
Mam ileś tam tysięcy rekordów w Orders i w Customers i jeśli w Orders kolumna ShipCountry == "Brazil", to w takim przypadku chcę wyświetlić ContactName z Customers.
Z użyciem LazyLoadingu kod będzie wyglądał tak:

foreach (Order order in context.Orders.OrderBy(x => x.ShipName))
{
    if(order.ShipCountry == "Brazil")
        Console.WriteLine("{0}, {1}, {2}", order.ShipName, order.ShipCountry, order.Customer.ContactName);
    else
        Console.WriteLine("{0}, {1}", order.ShipName, order.ShipCountry);
}

a do bazy danych idzie jeden prosty select na tabeli Orders plus tyle pojedynczych selectów na tabeli Customers ilu jest Customerów z Brazylii.

Bez Lazy Loadingu, kod będzie nieco bardziej skomplikowany i będzie wyglądał tak:

context.ContextOptions.LazyLoadingEnabled = false;
List<OrderCustomer> orders = (from o in context.Orders
                      join c in context.Customers
                      on o.CustomerID equals c.CustomerID
                      where o.ShipCountry == "Brazil"
                      select new OrderCustomer
                      {
                          ShipName = o.ShipName,
                          ShipCountry = o.ShipCountry,
                          ContactName = o.Customer.ContactName
                      })
                     .Union(from o in context.Orders
                            where o.ShipCountry != "Brazil"
                            select new OrderCustomer
                            {
                                ShipName = o.ShipName,
                                ShipCountry = o.ShipCountry,
                                ContactName = null
                            })
                    .OrderBy(o => o.ShipName)
                    .ToList();
                     
foreach (OrderCustomer order in orders)
{
    if(order.ShipCountry == "Brazil")
        Console.WriteLine("{0}, {1}, {2}", order.ShipName, order.ShipCountry, order.ContactName);
    else
        Console.WriteLine("{0}, {1}", order.ShipName, order.ShipCountry);
}
context.ContextOptions.LazyLoadingEnabled = true;

Do bazy danych pójdzie tylko jeden select. Taki:

SELECT 
[Distinct1].[C1] AS [C1], 
[Distinct1].[C2] AS [C2], 
[Distinct1].[C3] AS [C3], 
[Distinct1].[C4] AS [C4]
FROM ( SELECT DISTINCT 
	[UnionAll1].[C1] AS [C1], 
	[UnionAll1].[ShipName] AS [C2], 
	[UnionAll1].[ShipCountry] AS [C3], 
	[UnionAll1].[ContactName] AS [C4]
	FROM  (SELECT 
		1 AS [C1], 
		[Extent1].[ShipName] AS [ShipName], 
		[Extent1].[ShipCountry] AS [ShipCountry], 
		[Extent2].[ContactName] AS [ContactName]
		FROM  [dbo].[Orders] AS [Extent1]
		LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
		WHERE ([Extent1].[CustomerID] IS NOT NULL) AND (N'Brazil' = [Extent1].[ShipCountry])
	UNION ALL
		SELECT 
		1 AS [C1], 
		[Extent3].[ShipName] AS [ShipName], 
		[Extent3].[ShipCountry] AS [ShipCountry], 
		CAST(NULL AS varchar(1)) AS [C2]
		FROM [dbo].[Orders] AS [Extent3]
		WHERE N'Brazil' <> [Extent3].[ShipCountry]) AS [UnionAll1]
)  AS [Distinct1]
ORDER BY [Distinct1].[C2] ASC

Wydajność kodu z Lazy Loadingiem jest uzależniona od użytkownika. Jeśli użytkownik doda Zamówienia od wielu Klientów z Brazylii, to będzie dużo pojedynczych zapytań do tabeli Customers i aplikacja będzie niewydajna. Jeśli użytkownik doda Zamówienia od niewielu Klientów z Brazylii, to będzie mało pojedynczych zapytań i aplikacja będzie w miarę szybko chodziła. Czyli w takim przypadku, należałoby w instrukcji obsługi programu napisać użytkownikowi żeby nie wstawiał Zamówień od wielu Klientów z Brazylii bo aplikacja będzie niewydajna. A jak użytkownik nie przeczyta instrukcji obsługi? Albo jeśli użytkownik będzie musiał wstawić Zamówienia od 50 tys. Customerów z Brazylii, to co wtedy? Przerabiać aplikację?
W przypadku drugiego kodu, do bazy idzie tylko jedno zapytanie i są zwracane z bazy danych tylko takie dane jakie są potrzebne. Nie ma zbędnego pobierania połowy bazy danych.
Sorry za trochę sarkastyczny ton, ale ja wciąż nie rozumiem jaki jest sens Lazy Loadingu w aplikacjach webowych?
I byłbym wdzięczny za sensowne wytłumaczenie mi tego.

1

Relacja:
aparatament 1---* zamówienie
zamówienie 1---* rezerwacja
zamówienie --- rezerwacja
apartament 1 ---* sezon
apartament 1 ---* pomieszczenie
apartament 1 ---* tagi
apartament 1 ---* wsyposażenie
i jeszcze wiele wiele innych, Teraz wykonaj zapytanie bez lazy loadingu tylko o 1 apartament. Baza zrwóci kilkadziesiąt tysięcy wierszy, które orm następnie zpłaszczy do jednego.

0
Igor1981 napisał(a):

Wydajność kodu z Lazy Loadingiem jest uzależniona od użytkownika.

Wydajność kodu od użytkownika? WTF? Wydajność kodu jest uzależniona głównie od programisty, trochę mniej od technologii i maszyny.

a do bazy danych idzie jeden prosty select na tabeli Orders plus tyle pojedynczych selectów na tabeli Customers ilu jest Customerów z Brazylii.

Jeśli programista używa ORMa na pałę, bez zrozumienia i generuje ciągle problemy n+1, to jest tylko i wyłącznie wina jego ignorancji.

W przypadku drugiego kodu, do bazy idzie tylko jedno zapytanie i są zwracane z bazy danych tylko takie dane jakie są potrzebne. Nie ma zbędnego pobierania połowy bazy danych.

Żeby nie pobrać niepotrzebnych powiązań musisz zdefiniować dodatkową klasę, skomplikować zapytanie i przede wszystkim zrobić zrobić projekcję. Gdyby nie ona, wyciągnąłbyś z bazy wszystko powiązane z klientami (czyli pewno 90% całej bazy). Porównujesz to z celowo skopaną enumeracją, w której projekcji nie ma. Gdzie tu sens?
A wystarczyłoby użyć metody Include.

0
somekind napisał(a):

Żeby nie pobrać niepotrzebnych powiązań musisz zdefiniować dodatkową klasę, skomplikować zapytanie i przede wszystkim zrobić zrobić projekcję. Gdyby nie ona, wyciągnąłbyś z bazy wszystko powiązane z klientami (czyli pewno 90% całej bazy). Porównujesz to z celowo skopaną enumeracją, w której projekcji nie ma. Gdzie tu sens?
A wystarczyłoby użyć metody Include.

Nie interesują mnie czy to jest projekcja czy enumeracja. Interesuje mnie ile zapytań będzie szło do bazy danych, jakie to będą zapytania i co będzie miało wpływ na ilość zapytań. Odniosłem się do przykładu micc. Jedyne co zmieniłem, to zamiast sprawdzania w if czy property dupa jest true, ja zrobiłem sprawdzanie czy SkipCountry jest Brazil. Nie znalazłem nigdzie w standardowej bazie Microsoftu kolumny typu bit dlatego użyłem varchara. Czy ma to jakieś znaczenie dla Ciebie?
Dla mnie nie ma znaczenia czy to jest enumeracja czy projekcja. Jeśli do bazy danych będzie wysyłanych mniej zapytań, to o to właśnie chodzi.
Może masz jakiś inny przykład, który uzmysłowi nam potęgę lazy loadingu?

0

W przypadku prawidłowo użytego ORMa liczba zapytań do bazy jest minimalna, w tym przypadku jedno. Ty na podstawie źle napisanego kodu próbujesz chyba udowodnić brak sensu stosowaniu lazy loadingu w ogólności.
Lazy loading pozwala na niepobieranie wszystkich danych z bazy oraz odroczenie wykonania zapytań do momentu, w którym dane są faktycznie potrzebne i tyle... Jego stosowanie nie kłóci się z tym, że w aplikacji nieraz zdarzają się miejsca, w których trzeba pobrać dane w sposób zachłanny, i każdy świadomy programista o tym wie. Ale można to osiągnąć dużo prościej, bez definiowania dodatkowych typów, tworzenia projekcji i komplikowania zapytań. A wiadomo chyba, że im bardziej się kod komplikuje, tym bardziej jest podatny na błędy, wymaga więcej testów, itd.
Natomiast całkowita rezygnacja z lazy loadingu powodowałaby konieczność nawet do najprostszych rzeczy pisania dziesiątek linii kodu, jak w Twoim przykładzie.
No i najważniejsze - jeśli mamy nie stosować lazy loadingu, to po co nam w ogóle ORM?

0

Nie pobieranie wszystkich danych z bazy można też osiągnąć poprzez odpowiednią modyfikację zapytania.
Czy mógłbyś podać taki przykład gdzie nie da się zmodyfikować zapytania w LINQ w taki sposób żeby nie były pobierane wszystkie dane i gdzie jedynym wyjściem jest zastosowanie lazy loadingu?
Po co w aplikacjach webowych odraczać wykonanie zapytań do momentu, w którym dane są faktycznie potrzebne?
Zgadzam się z tym, że użycie lazy loadingu upraszcza kod. Przykładem mogą być moje dwa rozwiązania z powyższego posta, w których zostały użyte dwa podejścia: lazy loading i zachłanne pobranie danych do dodatkowego typu. I to można by uznać za plus lazy loadingu, ale jest jeszcze kwestia wydajności. Czy przy użyciu lazy loadingu nie dojdzie do takiej sytuacji, że po wypełnieniu danych w bazie, aplikacja zacznie bardzo często odpytywać bazę danych?
Po co nam ORM? Na przykład po to żeby nie używać zapytań w stringu lecz używać zapytań LINQ. Błędy w zapytaniach stringowych zostaną ujawnione dopiero po odpaleniu aplikacji (i często podczas jej użycia przez użytkownika), a błędy w zapytaniach LINQ zostaną wykryte już na etapie kompilacji.

@somekind
Ja nie chcę udowadniać braku sensu stosowania lazy loadingu w ogólności i nie pisz o tym co wiedzą doświadczeni programiści bo to są chwyty erystyczne.
Ja chcę się dowiedzieć jaki jest sens stosowania lazy loadingu w aplikacjach webowych, a póki co Twoje posty nie są w stanie mi tego wyjaśnić mimo szczerych chęci z mojej strony aby to zrozumieć.
Podaj najlepiej jakiś sensowny przykład i wtedy zakończymy ten temat. Póki co zgadzam się jedynie w kwestii, że użycie lazy loadingu upraszcza kod w C#, ale mam wątpliwości co do wydajności takiego rozwiązania.

0
Igor1981 napisał(a):

Czy mógłbyś podać taki przykład gdzie nie da się zmodyfikować zapytania w LINQ w taki sposób żeby nie były pobierane wszystkie dane i gdzie jedynym wyjściem jest zastosowanie lazy loadingu?

Raczej nie. Podobnie nie jestem w stanie wskazać miasta do którego nie da się dojechać samochodem i jedynym wyjściem jest pójście pieszo.
Lazy loading jest ułatwianiem sobie życia. Ułatwienia mają to do siebie, że są opcjonalne i nikt nie zmusza do ich stosowania.

Po co w aplikacjach webowych odraczać wykonanie zapytań do momentu, w którym dane są faktycznie potrzebne?

A po co to robić w aplikacjach desktopowych czy konsolowych? Przyczyna jest taka sama - żeby najpierw zbudować zapytanie, a potem pobrać jak najmniej danych. Np. jedna metoda generuje rdzeń zapytania, druga filtruje rekordy z flagą IsDeleted == true, trzecia nakłada warunki związane z zapytaniem wpisanym przez użytkownika, a czwarta związane ze stronicowaniem w gridzie. I dopiero tak zbudowane zapytanie jest wykonywane i wysyłane do bazy.

Czy przy użyciu lazy loadingu nie dojdzie do takiej sytuacji, że po wypełnieniu danych w bazie, aplikacja zacznie bardzo często odpytywać bazę danych?

No chyba wręcz przeciwnie - stosując lazy loading, gdy chcemy wyświetlić listę imion i nazwisk klientów, to napiszemy po prostu coś takiego:

var customers = session.QueryOver<Customer>().List();

i pobierzemy dane wyłączenie klientów, bez powiązanych z nimi zamówień, faktur, adresów, itd. Bez tworzenia innych obiektów i komplikowania zapytań.
Częste odpytywanie wiąże się jedynie z "problemem select n+1", który jest znany i którego obejście jest banalne.

Po co nam ORM? Na przykład po to żeby nie używać zapytań w stringu lecz używać zapytań LINQ. Błędy w zapytaniach stringowych zostaną ujawnione dopiero po odpaleniu aplikacji (i często podczas jej użycia przez użytkownika), a błędy w zapytaniach LINQ zostaną wykryte już na etapie kompilacji.

Tak, ale pisanie zapytań o każdą kolumnę oddzielnie wraz z dziesiątkami where i unionów jest równie słabe jak ręczne pisanie SQL. A na dodatek mniej wydajne.

Dla mnie lazy loading to integralna i nierozłączna część pracy z ORMami. Dzięki temu piszę mniej kodu, a kombinuję tylko tam, gdzie spotykam wąskie gardło. Alternatywą jest wyłącznie kombinowanie w każdym przypadku, a więc strata czasu.

0
somekind napisał(a):

Jeśli nie użyjesz lazy loadingu, to pobierając jeden obiekt z bazy pobierzesz cały powiązany z nim graf obiektów, w ekstremalnym przypadku pobierzesz całą bazę danych.

Zawsze? Czy tylko w przypadkach gdzie wszystkie encje sa powiazane ze soba relacjami najczesciej dwustronnymi? Bo ja widzialem aplikacje gdzie bylo jak mowisz, pobranei jednego bralo wszystko ('dzieki' chorym relacjom), ale widzialem tez aplikacje gdzie relacje byly lepiej zrobione i nie bylo az tak zle. Po co sie zastanawiac czy ma byc lazy czy nie, jak mozna w ogole nie miec relacji, ktora nie jest nigdy uzywana? Nie wiem czy masz taka przypadlosc, ale sa ludzie (moj byly team lead) ktory jak mial relacje w jedna strone to zawsze dodawal od razu w druga, a tak zeby bylo 'spojnie', whatever...

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.