Jak pobrać skojarzenia FetchType.LAZY z JPA i Hibernate w kontrolerze Spring

146

Mam klasę Person:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

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

Z leniwą relacją wiele do wielu.

W moim kontrolerze mam

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

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

A PersonRepository to tylko ten kod, napisany zgodnie z tym przewodnikiem

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

Jednak w tym kontrolerze faktycznie potrzebuję leniwych danych. Jak mogę uruchomić jego ładowanie?

Próba uzyskania do niego dostępu zakończy się niepowodzeniem

nie udało się leniwie zainicjować kolekcji ról: no.dusken.momus.model.Person.roles, nie można zainicjować serwera proxy - brak sesji

lub inne wyjątki w zależności od tego, co próbuję.

Mój opis XML , w razie potrzeby.

Dzięki.

Matsemann
źródło
Czy potrafisz napisać metodę, która utworzy zapytanie pobierające Personobiekt z jakimś parametrem? W tym Queryuwzględnij fetchklauzulę i załaduj Rolesteż dla osoby.
SudoRahul

Odpowiedzi:

206

Będziesz musiał wykonać jawne wywołanie leniwej kolekcji, aby ją zainicjować (powszechną praktyką jest wywoływanie .size()w tym celu). W Hibernate istnieje metoda this ( Hibernate.initialize()), ale JPA nie ma jej odpowiednika. Oczywiście będziesz musiał upewnić się, że wywołanie zostało wykonane, gdy sesja jest nadal dostępna, więc dodaj adnotację do metody kontrolera z @Transactional. Alternatywą jest utworzenie pośredniej warstwy usług między kontrolerem a repozytorium, która mogłaby ujawnić metody inicjujące leniwe kolekcje.

Aktualizacja:

Należy pamiętać, że powyższe rozwiązanie jest łatwe, ale skutkuje dwoma różnymi zapytaniami do bazy danych (jedno dla użytkownika, drugie dla jego ról). Jeśli chcesz osiągnąć lepszą wydajność, dodaj następującą metodę do interfejsu repozytorium Spring Data JPA:

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

}

Ta metoda wykorzysta klauzulę fetch join w JPQL, aby z niecierpliwością załadować skojarzenie ról w jednej rundzie do bazy danych, a zatem zmniejszy spadek wydajności poniesiony przez dwa różne zapytania w powyższym rozwiązaniu.

zagyi
źródło
3
Należy pamiętać, że jest to łatwe rozwiązanie, ale skutkuje dwoma różnymi zapytaniami do bazy danych (jedno dla użytkownika, drugie dla jego ról). Jeśli chcesz osiągnąć lepszą wydajność, spróbuj napisać dedykowaną metodę, która chętnie pobiera użytkownika i powiązane z nim role w jednym kroku przy użyciu JPQL lub interfejsu API Criteria, jak sugerowali inni.
zagyi
Poprosiłem teraz o przykład odpowiedzi Jose, muszę przyznać, że nie rozumiem do końca.
Matsemann
Proszę sprawdzić możliwe rozwiązanie dla żądanej metody zapytania w mojej zaktualizowanej odpowiedzi.
zagyi
7
Ciekawostka, jeśli po prostu joinbez fetch, zestaw zostanie zwrócony z initialized = false; dlatego nadal wysyła drugie zapytanie po uzyskaniu dostępu do zestawu. fetchjest kluczem do upewnienia się, że relacja jest całkowicie załadowana i uniknięcia drugiego zapytania.
FGreg
Wygląda na to, że problem z wykonywaniem obu operacji i pobieraniem oraz łączeniem polega na tym, że kryteria predykatu łączenia są ignorowane i w końcu dostajesz wszystko z listy lub mapy. Jeśli chcesz wszystkiego, użyj pobierania, jeśli chcesz czegoś konkretnego, a następnie złączenia, ale, jak zostało powiedziane, złączenie będzie puste. To mija się z celem używania ładowania .LAZY.
K.Nicholas
37

Chociaż jest to stary post, rozważ użycie @NamedEntityGraph (Persistence Javax) i @EntityGraph (Spring Data JPA). Połączenie działa.

Przykład

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

a następnie repozytorium wiosenne, jak poniżej

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

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

}
rakpan
źródło
1
Zauważ, że @NamedEntityGraphjest to część API JPA 2.1, która nie jest zaimplementowana w Hibernate przed wersją 4.3.0.
naXa
2
@EntityGraph(attributePaths = "employeeGroups")może być używany bezpośrednio w repozytorium danych Spring do dodawania adnotacji do metody bez potrzeby umieszczania @NamedEntityGraphkodu na @Entity - mniej kodu, łatwy do zrozumienia po otwarciu repozytorium.
Desislav Kamenov
13

Masz kilka opcji

  • Napisz metodę w repozytorium, która zwraca zainicjowaną jednostkę zgodnie z sugestią RJ.

