[PL] Backend Entity Framework SQL

EntityFramework.Extended czyli więcej możliwości ORM-a

Jakiś  czas temu w ramach serii CodeTip podzieliłem się z Wami trikiem, który pozwalał w Entity Framework na aktualizację property bez uprzedniego pobrania obiektu. W razie czego link do wpisu  macie tu. Jeden z czytelników zwrócił uwagę na możliwe użycie biblioteki, która ułatwia cały proces, a w dodatku oferuje kilka ciekawych opcji, niedostępnych w EF. Po zapoznaniu się z tym „wynalazkiem” jestem bardzo mile zaskoczony, w związku z czym chciałbym przedstawić jego możliwości.

 

Batch update i delete

Zacznijmy od batch update i delete, czyli dokładnie tego co próbowaliśmy osiągnąć z podlinkowanym wpisie. Dla przypomnienia, w standardowym scenariuszu chęć aktualizacji wartości jednego pola w ramach obiektu entity wiązała się z uprzednim pobraniem rekordu z bazy danych. Przykład takiej modyfikacji znajduje się poniżej:

 

public async Task LockUserAsync(string id)
{
    var context = new Context();
    var user = context.Users.SingleOrDefault(u => u.Id == id);
 
    user.IsLocked = true;
 
    context.Entry(user).State = EntityState.Modified;
 
    await context.SaveChangesAsync();
}

 

Widzimy tutaj mały problem optymalizacyjny. Za każdym razem kiedy metoda LockUserAsync zostanie wywołana, nasza aplikacja wykona dwa round tripy do bazy danych: pierwszy pobierający obiekt, drugi z instrukcją nakazującą aktualizację wartości IsLocked. Jak zwykle powtarzam, w skali 100 użytkowników problem nie wydaje się istnieć, ale systemy zawsze powinny być projektowane i implementowane pod maksymalnie dużą grupę użytkowników, która może kiedyś się pojawić. Co zatem możemy zrobić? Najlepiej byłoby całą operację zamknąć w jednej „wycieczce” do bazy danych. Z pomocą przychodzi nam tytułowe rozszerzenie do Entity Framework, które możemy pobrać z NuGeta poleceniem:

Install-Package EntityFramework.Extended

 

Zobaczmy zatem jak nasz problem moglibyśmy rozwiązać przy pomocy biblioteki:

 

public async Task LockUserAsync(string id)
{
    var context = new Contex();
    await context.Users.Where(u => u.Id == id).UpdateAsync(u => new UserEntity { IsLocked = true });
}

 

Nasz kod bardzo się uprościł i stał się one-linerem. Warto zwrócić także uwagę na to, że nie musieliśmy wywołać metody SaveChangesAsync na obiekcie kontekstowym. Ale czy całość wykonała się faktycznie w jednym round tripie? Po uruchomieniu SQL Server Profiler-a możemy podejrzeć co faktycznie trafiło do bazy danych:

 


exec sp_executesql N'UPDATE [dbo].[AspNetUsers] SET
[IsLocked] = @p__update__0
FROM [dbo].[AspNetUsers] AS j0 INNER JOIN (
SELECT
1 AS [C1],
[Extent1].[Id] AS [Id]
FROM [dbo].[AspNetUsers] AS [Extent1]
WHERE [Extent1].[Id] = @p__linq__0
) AS j1 ON (j0.[Id] = j1.[Id])',N'@p__linq__0 nvarchar(36),@p__update__0 bit',@p__linq__0=N'c9658557-7953-45d0-aedf-462e486cf33d',@p__update__0=1

 

Całość faktycznie działa tak jak tego oczekiwaliśmy. Analogicznie możemy usuwać nasze obiekty. Przykładowy listing wygląda następująco:

 


public async Task RemoveUserAsync(string id)
{
    var context = new Contex();
    await context.Users.Where(u => u.Id == id).DeleteAsync();
}

 

Tak wygląda natomiast wykonane na bazie zapytanie SQL:

 


exec sp_executesql N'DELETE [dbo].[AspNetUsers]
FROM [dbo].[AspNetUsers] AS j0 INNER JOIN (
SELECT 
    1 AS [C1], 
    [Extent1].[Id] AS [Id]
    FROM [dbo].[AspNetUsers] AS [Extent1]
    WHERE [Extent1].[Id] = @p__linq__0
) AS j1 ON (j0.[Id] = j1.[Id])',N'@p__linq__0 nvarchar(36)',@p__linq__0=N'5ade6144-7bf3-48ef-8057-850f37e9c0fd'

 

Future Queries

