Entity Framework – aktualizacja danych bez ich pobierania

Wprowadzenie

Dzisiejszy wpis jest zainspirowanym jednym z ostatnich code review jaki robiłem. Zauważyłem, że gdy pracujemy z Entity Framework często niektóre rzeczy zaczynamy robić nieefektywnie w stosunku do tego, jakbyśmy zrobili to w chociażby w przypadku ADO.NET. Taką właśnie czynnością jest aktualizowanie oraz usuwanie obiektów z bazy.

Aktualizacja obiektu z jego pobraniem

Standardowo, gdy zaczynamy uczyć się pracować z Entity Framework, do aktualizacji obiektu możemy użyć mniej więcej takiego kodu:

Po utworzeniu instancji obiektu kontekstowego, za pomocą metody FirstOrDefault wyciągany interesujący nas obiekt z bazy na podstawie klucza głównego (Id). Następnie aktualizujemy jakego właściwości oraz wywołujemy metodę SaveChanges, które wykona stosowny update na bazie. Wynik wykonania powyższego fragmentu kody wygląda tak (jest to log wygenerowany przez Entity Framework – pierwsza linijka w using):

Jak widać w pierwszej kolejności na bazie wykonywany jest select i później update. W niektórych sytuacjach takie podejście może rzutować na działanie aplikacji. Przykładowo, gdy tworzymy funkcjonalność blokowania edycji danych (tylko jeden użytkownik może na raz je edytować), moglibyśmy co 30 sekund (bo jakoś inną wartość) z widoku wysyłać informację, że obiekt jest edytowany i w bazie zapisywać aktualny czas. Następnie podczas wyświetlanie formularza analizowalibyśmy ten czas i odpowiednio blokowali lub nie widok. W takiej sytuacji potrzebujemy zaktualizować jedno pole w bazie na podstawie Id obiektu, gdzie inne pola nas nie interesują. Również nie interesuje nas poprzednia wartość tego pola, ważne, aby pojawiła się tam nowa wartość.

W takiej sytuacji pobieranie za każdym razem całego obiektu przy sporej ilości użytkowników, może dość mocno obciąży bazę danych przez wykonywanie niepotrzebnych operacji. W klasycznym ADO.NET wykonalibyśmy prosty update bez żadnych selectów.

Aktualizacja obiektu bez jego pobierania

Na szczęście Entity Framework daje nam możliwość zaktualizowania danych bez ich wcześniejszego pobierania. Co istotne, aby ten mechanizm zadziałał musimy bazować na kluczu głównym naszej encji (w tym przypadku Id). Poniżej zmieniony kod, który zaktualizuje rekord w bazie bez jego pobierania.

W pierwszej kolejności tworzymy nową instancje obiektu z ustawieniem klucza głównego (w przykładzie to Id). Następnie z wykorzystaniem metody Attach podpinamy utworzony obiekt do obiektu kontekstowego, który będzie teraz śledził zmiany. Dalsza część kodu jest taka sama jak wcześniej, czyli zmiana właściwości oraz wywołanie metody SaveChanges. W logu wyraźnie widać, że jest tylko update, bez select.

Powyższy fragment kodu nie jest do końca idealny i ma jeden problem. W wykonanym sql widać, że nastąpiła tylko aktualizacja kolumny LockTime, mimo, że w kodzie aktualizowane są dwie właściwości. Dzieje się tak, ponieważ Entity Framework wykrywa zmiany tylko w momencie, gdy wartość jest różna od wartości domyślnej dla danego typu. W przykładzie Count jest wartością typu int, dlatego próba wyzerowania wartości w bazie się nie powiodła, warto o tym pamiętać. Na szczęście i na to jest rozwiązanie, trzeba dodać jedną linijką dla właściwości:

Ręcznie informujemy Entity Framework, że właściwość została zaktualizowana, więc teraz w update będą zaktualizowane dwie kolumny:

Usuwanie obiektu bez jego pobierania

Podobna sytuacja jest w przypadku usuwania obiektu – możemy ją wykonać bez pobierania encji. Rozwiązanie tego problemu jest bardzo podobne – tutaj też bazujemy na kluczu naszej encji:

Sam log wykonania powyższego kodu wygląda tak:

W tym przypadku tym bardziej widać jak niepotrzebna jest pobieranie obiektu, w momencie, gdy chcemy go usunąć. Zachęcam do usuwania obiektu w ten sposób, bez wcześniejszego jego pobierania.

W obu przypadkach, gdy obiektu nie ma w bazie, to podczas wykonywania operacji nie dostaniemy błędy. Po prostu zapytanie wykona się ale żadne dane nie zostaną zmienione lub usunięte z bazy.

Przykład

