FetchMode hoạt động như thế nào trong Spring Data JPA


91

Tôi thực sự có mối quan hệ giữa ba đối tượng mô hình trong dự án của mình (mô hình và đoạn mã kho lưu trữ ở cuối bài đăng.

Khi tôi gọi, PlaceRepository.findByIdnó sẽ kích hoạt ba truy vấn được chọn:

("sql")

  1. SELECT * FROM place p where id = arg
  2. SELECT * FROM user u where u.id = place.user.id
  3. SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id

Đó là hành vi khá bất thường (đối với tôi). Theo như tôi có thể nói sau khi đọc tài liệu Hibernate, nó sẽ luôn sử dụng các truy vấn JOIN. Không có sự khác biệt nào trong các truy vấn khi được FetchType.LAZYthay đổi thành FetchType.EAGERtrong Placelớp (truy vấn có thêm SELECT), cũng giống như đối với Citylớp khi được FetchType.LAZYthay đổi thành FetchType.EAGER(truy vấn với JOIN).

Khi tôi sử dụng CityRepository.findByIdđám cháy dập tắt, hai lựa chọn:

  1. SELECT * FROM city c where id = arg
  2. SELECT * FROM state s where id = city.state.id

Mục tiêu của tôi là có một hành vi sam trong tất cả các tình huống (luôn luôn THAM GIA hoặc CHỌN, mặc dù ưu tiên THAM GIA).

Định nghĩa mô hình:

Địa điểm:

@Entity
@Table(name = "place")
public class Place extends Identified {

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_user_author")
    private User author;

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_city_id")
    private City city;
    //getters and setters
}

Thành phố:

@Entity
@Table(name = "area_city")
public class City extends Identified {

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_woj_id")
    private State state;
    //getters and setters
}

Kho lưu trữ:

PlaceRepository

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
    Place findById(int id);
}

UserRepository:

public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findAll();
    User findById(int id);
}

CityRepository:

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    City findById(int id);
}

Hava xem 5 cách để khởi tạo relationsships lười biếng: thoughts-on-java.org/...
Grigory Kislin

Câu trả lời:


109

Tôi nghĩ rằng Dữ liệu mùa xuân bỏ qua FetchMode. Tôi luôn sử dụng @NamedEntityGraph@EntityGraphchú thích khi làm việc với Dữ liệu mùa xuân

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  
}

@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}

Kiểm tra tài liệu tại đây


1
Tôi dường như không làm việc cho tôi. Ý tôi là nó hoạt động nhưng ... Khi tôi chú thích kho lưu trữ bằng '@EntityGraph', nó không tự hoạt động (thường). Ví dụ: `Place findById (int id);` hoạt động nhưng List<Place> findAll();kết thúc bằng Ngoại lệ org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Nó hoạt động khi tôi thêm thủ công @Query("select p from Place p"). Có vẻ như giải pháp thay thế.
SirKometa

Có thể nó hoạt động liều lượng trên findAll () vì nó là một phương thức hiện có từ giao diện JpaRepository trong khi phương thức khác của bạn "findById" là một phương thức truy vấn tùy chỉnh được tạo trong thời gian chạy.
wesker317

Tôi đã quyết định đánh dấu đây là câu trả lời thích hợp vì nó là câu trả lời tốt nhất. Nó không phải là hoàn hảo mặc dù. Nó hoạt động trong hầu hết các tình huống nhưng cho đến nay tôi đã nhận thấy lỗi trong spring-data-jpa với EntityGraphs phức tạp hơn. Cảm ơn :)
SirKometa

2
@EntityGraphgần như ununsable trong kịch bản thực tế vì nó không thể được xác định những loại Fetchchúng tôi muốn sử dụng ( JOIN, SUBSELECT, SELECT, BATCH). Điều này kết hợp với @OneToManyliên kết và làm cho Hibernate Tìm nạp toàn bộ bảng vào bộ nhớ ngay cả khi chúng ta sử dụng truy vấn MaxResults.
Ondrej Bozek

1
Cảm ơn, tôi muốn nói rằng các truy vấn JPQL có thể ghi đè chiến lược tìm nạp mặc định với chính sách tìm nạp chọn lọc .
adrhc

51

