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 !

 

 

You may also like...