Inicjowanie i wypełnianie bazy danych w Entity Framework Code First

By | May 4, 2016

Podczas pracy nad projektem lubię dysponować zestawem poglądowych danych, na których mogę operować podczas postępujących prac, spojrzeć jak prezentują się po odpaleniu aplikacji itd. Dziś opowiem o szybkim, łatwym i w pełni zgodnym z ideą Code First sposobie na wypełnienie bazy różnego rodzaju danymi podczas uruchamiania aplikacji i strategiami inicjalizacji bazy podczas każdego startu. Kiedy powinna być tworzona na nowo, kiedy powinna być pozostawiona w spokoju i co jeśli chcemy przy każdym debugowaniu mieć do czynienia ze świeżą porcją danych?

O ile testowanie rozwiązań sprawdza czy wszystko spina się i działa jak należy, to niestety nie wszystko mamy przed oczami i osobiście nie znam automatycznego testu, który sprawdzi nam czytelność na przykład jakiegoś wykresu generowanego w oparciu o rekordy w bazie danych. Dlatego też lubię mieć tabele wypełnione różnymi bzdurami, na które mogę spojrzeć, przeklikać kilka formularzy i sprawdzić, czy to co widzę ma w ogóle jakiś sens (restauracja oferująca tylko bekon naprawdę ma rację bytu!). 

O ile różnego rodzaju dane, które chcemy umieścić w tabelach możemy z powodzeniem wymyślić, to wprowadzenie ich do bazy ręcznie jest strasznym marnotrawstwem czasu. Dlatego też korzystam (a raczej będę korzystał, bo w tym projekcie jeszcze sobie nie skompletowałem takiego zestawu) z własnego initializera. Zacznijmy od odpowiedzi na bardzo istotne pytanie pytanie.

Czym jest initializer w Entity Framework?

To klasa, która odpowiada za czynności, które zachodzą podczas każdorazowego uruchomienia aplikacji, korzystającej z Entity Framework. Zauważcie, że gdy stworzycie model danych i uzupełnicie swój DbContext, to odpowiednia baza jest tworzona pod wskazanych adresem, a następnie automatycznie tworzone są tabele, kolumny, relacje itd. Ale dzieje się tak tylko i wyłącznie przy pierwszym uruchomieniu, później jesteśmy zdani na siebie i migracje lub na użycie innego niż domyślny initializera. A gotowych do użycia initializerów mamy trzy rodzaje, każdy z nich zachowuje się odrobinę inaczej:

  • CreateDatabaseIfNotExists – Domyślny initializer, który tworzy bazę na podstawie modelu w momencie gdy baza nie istnieje.
  • DropCreateDatabaseIfModelChanges – Usuwa bazę i tworzy nową, gdy model uległ zmianie.
  • DropCreateDatabaseAlways – Usuwa bazę i tworzy ją od nowa przy każdym uruchomieniu.

Czy któryś z nich robi “stoliczku nakryj się” i wypełnia rekordy zmyślonymi danymi? Jeszcze nie. Ale do tego dojdziemy za chwilę. Na początku zacznijmy od tego, jak ustawić pożądany przez nas initializer dla klasy naszego DbContextu. Robimy to w jego konstruktorze:

Tworzenie własnego initializera i wypełnienie bazy

Własny, customowy initializer będziemy tworzyć najczęściej przez dziedziczenie jednej z trzech podanych wyżej klas i override metody Seed(), w celu wypełnienia bazy ustalonymi z góry danymi. W zależności od tego, na której z nich zdecydujemy się oprzeć, taka strategia inicjalizowania bazy zostanie przyjęta przez nasz Context. Ma to spore znaczenie jeśli chodzi o zastosowanie danej klasy Contextu, o czym napiszę odrobinę później, bo dziedziczenie na przykład od DropCreateDatabaseAlways sprawi, że przy każdym uruchomieniu będziemy mieli do czynienia ze świeżą bazą, wypełnioną z góry ustaloną treścią, która nie będzie dotknięta przez wszelkiego rodzaju modyfikacje, które mogliśmy wprowadzić podczas testów czy przeklikiwaniu się przez formularze.

Przykładowa klasa Customowego initializera może wyglądać w ten sposób.

Ważnym elementem jest linia base.Seed(context), która dba o poprawne zbudowanie całej schemy. A jak możemy wypełnić bazę? Czysta dowolność! Możemy zrobić pętlę i potworzyć encje o nazwach test1, test2 itd., możemy spróbować umieścić w bazie dane losowe, wszystko zależy tylko i wyłącznie od naszych potrzeb. Zastanawiałem się nawet nad tym, czy możliwe byłoby skopiowanie bazy z innego DbContextu, z zupełnie innej bazy. Podejrzewam, że jeśli zaistniałaby taka potrzeba, to nie byłoby to zbyt trudne w zorganizowaniu.

Zastosowania

Oprócz oczywistego zapełnienia bazy danymi poglądowymi przychodzi mi do głowy jeszcze jedno zastosowanie, o którym moim zdaniem warto wspomnieć i któremu mam zamiar się przyjrzeć w najbliższym czasie.

W związku z tym, że pomijam repozytoria i unit of work, mam do czynienia z odrobinę utrudnionym testowaniem, niż byłoby to możliwe przy korzystaniu ze wspomnianych wzorców. Moim zdaniem, zastosowanie oddzielnego Contextu oraz  własnego initializera dziedziczącego po DropCreateDatabaseAlways i wypełniającego lokalną, testową bazę predefiniowaną przez nas treścią, może być ciekawą opcją do wykonywania testów jednostkowych bezpośrednio na bazie i jednocześnie nie dotykając rzeczywiście istniejących danych.

Podbij ↑