Trước hết, @Fetch(FetchMode.JOIN)@ManyToOne(fetch = FetchType.LAZY)là đối kháng, một hướng dẫn tìm nạp EAGER, trong khi hướng dẫn kia đề xuất tìm nạp LAZY.

Tìm nạp háo hức hiếm khi là một lựa chọn tốt và đối với một hành vi có thể dự đoán được, bạn tốt hơn nên sử dụng JOIN FETCHchỉ thị thời gian truy vấn :

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {

    @Query(value = "SELECT p FROM Place p LEFT JOIN FETCH p.author LEFT JOIN FETCH p.city c LEFT JOIN FETCH c.state where p.id = :id")
    Place findById(@Param("id") int id);
}

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom { 
    @Query(value = "SELECT c FROM City c LEFT JOIN FETCH c.state where c.id = :id")   
    City findById(@Param("id") int id);
}

3
Có cách nào để đạt được kết quả tương tự với API tiêu chí và Đặc tả dữ liệu mùa xuân không?
svlada

2
Không phải phần tìm nạp, yêu cầu cấu hình tìm nạp JPA.
Vlad Mihalcea

Vlad Mihalcea, bạn có thể chia sẻ liên kết với ví dụ về cách thực hiện việc này bằng cách sử dụng tiêu chí Spring Data JPA (đặc điểm kỹ thuật) không? Please
Yan Khonski 20/09/16

Tôi không có bất kỳ ví dụ nào như vậy, nhưng bạn chắc chắn có thể tìm thấy một ví dụ trong các hướng dẫn về Spring Data JPA.
Vlad Mihalcea

nếu sử dụng thời gian truy vấn ..... bạn có cần xác định @OneToMany ... vv trên thực thể không?
Eric Huang

19

Spring-jpa tạo truy vấn bằng trình quản lý thực thể và Hibernate sẽ bỏ qua chế độ tìm nạp nếu truy vấn được tạo bởi trình quản lý thực thể.

Sau đây là công việc xung quanh mà tôi đã sử dụng:

  1. Triển khai một kho lưu trữ tùy chỉnh kế thừa từ SimpleJpaRepository

  2. Ghi đè phương thức getQuery(Specification<T> spec, Sort sort):

    @Override
    protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) { 
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> query = builder.createQuery(getDomainClass());
    
        Root<T> root = applySpecificationToCriteria(spec, query);
        query.select(root);
    
        applyFetchMode(root);
    
        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }
    
        return applyRepositoryMethodMetadata(entityManager.createQuery(query));
    }

    Ở giữa phương pháp, thêm applyFetchMode(root);để áp dụng chế độ tìm nạp, để làm cho Hibernate tạo truy vấn với kết nối chính xác.

    (Thật không may, chúng tôi cần sao chép toàn bộ phương thức và các phương thức riêng có liên quan từ lớp cơ sở vì không có điểm mở rộng nào khác.)

  3. Thực hiện applyFetchMode:

    private void applyFetchMode(Root<T> root) {
        for (Field field : getDomainClass().getDeclaredFields()) {
    
            Fetch fetch = field.getAnnotation(Fetch.class);
    
            if (fetch != null && fetch.value() == FetchMode.JOIN) {
                root.fetch(field.getName(), JoinType.LEFT);
            }
        }
    }

Rất tiếc, điều này không hoạt động đối với các truy vấn được tạo bằng tên phương thức kho lưu trữ.
Ondrej Bozek

bạn có thể vui lòng thêm tất cả các báo cáo nhập khẩu không? cảm ơn bạn.
granadaCoder

3

" FetchType.LAZY" sẽ chỉ kích hoạt cho bảng chính. Nếu trong mã của bạn, bạn gọi bất kỳ phương thức nào khác có phụ thuộc bảng mẹ thì nó sẽ kích hoạt truy vấn để lấy thông tin bảng đó. (CÓ NHIỀU LỰA CHỌN)

" FetchType.EAGER" sẽ tạo liên kết của tất cả các bảng bao gồm các bảng mẹ có liên quan trực tiếp. (CÔNG DỤNG JOIN)

Khi nào sử dụng: Giả sử bạn bắt buộc phải sử dụng thông báo bảng cha phụ thuộc rồi chọn FetchType.EAGER. Nếu bạn chỉ cần thông tin cho một số hồ sơ nhất định thì hãy sử dụngFetchType.LAZY .