Na github (https://github.com/danielplawgo/UpdateInEntityFramework) znajduje się przykład pokazujący działanie tego mechanizmu. Aplikacja korzysta z mechanizmu migracji Entity Framework, w tym metody Seed, która dodaje testowy obiekt do bazy. Dlatego przed uruchomieniem aplikacji w app.config należy ustawić connection stringa do testowej bazy.

Aplikacja na końcu usuwa testowy obiekt, dlatego przed jej kolejnym uruchomieniem należy wykonać aktualizację bazy (update-database w konsoli nugeta), który wywoła metodę Seed przywracającą dane testowe.

Gorąco zachęcam do pobrania projektu oraz testów tego rozwiązania.

Podsumowanie

Jednym z najczęściej popełnianych błędów podczas pracy z Entity Framework jest zbyt częste pobieranie danych do aplikacji. W szczególności, gdy chcemy zmienić tylko niektóre właściwości obiektu lub go usunąć. Takie podejście może wpłynąć negatywnie na działanie aplikacji szczególnie, gdy jest bardzo często wykonywane (patrz przykład z aktualizacją co 30 sekund informacji o tym, że obiekt jest aktualizowany).

Dlatego warto zwiększyć wydajność aplikacji poprzez zminimalizowanie ilości pobieranych obiektów do aplikacji podczas korzystania z Entity Framework. Oczywiście są również inne sposoby na wykonanie tych operacji efektywnie – na przykład skorzystanie z klasycznego ADO.NET. Ale to temat na inny wpis 🙂

A w jaki sposób Ty aktualizujesz lub usuwasz dane w bazie, gdy korzystasz z Entity Framework w aplikacji?

9 thoughts on “Entity Framework – aktualizacja danych bez ich pobierania

  • Pingback: dotnetomaniak.pl
    • Dzięki za komentarz, nie znałem tej biblioteki, w wolnej chwili trzeba rzucić okiem.

      A co do wpisu to chciałem pokazać, że czasami prosta zmiana w kodzie, potrafi wpłynać na wydajność. A zauważyłem, że właśnie zbyt częste pobieranie obiektów (tak jak w wpisie) oraz nieużywanie metody Include są chyba najczęstszym problemem w pracy z Entity Framework.

  • Robię bardzo podobnie, z jednym małym wyjątkiem – zamiast korzystać z właściwości „IsModified” ustawiam przy zerowanych wartościach (i przy false dla typu bool) inne wartości.
    Przy takim podejściu, jednak zetknąłem się z problemem dotyczącym pola typu string.
    Załóżmy, że w opisywanym przykładzie tabela Product posiada jeszcze jedno pole typu string – „Name”. Załóżmy też, że wszystkie kolumny w tabeli są „Not null”. W tej sytuacji chcąc zmienić LockTime nie wystarczy utworzyć obiektu klasy Product ustawiając jedynie ID. Z mojego doświadczenia wynika, że db.SaveChanges() rzuci wyjątkiem mówiącym, że nie może zaakceptować pola „Name” z nullem (a taka będzie jego wartość przy utworzeniu pustego obiektu), bo w bazie jest to „not null”. Stanie się tak nawet w przypadku, gdy pola Name wcale nie chcemy aktualizować. Z tego, co zaobserwowałem, wyjątek jest wyrzucany nie ze względu na wykonane finalnie zapytanie SQL, ale dlatego, że przed wykonaniem polecenia EF dokonuje walidacji zgodności danych ze strukturą danej tabeli w bazie.
    Aby rozwiązać ten problem, wszystkie pola tekstowe które w bazie nie mogą być „null” należy ustawić co najmniej na string.Empty (albo cokolwiek innego). Osobiście ten problem rozwiązuję tak, że mam klasę pomocniczą, która generuje mi „puste” (ale zgodne z bazą) obiekty.

    • Ciekawe podejście, tylko zastanawiam się, czy na dłuższą metę nie jest to problematyczne? Ja mam do tego snippeta, więc dodanie tej linijki z IsModified nie jest dużym problemem.

      • Biorąc pod uwagę problemy ze stringami które muszą byc not null, nie jest dużym kłopotem ustawienie innych wartości domyślnych dla parametrów. Zazwyczaj mam klasę z sufixem „Factory” dla każdej schemy w bazie danych i ta klasa generuje mi „puste” obiekty, ustawiając przy okazji wartości dla pól, które nie mogą być inicjowane domyślnie. Prawie zawsze metoda tworząca taki obiekt oprócz ID przyjmuje jeszcze opcjonalną wartość dla pola IsDeleted – jako że nie uznąję polecenia DELETE na bazie danych, kasowanie informacji załatwiam przez UPDATE zmieniający status wiersza na skasowany.

    • Jeszcze nie miałem okazji pracować z EF w .NET Core (akturat w aktualnym projekcie używamy Dappera), ale z ciekawości w wolnej chwili sprawdę.

  • Biorąc pod uwagę problemy ze stringami które muszą byc not null, nie jest dużym kłopotem ustawienie innych wartości domyślnych dla parametrów. Zazwyczaj mam klasę z sufixem „Factory” dla każdej schemy w bazie danych i ta klasa generuje mi „puste” obiekty, ustawiając przy okazji wartości dla pól, które nie mogą być inicjowane domyślnie. Prawie zawsze metoda tworząca taki obiekt oprócz ID przyjmuje jeszcze opcjonalną wartość dla pola IsDeleted – jako że nie uznąję polecenia DELETE na bazie danych, kasowanie informacji załatwiam przez UPDATE zmieniający status wiersza na skasowany.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *