hibernate.hbm2dll.auto

Czy automatyczne tworzenie bazy przez Hibernate jest dobre?

Hibernate to świetne narzędzie i sprawdza się doskonale w wielu projektach, zwłaszcza tych, które są w dużej mierze oparte o funkcjonalności CRUD. Hibernate ma też wiele ciekawych funkcji, które pomagają deweloperom w codziennej pracy. Jedną z takich funkcji jest automatyczne tworzenie i aktualizacja schematu bazy danych. Funkcja ta może wydawać się bardzo przydatna, ale czy w ogóle powinieneś jej używać?

Jak działa automatyczne aktualizowanie schematu bazy w Hibernate?

Żeby Hibernate miał możliwość stworzenia schematu automatycznie, najpierw musimy stworzyć encje i oznaczyć je odpowiednią adnotacją @Entity. Każda klasa oznaczona tą adnotacją zostanie odwzorowana w bazie w postaci tabeli.

Domyślnie aktualizowanie schematu w Hibernate jest wyłączone. Żeby włączyć tę funkcjonalność musimy użyć odpowiedniego properiesa i przypisać mu odpowiednią wartość.

Properties ten to:

hibernate.hbm2ddl.auto

a jeśli używasz Spring Boota:

spring.jpa.hibernate.ddl-auto

Wartości, jakie może przyjmować parametr:hibernate.hbm2ddl.auto.

  • none – żadna akcja nie zostanie podjęta
  • create-only – zostanie wygenerowane utworzenie bazy danych
  • drop – zostanie wygenerowane usuwanie bazy danych
  • create – zostanie wygenerowane usuwanie bazy danych, a następnie tworzenie bazy danych
  • create-drop – schemat zostanie usunięty i stworzony podczas uruchamiania SessionFactory. Dodatkowo zostanie on usunięty podczas kończenia działania SessionFactory.
  • validate – waliduje schemat bazy danych
  • update – aktualizuje schemat bazy danych

Możesz też znaleźć te wartości w dokumentacji.

UWAGA: Hibernate generuje skrypt, który usuwa poszczególne tabele, ale z różnych względów poszczególne zapytania mogą zakończyć się błędem i schemat może zostać zaktualizowany tylko częściowo.

 

Dlaczego nie powinieneś używać hibernate.ddl-auto?

Mimo że ta opcja wydaje się bardzo wygodna i pozornie może oszczędzić nam wiele czasu. To w rzeczywistości może być zupełnie odwrotnie, zwłaszcza w dużych projektach korporacyjnych.

Jeśli pozwalasz Hibernate’owi zarządzać schematem, to robi on to na swój specyficzny sposób, a Ty tracisz kontrolę nad tym co i jak jest tworzone w twojej bazie.

Niepoprawne Mapowanie

Jednym z podstawowych problemów, które możesz napotkać  w takiej sytuacji, jest to, że Hibernate odwzoruje 1:1 to co z mapowałeś za pomocą encji. Jeśli używasz relacji w encjach to złe mapowanie tych relacji sprawi, że Hibernate odwzoruje takie mapowanie w zły sposób w tabelach. Przykładem może być tu relacja jeden-do-wielu (OneToMany), gdzie normalnie powinna być ona zrobiona na dwóch tabelach. Hibernate natomiast potrafi ją z mapować zarówno na dwóch jak i na trzech tabelach, dodając dodatkową tabelę łączącą. Co ważne, nie dostaniemy przy tym żadnego błędu, ponieważ z punktu widzenia Hibernate’a oba mapowania są poprawne.

Dodatkowa tabela łącząca może być powodem problemów wydajnościowych nawet w małych aplikacjach. Jeśli ten sam błąd powielisz wielokrotnie w swojej bazie danych. Może to doprowadzić do sporych problemów.

 

Bałagan w bazie danych

Jedynym sensownym ustawieniem, którego mógłbyś użyć na produkcji, jest opcja update. Nie usuwa ona tabel dlatego teoretycznie „zabezpiecza” nas przed przypadkową utratą danych. Ale przyczynia się ona do powstania kolejnego problemu, czyli bałaganu w bazie danych. Ponieważ Hibernate nie sprząta po sobie.

Jeśli dodajemy jakieś nowe encje, dodajemy nowe pola do już istniejących encji, usuwamy encje, usuwamy pola już z istniejących encji lub zmieniamy ich nazwy, to tak naprawdę w bazie danych tylko przybywa table, a w poszczególnych tabelach pól. Nic nie jest sprzątane.

