Cách tìm nạp các liên kết FetchType.LAZY với JPA và Hibernate trong Bộ điều khiển mùa xuân


146

Tôi có một lớp Người:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

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

Với một mối quan hệ nhiều-nhiều mà lười biếng.

Trong bộ điều khiển của tôi, tôi có

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

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

Và PersonRep repository chỉ là mã này, được viết theo hướng dẫn này

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

Tuy nhiên, trong bộ điều khiển này tôi thực sự cần dữ liệu lười biếng. Làm thế nào tôi có thể kích hoạt tải của nó?

Cố gắng truy cập nó sẽ thất bại với

thất bại trong việc khởi tạo một cách lười biếng một bộ sưu tập vai trò: no.dusken.momus.model.Person.roles, không thể khởi tạo proxy - không có Phiên

hoặc các ngoại lệ khác tùy thuộc vào những gì tôi cố gắng.

Mô tả xml của tôi , trong trường hợp cần thiết.

Cảm ơn.


Bạn có thể viết một phương thức, nó sẽ tạo một truy vấn để tìm nạp một Personđối tượng được cung cấp một số tham số không? Trong đó Query, bao gồm fetchmệnh đề và tải Rolesquá cho người.
SudoRahul

Câu trả lời:


206

Bạn sẽ phải thực hiện một cuộc gọi rõ ràng trên bộ sưu tập lười biếng để khởi tạo nó (thực tế phổ biến là gọi .size()cho mục đích này). Trong Hibernate có một phương thức dành riêng cho điều này ( Hibernate.initialize()), nhưng JPA không có phương thức tương đương. Tất nhiên bạn sẽ phải đảm bảo rằng việc gọi được thực hiện, khi phiên vẫn còn, vì vậy hãy chú thích phương thức điều khiển của bạn với @Transactional. Một cách khác là tạo một lớp Dịch vụ trung gian giữa Bộ điều khiển và Kho lưu trữ có thể hiển thị các phương thức khởi tạo các bộ sưu tập lười biếng.

Cập nhật:

Xin lưu ý rằng giải pháp trên rất dễ, nhưng dẫn đến hai truy vấn riêng biệt cho cơ sở dữ liệu (một cho người dùng, một cho các vai trò của nó). Nếu bạn muốn đạt được hiệu suất tốt hơn, hãy thêm phương thức sau vào giao diện kho lưu trữ Spring Data JPA của bạn:

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

}

Phương pháp này sẽ sử dụng mệnh đề tham gia tìm nạp của JPQL để háo hức tải liên kết vai trò trong một chuyến đi khứ hồi vào cơ sở dữ liệu và do đó sẽ giảm nhẹ hình phạt hiệu suất do hai truy vấn riêng biệt trong giải pháp trên đưa ra.


3
Xin lưu ý rằng đây là một giải pháp dễ dàng, nhưng dẫn đến hai truy vấn riêng biệt cho cơ sở dữ liệu (một cho người dùng, một truy vấn khác cho vai trò của nó). Nếu bạn muốn đạt được hiệu suất tốt hơn, hãy thử viết một phương pháp chuyên dụng, háo hức tìm nạp người dùng và các vai trò liên quan của nó trong một bước duy nhất bằng cách sử dụng JPQL hoặc API Tiêu chí như những người khác đề xuất.
zagyi

Bây giờ tôi đã yêu cầu một ví dụ cho câu trả lời của Jose, phải thừa nhận tôi không hiểu hoàn toàn.
Matsemann

Vui lòng kiểm tra một giải pháp có thể cho phương pháp truy vấn mong muốn trong câu trả lời được cập nhật của tôi.
zagyi

7
Điều thú vị cần lưu ý, nếu bạn chỉ đơn giản là joinkhông có fetch, bộ sẽ được trả lại initialized = false; do đó vẫn đưa ra truy vấn thứ hai sau khi tập hợp được truy cập. fetchlà chìa khóa để đảm bảo mối quan hệ được tải hoàn toàn và tránh truy vấn thứ hai.
FGreg

Có vẻ như vấn đề với việc thực hiện cả hai và tìm nạp và tham gia là tiêu chí vị ngữ tham gia bị bỏ qua và cuối cùng bạn nhận được mọi thứ trong danh sách hoặc bản đồ. Nếu bạn muốn tất cả mọi thứ, sau đó sử dụng một tìm nạp, nếu bạn muốn một cái gì đó cụ thể, sau đó tham gia, nhưng, như đã nói, tham gia sẽ trống. Điều này đánh bại mục đích sử dụng .LAZY tải.
K.Nicholas

37

Mặc dù đây là một bài viết cũ, vui lòng xem xét sử dụng @NamedEntityGraph (Javax Persistence) và @EntityGraph (Spring Data JPA). Sự kết hợp hoạt động.

Thí dụ

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

và sau đó là repo mùa xuân như dưới đây

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

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

}

1
Lưu ý rằng đó @NamedEntityGraphlà một phần của API JPA 2.1, không được triển khai trong Hibernate trước phiên bản 4.3.0.
naXa

2
@EntityGraph(attributePaths = "employeeGroups")có thể được sử dụng trực tiếp trong Kho lưu trữ dữ liệu mùa xuân để chú thích một phương thức mà không cần @NamedEntityGraphđến @Entity của bạn - ít mã hơn, dễ hiểu khi bạn mở repo.
Desislav Kamenov

13

Bạn có một số lựa chọn

  • Viết một phương thức trên kho lưu trữ trả về một thực thể được khởi tạo như đề xuất của RJ.

