วิธีดึงข้อมูลการเชื่อมโยง FetchType.LAZY กับ JPA และไฮเบอร์เนตใน Spring Controller


146

ฉันมีคลาสบุคคล:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

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

ด้วยความสัมพันธ์แบบหลายต่อหลายคนที่ขี้เกียจ

ในตัวควบคุมของฉันฉันมี

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

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

และ PersonRepository เป็นเพียงรหัสนี้เขียนตามคู่มือนี้

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

อย่างไรก็ตามในคอนโทรลเลอร์นี้ฉันต้องการข้อมูลแบบขี้เกียจ ฉันจะทริกเกอร์การโหลดได้อย่างไร

การพยายามเข้าถึงมันจะล้มเหลวด้วย

ไม่สามารถเริ่มต้นการรวบรวมบทบาทอย่างเกียจคร้าน: no.dusken.momus.model.Person.roles ไม่สามารถเริ่มต้นพร็อกซี - ไม่มีเซสชัน

หรือข้อยกเว้นอื่น ๆ ขึ้นอยู่กับสิ่งที่ฉันลอง

คำอธิบาย xmlของฉันในกรณีที่จำเป็น

ขอบคุณ


คุณสามารถเขียนวิธีซึ่งจะสร้างแบบสอบถามเพื่อดึงPersonวัตถุที่กำหนดพารามิเตอร์ได้หรือไม่? ในนั้นQueryรวมถึงfetchข้อและโหลดRolesเกินไปสำหรับบุคคล
SudoRahul

คำตอบ:


206

คุณจะต้องโทรหาคอลเล็กชั่นขี้เกียจอย่างชัดเจนเพื่อเริ่มต้น (การปฏิบัติทั่วไปคือการโทร.size()เพื่อจุดประสงค์นี้) ในไฮเบอร์เนตมีวิธีการเฉพาะสำหรับสิ่งนี้ ( Hibernate.initialize()) แต่ JPA ไม่มีสิ่งใดเทียบเท่า @Transactionalแน่นอนคุณจะต้องให้แน่ใจว่าการภาวนาจะทำเมื่อเซสชั่นยังคงมีอยู่ดังนั้นคำอธิบายวิธีการควบคุมของคุณด้วย อีกทางเลือกหนึ่งคือการสร้างเลเยอร์บริการขั้นกลางระหว่างตัวควบคุมและพื้นที่เก็บข้อมูลที่สามารถเปิดเผยวิธีการที่เริ่มต้นคอลเลกชันขี้เกียจ

ปรับปรุง:

โปรดทราบว่าวิธีการแก้ปัญหาข้างต้นนั้นง่าย แต่ผลลัพธ์ในการสืบค้นสองแบบที่แตกต่างกันไปยังฐานข้อมูล หากคุณต้องการบรรลุผลการดำเนินงานที่ดีขึ้นให้เพิ่มวิธีการต่อไปนี้ลงในอินเตอร์เฟสที่เก็บ 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);

}

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


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

ตอนนี้ฉันขอตัวอย่างจากคำตอบของ Jose ต้องยอมรับว่าฉันไม่เข้าใจทั้งหมด
Matsemann

โปรดตรวจสอบวิธีแก้ปัญหาที่เป็นไปได้สำหรับวิธีการค้นหาที่ต้องการในคำตอบที่อัปเดตของฉัน
zagyi

7
สิ่งที่น่าสนใจที่ควรทราบหากคุณjoinไม่มีfetchชุดจะส่งคืนพร้อมinitialized = false; ดังนั้นจึงยังคงออกแบบสอบถามที่สองเมื่อเข้าถึงชุด fetchเป็นกุญแจสำคัญในการตรวจสอบให้แน่ใจว่าความสัมพันธ์จะถูกโหลดอย่างสมบูรณ์และหลีกเลี่ยงแบบสอบถามที่สอง
FGreg