Hãy nhớ rằng, FetchType.LAZYcần một nhà máy phiên db đang hoạt động tại vị trí trong mã của bạn, nơi nếu bạn chọn truy xuất thông tin bảng mẹ.

Ví dụ LAZY:

.. Place fetched from db from your dao loayer
.. only place table information retrieved
.. some code
.. getCity() method called... Here db request will be fired to get city table info

Tham khảo thêm


Thật thú vị, câu trả lời này đã giúp tôi đi đúng hướng để sử dụng NamedEntityGraphvì tôi muốn có một biểu đồ đối tượng không ngậm nước.
JJ Zabkar

câu trả lời này xứng đáng nhận được nhiều ủng hộ hơn. Nó ngắn gọn và đã giúp tôi rất nhiều để hiểu tại sao tôi lại thấy nhiều truy vấn "được kích hoạt một cách kỳ diệu" ... cảm ơn rất nhiều!
Clint Eastwood

3

Chế độ tìm nạp sẽ chỉ hoạt động khi chọn đối tượng bằng id tức là sử dụng entityManager.find(). Vì Dữ liệu mùa xuân sẽ luôn tạo một truy vấn, nên cấu hình chế độ tìm nạp sẽ không có ích cho bạn. Bạn có thể sử dụng các truy vấn chuyên dụng với các phép nối tìm nạp hoặc sử dụng đồ thị thực thể.

Khi bạn muốn có hiệu suất tốt nhất, bạn chỉ nên chọn tập hợp con dữ liệu bạn thực sự cần. Để thực hiện việc này, bạn nên sử dụng phương pháp tiếp cận DTO để tránh tìm nạp dữ liệu không cần thiết, nhưng điều đó thường dẫn đến khá nhiều mã soạn sẵn dễ bị lỗi, vì bạn cần xác định một truy vấn chuyên dụng xây dựng mô hình DTO của bạn thông qua JPQL biểu thức hàm tạo.

Các phép chiếu Spring Data có thể hữu ích ở đây, nhưng tại một số thời điểm, bạn sẽ cần một giải pháp như Blaze-Persistence Entity Views , giúp điều này trở nên khá dễ dàng và có nhiều tính năng hơn trong tay áo sẽ hữu ích! Bạn chỉ cần tạo một giao diện DTO cho mỗi thực thể trong đó các getters đại diện cho tập con dữ liệu bạn cần. Giải pháp cho vấn đề của bạn có thể trông như thế này

@EntityView(Identified.class)
public interface IdentifiedView {
    @IdMapping
    Integer getId();
}

@EntityView(Identified.class)
public interface UserView extends IdentifiedView {
    String getName();
}

@EntityView(Identified.class)
public interface StateView extends IdentifiedView {
    String getName();
}

@EntityView(Place.class)
public interface PlaceView extends IdentifiedView {
    UserView getAuthor();
    CityView getCity();
}

@EntityView(City.class)
public interface CityView extends IdentifiedView {
    StateView getState();
}

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
    PlaceView findById(int id);
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserView> findAllByOrderByIdAsc();
    UserView findById(int id);
}

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    CityView findById(int id);
}

Tuyên bố từ chối trách nhiệm, tôi là tác giả của Blaze-Persistence, vì vậy tôi có thể có thành kiến.


2

Tôi đã giải thích cặn kẽ về câu trả lời dream83619 để làm cho nó xử lý các @Fetchchú thích Hibernate lồng nhau . Tôi đã sử dụng phương pháp đệ quy để tìm các chú thích trong các lớp liên kết lồng nhau.

