Jak sprawić, by relacja JPA OneToOne była leniwa

212

W tej aplikacji, którą opracowujemy, zauważyliśmy, że widok był wyjątkowo wolny. Profilowałem widok i zauważyłem, że hibernacja wykonała jedno zapytanie, które zajęło 10 sekund, nawet jeśli w bazie danych były tylko dwa obiekty do pobrania. Wszystko OneToManyi ManyToManystosunki były leniwy, więc to nie był problem. Podczas sprawdzania faktycznie wykonywanego SQL-a zauważyłem, że w zapytaniu było ponad 80 złączeń.

Podczas dalszej analizy problemu zauważyłem, że problem był spowodowany głęboką hierarchią OneToOnei ManyToOnerelacjami między klasami jednostek. Pomyślałem, że sprowadzę ich na lenistwo, co powinno rozwiązać problem. Ale adnotacji albo @OneToOne(fetch=FetchType.LAZY)czy @ManyToOne(fetch=FetchType.LAZY)nie wydaje się do pracy. Albo otrzymuję wyjątek, albo nie są one faktycznie zastępowane przez obiekt proxy i dlatego są leniwe.

Jakieś pomysły, jak to zrobić? Zauważ, że nie używam persistence.xmldo definiowania relacji ani szczegółów konfiguracji, wszystko odbywa się w kodzie Java.

Kim L.
źródło

Odpowiedzi:

218

Po pierwsze, kilka wyjaśnień do odpowiedzi KLE :

  1. Nieograniczone (zerowalne) skojarzenie jeden do jednego jest jedynym, które nie może być proxy bez oprzyrządowania z kodem bajtowym. Powodem tego jest to, że jednostka właściciela MUSI wiedzieć, czy właściwość asocjacji powinna zawierać obiekt proxy, czy NULL, i nie może tego ustalić, patrząc na kolumny swojej tabeli podstawowej ze względu na normalne odwzorowanie jeden do jednego za pośrednictwem współdzielonego PK, więc i tak musi być chętnie pobierany, przez co proxy nie ma sensu. Oto bardziej szczegółowe wyjaśnienie.

  2. stowarzyszenia wiele do jednego (i oczywiście jeden do wielu) nie cierpią z powodu tego problemu. Podmiot będący właścicielem może łatwo sprawdzić swój FK (aw przypadku „jeden do wielu” początkowo tworzony jest pusty serwer proxy kolekcji i zapełniany na żądanie), więc powiązanie może być leniwe.

  3. Zamiana jeden na jeden na jeden na wielu nigdy nie jest dobrym pomysłem. Możesz go zastąpić unikatowym rozwiązaniem wiele do jednego, ale istnieją inne (być może lepsze) opcje.

Rob H. ma rację, ale możesz nie być w stanie go wdrożyć w zależności od modelu (np. Jeśli twoje powiązanie jeden do jednego jest zerowane).

Jeśli chodzi o oryginalne pytanie:

A) @ManyToOne(fetch=FetchType.LAZY)powinien działać dobrze. Czy na pewno nie jest zastępowane w samym zapytaniu? Możliwe jest określenie join fetchw HQL i / lub jawne ustawienie trybu pobierania poprzez Criteria API, który miałby pierwszeństwo przed adnotacjami klas. Jeśli tak nie jest i nadal masz problemy, opublikuj swoje klasy, zapytanie i wynikowy SQL, aby uzyskać bardziej szczegółową rozmowę.

B) @OneToOnejest trudniejsze. Jeśli zdecydowanie nie ma wartości zerowej, skorzystaj z sugestii Roba H. i określ ją jako taką:

@OneToOne(optional = false, fetch = FetchType.LAZY)

W przeciwnym razie, jeśli możesz zmienić bazę danych (dodaj kolumnę klucza obcego do tabeli właściciela), zrób to i zamapuj ją jako „połączoną”:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

oraz w OtherEntity:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

Jeśli nie możesz tego zrobić (i nie możesz żyć z chętnym pobieraniem) instrumentacja z kodem bajtowym jest twoją jedyną opcją. Muszę jednak zgodzić się z CPerkins - jeśli masz 80 !!! dołącza z powodu chętnych skojarzeń OneToOne, masz większe problemy niż to :-)

ChssPly76
źródło
Być może istnieje inna opcja, ale nie przetestowałem jej osobiście: po stronie bez ograniczeń użyj one-to-onez formułą podobną do select other_entity.id from other_entity where id = other_entity.id. Oczywiście nie jest to idealne rozwiązanie do wykonywania zapytań.
Frédéric
1
opcjonalne = fałsz, nie działa dla mnie. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", opcjonalnie = false) private FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts
21

Aby uzyskać leniwe ładowanie podczas pracy nad zerowalnymi mapowaniami jeden-do-jednego, musisz pozwolić hibernacji skompilować instrumentację czasu i dodać @LazyToOne(value = LazyToOneOption.NO_PROXY)relację jeden-do-jednego.

Przykładowe mapowanie:

@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

Przykład rozszerzenia pliku Ant Build (do wykonania instrumentacji czasu kompilacji Hibernacja):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>
Kdeveloper
źródło
3
Dlaczego LazyToOneOption.NO_PROXYnie LazyToOneOption.PROXY?
Telmo Marques,
To nie odpowiada na pytanie „dlaczego”, ale fakt ten jest tutaj również potwierdzony (pod koniec sekcji „Typowe mapowanie”): vladmihalcea.com/…
DanielM
12

Podstawową ideą XToOnes w Hibernacji jest to, że w większości przypadków nie są leniwi.