Wraz z rozwojem aplikacji robi się to prawdziwym problemem. Czasem trzeba popatrzeć, co jest w bazie danych i wtedy taki bałagan bardzo skutecznie utrudnia znajdowanie tego, co nas tak naprawdę interesuje. A dodatkowo wprowadza niepewność. Deweloper zaczyna się zastanawiać, po co są te wszystkie dodatkowe pola, czy są potrzebne, może jakiś dodatkowy skrypt z nich korzysta, czy można je zmienić, usunąć itd.

To wszystko sprawia, że zaczynasz czuć się niepewnie pracując z taką aplikacją. No, chyba że codziennie robisz dropa bazy na produkcji i masz gdzieś czy coś się popsuje, czy nie 😉

Podsumowując ten punkt, im większy porządek w bazie tym lepiej dla Ciebie.

 

Aktualizowanie indeksów

Kolejna rzecz, to jeśli tworzysz w encjach indeksy, klucze obce lub ograniczenia, to Hibernate nadaje im losowe nazwy np. „FK2VL1PWD258E41JNAM1CNC3VNA”.  I teraz powstaje problem, jak zmienić taki indeks jak zmienić taki klucz obcy? Czasem zachodzi potrzeba zmiany tych elementów i niestety jedyną opcją zmiany jest zmiana ręczna. I każdy deweloper z osobna musi wykonać sobie taką zmianę lokalnie jeśli pracuje z lokalną bazą deweloperską. Gdy w projekcie jest kilka środowisk testowych i każde ma swoją bazę, to taką zmianę trzeba też odwzorować we wszystkich środowiskach. Ale problem zaczyna się dopiero wtedy, gdy np. nie mamy dostępu do bazy produkcyjnej. Co wtedy?

Niestety dzieje się tak bardzo często. W większości projektów, w których pracowałem, nie miałem dostępu ani do produkcyjnego serwera aplikacji, ani do produkcyjnego serwera baz danych. A aktualizowanie schematu danych, można było zrobić tylko na dwa sposoby, automatycznym skryptem lub skryptem, który odpala administrator bazy ręcznie. Oczywiście nie da się w łatwy sposób napisać skryptu, który usunie indeks czy klucz obcy nie znając jego nazwy. Zostaje nam wtedy kontakt osobisty lub telefoniczny z adminem, co zwykle ma charakter „Mission Impossible” 😉

 

Odświeżanie indeksów unique

Hibernate przy aktualizowaniu schematu najpierw usuwa indeksy unique, później aktualizuje schemat i tworzy ponownie te indeksy. Jest to sposób, który pozwala uniknąć ewentualnych problemów związanych właśnie z tymi indeksami. Ale jeśli mamy dużo danych w tabeli, to ponowne stworzenie takiego indeksu może zajmować bardzo dużo czasu i przez to opóźnić znacznie czas restartu aplikacji, czyli czas jej niedostępności. Na szczęście jest to konfigurowalne zachowanie (szczegóły tutaj).

 

Uruchamianie wiele instancji aplikacji

Kolejny problem, to uruchamianie aplikacji w wielu instancjach, które podłączone są do tej samej bazy. Wtedy musimy dodatkowo konfigurować te aplikacje tak, żeby tylko jedna instancja aplikacji aktualizowała bazę danych. W innym wypadku aktualizowanie bazy przez kilka instancji aplikacji może skończyć się małymi problemami.

 

Jakiej opcji powinieneś używać?

Oczywiście są aplikacje, które nie są krytyczne i można próbować aktualizować bazę danych za pomocą Hibernate’a. Wszelkiego rodzaju dema, aplikacje które piszesz tylko dla nauczenia się czegoś. Także aplikacje, które nie są krytyczne dla prowadzenia biznesu w firmie, dla której pracujesz. Takie aplikacje gdzie utrata danych nie jest większym problemem.

We wszystkich tych aplikacjach możesz używać opcji update.

Natomiast dla wszystkich pozostałych aplikacji produkcyjnych jedyne opcje, jakie możesz użyć, to none lub validate. Nigdy nie powinieneś używać innych.

 

Jak prawidłowo tworzyć bazę danych?

