Blog Dla Programistów C#/.NET

środa, 3 września 2025

Czy zastanawiałeś się kiedyś, co się stanie, jeśli w trakcie zapisywania danych do bazy coś pójdzie nie tak? Właśnie do takich sytuacji służą transakcje. Transakcja to mechanizm, który gwarantuje, że zestaw operacji bazodanowych wykona się w całości albo wcale – nawet jeśli wystąpi błąd w trakcie ich wykonywania. Dzięki temu nasze dane pozostają spójne i unikamy sytuacji, w której połowa zmian trafiła do bazy, a połowa nie. W tym artykule wyjaśnię, jak Entity Framework Core (EF Core) domyślnie obsługuje transakcje oraz jak możemy zarządzać transakcjami samodzielnie, gdy potrzebujemy większej kontroli.

Transakcje w Entity Framework Core – Jak Działają i Jak Je Wykorzystać w Praktyce

Domyślne zachowanie transakcji w EF Core


Na początek dobra wiadomość: w większości przypadków nie musisz nic robić, aby korzystać z transakcji w EF Core. Gdy wywołujesz metodę SaveChanges() na swoim kontekście danych, wszystkie zmiany, które mają zostać zapisane, są domyślnie wykonywane wewnątrz transakcji. Oznacza to, że jeśli w ramach pojedynczego SaveChanges() zapisujesz wiele obiektów lub wykonujesz wiele operacji (INSERT/UPDATE/DELETE), EF Core zadba o to, by zostały one zatwierdzone atomowo. Jeśli którakolwiek z tych operacji się nie powiedzie, cała transakcja zostanie wycofana, a baza danych pozostanie w stanie, jak przed wywołaniem SaveChanges(). Innymi słowy, SaveChanges() gwarantuje, że albo wszystkie zmiany się udadzą, albo żadna z nich nie zostanie zastosowana.

Rozważmy prosty przykład. Załóżmy, że w ramach jednej operacji chcemy dodać nowy element zamówienia (np. pozycję w koszyku) oraz zmniejszyć stan magazynowy danego produktu. Możemy to zrobić w następujący sposób:

using var context = new ShopContext();

/* Dodajemy nowy element zamówienia (np. pozycję w koszyku) */
context.LineItems.Add(new LineItem { ProductId = productId, Quantity = quantity });

/* Zmniejszamy stan magazynowy dla danego produktu */
var stock = context.Stock.First(s => s.ProductId == productId);
stock.Quantity -= quantity;

/* Zapisujemy zmiany do bazy danych */
context.SaveChanges();

Gdy wywołamy SaveChanges(), obie powyższe zmiany zostaną zapisane w bazie jednocześnie, w ramach jednej transakcji. Dzięki temu możemy zagwarantować, że baza danych pozostanie spójna – jeśli zapis jednej z tych zmian by się nie powiódł, żadna z nich nie zostanie zatwierdzona. W praktyce EF Core automatycznie otwiera transakcję, wykonuje wszystkie operacje SQL (np. INSERT dla nowego LineItem i UPDATE dla zmiany stanu Stock), a na końcu ją zatwierdza. Jeśli któraś z operacji zgłosi błąd (np. naruszenie ograniczenia integracyjnego w bazie), EF Core wycofa całą transakcję, tak że żadne częściowe zmiany nie zostaną wprowadzone.

Podsumowując: jedno wywołanie SaveChanges() jest już "otoczone" transakcją przez EF Core. To bardzo wygodne, bo zazwyczaj nie musimy się martwić o spójność danych przy pojedynczym zapisie – ORM zrobi to za nas.


Ręczne zarządzanie transakcjami (gdy potrzebujesz więcej kontroli)


Skoro EF Core sam dba o transakcje przy SaveChanges(), kiedy w ogóle musimy się tym przejmować? Otóż są sytuacje, w których chcemy mieć większą kontrolę nad przebiegiem transakcji. Najczęstszy przypadek to konieczność wykonania kilku oddzielnych operacji zapisu w ramach jednej logiki biznesowej.

 Na przykład:

    • Musimy wywołać metodę SaveChanges() kilkukrotnie, bo po pierwszym zapisie potrzebujemy wygenerowanych danych (np. identyfikatora nowo dodanego rekordu) zanim wykonamy kolejne operacje.

    • Wykonujemy serię operacji na bazie, pomiędzy którymi następują inne czynności (np. wywołania zewnętrznego API) i chcemy, by cała sekwencja była atomowa.

    • Łączymy operacje EF Core z innymi operacjami na bazie (np. surowymi zapytaniami SQL) i chcemy je zsynchronizować w jednej transakcji.

