Hibernate - najczęstsze błędy

Hibernate – najczęściej popełniane błędy

Hibernate to codzienność dla większości programistów Javy. Często jednak programiści wykorzystują go nie do końca w poprawny sposób, czego konsekwencje bywają bardzo bolesne. W poniższym artykule zebrałem kilka bardzo popularnych błędów, które często pojawiają się w projektach korzystających z tego narzędzia.

Zarządzanie schematem bazy danych przy pomocy Hibernate

Hibernate daje nam możliwość zarządzania schematem bazy danych. Wystarczy ustawić odpowiednio properties hibernate.ddl-auto i Hibernate utworzy cały schemat za nas, może go także aktualizować. Co wydaje się być bardzo wygodnie i bardzo sensowne. Skoro narzędzie może dodawać za mnie tabele i kolumny do istniejących tabel, to czemu z tego nie skorzystać? Cały problem polega na tym, jak Hibernate to robi. Po pierwsze: jest to trochę mało estetyczne podejście, ponieważ niektóre nazwy np. klucze obce, czy indeksy są mało czytelne. Po drugie: jeśli popełnimy gdzieś błąd to Hibernate stworzy nam strukturę, którą on uważa za słuszną – nie koniecznie będzie to optymalne rozwiązanie. W tym wypadku praktycznie skazujemy się na zarządzenie bazą przez Hibernate’a, bo jakiekolwiek inne działania na bazie będą trudne.

Oczywiście można używać opcji: create, create-drop, czy update w środowiskach testowych, ale produkcyjnie najlepiej ustawić opcję validate lub none. A schematem bazy zarządzać specjalnie do tego stworzonymi narzędziami.

 

Problem n + 1

Kolejny bardzo często spotykany błąd to n + 1 zapytań, o którym już pisałem bardzo szczegółowo jakiś czas temu w artykule Hibernate i problem N + 1 zapytań. Problem ten często powstaje przez nieuwagę, a jego konsekwencje to zwykle bardzo poważny spadek wydajności. Zazwyczaj występuje przy relacjach jeden do wielu, gdy pobieramy leniwie listę encji powiązanych do encji nadrzędnej. np. gdy pobieramy listę adresów dla Usera, albo listę faktur dla klienta. Jeśli nie stworzyliśmy odpowiedniego zapytania z join fetch, to każda powiązana encja zostanie pobrana dodatkowym zapytaniem.

@Entity
class User {
  @OneToMany
  private List<Address> addresses;
  // gettery i settery
}

List<User> users = userRepository.findAll();
for(User user: users) {
  for(Address address: user.getAddresses()){ // w tym miejscu będzie dodatkowe zapytanie
    System.out.println(address);
  }
}

 

Brak adnotacji @JoinColumn

Ten błąd często występuje w połączeniu z pierwszym błędem, czyli zarządzaniem bazą poprzez Hibernate’a. Gdy ustawimy hibernate.ddl-auto=update Hibernate stworzy nam odpowiednie kolumny i tabele, jeśli są potrzebne. Gdy tworzymy relacje @OneToMany i @ManyToMany i nie określimy odpowiednio kolumny, po której następuje połączenie tabel, Hibernate wygeneruje nam dodatkową tabelę łączącą w przypadku @OneToMany i dodatkowe dwie tabele łączące w przypadku @ManyToMany. Jest to bardzo nieoptymalne. Dodatkowo komplikuje nam schemat bazy danych i może prowadzić do problemów wydajnościowych.

@Entity
class User {
  @OneToMany
  @JoinColumn(name="user_id")
  private List<Address> addresses;
  // gettery i settery
}

 

Użycie metody save()

Hibernate ma w budowany mechanizm dirty checking, który sprawdza czy encja, która wcześniej została pobrana przez Hibernate’a, zmieniła swój stan. Jeśli tak, jest zapisywana do bazy danych. Jeśli nie, to nic się nie dzieje i użycie metody save() nie daje żadnego efektu.

Natomiast wielu programistów używa tej metody, żeby „zasygnalizować” sobie i innym developerom, co się w danej metodzie dzieje. Można to wytłumaczyć w taki sposób: pobieram coś, zmieniam, zapisuję. I wszyscy wiedzą o co chodzi 😉 Błąd nie jest szkodliwy, po prostu mamy dodatkową linijkę kodu. Może to nie jest już błąd, może to feature?

