FetchMode ทำงานอย่างไรใน Spring Data JPA


95

ฉันมีความสัมพันธ์ระหว่างโมเดลอ็อบเจ็กต์สามตัวในโปรเจ็กต์ของฉัน (โมเดลและส่วนย่อยที่เก็บในตอนท้ายของโพสต์

เมื่อฉันเรียกPlaceRepository.findByIdมันจะทำให้เกิดการค้นหาสามรายการ:

("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

นั่นเป็นพฤติกรรมที่ค่อนข้างผิดปกติ (สำหรับฉัน) เท่าที่ฉันสามารถบอกได้หลังจากอ่านเอกสาร Hibernate แล้วก็ควรใช้แบบสอบถาม JOIN เสมอ ไม่มีความแตกต่างในแบบสอบถามเมื่อFetchType.LAZYเปลี่ยนเป็น FetchType.EAGERในPlaceคลาส (แบบสอบถามที่มี SELECT เพิ่มเติม) เช่นเดียวกับCityคลาสเมื่อFetchType.LAZYเปลี่ยนเป็น FetchType.EAGER(แบบสอบถามที่มี JOIN)

เมื่อฉันใช้ CityRepository.findByIdการระงับการยิงสองตัวเลือก:

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

เป้าหมายของฉันคือการมีพฤติกรรมที่เหมือนกันในทุกสถานการณ์ (ไม่ว่าจะ JOIN หรือ SELECT เสมอก็ตามควรเข้าร่วม)

คำจำกัดความของโมเดล:

สถานที่:

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

เมือง:

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

ที่เก็บ:

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 ลักษณะที่ 5 วิธีที่จะเริ่มต้น relationsships ขี้เกียจ: thoughts-on-java.org/...
กริก Kislin

คำตอบ:


114

ฉันคิดว่า Spring Data ไม่สนใจ FetchMode ฉันมักจะใช้คำอธิบายประกอบ@NamedEntityGraphและ@EntityGraphคำอธิบายประกอบเมื่อทำงานกับ Spring Data

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

}

ตรวจสอบเอกสารที่นี่


1
ดูเหมือนจะไม่ได้ผลสำหรับฉัน ฉันหมายความว่ามันใช้งานได้ แต่ ... เมื่อฉันใส่คำอธิบายประกอบที่เก็บด้วย '@EntityGraph' มันไม่ทำงานเอง (โดยปกติ) ตัวอย่างเช่น: `เพลส findById (int ID);` งาน แต่ จบลงด้วยข้อยกเว้นList<Place> findAll(); มันทำงานได้เมื่อฉันตนเองเพิ่มorg.springframework.data.mapping.PropertyReferenceException: No property find found for type Place! @Query("select p from Place p")ดูเหมือนว่าจะเป็นวิธีแก้ปัญหาชั่วคราว
SirKometa

อาจจะใช้งานได้ดีกับ findAll () เนื่องจากเป็นวิธีการที่มีอยู่แล้วจากอินเทอร์เฟซ JpaRepository ในขณะที่วิธีอื่นของคุณ "findById" เป็นวิธีการสืบค้นแบบกำหนดเองที่สร้างขึ้นในรันไทม์
wesker317

ฉันตัดสินใจทำเครื่องหมายว่านี่เป็นคำตอบที่ถูกต้องเนื่องจากเป็นคำตอบที่ดีที่สุด มันไม่สมบูรณ์แบบแม้ว่า มันใช้งานได้ในสถานการณ์ส่วนใหญ่ แต่จนถึงตอนนี้ฉันสังเกตเห็นข้อบกพร่องใน spring-data-jpa ที่มี EntityGraphs ที่ซับซ้อนมากขึ้น ขอบคุณ :)
SirKometa

2
@EntityGraphเกือบ ununsable ในสถานการณ์จริงเพราะมันไม่สามารถระบุชนิดของFetchเราต้องการที่จะใช้ ( JOIN, SUBSELECT, SELECT, BATCH) นี้ร่วมกับ@OneToManyสมาคมและทำให้ Hibernate MaxResultsดึงข้อมูลตารางทั้งหมดเพื่อความทรงจำถึงแม้ว่าเราจะใช้แบบสอบถาม
Ondrej Bozek

1
ขอบคุณฉันอยากจะบอกว่าการค้นหาของ JPQL อาจแทนที่กลยุทธ์การดึงข้อมูลเริ่มต้นด้วยนโยบายการดึงข้อมูลที่เลือก
adrhc

53

ก่อนอื่น@Fetch(FetchMode.JOIN)และ@ManyToOne(fetch = FetchType.LAZY)เป็นปฏิปักษ์กันคนหนึ่งสั่งให้ EAGER ดึงข้อมูลในขณะที่อีกคนหนึ่งแนะนำให้ดึง LAZY

