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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public EFContext() : base("connectionString") { this.Configuration.LazyLoadingEnabled = false; //Initializer settings Database.SetInitializer<EFContext>(new CreateDatabaseIfNotExists<EFContext>()); //OR //Database.SetInitializer<EFContext>(new DropCreateDatabaseIfModelChanges<EFContext>()); //OR //Database.SetInitializer<EFContext>(new DropCreateDatabaseAlways<EFContext>()); //OR //Database.SetInitializer<EFContext>(new CustomDatabaseInitializer<EFContext>()); } public DbSet<Ingredient> Ingredients { get; set; } public DbSet<Product> Products { get; set; } //etc.. |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CustomDBInitializer : CreateDatabaseIfNotExists<EFContext> { protected override void Seed(EFContext context) { context.Products.Add(new Product { Description = "opis", Name = "produkt", Price = 10 }); //Add more entities here base.Seed(context); //IMPORTANT } } |
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.