So rufen Sie FetchType.LAZY-Zuordnungen mit JPA und Hibernate in einem Spring Controller ab

146

Ich habe eine Personenklasse:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

Mit einer Viele-zu-Viele-Beziehung ist das faul.

In meinem Controller habe ich

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

Und das PersonRepository ist genau dieser Code, der gemäß dieser Anleitung geschrieben wurde

public interface PersonRepository extends JpaRepository<Person, Long> {
}

In diesem Controller benötige ich jedoch tatsächlich die Lazy-Daten. Wie kann ich das Laden auslösen?

Der Versuch, darauf zuzugreifen, schlägt mit fehl

Fehler beim Initialisieren einer Rollensammlung: no.dusken.momus.model.Person.roles, Proxy konnte nicht initialisiert werden - keine Sitzung

oder andere Ausnahmen, je nachdem, was ich versuche.

Meine XML-Beschreibung , falls benötigt.

Vielen Dank.

Matsemann
quelle
Können Sie eine Methode schreiben, die eine Abfrage erstellt, um ein PersonObjekt mit einem bestimmten Parameter abzurufen ? Darin Queryenthalten die fetchKlausel und laden die Rolesauch für die Person.
SudoRahul

Antworten:

206

Sie müssen die Lazy-Sammlung explizit aufrufen, um sie zu initialisieren (es ist üblich, zu .size()diesem Zweck aufzurufen ). Im Ruhezustand gibt es eine dedizierte Methode für this ( Hibernate.initialize()), aber JPA hat kein Äquivalent dazu. Natürlich müssen Sie sicherstellen, dass der Aufruf erfolgt, wenn die Sitzung noch verfügbar ist. Kommentieren Sie daher Ihre Controller-Methode mit @Transactional. Eine Alternative besteht darin, eine Zwischen-Service-Schicht zwischen dem Controller und dem Repository zu erstellen, die Methoden verfügbar machen kann, die verzögerte Sammlungen initialisieren.

Aktualisieren:

Bitte beachten Sie, dass die obige Lösung einfach ist, jedoch zu zwei unterschiedlichen Abfragen an die Datenbank führt (eine für den Benutzer, eine andere für seine Rollen). Wenn Sie eine bessere Leistung erzielen möchten, fügen Sie Ihrer Spring Data JPA-Repository-Schnittstelle die folgende Methode hinzu:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

Diese Methode verwendet die Fetch-Join- Klausel von JPQL, um die Rollenzuordnung in einem einzigen Roundtrip in die Datenbank eifrig zu laden, und verringert daher die Leistungseinbußen, die durch die beiden unterschiedlichen Abfragen in der obigen Lösung entstehen.

zagyi
quelle
3
Bitte beachten Sie, dass dies eine einfache Lösung ist, jedoch zu zwei unterschiedlichen Abfragen an die Datenbank führt (eine für den Benutzer, eine andere für seine Rollen). Wenn Sie eine bessere Leistung erzielen möchten, versuchen Sie, eine dedizierte Methode zu schreiben, die den Benutzer und die zugehörigen Rollen in einem einzigen Schritt mithilfe von JPQL oder der Criteria-API abruft, wie von anderen vorgeschlagen.
Zagyi
Ich bat jetzt um ein Beispiel für Joses Antwort, muss zugeben, dass ich nicht ganz verstehe.
Matsemann
Bitte überprüfen Sie eine mögliche Lösung für die gewünschte Abfragemethode in meiner aktualisierten Antwort.
Zagyi
7
Interessant zu beachten, wenn Sie einfach joinohne die fetch, wird das Set mit zurückgegeben initialized = false; Daher wird nach dem Zugriff auf die Gruppe immer noch eine zweite Abfrage ausgegeben. fetchDies ist der Schlüssel, um sicherzustellen, dass die Beziehung vollständig geladen ist, und um die zweite Abfrage zu vermeiden.
FGreg
Es scheint, dass das Problem beim Abrufen und Verknüpfen und Verbinden darin besteht, dass die Prädikatkriterien für Verknüpfungen ignoriert werden und Sie am Ende alles in der Liste oder Karte erhalten. Wenn Sie alles wollen, dann verwenden Sie einen Abruf, wenn Sie etwas Bestimmtes wollen, dann einen Join, aber der Join ist, wie gesagt, leer. Dies macht den Zweck der Verwendung von .LAZY-Laden zunichte.
K.Nicholas
37

Obwohl dies ein alter Beitrag ist, sollten Sie @NamedEntityGraph (Javax Persistence) und @EntityGraph (Spring Data JPA) verwenden. Die Kombination funktioniert.

Beispiel

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

und dann das Feder-Repo wie unten

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}
Rakpan
quelle
1
Beachten Sie, dass dies @NamedEntityGraphTeil der JPA 2.1-API ist, die vor der Version 4.3.0 nicht in Hibernate implementiert wurde.
NaXa
2
@EntityGraph(attributePaths = "employeeGroups")kann direkt in einem Spring Data Repository verwendet werden, um eine Methode zu kommentieren, ohne dass ein @NamedEntityGraphCode auf Ihrem @Entity-freien Code erforderlich ist, der beim Öffnen des Repos leicht zu verstehen ist.
Desislav Kamenov
13

Sie haben einige Möglichkeiten

  • Schreiben Sie eine Methode in das Repository, die eine initialisierte Entität zurückgibt, wie von RJ vorgeschlagen.