W takich sytuacjach domyślne podejście (oddzielna transakcja dla każdego SaveChanges()) może skutkować niespójnością danych. Jeżeli pierwsze wywołanie SaveChanges() się powiedzie (i zostanie zatwierdzone), a drugie wywołanie zawiedzie, to efekty pierwszego pozostaną w bazie – bo były w osobnej transakcji, która już się zatwierdziła. Druga transakcja się wycofa, ale baza zostanie częściowo zmodyfikowana. To zazwyczaj niepożądana sytuacja. Idealnie chcielibyśmy, aby obie operacje zapisu były traktowane jako jedna całość – czyli żeby druga porażka spowodowała również anulowanie pierwszej operacji.

Rozwiązanie: możemy samodzielnie zarządzać transakcją, obejmując nią dowolną liczbę wywołań SaveChanges(). EF Core udostępnia do tego celu metodę BeginTransaction() na obiekcie Database naszego kontekstu. Pokaże Ci to na przykładzie kodu, gdzie celowo wykonujemy dwie operacje zapisu osobno, ale chcemy je zrzeszyć w jednej transakcji:

using var context = new ShopContext();
using var transaction = context.Database.BeginTransaction();

try
{
/* 1. Dodajemy nowy element zamówienia */
context.LineItems.Add(new LineItem { ProductId = productId, Quantity = quantity });
context.SaveChanges();

/* 2. Aktualizujemy stan magazynu dla produktu */
var stock = context.Stock.First(s => s.ProductId == productId);
stock.Quantity -= quantity;
context.SaveChanges();

/* 3. Jeśli dotarliśmy tutaj, wszystkie operacje się powiodły – zatwierdzamy transakcję */
transaction.Commit();
}
catch (Exception)
{
/* W razie błędu, wycofujemy całą transakcję */
transaction.Rollback();
}

Powyżej rozpoczynamy transakcję poleceniem context.Database.BeginTransaction(). Następnie, wewnątrz bloku try, wykonujemy dwie operacje zapisu do bazy, każdą zakończoną osobnym SaveChanges(). Gdy obie się powiodą, wywołujemy transaction.Commit(), aby zatwierdzić transakcję – spowoduje to trwałe zapisanie wszystkich zmian w bazie naraz. Jeśli zaś którakolwiek operacja zgłosi wyjątek, trafimy do bloku catch i wykonamy transaction.Rollback(), które wycofa całą transakcję, anulując wszystkie zmiany dokonane od czasu jej rozpoczęcia. Dzięki temu nawet jeśli druga operacja się nie uda, pierwsza zostanie cofnięta i żadna zmiana nie pozostanie w bazie – dokładnie tak, jak chcieliśmy.

Warto zauważyć, że dopóki nie wywołamy Commit(), transakcja pozostaje niezatwierdzona. Jeżeli nasz kod opuści blok try-catch bez wywołania Commit() (np. z powodu wyjątku), samo wyrzucenie obiektu transaction z użyciem using spowoduje automatyczne wycofanie transakcji. Mimo to, dobrą praktyką jest jawne wywołanie Rollback() w bloku catch, aby czytelnie zaznaczyć, że obsłużyliśmy scenariusz błędu.


Podsumowanie


Transakcje to kluczowy mechanizm zapewniający niepodzielność i spójność operacji na bazie danych. Na szczęście, Entity Framework Core domyślnie zarządza transakcjami za nas w trakcie zapisu – pojedyncze wywołanie SaveChanges() albo w całości się udaje, albo nie zmienia bazy wcale. Gdy jednak nasza logika wymaga wykonania wielu oddzielnych zapisów jako jednej operacji (atomowo), EF Core daje nam narzędzia do manualnego zarządzania transakcjami. Możemy otoczyć dowolną liczbę instrukcji zapisu jedną transakcją, by mieć pewność, że w przypadku błędu wszystkie zmiany zostaną wycofane.

Na koniec drobna sugestia: jeśli zainteresował Cię temat transakcji w EF Core i chcesz dogłębniej opanować pracę z bazami danych przy użyciu tego ORM, rozważ dołączenie do mojego kompletnego szkolenia online "Szkoła Entity Framework Core". W szkoleniu omawiam szczegółowo nie tylko transakcje, ale również wiele innych aspektów EF Core – od podstaw po bardziej zaawansowane zagadnienia – wszystko krok po kroku i z praktycznymi przykładami. Dzięki temu zdobędziesz solidne fundamenty i pewność w budowaniu aplikacji .NET z użyciem Entity Framework Core.

To wszystkie na dzisiaj. Jeżeli taki artykuł Ci się spodobał, to koniecznie dołącz do mojej społeczności – darmowe zapisy, gdzie będziesz również miał dostęp do dodatkowych materiałów i przede wszystkim bonusów. Do zobaczenia w kolejnym artykule.

Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Dodaj komentarz

Wyszukiwarka

© Copyright 2025 modestprogrammer.pl | Sztuczna Inteligencja | Regulamin | Polityka prywatności. Design by Kazimierz Szpin. Wszelkie prawa zastrzeżone.