ดูเหมือนว่าปัญหาที่เกิดขึ้นกับการทำทั้งสองอย่างและการดึงข้อมูลและการเข้าร่วมก็คือการเข้าร่วมเกณฑ์ภาคแสดงนั้นจะถูกเพิกเฉยและคุณจะได้รับทุกอย่างในรายการหรือแผนที่ หากคุณต้องการทุกอย่างจากนั้นใช้การดึงข้อมูลหากคุณต้องการบางสิ่งที่เฉพาะเจาะจงจากนั้นเข้าร่วม แต่ตามที่ได้กล่าวไว้การเข้าร่วมจะว่างเปล่า นี่เป็นจุดประสงค์ของการใช้. LAZY loading
K.Nicholas

37

แม้ว่านี่จะเป็นโพสต์เก่าโปรดลองใช้ @NamedEntityGraph (Javax Persistence) และ @EntityGraph (Spring Data JPA) การรวมกันทำงาน

ตัวอย่าง

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

และจากนั้น repo ฤดูใบไม้ผลิดังต่อไปนี้

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

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

}

1
โปรดทราบว่า@NamedEntityGraphเป็นส่วนหนึ่งของ JPA 2.1 API ซึ่งไม่ได้นำมาใช้ในไฮเบอร์เนตก่อนรุ่น 4.3.0
naXa

2
@EntityGraph(attributePaths = "employeeGroups")สามารถนำไปใช้โดยตรงใน Spring Data Repository เพื่อทำหมายเหตุประกอบเมธอดโดยไม่ต้องใช้@NamedEntityGraph@Entity บนโค้ดของคุณน้อยลงเข้าใจง่ายเมื่อคุณเปิด repo
Desislav Kamenov

13

คุณมีตัวเลือกบางอย่าง

  • เขียนวิธีการในพื้นที่เก็บข้อมูลที่ส่งกลับนิติบุคคลเริ่มต้นตามที่แนะนำ RJ

ทำงานได้มากขึ้นประสิทธิภาพที่ดีที่สุด

  • ใช้ OpenEntityManagerInViewFilter เพื่อเปิดเซสชันสำหรับคำขอทั้งหมด

งานน้อยลงซึ่งเป็นที่ยอมรับในสภาพแวดล้อมทางเว็บ

  • ใช้คลาสตัวช่วยเพื่อเริ่มต้นเอนทิตีเมื่อจำเป็น

ทำงานน้อยลงมีประโยชน์เมื่อไม่มีตัวเลือก OEMIV ตัวอย่างเช่นในแอปพลิเคชัน Swing แต่อาจมีประโยชน์เช่นกันในการใช้งานที่เก็บเพื่อเริ่มต้นเอนทิตีใด ๆ ในนัดเดียว

สำหรับตัวเลือกสุดท้ายฉันเขียนคลาสยูทิลิตี้JpaUtilsเพื่อเริ่มต้นเอนทิตีที่ deph

ตัวอย่างเช่น:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

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

เนื่องจากคำขอทั้งหมดของฉันเป็นการเรียกแบบ REST แบบง่ายโดยไม่มีการเรนเดอร์ ฯลฯ ธุรกรรมจึงเป็นคำขอทั้งหมดของฉัน ขอบคุณสำหรับข้อมูลของคุณ
Matsemann

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

zagyi ให้ตัวอย่างในคำตอบของเขาขอบคุณที่ชี้ให้ฉันไปในทิศทางที่ถูกต้องอย่างไรก็ตาม
Matsemann

ฉันไม่รู้ว่าคลาสของคุณจะถูกเรียก! การแก้ปัญหายังไม่เสร็จสิ้นทำให้ผู้อื่นเสียเวลา
Shady Sherif

ใช้ OpenEntityManagerInViewFilter เพื่อเปิดเซสชันสำหรับคำขอทั้งหมด - แนวคิดที่ไม่ดี ฉันจะขอเพิ่มเติมเพื่อดึงคอลเลกชันทั้งหมดสำหรับเอนทิตีของฉัน
Yan Khonski

8