public void updateEmail(Long userId, String email) {
  User user = userRepository.findById(userId);
  user.setEmail(email);
  userRepository.save(user); // to wywołanie jest niepotrzebne
}

 

Niepotrzebna adnotacja @Column

Często programiści oznaczają wszystkie pola w encji adnotacją @Column bez żadnych parametrów. Generuje to dodatkową linijkę kodu dla każdego pola encji i jest po prostu niepotrzebne. Pół biedy, jeśli dokleja to jakieś narzędzie do generowania encji, gorzej jak programista traci czas i dokleja tę adnotację ręcznie.

@Column
private String name;

Modyfikacją tego błędu jest oznaczanie pola tą adnotacją z parametrem oznaczającym nazwę pola, w taki sposób:

@Column(name = "customer_id")
private String customerId;

Hibernate ma wbudowany mechanizm, który konwertuje nazwy w konwencji camelCase na snake_case. W obu powyższych przypadkach adnotacja @Column jest niepotrzebna. Jeśli z jakiś powodów w twoim projekcie musisz podawać adnotację z nazwą kolumny @Column(name = "customer_id"), powinieneś dążyć do wyeliminowania tych powodów i korzystać z automatycznej konwersji, którą zapewnia Ci Hibernate.

 

Używanie Hibernate’a wszędzie

Sam kilka razy wpadłem w tę pułapkę i używałem Hibernata w wielu miejscach, tam gdzie łatwiej by było skorzystać z jakiegoś prostego rozwiązania np. JdbcTemplet i sqla. Często proste rozwiązania są najlepsze do skomplikowanych przypadków.

Hibernate sprawdza się świetnie w prostych przypadkach np. w aplikacja CRUD’owych.  Oraz tam, gdzie mamy częste odczyty i zapisy do pojedynczych tabel. A także tam, gdzie relacje nie są zbyt skomplikowane albo relacje nie tworzą skomplikowanej hierarchii obiektów.

 

Podsumowanie

To oczywiście tylko wybrane błędy, z którymi można się spotkać korzystając z Hibernate’a. Zachęcam do podzielenia się swoimi spostrzeżeniami na  temat błędów, które spotykasz na co dzień. Zachęcam także do przeczytania poprzednich artykułów o Hibernate: Trzy rzeczy, które powinieneś wiedzieć o Hibernate (tu znajdziesz podlinkowane pozostałe artykuły).

 

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<<