Công việc nhiều hơn, hiệu suất tốt nhất.

  • Sử dụng OpenEntityManagerInViewFilter để giữ phiên mở cho toàn bộ yêu cầu.

Công việc ít hơn, thường được chấp nhận trong môi trường web.

  • Sử dụng một lớp trợ giúp để khởi tạo các thực thể khi được yêu cầu.

Ít hoạt động hơn, hữu ích khi OEMIV không có tùy chọn, ví dụ như trong ứng dụng Xoay, nhưng cũng có thể hữu ích đối với việc triển khai kho lưu trữ để khởi tạo bất kỳ thực thể nào trong một lần chụp.

Đối với tùy chọn cuối cùng, tôi đã viết một lớp tiện ích, JpaUtils để khởi tạo các thực thể tại một số deph.

Ví dụ:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}

Vì tất cả các yêu cầu của tôi là các cuộc gọi REST đơn giản không có kết xuất, v.v., về cơ bản giao dịch là toàn bộ yêu cầu của tôi. Cảm ơn vì đầu vào của bạn.
Matsemann

Làm thế nào để tôi làm cái đầu tiên? Tôi biết cách viết một truy vấn, nhưng không biết làm thế nào để nói những gì bạn nói. Bạn có thể vui lòng cho thấy một ví dụ? Sẽ rất hữu ích.
Matsemann

zagyi cung cấp một ví dụ trong câu trả lời của anh ấy, dù sao cũng cảm ơn tôi đã chỉ cho tôi đi đúng hướng.
Matsemann

Tôi không biết lớp của bạn sẽ được gọi như thế nào! không hoàn thành giải pháp lãng phí thời gian của người khác
Shady Sherif

Sử dụng OpenEntityManagerInViewFilter để giữ phiên mở cho toàn bộ yêu cầu.- Ý tưởng tồi. Tôi sẽ thực hiện một yêu cầu bổ sung để tìm nạp tất cả các bộ sưu tập cho các thực thể của tôi.
Yan Khonski


6

Tôi nghĩ rằng bạn cần OpenSessionInViewFilter để giữ phiên của bạn mở trong khi hiển thị chế độ xem (nhưng thực tế không phải là quá tốt).


1
Vì tôi không sử dụng JSP hay bất cứ thứ gì, chỉ cần tạo một REST-api, @Transactional sẽ giúp tôi. Nhưng sẽ hữu ích vào những lúc khác. Cảm ơn.
Matsemann

@Matsemann Tôi biết bây giờ đã muộn ... nhưng bạn có thể sử dụng OpenSessionInViewFilter ngay cả trong bộ điều khiển cũng như phiên sẽ tồn tại cho đến khi phản hồi được biên dịch ...
Vishwas Shashidhar

@Matsemann Cảm ơn! Chú thích giao dịch đã lừa tôi! fyi: Nó thậm chí hoạt động nếu bạn chỉ chú thích siêu lớp của một lớp còn lại.
tuyệt

3

Dữ liệu mùa xuân JpaRepository

Dữ liệu Spring JpaRepositoryđịnh nghĩa hai phương thức sau:

  • getOne, trả về một proxy thực thể phù hợp để thiết lập một liên kết cha @ManyToOnehoặc @OneToOnemẹ khi duy trì một thực thể con .
  • findById, trả về POJO thực thể sau khi chạy câu lệnh SELECT tải thực thể từ bảng được liên kết

Tuy nhiên, trong trường hợp của bạn, bạn đã không gọi một trong hai getOnehoặc findById:

Person person = personRepository.findOne(1L);

Vì vậy, tôi giả sử findOnephương thức này là một phương thức bạn đã xác định trong PersonRepository. Tuy nhiên, findOnephương pháp này không hữu ích lắm trong trường hợp của bạn. Vì bạn cần tìm nạp Personcùng với rolesbộ sưu tập, tốt hơn là sử dụng một findOneWithRolesphương thức thay thế.

Phương pháp dữ liệu mùa xuân tùy chỉnh

Bạn có thể xác định một PersonRepositoryCustomgiao diện, như sau:

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

}

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

Và xác định thực hiện của nó như thế này:

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

Đó là nó!


Có lý do nào khiến bạn tự viết truy vấn và không sử dụng giải pháp như EntityGraph trong câu trả lời @rakpan không? Điều này sẽ không tạo ra kết quả tương tự?
Jeroen Vandevelde

Chi phí sử dụng EntityGraph cao hơn truy vấn JPQL. Về lâu dài, bạn nên viết truy vấn.
Vlad Mihalcea

Bạn có thể giải thích chi tiết trên không (Nó đến từ đâu, có đáng chú ý không, ...)? Vì tôi không hiểu tại sao có chi phí cao hơn nếu cả hai cùng tạo ra một truy vấn.
Jeroen Vandevelde

1
Bởi vì các gói EntityGraphs không được lưu trong bộ nhớ cache như JPQL. Đó có thể là một hit hiệu suất đáng kể.
Vlad Mihalcea

1
Chính xác. Tôi sẽ phải viết một bài báo về nó khi tôi có thời gian.
Vlad Mihalcea

1

Bạn có thể làm tương tự như thế này:

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

Chỉ cần sử dụng faqQuestions.getFaqAnswers (). Size () nin bộ điều khiển của bạn và bạn sẽ nhận được kích thước nếu liệt kê danh sách một cách lười biếng, mà không cần tìm nạp danh sách.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.