Jednym z powodów jest to, że gdy Hibernacja musi zdecydować się na ustawienie serwera proxy (z identyfikatorem) lub wartości zerowej,
musi i tak spojrzeć na drugą tabelę, aby dołączyć. Koszt dostępu do drugiej tabeli w bazie danych jest znaczny, więc równie dobrze może pobrać dane dla tej tabeli w tym momencie (zachowanie nie leniwe), zamiast pobierać to w późniejszym żądaniu, które wymagałoby drugiego dostępu do ten sam stół.

Edytowane: szczegółowe informacje znajdują się w odpowiedzi ChssPly76 . Ten jest mniej dokładny i szczegółowy, nie ma nic do zaoferowania. Dzięki ChssPly76.

KLE
źródło
Jest tu kilka rzeczy błędnych - poniżej
podaję
8

Oto coś, co działało dla mnie (bez oprzyrządowania):

Zamiast używać @OneToOnepo obu stronach, używam @OneToManyw odwrotnej części relacji (tej z mappedBy). To sprawia, że ​​właściwość jest kolekcją ( Listw poniższym przykładzie), ale tłumaczę ją na element w module pobierającym, dzięki czemu jest przezroczysty dla klientów.

Ta konfiguracja działa leniwie, to znaczy, że wybiera się tylko gdy wykonane getPrevious()lub getNext()są nazywane - i tylko jeden wybór dla każdego połączenia.

Struktura tabeli:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

Klasa:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}
acdcjunior
źródło
7

Jak wyjaśniłem w tym artykule , chyba że korzystasz z Bytecode Enhancement , nie możesz leniwie pobrać @OneToOnepowiązania strony nadrzędnej .

Jednak najczęściej nie potrzebujesz nawet powiązania strony nadrzędnej, jeśli używasz @MapsIdpo stronie klienta:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

Z @MapsIdThe idnieruchomość w tabeli podrzędnej służy zarówno jako klucz podstawowy i klucz obcy do tabeli nadrzędnej klucz podstawowy.

Jeśli więc masz odniesienie do Postencji nadrzędnej , możesz łatwo pobrać encję podrzędną za pomocą identyfikatora encji nadrzędnej:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

W ten sposób nie będziesz mieć problemów z zapytaniami N + 1, które mogą być spowodowane przez mappedBy @OneToOnepowiązanie po stronie nadrzędnej.

Vlad Mihalcea
źródło
w ten sposób nie możemy już wykonywać kaskadowych operacji od rodzica do dziecka: /
Hamdi
W przypadku persist jest to tylko dodatkowe wywołanie trwałe, w przypadku usuwania można użyć kaskady DDL.
Vlad Mihalcea
6

W natywnych mapowaniach XML Hibernacji można to osiągnąć, deklarując mapowanie jeden do jednego z ograniczonym atrybutem ustawionym na wartość true. Nie jestem pewien, co to jest odpowiednik adnotacji Hibernacja / JPA, a szybkie przeszukanie dokumentu nie dało odpowiedzi, ale mam nadzieję, że da ci to przewagę.

Rob H.
źródło
5
+1 za dobrą sugestię; niestety nie zawsze ma to zastosowanie, ponieważ model domeny może w rzeczywistości wymagać zerowania. Właściwy sposób odwzorowania tego za pomocą adnotacji to@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76 18.09.2009
Próbowałem tego i nie zauważyłem poprawy wydajności. Wciąż widziałem wiele zapytań w wyjściu hibernacji za pośrednictwem debugera.
P.Brian.Mackey,
3

Jak już doskonale wyjaśniono w ChssPly76, serwery proxy Hibernacji nie pomagają w nieskrępowanych (zerowalnych) skojarzeniach jeden-do-jednego, ALE jest wyjaśniona tutaj sztuczka , aby uniknąć konfiguracji instrumentacji. Chodzi o to, aby oszukać Hibernację, że klasa encji, której chcemy użyć, została już instrumentowana: instrumentujesz ją ręcznie w kodzie źródłowym. To jest łatwe! Zaimplementowałem go z CGLib jako dostawcą kodu bajtowego i działa (upewnij się, że w HBM skonfigurujesz lazy = "no-proxy" i fetch = "select", a nie "join").

Myślę, że jest to dobra alternatywa dla prawdziwych (tzn. Automatycznych) instrumentów, kiedy masz tylko jeden związek zerowy, który chcesz rozluźnić. Główną wadą jest to, że rozwiązanie zależy od dostawcy kodu bajtowego, którego używasz, więc dokładnie komentuj swoją klasę, ponieważ w przyszłości będziesz musiał zmienić dostawcę kodu bajtowego; oczywiście modyfikujesz również model bean z przyczyn technicznych i to nie jest w porządku.

Pino
źródło
1

To pytanie jest dość stare, ale w Hibernacji 5.1.10 pojawiły się nowe, bardziej wygodne rozwiązania.

Leniwe ładowanie działa z wyjątkiem strony nadrzędnej powiązania @OneToOne. Wynika to z faktu, że Hibernacja nie ma innego sposobu dowiedzenia się, czy przypisać wartość zerową czy proxy do tej zmiennej. Więcej informacji można znaleźć w tym artykule

  • Możesz aktywować leniwe ładowanie rozszerzenia kodu bajtowego
  • Lub możesz po prostu usunąć stronę nadrzędną i użyć strony klienta z @MapsId, jak wyjaśniono w powyższym artykule. W ten sposób przekonasz się, że tak naprawdę nie potrzebujesz strony nadrzędnej, ponieważ dziecko dzieli ten sam identyfikator z rodzicem, dzięki czemu możesz łatwo pobrać dziecko, znając identyfikator rodzica.
Toumi
źródło
0

Jeśli relacja nie może być dwukierunkowa, wówczas @ElementCollection może być łatwiejsze niż używanie leniwej kolekcji One2Many.

Stefan
źródło