Mehr Arbeit, beste Leistung.

  • Verwenden Sie OpenEntityManagerInViewFilter, um die Sitzung für die gesamte Anforderung offen zu halten.

Weniger Arbeit, normalerweise in Webumgebungen akzeptabel.

  • Verwenden Sie eine Hilfsklasse, um Entitäten bei Bedarf zu initialisieren.

Weniger Arbeit, nützlich, wenn OEMIV nicht verfügbar ist, z. B. in einer Swing-Anwendung, aber auch bei Repository-Implementierungen, um eine Entität auf einmal zu initialisieren.

Für die letzte Option habe ich eine Utility-Klasse geschrieben, JpaUtils , um Entitäten bei einem Deph zu initialisieren.

Beispielsweise:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
Jose Luis Martin
quelle
Da alle meine Anfragen einfache REST-Aufrufe ohne Rendering usw. sind, ist die Transaktion im Grunde meine gesamte Anfrage. Danke für deinen Beitrag.
Matsemann
Wie mache ich den ersten? Ich kann eine Abfrage schreiben, aber nicht, was Sie sagen. Könnten Sie bitte ein Beispiel zeigen? Wäre sehr hilfreich.
Matsemann
zagyi gab in seiner Antwort ein Beispiel, danke, dass Sie mich trotzdem in die richtige Richtung gelenkt haben.
Matsemann
Ich weiß nicht, wie deine Klasse heißen würde! nicht abgeschlossene Lösung verschwenden andere Zeit
Shady Sherif
Verwenden Sie OpenEntityManagerInViewFilter, um die Sitzung für die gesamte Anforderung offen zu halten. - Schlechte Idee. Ich würde eine zusätzliche Anfrage stellen, um alle Sammlungen für meine Entitäten abzurufen.
Yan Khonski
6

Ich denke, Sie benötigen OpenSessionInViewFilter , um Ihre Sitzung während des Renderns der Ansicht offen zu halten (dies ist jedoch keine allzu gute Vorgehensweise).

Michail Nikolaev
quelle
1
Da ich keine JSP oder ähnliches verwende und nur eine REST-API erstelle, reicht @Transactional für mich aus. Wird aber zu anderen Zeiten nützlich sein. Vielen Dank.
Matsemann
@Matsemann Ich weiß, dass es jetzt spät ist ... aber Sie können OpenSessionInViewFilter auch in einem Controller verwenden, und die Sitzung wird bestehen, bis eine Antwort kompiliert wird ...
Vishwas Shashidhar
@ Matsemann Danke! Transaktionsanmerkungen haben den Trick für mich getan! fyi: Es funktioniert sogar, wenn Sie nur die Oberklasse einer Rastklasse kommentieren.
desperateCoder
3

Federdaten JpaRepository

Die Federdaten JpaRepositorydefinieren die folgenden zwei Methoden:

  • getOne, der einen Entitäts-Proxy zurückgibt, der zum Festlegen einer @ManyToOneoder einer @OneToOneübergeordneten Zuordnung geeignet ist, wenn eine untergeordnete Entität beibehalten wird .
  • findById, der die Entität POJO zurückgibt, nachdem die SELECT-Anweisung ausgeführt wurde, die die Entität aus der zugehörigen Tabelle lädt

In Ihrem Fall haben Sie jedoch weder angerufen getOnenoch findById:

Person person = personRepository.findOne(1L);

Ich gehe also davon aus, dass die findOneMethode eine Methode ist, die Sie in der definiert haben PersonRepository. Die findOneMethode ist in Ihrem Fall jedoch nicht sehr nützlich. Da Sie die Sammlung Personzusammen mit is rolesabrufen müssen, ist es besser, findOneWithRolesstattdessen eine Methode zu verwenden.

Benutzerdefinierte Spring Data-Methoden

Sie können eine PersonRepositoryCustomSchnittstelle wie folgt definieren:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

Und definieren Sie die Implementierung folgendermaßen:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

Das ist es!

Vlad Mihalcea
quelle
Gibt es einen Grund, warum Sie die Abfrage selbst geschrieben und keine Lösung wie EntityGraph in der Antwort von @rakpan verwendet haben? Würde dies nicht das gleiche Ergebnis bringen?
Jeroen Vandevelde
Der Aufwand für die Verwendung eines EntityGraph ist höher als bei einer JPQL-Abfrage. Auf lange Sicht ist es besser, die Abfrage zu schreiben.
Vlad Mihalcea
Können Sie den Overhead näher erläutern (Woher kommt er, fällt es auf, ...)? Weil ich nicht verstehe, warum es einen höheren Overhead gibt, wenn beide dieselbe Abfrage generieren.
Jeroen Vandevelde
1
Weil EntityGraphs-Pläne nicht wie JPQL zwischengespeichert werden. Das kann ein erheblicher Leistungseinbruch sein.
Vlad Mihalcea
1
Genau. Ich muss einen Artikel darüber schreiben, wenn ich etwas Zeit habe.
Vlad Mihalcea
1

Sie können dasselbe wie folgt tun:

@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

Verwenden Sie einfach faqQuestions.getFaqAnswers (). Size () in Ihrem Controller, und Sie erhalten die Größe, wenn Sie die Liste träge initialisiert haben, ohne die Liste selbst abzurufen.

neel4soft
quelle