การดึงข้อมูลอย่างกระตือรือร้นไม่ค่อยเป็นทางเลือกที่ดีและสำหรับพฤติกรรมที่คาดเดาได้คุณควรใช้JOIN FETCHคำสั่งเวลาสืบค้น:

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
มีวิธีใดบ้างที่จะได้ผลลัพธ์เดียวกันกับ Criteria API และ Spring Data Specifications
svlada

2
ไม่ใช่ส่วนการดึงข้อมูลซึ่งต้องใช้โปรไฟล์การดึงข้อมูล JPA
Vlad Mihalcea

Vlad Mihalcea คุณช่วยแชร์ลิงค์พร้อมตัวอย่างวิธีการทำได้โดยใช้เกณฑ์ Spring Data JPA (ข้อมูลจำเพาะ) ได้ไหม กรุณา
Yan Khonski

ฉันไม่มีตัวอย่างเช่นนี้ แต่คุณสามารถหาได้จากบทเรียน Spring Data JPA
Vlad Mihalcea

หากใช้เวลาสืบค้น ..... คุณจะยังต้องกำหนด @OneToMany ... ฯลฯ บนเอนทิตีหรือไม่
Eric Huang

19

Spring-jpa สร้างคิวรีโดยใช้ตัวจัดการเอนทิตีและไฮเบอร์เนตจะละเว้นโหมดการดึงข้อมูลหากคิวรีสร้างโดยตัวจัดการเอนทิตี

ต่อไปนี้เป็นวิธีแก้ปัญหาที่ฉันใช้:

  1. ใช้ที่เก็บแบบกำหนดเองซึ่งสืบทอดมาจาก SimpleJpaRepository

  2. แทนที่วิธีการ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));
    }
    

    ตรงกลางของวิธีการเพิ่ม applyFetchMode(root);เพื่อใช้โหมดการดึงข้อมูลเพื่อให้ Hibernate สร้างแบบสอบถามด้วยการรวมที่ถูกต้อง

    (น่าเสียดายที่เราจำเป็นต้องคัดลอกเมธอดทั้งหมดและเมธอดส่วนตัวที่เกี่ยวข้องจากคลาสพื้นฐานเนื่องจากไม่มีจุดขยายอื่น ๆ )

  3. ดำเนินการ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);
            }
        }
    }
    

น่าเสียดายที่สิ่งนี้ใช้ไม่ได้กับการสืบค้นที่สร้างโดยใช้ชื่อเมธอดที่เก็บ
Ondrej Bozek

คุณช่วยเพิ่มข้อความนำเข้าทั้งหมดได้ไหม ขอบคุณ.
granadaCoder

3

" FetchType.LAZY" จะเริ่มทำงานสำหรับตารางหลักเท่านั้น หากในรหัสของคุณคุณเรียกใช้วิธีการอื่นใดที่มีการอ้างอิงตารางหลักระบบจะเริ่มการค้นหาเพื่อรับข้อมูลตารางนั้น (เลือกหลายไฟ)

" FetchType.EAGER" จะสร้างการรวมของตารางทั้งหมดรวมถึงตารางหลักที่เกี่ยวข้องโดยตรง (ใช้JOIN)

เมื่อมีการใช้งาน: สมมติว่าคุณ compulsorily ต้องใช้ขึ้นอยู่กับ Informartion FetchType.EAGERตารางแม่แล้วเลือก FetchType.LAZYหากท่านต้องการข้อมูลสำหรับบันทึกบางเพียงแล้วการใช้งาน

จำไว้ว่าFetchType.LAZYจำเป็นต้องมีโรงงานเซสชันฐานข้อมูลที่ใช้งานอยู่ที่ตำแหน่งในรหัสของคุณซึ่งหากคุณเลือกที่จะดึงข้อมูลตารางหลัก

เช่นสำหรับ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

ข้อมูลอ้างอิงเพิ่มเติม


ที่น่าสนใจคือคำตอบนี้ทำให้ฉันมาถูกทางในการใช้NamedEntityGraphเนื่องจากฉันต้องการกราฟวัตถุที่ไม่ไฮเดรต
JJ Zabkar

คำตอบนี้สมควรได้รับการโหวตเพิ่มขึ้น มันรวบรัดและช่วยให้ฉันเข้าใจได้มากว่าทำไมฉันถึงเห็นข้อความค้นหา "ถูกกระตุ้นอย่างน่าอัศจรรย์" มากมาย ... ขอบคุณมาก!
Clint Eastwood

3

โหมดดึงข้อมูลจะใช้งานได้เมื่อเลือกวัตถุด้วยรหัสเท่านั้นentityManager.find()คือการใช้ เนื่องจาก Spring Data จะสร้างแบบสอบถามเสมอการกำหนดค่าโหมดดึงข้อมูลจึงไม่มีประโยชน์สำหรับคุณ คุณสามารถใช้การสืบค้นเฉพาะกับการรวมการดึงข้อมูลหรือใช้กราฟเอนทิตี