Ta funkcja chyba najbardziej mnie zaskoczyła (pozytywnie). Wyobraźmy sobie, że naszym zadaniem jest pobranie wszystkich książek oraz autorów dostępnych w bazie danych. W tym celu musimy napisać dwa zapytania, które pobiorą nam pożądane dane. Przykładowy kod wygląda tak:

 


public async Task TestAsync()
{
    var context = new Contex();

    var books = await context.Books.Where(b => b.IsActive).ToListAsync();
    var authors = await context.Authors.Where(a => a.IsActive).ToListAsync();
}

 

Tu jednak znów stajemy przed problemem optymalizacyjnym. Zarówno pobranie kolekcji książek jak i autorów będzie wymagało odwołania się do bazy danych. Lepszym rozwiązaniem byłoby użycie mechanizmu, który połączyłby te zapytania w jedną „paczkę” i pobrał wszystkie dane na raz. I własnie ów mechanizmem są future queries. Przebudujmy nasz kod do następującej postaci:

 


public async Task TestAsync()
{
    var context = new Contex();

    var qBooks = context.Books.Where(b => b.IsActive).Future();
    var qAuthors = context.Authors.Where(a => a.IsActive).Future();

    var books = await qBooks.ToListAsync();
    var authors = await qAuthors.ToListAsync();
}


 

Słowo komentarza. Widzimy, że obie kolekcje nie zostały bezpośrednio zmaterializowane do postaci list. Zamiast tego metodą Future zostały oznaczone jako future query. Mechanizm jest prosty, w momencie materializowania pierwszego future query wszystkie podlegają batchowaniu i zostają pobrane w jednym round tripie do bazy danych. Wobec tego drugie wywołanie metody ToListAsync na kolekcji qAuthors skutkowało jedynie przekształceniem pobranych wcześniej danych do postaci listy. Kod SQL wygenerowany podczas wykonywania przedstawionej metody wygląda następująco:

 


-- Query #1

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Title] AS [Title],
[Extent1].[Genre] AS [Genre],
[Extent1].[Iso] AS [Iso],
[Extent1].[Description] AS [Description],
[Extent1].[Quantity] AS [Quantity],
[Extent1].[AuthorId] AS [AuthorId],
[Extent1].[GraphicId] AS [GraphicId],
[Extent1].[CreatedDate] AS [CreatedDate],
[Extent1].[UpdatedDate] AS [UpdatedDate],
[Extent1].[IsActive] AS [IsActive]
FROM [dbo].[Books] AS [Extent1]
WHERE [Extent1].[IsActive] = 1;

-- Query #2

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Surname] AS [Surname],
[Extent1].[Description] AS [Description],
[Extent1].[Age] AS [Age],
[Extent1].[CreatedDate] AS [CreatedDate],
[Extent1].[UpdatedDate] AS [UpdatedDate],
[Extent1].[IsActive] AS [IsActive]
FROM [dbo].[Authors] AS [Extent1]
WHERE [Extent1].[IsActive] = 1;

 

Dodam tylko, że oprócz Future dostępne są również metody FutureFirstOrDefault, FutureValue oraz FutureCount (bardzo pomocne przy stronicowaniu danych).

 

Query Result Cache

Kolejną rzeczą oferowaną przez rozszerzenie jest cache-owanie rezultatów zapytań. Dzięki temu zabiegowi będziemy w stanie pobierać rekordy z pamięci zamiast odwoływać się każdorazowa do bazy danych. Co lepsze, posiadamy możliwość  zarządzania żywotnością stworzonych cache-ów poprzez ustawienie czasu ich wygaśnięcia. Dostępne opcje to:

  • WithAbsoluteExpiration – w zadanym czasie wygaśnięcia cache zostanie zwolniony bez względu na to czy wcześniej następowały do niego odwołania.
  • WithDurationExpiration – jest to tak na prawdę Absolute Expiration. Różnica polega na tym, że określamy czas jako TimeSpan, a nie DateTime.
  • WithSlidingExpiration – po zadanym czasie nieaktywności, cache zostaje zwolniony. Jeżeli w momencie oczekiwania nastąpi odwołanie, odliczanie odbywa się od początku.

 

Przykład użycia mechanizmu przedstawia poniższy listing:

 


//cache bez określonego czasu wygaśnięcia

var allBooks = await context.Books.FromCacheAsync();

//po 30 sekundach oczekiwania na odwołanie cache wygaśnie
var actibeAuthors = await context.Authors.Where(a => a.IsActive).FromCacheAsync(CachePolicy.WithSlidingExpiration(TimeSpan.FromSeconds(30)));

 