Ręcznie tworzenie bazy danych nie jest wcale tak bardzo pracochłonne. Zwykle taka baza powstaje krok po kroku. Najpierw dodajemy jedną tabelę, tworzymy jakąś funkcjonalność, potem kolejną, tworzymy kolejna funkcjonalność, potem kolejne dwie itd. Takie podejście daje nam pełną kontrolę nad schematem. Odwzorowanie takiego schematu na encje też jest stosunkowo proste (pomaga nam też tu opcja validate, więc większość nieprawidłowości objawia się w postaci błędów). Jeśli stworzymy schemat poprawnie to też będziemy musieli odwzorować ten schemat na encje w poprawny sposób. Zyskujemy poprawny design naszej bazy danych i wszystko jest w dużo bardziej uporządkowanej formie.

Oczywiście musimy znać podstawowe relacje jakie występują w bazie danych i umieć je zaimplementować w prawidłowy sposób, ale z drugiej strony, to te same relacje jakie możemy spotkać w encjach Hibernate’a. Jest to ta sama wiedza tylko przeniesiona na inny poziom szczegółowości (poziom bazy danych).

 

Alternatywa dla zarządzania schematem

Jest co najmniej kilka rozwiązań/narzędzi, jakie możemy zastosować. Pierwsza i najprostsza opcja to ręcznie pisanie skryptów sql dla schematu i dla danych (jeśli ich potrzebujemy). W małych aplikacjach sprawdza się to całkiem dobrze. Niestety pozostaje nam ręczne wykonywanie tych skryptów lub stworzenie jakiegoś prostego narzędzia do tego, ale nie ma to trochę sensu bo takie narzędzia już istnieją.

Jednym z narzędzi, które może pomóc w zarządzaniu schematem bazy jest Liquibase. Dosyć proste narzędzie, które pozwala nam dodawać skrypty w postaci changesetów. Każdy changeset to osobna zmiana, która jest wykonywana tylko raz na danej bazie. Każdy changeset jest oznaczany w specjalnej tabeli jako wykonany co uniemożliwia jego ponowne wykonanie.

Liquibase pozwala na zapisywanie changesetów zarówno w formie sqla, jak i w formie plików xml. Jeśli korzystamy z plików xml, to daje nam to dodatkową elastyczność i możemy odpalić taki projekt na dowolnej bazie danych, bez zmiany jakiegokolwiek changeseta czy jakiegokolwiek zapytania. Liquibase można odpalać wraz ze startem aplikacji (jest zintegrowany też ze Spring Boot). Korzystam z tego narzędzia już od dawna i pracuje się z nim całkiem przyjemnie.

Kolejnym tego typu narzędziem jest Flyway. Jest to podobne narzędzie do Liquibase, ale wydaje mi się dużo bardziej rozbudowane. Nie znam niestety kompletnie tego narzędzia, więc odsyłam  do dokumentacji. Dodam tylko, że można go używać tak samo jak Liquibase’a wewnątrz aplikacji, ale również jest dostępny w formie narzędzia command line.

 

 

Podsumowanie

Mam nadzieję, że udało mi się dobrze uzasadnić dlaczego nie powinieneś tworzyć bazy danych automatycznie przy użyciu Hibernate’a. Przynajmniej w tych poważnych aplikacjach produkcyjnych, w których bazach można znaleźć setki, a czasem tysiące tabel. Narzędzia automatyzujące pracę są bardzo przydatne, ale nie wszystkie są dostosowane do prawdziwych aplikacji i sytuacji, które zdarzają się w życiu. Dlatego zawsze do zarządzania schematem bazy danych polecam używać Liquibase lub Flyway, które powstały z myślą o takich właśnie sytuacjach. Poza tym oba te narzędzia dają pełny wgląd w to co działa się ze schematem bazy danych w czasie. Zawsze możemy przejrzeć zmiany jakie zachodziły w bazie danych w przeszłości, a ewentualne naprawianie powstałych błędów jest wtedy dużo prostsze.

 

Źródła:

https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#configurations-hbmddl
https://discourse.hibernate.org/t/hbm2ddl-auto-update-recreating-existing-indexes-after-drop/3991
https://rules.sonarsource.com/java/tag/hibernate/RSPEC-3822
https://docs.liquibase.com/home.html
https://flywaydb.org/documentation/

 

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<