เมื่อคุณต้องการประสิทธิภาพที่ดีที่สุดคุณควรเลือกเฉพาะส่วนย่อยของข้อมูลที่คุณต้องการจริงๆ ในการทำเช่นนี้โดยทั่วไปขอแนะนำให้ใช้แนวทาง DTO เพื่อหลีกเลี่ยงการดึงข้อมูลที่ไม่จำเป็น แต่โดยปกติจะส่งผลให้เกิดข้อผิดพลาดค่อนข้างมากในการสร้างรหัสสำเร็จรูปเนื่องจากคุณต้องกำหนดแบบสอบถามเฉพาะที่สร้างแบบจำลอง DTO ของคุณผ่านทาง JPQL นิพจน์ตัวสร้าง

การคาดการณ์ Spring Data สามารถช่วยได้ที่นี่ แต่ในบางจุดคุณจะต้องมีโซลูชันเช่นBlaze-Persistence Entity Viewsซึ่งทำให้สิ่งนี้ค่อนข้างง่ายและมีคุณสมบัติอื่น ๆ อีกมากมายในปลอกแขนที่จะมีประโยชน์! คุณเพียงแค่สร้างอินเทอร์เฟซ DTO ต่อเอนทิตีโดยที่ getters แสดงถึงชุดข้อมูลย่อยที่คุณต้องการ วิธีแก้ปัญหาของคุณอาจมีลักษณะดังนี้

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

ข้อจำกัดความรับผิดชอบฉันเป็นผู้เขียน Blaze-Persistence ดังนั้นฉันอาจจะลำเอียง


2

ฉันอธิบายรายละเอียดเกี่ยวกับคำตอบของdream83619เพื่อจัดการกับ@Fetchคำอธิบายประกอบไฮเบอร์เนตที่ซ้อนกัน ฉันใช้วิธีเรียกซ้ำเพื่อค้นหาคำอธิบายประกอบในคลาสที่เชื่อมโยงกัน

ดังนั้นคุณต้องใช้ที่เก็บแบบกำหนดเองและgetQuery(spec, domainClass, sort)วิธีการแทนที่ น่าเสียดายที่คุณต้องคัดลอกวิธีการส่วนตัวที่อ้างอิงทั้งหมด :(

นี่คือรหัสวิธีการส่วนตัวที่คัดลอกจะถูกละไว้
แก้ไข:เพิ่มวิธีการส่วนตัวที่เหลือ

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

ฉันกำลังลองวิธีแก้ปัญหาของคุณ แต่ฉันมีตัวแปรข้อมูลเมตาส่วนตัวในหนึ่งในวิธีการคัดลอกที่ทำให้เกิดปัญหา คุณสามารถแบ่งปันรหัสสุดท้ายได้หรือไม่?
Homer1980ar

การเรียกซ้ำไม่ทำงาน ถ้าฉันมี OneToMany มันจะผ่าน java.util.List ไปยังการทำซ้ำครั้งต่อไป
antohoho

ยังไม่ได้ทดสอบดี แต่คิดว่าน่าจะเป็นแบบนี้ ((Join) โคตร) .getJavaType () แทน field.getType () เมื่อเรียกใช้แบบเรียกซ้ำ FetchMode
antohoho

2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
จากลิงค์นี้:

หากคุณใช้ JPA ที่ด้านบนของ Hibernate จะไม่มีวิธีใดที่จะตั้งค่า FetchMode ที่ Hibernate ใช้เป็น JOIN ได้อย่างไรก็ตามหากคุณใช้ JPA ที่ด้านบนของ Hibernate จะไม่มีวิธีตั้งค่า FetchMode ที่ Hibernate ใช้เป็น JOIN

ไลบรารี Spring Data JPA มี Domain Driven Design Specifications API ที่ช่วยให้คุณสามารถควบคุมลักษณะการทำงานของแบบสอบถามที่สร้างขึ้น

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

อ้างอิงจาก Vlad Mihalcea (ดูhttps://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

ข้อความค้นหา JPQL อาจแทนที่กลยุทธ์การดึงข้อมูลเริ่มต้น หากเราไม่ประกาศอย่างชัดเจนว่าต้องการดึงข้อมูลอะไรโดยใช้คำสั่งการดึงข้อมูลเข้าร่วมด้านในหรือด้านซ้ายระบบจะใช้นโยบายการดึงข้อมูลเริ่มต้น

ดูเหมือนว่าแบบสอบถาม JPQL อาจแทนที่กลยุทธ์การดึงข้อมูลที่คุณประกาศไว้ดังนั้นคุณจะต้องใช้join fetchเพื่อโหลดเอนทิตีที่อ้างอิงบางส่วนอย่างกระตือรือร้นหรือเพียงแค่โหลดตาม id ด้วย EntityManager (ซึ่งจะเป็นไปตามกลยุทธ์การดึงข้อมูลของคุณ แต่อาจไม่ใช่วิธีแก้ปัญหาสำหรับกรณีการใช้งานของคุณ ).

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.