Przejdź do głównej zawartości

One-to-one i lazy loading

Hibernate jest frameworkiem, który nie przestaje zaskakiwać. Kryje się w nim mnóstwo tajemnic, pułapek i zagadek. Jedną taką pułapkę postaram się opisać w tym wpisie, a dotyczy ona nieoczywistego na pierwszy rzut oka zachowania adnotacji @OneToOne z parametrem fetchType=LAZY.

One-to-one

Adnotacja @OneToOne służy w JPA do oznaczania pól encji, które odnoszą się do obiektów będących z nią w relacji jeden-do-jeden. W znormalizowanym schemacie relacyjnej bazy danych oznacza to sytuację, w której mamy do czynienia z dwoma tabelami A oraz B, a jedna z nich posiada kolumnę, której wartości wskazują na klucz identyfikujący krotkę w drugiej z nich. Np:

Lazy fetch

Adnotacja @OneToOne zawiera również atrybut fetch, który może przyjąć wartości EAGER lub LAZY (domyślnie EAGER). O ile ustawienie wartości EAGER oznacza, że Hibernate musi pobrać dodatkowy wiersz natychmiast (za pomocą klauzuli JOIN bądź dodatkowego zapytania) to zastosowanie LAZY może ale nie musi spowodować, że dane zostaną dociągnięte tylko jeśli faktycznie będą potrzebne w późniejszym czasie. Warto jednak zadać sobie pytanie, od czego to zależy?

Właściciel relacji

Należy zwrócić uwagę, że adnotację @OneToOne można umieścić w obu powiązanych encjach. W przedstawionym wyżej przykładzie, zarówno klasa A reprezentująca tabelę A jak i klasa B reprezentująca tabelę B może posiadać pole oznaczone przez @OneToOne wskazujące na obiekt drugiej z nich. Jednak jeśli przyjrzymy się schematowi bazy danych, zauważymy, że tylko jedna tabela z nich posiada fizyczne wskazanie na wiersz w drugiej. To ona jest właścicielem relacji. Encja, która nie jest właścicielem relacji musi określić pole właściciela za pomocą atrybutu mappedBy adnotacji @OneToOne.

Hibernate Lazy Proxy

Kolejnym elementem, który trzeba sobie uświadomić jest sposób w jaki Hibernate reprezentuje późno zaciągane obiekty w pamięci. Otóż w przypadku późnego dociągania zamiast obiektów danej klasy tworzone są obiekty proxy (anonimowych klas wygenerowanych przez Hibernate), które są odpowiedzialne za wykonanie dodatkowych zapytań w razie potrzeby. Problem pojawia się gdy podrzędny obiekt może nie istnieć w bazie danych. Co wtedy powinien zrobić ORM? Wstawić wartość null czy obiekt proxy? Hibernate jest w takiej sytuacji zmuszony zastosować strategię EAGER.

Do sedna

Do wyjaśnienia w jaki sposób odbywa się zarządzanie relacją one-to-one posłużę się prostym przykładem. Utworzyłem dwie proste encje: A oraz B, która jest właścicielem relacji. Dodatkowo napisałem dwa testy wyciągające obiekty tych klas za pomocą repozytoriów Spring Data.

@Data
@Entity
@Table(name = "A")
public class A {

    @Id
    private Long id;

    @Column
    private String someProperty;

    @OneToOne(mappedBy = "a", fetch = FetchType.LAZY)
    private B b;
}
@Data
@Entity
@Table(name = "B")
public class B {

    @Id
    private Long id;

    @Column
    private String someProperty;

    @OneToOne(fetch = FetchType.LAZY)
    private A a;
}
@DataJpaTest
class RepositoryIT {

    @Autowired
    private ARepository aRepository;

    @Autowired
    private BRepository bRepository;

    @Test
    void testA() {
        List<A> all = aRepository.findAll();
        all.stream().findFirst().ifPresent(first -> {
            System.out.println(first.getClass());
            System.out.println(first.getB().getClass());
        });
    }

    @Test
    void testB() {
        List<B> all = bRepository.findAll();
        all.stream().findFirst().ifPresent(first -> {
            System.out.println(first.getClass());
            System.out.println(first.getA().getClass());
            System.out.println(first.getA().getSomeProperty());
        });
    }
}
Output z testów prezentuje się następująco:
testA:
Hibernate: select a0_.id as id1_0_, a0_.some_property as some_pro2_0_ from a a0_
Hibernate: select b0_.id as id1_1_0_, b0_.a_id as a_id3_1_0_, b0_.some_property as some_pro2_1_0_ from b b0_ where b0_.a_id=?
class dev.maczkowski.onetooneexample.entity.A
class dev.maczkowski.onetooneexample.entity.B
example_value
testB:
Hibernate: select b0_.id as id1_1_, b0_.a_id as a_id3_1_, b0_.some_property as some_pro2_1_ from b b0_
class dev.maczkowski.onetooneexample.entity.B
class dev.maczkowski.onetooneexample.entity.A$HibernateProxy$3Yd2tM5i
Hibernate: select a0_.id as id1_0_0_, a0_.some_property as some_pro2_0_0_ from a a0_ where a0_.id=?
example_value
Łatwo zauważyć, że przy posługiwaniu się ARepository zostały wykonane dwa zapytania od razu. Ponadto pole b jest klasy dev.maczkowski.onetooneexample.entity.B. Stało się tak pomimo ustawienia atrybutu fetch = FetchType.LAZY. W drugim przypadku najpierw zostało wykonane jedno zapytanie, a pole a jest typu dev.maczkowski.onetooneexample.entity.A$HibernateProxy$3Yd2tM5i. Dopiero później zostało wykonane zapytanie inicjalizujące zawartość tego obiektu.

To tyle :)

Miał to być krótki wpis ale ostatecznie zdecydowałem się wszystko wyjaśnić od początku do końca. Jako ciekawostkę dopowiem, że w przypadku relacji many-to-one sprawa wygląda nieco prościej ale domyślna wartość atrybutu fetchType jest inna i warto zwrócić na to uwagę.

Komentarze

Popularne posty z tego bloga

Sneaky throws, czyli checked i unchecked exception w jednym!

Często podczas pisania kodu Java korzystam z wyrażeń lambda wprowadzonych wraz z Java 8. Często prowadzi to też do pewnych nowych problemów i zmusza do szukania rozwiązań. Przyjrzyjmy się jednemu z nich. Checked exception w wyrażeniu lambda Często zdarza się, że metoda wywoływana wewnątrz lambdy rzuca wyjątek. Nie ma sprawy gdy wyjątek jest obiektem klasy będącej podklasą RuntimeException . Wtedy po prostu się nim nie przejmujemy. Wyjątek jest przekazywany w górę stosu wywołań. Problem zaczyna się gdy kompilator zmusza nas do obsługi wyjątku. Jak zwykle mamy 2 wyjścia: obsłużyć wyjątek za pomocą bloku try-catch , bądź zadeklarować przekazanie wyjątku dalej za pomocą słowa kluczowego throws . O ile wiemy co zrobić po złapaniu wyjątku to wszystko gra. Co natomiast gdy chcemy wybrać drugą opcję? Większość interfejsów stosowanych jako typy parametrów, często przekazywanych jako lambdy jak np: Function , Consumer czy Supplier nie deklarują, że mogą rzucić wyjątek. W takim wypadku druga

Wzorzec Open Session in View w Spring Boot

Dzisiejszy post będzie z cyklu: "Wtf? Dlaczego to działa?". A dotyczy on pewnego wzorca, którego implementacja jak się okazuje jest we frameworku Spring Boot domyślnie włączona, czego nie wszyscy programiści mogą się spodziewać. Geneza Zaczęło się od tego, że pisałem kolejny test integracyjny do kolejnego kontrolera. Test napisany, uruchamiam, zielono. No i przeglądam sobie jeszcze raz kod zanim wypchnę zmiany do review. I rzuciło mi się w oczy, że zapomniałem dodać adnotacji @Transactional (do tej pory uważałem, że to jest zawsze wymagane). I wtedy zadałem sobie przytoczone na wstępie pytanie. Dlaczego test przeszedł skoro mój serwis dociąga sobie obiekt oznaczony jako LAZY ? Dużo się nagimnastykowałem by wpisać odpowiednie query do Google'a i znaleźć odpowiedź: Open Session in View. Ale o tym za chwilę. Jak działa Hibernate Żeby lepiej zrozumieć opisywane zagadnienie warto przypomnieć jakie kroki w uproszczeniu musi wykonać Hibernate podczas komunikacji z bazą d

Pułapki logowania w Javie

Czy spotkałeś kiedyś się z kodem takim jak poniższy? if (logger.isDebugEnabled()) { logger.debug("Request: " + requestObject); } Ja tak. Jakiś czas temu napotkałem na mnóstwo takich fragmentów w kodzie, który analizowałem. Zastanowiło mnie po co została wykorzystana tutaj instrukcja warunkowa - a jako, że if-y uwazam za zło konieczne postanowiłem to zbadać. Czy nie do tego właśnie służą biblioteki do logowania i udostępniane przez nie metody jak debug żeby właśnie sterowane konfiguracją decydowały co zalogować a co nie? Co więcej, okazało się, że instrukcję warunkową wprowadził ktoś w ramach większej akcji. I jak się okazuje, prawdopodobnie znacznie zwiększył dzieki temu wydajność aplikacji. W dzisiejszym wpisie postaram się wyjaśnić jak korzystać z loggera aby nie zaszkodziło to wydajności. Po co if-y? Na początek wyjaśnić trzeba czemu ten if tak na prawdę służy. Korzystamy z biblioteki do logowania i spodziewamy się, że wykorzystujac wybraną metodę, zostanie