Vì vậy, bạn phải triển khai kho lưu trữ tùy chỉnh và ghi đègetQuery(spec, domainClass, sort) phương pháp . Thật không may, bạn cũng phải sao chép tất cả các phương thức riêng được tham chiếu :(.

Đây là mã, các phương thức riêng tư đã sao chép bị bỏ qua.
EDIT: Đã thêm các phương thức riêng tư còn lại.

@NoRepositoryBean
public class EntityGraphRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {

    private final EntityManager em;
    protected JpaEntityInformation<T, ?> entityInformation;

    public EntityGraphRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.em = entityManager;
        this.entityInformation = entityInformation;
    }

    @Override
    protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);

        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

        query.select(root);
        applyFetchMode(root);

        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }

        return applyRepositoryMethodMetadata(em.createQuery(query));
    }

    private Map<String, Join<?, ?>> joinCache;

    private void applyFetchMode(Root<? extends T> root) {
        joinCache = new HashMap<>();
        applyFetchMode(root, getDomainClass(), "");
    }

    private void applyFetchMode(FetchParent<?, ?> root, Class<?> clazz, String path) {
        for (Field field : clazz.getDeclaredFields()) {
            Fetch fetch = field.getAnnotation(Fetch.class);

            if (fetch != null && fetch.value() == FetchMode.JOIN) {
                FetchParent<?, ?> descent = root.fetch(field.getName(), JoinType.LEFT);
                String fieldPath = path + "." + field.getName();
                joinCache.put(path, (Join) descent);

                applyFetchMode(descent, field.getType(), fieldPath);
            }
        }
    }

    /**
     * Applies the given {@link Specification} to the given {@link CriteriaQuery}.
     *
     * @param spec can be {@literal null}.
     * @param domainClass must not be {@literal null}.
     * @param query must not be {@literal null}.
     * @return
     */
    private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
        CriteriaQuery<S> query) {

        Assert.notNull(query);
        Assert.notNull(domainClass);
        Root<U> root = query.from(domainClass);

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
        if (getRepositoryMethodMetadata() == null) {
            return query;
        }

        LockModeType type = getRepositoryMethodMetadata().getLockModeType();
        TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);

        applyQueryHints(toReturn);

        return toReturn;
    }

    private void applyQueryHints(Query query) {
        for (Map.Entry<String, Object> hint : getQueryHints().entrySet()) {
            query.setHint(hint.getKey(), hint.getValue());
        }
    }

    public Class<T> getEntityType() {
        return entityInformation.getJavaType();
    }

    public EntityManager getEm() {
        return em;
    }
}

Tôi đang thử giải pháp của bạn nhưng tôi có một biến siêu dữ liệu riêng trong một trong các phương pháp sao chép đang gây ra sự cố. Bạn có thể chia sẻ mã cuối cùng?
Homer1980ar

Tìm nạp đệ quy không hoạt động. Nếu tôi có OneToMany, nó sẽ vượt qua java.util. Hãy nghe lần lặp tiếp theo
antohoho

chưa thử nghiệm nó tốt, nhưng nghĩ rằng nó phải là một cái gì đó như thế này ((Tham gia) gốc) .getJavaType () thay vì field.getType () khi gọi đệ quy applyFetchMode
antohoho

2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
từ liên kết này:

nếu bạn đang sử dụng JPA trên Hibernate, không có cách nào để đặt FetchMode được Hibernate sử dụng thành JOINH Tuy nhiên, nếu bạn đang sử dụng JPA trên Hibernate, không có cách nào để đặt FetchMode được Hibernate sử dụng để THAM GIA.

Thư viện Spring Data JPA cung cấp API Thông số kỹ thuật thiết kế theo hướng miền cho phép bạn kiểm soát hành vi của truy vấn được tạo.

final long userId = 1;

final Specification<User> spec = new Specification<User>() {
   @Override
    public Predicate toPredicate(final Root<User> root, final 
     CriteriaQuery<?> query, final CriteriaBuilder cb) {
    query.distinct(true);
    root.fetch("permissions", JoinType.LEFT);
    return cb.equal(root.get("id"), userId);
 }
};

List<User> users = userRepository.findAll(spec);

2

Theo Vlad Mihalcea (xem https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

Các truy vấn JPQL có thể ghi đè chiến lược tìm nạp mặc định. Nếu chúng tôi không khai báo rõ ràng những gì chúng tôi muốn tìm nạp bằng cách sử dụng chỉ thị tìm nạp kết hợp bên trong hoặc bên trái, thì chính sách tìm nạp lựa chọn mặc định sẽ được áp dụng.

Có vẻ như truy vấn JPQL có thể ghi đè chiến lược tìm nạp đã khai báo của bạn, vì vậy bạn sẽ phải sử dụng join fetchđể tải một cách háo hức một số thực thể được tham chiếu hoặc chỉ cần tải bằng id với EntityManager (sẽ tuân theo chiến lược tìm nạp của bạn nhưng có thể không phải là giải pháp cho trường hợp sử dụng của bạn ).

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.