Oprócz zarządzania żywotnością możemy także grupować nasze cache przy użyciu tagów. Wówczas przy ręcznym ich zwalnianiu wystarczy, że podamy wybrany tag, aby określić które zasoby chcemy zwolnić:

 


var allBooks = await context.Books.FromCacheAsync(tags: new List<string> {"my-cache-group"});

var actibeAuthors = await context.Authors.Where(a => a.IsActive).
    FromCacheAsync(CachePolicy.WithSlidingExpiration(TimeSpan.FromSeconds(30)), tags: new List<string> { "my-cache-group" });

// zwolnione zostaną oba cache
CacheManager.Current.Expire("my-cache-group");

 

Oprócz opisanych opcji biblioteka daje także możliwość założeniu audytu, czyli dokumentowania wszelkich zmian stanu bazy danych. Celowo jednak to pominę, ponieważ planuję oddzielny post poświęcony temu zagadnieniu. Gdybyście jednak byli ciekawi jak przebiega konfiguracja takiego audytu to odsyłam Was na githuba projektu.  Nie wiem jak Wam, ale mi osobiście biblioteka się bardzo spodobała i pomimo tylko kilku opcji może ona realnie wpłynąć na wydajność naszych aplikacji. Jak zwykle zachęcam Was do śledzenia mnie na twitterze oraz facebooku, gdzie pojawią się kolejne wpisy oraz newsy ze świata .NET-a i nie tylko 😉 A my widzimy się już w niedzielę.

Na razie !

 

 

  • Pingback: dotnetomaniak.pl()

  • Dobra dawka informacja, dzięki wielkie!

  • NDIE

    zacny tekst. dzisiaj wieczorem z tego skorzystam 🙂

  • ŁB

    Widziałem już bibliotekę, wygląda ciekawie, zwłaszcza przy usuwaniu/aktualizowaniu dużego zbioru rekordów. Jak to jednak współdziała z „klasycznym” podejściem EF? Każde wywołanie metod EF.Extended zapisuje dane do bazy, a obiekty znajdujące się w kontekście zostawia bez zmian? Jeżeli tak, to jak to zmusić do współpracy w ramach jednej transakcji?

    • Jest dokładnie tak jak piszesz. Jeżeli pobierzesz jakiś obiekt i zmienisz wartość jego property, a następnie będziesz chciał zmodyfikować drugi obiekt przy pomocy EF.Extended, to zmiana będzie tyczyć się jedynie tego drugiego. Jeżeli chciałbyś zapisać zmiany pierwszego obiektu pobranego „klasycznie” wtedy musisz wywołać SaveChanges(). Jak puścić to w jednej transakcji? Najprościej chyba oba przepisać na Batch Update. Ewentualnie obie modyfikacje umieścić w ramach TransactionScope w usingu.

  • Nice. Jeszcze nie przerabiam całego repo, ale może się okazać przydatne.

  • Widać, że użytkownicy EF biorą sprawy w swoje ręce i w czynie społecznym dopisują rzeczy, które prawdziwe ORMy mają od lat.

    Jednego tylko nie rozumiem – po co przy tym UpdateAsync/DeleteAsync w wygenerowanym kodzie SQL są joiny?

  • Lukas Szumylo

    Bardzo mi się podoba rozwiązanie z UpdateAsync/DeleteAsync mam tylko jedno pytanie: co z detekcją konkurencji (Concurrency Conflicts) ?
    Z tego co pokazałeś, wynika, że takowej nie ma i używając tych metod nadpisujemy dane (zakładam, że rowversion nie jest podnoszony).

    Druga sprawa dotyczy cache’owania. Jak to się ma w kontekście cyklu życia AppDomain w aplikacjach typu klient-server gdzie DAL mamy po stronie serwera ?

  • Pingback: nhl picks()

  • Pingback: 192.168.l0.l()

  • Pingback: mold removal()

  • Pingback: Christ Gospel Church Cult()

  • Pingback: pendaftaran cpns 2018 khusus sma()

  • Pingback: must watch()

  • Pingback: bandarbola()

  • Pingback: Stix Corporate Event Managers()

  • Pingback: Bdsm chat()

  • Pingback: lowongan pekerjaan bank()

  • Pingback: serviços informática()

  • Pingback: informatica()

  • Pingback: iraqi finart()

  • Pingback: aws alkhazraji()

  • Pingback: judi kartu online()

  • Pingback: https://goo.gl/RfxWPu()

  • Pingback: emergency 24 hour locksmith richmond va()

  • Pingback: amazon product ads()

  • Pingback: http://www.guaranteedppc.com()

  • Pingback: ppc consultant jobs()

  • Pingback: ORM Solutions()

  • Pingback: Klotho gene()