Więcej pracy, najlepsza wydajność.

  • Użyj OpenEntityManagerInViewFilter, aby sesja była otwarta dla całego żądania.

Mniej pracy, zwykle akceptowalne w środowiskach internetowych.

  • Użyj klasy pomocniczej, aby zainicjować jednostki, gdy jest to wymagane.

Mniej pracy, przydatne, gdy OEMIV nie jest dostępny, na przykład w aplikacji Swing, ale może być również przydatny w implementacjach repozytorium, aby zainicjować dowolną jednostkę za jednym razem.

W przypadku ostatniej opcji napisałem klasę narzędziową JpaUtils do inicjowania jednostek w pewnym deph.

Na przykład:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
Jose Luis Martin
źródło
Ponieważ wszystkie moje żądania to proste wywołania REST bez renderowania itp., Transakcja jest w zasadzie całym moim żądaniem. Dzięki za wkład.
Matsemann
Jak mam zrobić pierwszy? Wiem, jak napisać zapytanie, ale nie wiem, jak zrobić to, co mówisz. Czy mógłbyś pokazać przykład? Byłoby bardzo pomocne.
Matsemann
zagyi podał przykład w swojej odpowiedzi, ale i tak dziękuję za wskazanie mi właściwego kierunku.
Matsemann
Nie wiem, jak nazwanoby twoją klasę! nieukończone rozwiązanie marnować czas innym
Shady Sherif
Użyj OpenEntityManagerInViewFilter, aby sesja była otwarta dla całego żądania. - Zły pomysł. Zrobiłbym dodatkową prośbę o pobranie wszystkich kolekcji dla moich podmiotów.
Yan Khonski,
6

Myślę, że potrzebujesz OpenSessionInViewFilter, aby sesja była otwarta podczas renderowania widoku (ale nie jest to zbyt dobra praktyka).

Michail Nikolaev
źródło
1
Ponieważ nie używam JSP ani niczego innego, po prostu robię REST-api, @Transactional zrobi za mnie. Ale przyda się innym razem. Dzięki.
Matsemann
@Matsemann Wiem, że jest już późno ... ale możesz skorzystać z OpenSessionInViewFilter nawet w kontrolerze, a sesja będzie istnieć do skompilowania odpowiedzi ...
Vishwas Shashidhar
@Matsemann Thanks! Adnotacja transakcyjna załatwiła mi sprawę! fyi: To działa nawet, jeśli po prostu dodasz adnotacje do nadklasy klasy odpoczynku.
desperateCoder
3

Dane wiosenne JpaRepository

Spring Data JpaRepositorydefiniuje następujące dwie metody:

  • getOne, która zwraca serwer proxy jednostki, który jest odpowiedni do ustawienia powiązania @ManyToOnelub @OneToOnenadrzędnego podczas utrwalania jednostki podrzędnej .
  • findById, która zwraca jednostkę POJO po uruchomieniu instrukcji SELECT, która ładuje jednostkę z powiązanej tabeli

Jednak w Twoim przypadku nie zadzwoniłeś ani getOneteż findById:

Person person = personRepository.findOne(1L);

Więc zakładam, że findOnemetoda jest metodą zdefiniowaną w PersonRepository. Jednak findOnemetoda nie jest zbyt przydatna w twoim przypadku. Ponieważ musisz pobrać kolekcję Personwraz z is roles, lepiej findOneWithRoleszamiast tego użyć metody.

Niestandardowe metody Spring Data

Możesz zdefiniować PersonRepositoryCustominterfejs w następujący sposób:

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

}

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

I zdefiniuj jego implementację w ten sposób:

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();
    }
}

Otóż ​​to!

Vlad Mihalcea
źródło
Czy jest powód, dla którego sam napisałeś zapytanie i nie użyłeś rozwiązania takiego jak EntityGraph w odpowiedzi @rakpan? Czy nie przyniosłoby to takiego samego rezultatu?
Jeroen Vandevelde
Narzut związany z użyciem EntityGraph jest wyższy niż w przypadku zapytania JPQL. Na dłuższą metę lepiej napisać zapytanie.
Vlad Mihalcea
Czy możesz rozwinąć ogólny koszt (skąd się bierze, czy jest zauważalny ...)? Ponieważ nie rozumiem, dlaczego jest wyższy narzut, jeśli oba generują to samo zapytanie.
Jeroen Vandevelde
1
Ponieważ plany EntityGraphs nie są buforowane, tak jak JPQL. To może być znaczący spadek wydajności.
Vlad Mihalcea
1
Dokładnie. Będę musiał napisać o tym artykuł, gdy będę miał trochę czasu.
Vlad Mihalcea
1

Możesz zrobić to samo w ten sposób:

@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;
}

Po prostu użyj faqQuestions.getFaqAnswers (). Size () nin swojego kontrolera, a otrzymasz rozmiar, jeśli leniwie zainicjujesz listę, bez pobierania samej listy.

neel4soft
źródło