12 thoughts to “Hibernate – najczęściej popełniane błędy”

  1. Napisałeś:
    List users = userRepository.findAll();

    Czy nie powinno być?:
    Set users = userRepository.findAll();

    I tutaj pojawia się kolejny, często popełniany błąd: wykorzystywanie List do przechowywania zamiast Set, kiedy w większości przypadków to właśnie Set jest wystarczający.

    1. findAll() to metoda z JpaRepository i zwraca List, więc w tym miejscu jest poprawnie. Ale w klasie User private List<Address> addresses; tu mógłbym użyć Seta.

  2. Użycie metody save() – bez @Transactional nad taką metodą/klasą się nie zapisze 😉 Wtedy save jest potrzebny.

    Niepotrzebna adnotacja @Column – zmieniasz wersję hibernate’a, zmieniasz PhysicalNamingStrategy i jesteś w ciemnej czarnej D. Albo masz pole, które Ci wygeneruje nazwę kolumny jak keyword zastrzeżony w danym serwerze BD. Nie musi to być dzisiaj. Klient sobie strzela upgrade SQL Servera z 2005 na 2019 – bum. Analogicznie można by zaproponować rezygnację z annotacji @Table(name=… .
    Osobiście jestem fanem jak najsztywniejszego określenia struktury bazy w encji, łącznie z nazwami FK, indexami, itd. Daje mi to większe poczucie bezpieczeństwa.

    1. zmieniasz wersję hibernate’a, zmieniasz PhysicalNamingStrategy

      Wersji Hibernate’a nie zmienia się aż tak często. Pytanie po co zmieniasz PhysicalNamingStrategy? Jak nie zmienisz nie będziesz miał z tym problemu.

      1. Racja, wersji hibernate’a nie zmienia się często, ale jak zmieniasz to wewnętrzne zachowanie hibernate może się zmienić. Takim wewnętrznym zachowaniem są np. Naming Strategies. Duża zmiana nastąpiła np. przy przejściu z 4.3 na 5.0: https://github.com/hibernate/hibernate-orm/blob/5.0/migration-guide.adoc#naming-strategies. Bierzesz nową wersję Hibernate’a i nagle Ci się schema nie waliduje. Nie chcę przy upgrade Hibernate’a musieć zmieniać strukturę bazy.
        Zresztą nie podając @Column nie mam możliwości ustawienia czy kolumna jest not null. Nie mogę ustawić długości np. pola VARCHAR – po co mam PESEL zapisywać w kolumnie o długości 255? Nie mogę ustawić sobie prostego unique indexu na wyżej rzeczonym PESELu. Przy BigDecimalach nie mogę podać skali i precyzji.
        Nie zgadzam się z tym, że powinno się dążyć do eliminacji tej i innych annotacji określających strukturę bazy. Wręcz przeciwnie. Zresztą podajesz też wyżej przykład z @JoinColumn(name=”user_id”) gdzie podajesz nazwę kolumny FK. Będą konsekwentnym powinieneś tego name’a nie podawać.

        1. W artykule chodziło mi o użycie adnotacji @Column bez żadnych parametrów, co nie daje żadnego efektu, lub tylko z parametrem name co jest nadmiarowe moim zdaniem. Jeśli używasz tej adnotacji, żeby określić pewne parametry to jest jak najbardziej ok.

          Co do migracji to często występują jakieś przełomowe zmiany pomiędzy „dużymi” wersjami. I zastanów się czy nie lepiej jest wykonać pewną pracę przy migracji raz(może nawet trzeba będzie zmieniać nazwy kolumn) zamiast skazywać się na ciągłe dodawanie do encji @Column(name = „nazwa kolumny”).

  3. Do listy błędów bym jeszcze dodał używanie Lombok’a w encjach. Takie kwiatki u nas wychodzą w pracy, że szok. Im mniej doświadczony programista tym dłuższe dochodzenie o co chodzi.

    1. Używałem Lomboka z Hibernate i szczerze mówiąc nie miałem, większych problemów z tym połączeniem. Może podzielisz się jakimiś szczegółami. Chętnie poczytam.

      1. W zeszłym tygodniu był problem z @Equals i @Hashcode gdzie przez stosowanie tych annotacji duplikowały się encje przy zapisie do DB. Wywalenie tych annotacji i ręczne napisanie equalsa naprawiło problem. Inny problem niedawno był jak było @ToString i była kolekcja @OneToMany i Hibernate wpadł w jakąś nieskończoną pętlę przy próbie wygenerowanie toStringa. Ręczny toString z pominięciem tej listy pomógł.
        Na stacku jest parę problemów z Hibernate i Lombokiem: https://www.google.com/search?q=hibernate+lombok+problem+site:stackoverflow.com

        Jeden kwiatek nie związany bezpośrednio z Lombok i Hibernate:
        Miej w klasie (encji) dwa pola stringowe jedno po drugim:
        private String fieldA;

        prrivate String fieldB:

        Zastosuj na klasie @AllArgsConstructor. Wykorzystaj ten kontstruktor i zapisz dane do bazy. Następnie zamień kolejnością te pola, zbuilduj i zapisz ponownie dane. Następnie sprawdź w jakich kolumnach wylądowały dane z pól fieldA i fieldB przy drugim zapisie 😉

        Jeśli się wie jak to wszystko działa to można stosować. W mojej pracy się zrobił hype na lomboka w ostatnim roku. Niestety koledzy przychodzil kilka razy z podobnymi problemami jak opisanymi wyżej.

        1. Dzięki za komentarz 😉 Tak, to prawda, że trzeba wiedzieć jak to działa, żeby tego prawidłowo używać.

  4. Jak dla mnie zabrakło adnotacji @Entity na class, no chyba że chciałeś się pozbyć nadmiarowych adnotacji, ale automagiczne mapowanie pól klasy na pola bazy działa tylko wtedy.

    btw i ten
    @AllArgsConst
    Kto tego używa na encjach? Pierwszy raz widzę taki pomysł 😉

Komentarze są zamknięte.