10 thoughts to “Czy automatyczne tworzenie bazy przez Hibernate jest dobre?”

  1. Hej, fajny artykuł. Może dałbyś radę zrobić filmik jak to wszystko skonfigurować na jakimś przykładzie?

    1. Jasne, jak wrócę do nagrywania to pomyślę o tym 😉

  2. „Niepoprawne Mapowanie” – „z punktu widzenia Hibernate’a oba mapowania są poprawne.”

    No to w końcu poprawne czy nie? 😉 To nie jest problem ani wina Hibernate i mechanizmu hbm2ddl, że dev nie dodał @JoinColumn. Skąd biedny Hibernate ma wiedzieć którą kolumnę użyć jako FK bez anotacji @JoinColumn? 😛

    „Hibernate nadaje im losowe nazwy np. „FK2VL1PWD258E41JNAM1CNC3VNA”. ”
    Rozwiązanie:
    https://docs.oracle.com/javaee/7/api/javax/persistence/Index.html#name–
    https://docs.oracle.com/javaee/7/api/javax/persistence/ForeignKey.html#name–

    Już gdzieś mieliśmy tą dyskusję. Jestem fanem jak najdokładniejszego opisania schematu za pomocą annotacji które dają JPA/Hibernate. Po to, żeby m.in. takich problemów nie mieć.

    Osobiście w projektach używamy update + skrypty migracyjne (jeśli konieczne) wywoływane ręcznie + automat przy każdym starcie aplikacji dropuje wszystkie widoki i zakłada na nowo z odrębnego skryptu. Niektóre projekty tak już chodzą 8 lat. Skrypty migracyjne wersjonowane razem z WARem z releasu w VCSie. Zero problemów 😛

    1. No to w końcu poprawne czy nie? 😉 To nie jest problem ani wina Hibernate i mechanizmu hbm2ddl, że dev nie dodał @JoinColumn. Skąd biedny Hibernate ma wiedzieć którą kolumnę użyć jako FK bez anotacji @JoinColumn?

      Trudno powiedzieć, nie wiem, czy ktoś zapomniał tej adnotacji, czy zrobił tak celowo 😉

      Tak wiem, że można nadawać nazwy indeksom i kluczom obcym, tylko czy ktoś o tym pamięta?

      Co do reszty, to dzięki za podzielenie się swoimi doświadczeniami 😉

  3. Hej! Bardzo lubię czytać Twoje wpisy, kiedy tylko się pojawiają. Piszesz zazwyczaj spójnie, na temat, bez offtopów – cenię to sobie, i myślę, że inni czytelnicy również. Pisz więcej, bo z całą pewnością Twoja praca nie pozostaje niedoceniana 😉 Pozdrowienia!

  4. Świetna wartość merytoryczna zarówno bloga jak i kanału YT.
    Oby jak najwięcej materiałów.
    Dziękuję 🙂

  5. Mam pytanie o Liquibase, jak to wyglada w praktyce – pisze się te skrypty w tej specyficznej składni (XML, json), czy może tworzy się tylko ten „główny” plik konfiguracyjny a create lub ladowanie danych normalnie w sqlu?

    1. To zależy od projektu. Właściwie wypróbowałem wszystkie możliwe opcje w różnych projektach. Tworzenie changesetów w XMLu stosowałem w projekcie, który był developowany w Postgresie, a na produkcji działał na Mssql. I generalnie działało to całkiem dobrze, nie było większych problemów. Taka była specyfika tego projektu (ale to raczej rzadkość).

      Słyszałem też, że inni stosują XMLa w projektach z jednym silnikiem bazy. Oczywiście wygodnie jest pisać wszystko w sqlu i tak w większości przypadków to robię (jak korzystam z Oracla czy Mssql, to raczej nie przewiduję migracji bazy).

      Oczywiście zachodzi ryzyko, że kiedyś będziesz chciał zmienić silnik bazy, wtedy będziesz musiał z migrować bazę (i to jest normalne), ale pytanie, czy też warto migrować wszystkie changesety? Chyba tak, jeśli chcemy, żeby developerzy mogli odpalić projekt lokalnie od zera (bez zabawy w przenoszenie bazy z np. jakiegoś serwera testowego). I generalnie taka migracja changesetów jest bardzo trudna, bo hashe changesetów zapisują się we wszystkich bazach i generalnie trzeb kombinować jak to wszystko ogarnąć. Ale zmiany silnika zdarzają się raczej rzadko, więc nie jest to aż tak straszny scenariusz.

      Potencjalnie jest wiele problemów związanych ze stosowaniem sql w changesetach…

Komentarze są zamknięte.