สามารถโหลดได้อย่างเกียจคร้านขณะทำธุรกรรมเท่านั้น ดังนั้นคุณสามารถเข้าถึงคอลเลกชันในพื้นที่เก็บข้อมูลของคุณซึ่งมีธุรกรรม - หรือสิ่งที่ฉันทำตามปกติคือ a get with associationหรือตั้งค่า fetchmode เป็นกระตือรือร้น


6

ฉันคิดว่าคุณต้องการOpenSessionInViewFilterเพื่อให้เซสชันของคุณเปิดอยู่ในระหว่างการแสดงผลมุมมอง (แต่มันก็ไม่ใช่วิธีปฏิบัติที่ดีเกินไป)


1
เนื่องจากฉันไม่ได้ใช้ JSP หรืออะไรเลยเพียงแค่สร้าง REST-api @Transactional จะทำเพื่อฉัน แต่จะมีประโยชน์ในเวลาอื่น ๆ ขอบคุณ
Matsemann

@Matsemann ฉันรู้ว่ามันสายไปแล้ว ... แต่คุณสามารถใช้ประโยชน์จาก OpenSessionInViewFilter ได้แม้ในคอนโทรลเลอร์และเซสชั่นจะมีอยู่จนกว่าคำตอบจะถูกคอมไพล์ ...
Vishwas Shashidhar

@Matsemann ขอบคุณ! ทรานแซกชันหมายเหตุประกอบทำเคล็ดลับสำหรับฉัน! fyi: มันทำงานได้ดีถ้าคุณแค่ใส่คำอธิบายประกอบ superclass ของคลาสเรียน
desperateCoder

3

Spring Data JpaRepository

Spring Data JpaRepositoryกำหนดสองวิธีต่อไปนี้:

  • getOneซึ่งส่งกลับพร็อกซี่นิติบุคคลที่เหมาะสำหรับการตั้งค่า@ManyToOneหรือ@OneToOneสมาคมผู้ปกครองเมื่อ persisting นิติบุคคลเด็ก
  • findByIdซึ่งส่งคืนเอนทิตี POJO หลังจากรันคำสั่ง SELECT ที่โหลดเอนทิตีจากตารางที่เกี่ยวข้อง

อย่างไรก็ตามในกรณีของคุณคุณไม่ได้โทรอย่างใดอย่างหนึ่งgetOneหรือfindById:

Person person = personRepository.findOne(1L);

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

Custom Spring Data วิธีการ

คุณสามารถกำหนดPersonRepositoryCustomอินเตอร์เฟสดังนี้:

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

}

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

และกำหนดการใช้งานเช่นนี้:

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

แค่นั้นแหละ!


มีเหตุผลที่คุณเขียนแบบสอบถามด้วยตัวเองและไม่ได้ใช้โซลูชันเช่น EntityGraph ใน @rakpan คำตอบหรือไม่ สิ่งนี้จะไม่ให้ผลลัพธ์เดียวกันหรือไม่
Jeroen Vandevelde

ค่าใช้จ่ายในการใช้ EntityGraph สูงกว่าแบบสอบถาม JPQL ในระยะยาวคุณควรเขียนแบบสอบถาม
Vlad Mihalcea

คุณสามารถอธิบายรายละเอียดเกี่ยวกับค่าใช้จ่าย (มันมาจากไหนมันเป็นที่สังเกตได้ ... )? เพราะฉันไม่เข้าใจว่าทำไมมีค่าใช้จ่ายสูงกว่าหากพวกเขาทั้งสองสร้างแบบสอบถามเดียวกัน
Jeroen Vandevelde

1
เพราะแผน EntityGraphs ไม่ได้ถูกแคชเหมือน JPQL ที่สามารถตีประสิทธิภาพที่สำคัญ
Vlad Mihalcea

1
เผง ฉันจะต้องเขียนบทความเกี่ยวกับเรื่องนี้เมื่อฉันมีเวลา
Vlad Mihalcea

1

คุณสามารถทำเช่นนี้ได้:

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

เพียงใช้ faqQuestions.getFaqAnswers (). size () nin ตัวควบคุมของคุณและคุณจะได้รับขนาดถ้ารายการ intialised ขี้เกียจโดยไม่ต้องดึงรายการตัวเอง

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