เมื่อใช้ getOne และ findOne วิธี Spring Data JPA


154

ฉันมีกรณีใช้งานซึ่งมันเรียกสิ่งต่อไปนี้:

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl getUserControlById(Integer id){
    return this.userControlRepository.getOne(id);
}

สังเกตการแพร่กระจาย@Transactionalมี. REQUIRES_NEWและที่เก็บใช้getOne เมื่อฉันเรียกใช้แอพฉันได้รับข้อความแสดงข้อผิดพลาดต่อไปนี้:

Exception in thread "main" org.hibernate.LazyInitializationException: 
could not initialize proxy - no Session
...

แต่ถ้าผมเปลี่ยนgetOne(id)โดยfindOne(id)ทั้งหมดทำงานได้ดี

BTW ก่อนที่กรณีการใช้งานจะเรียกเมธอดgetUserControlByIdมันได้เรียกใช้เมธอดinsertUserControl แล้ว

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl insertUserControl(UserControl userControl) {
    return this.userControlRepository.save(userControl);
}

ทั้งสองวิธีเป็นPropagation.REQUIRES_NEWเพราะฉันใช้การควบคุมการตรวจสอบอย่างง่าย

ฉันใช้getOneวิธีการเพราะมันถูกกำหนดไว้ในส่วนต่อประสานJpaRepositoryและส่วนต่อประสานที่เก็บของฉันขยายจากตรงนั้นฉันทำงานกับ JPA แน่นอน

JpaRepositoryอินเตอร์เฟซที่ยื่นออกมาจากCrudRepository วิธีการที่กำหนดไว้ในfindOne(id)CrudRepository

คำถามของฉันคือ:

  1. ทำไมgetOne(id)วิธีการล้มเหลว?
  2. เมื่อใดที่ฉันควรใช้getOne(id)วิธีการ?

ฉันกำลังทำงานกับที่เก็บอื่น ๆ และทุกคนใช้getOne(id)วิธีนี้และทำงานได้ดีทุกครั้งเฉพาะเมื่อฉันใช้Propagation.REQUIRES_NEWมันล้มเหลว

ตามgetOne API:

ส่งคืนการอ้างอิงไปยังเอนทิตีที่มีตัวระบุที่กำหนด

ตามfindOne API:

ดึงข้อมูลเอนทิตี้ของ id

3) ฉันควรใช้findOne(id)วิธีนี้เมื่อใด?

4) วิธีการใดที่แนะนำให้ใช้?

ขอบคุณล่วงหน้า.


คุณไม่ควรใช้ getOne () เพื่อทดสอบการมีอยู่ของวัตถุในฐานข้อมูลโดยเฉพาะเนื่องจากด้วย getOne คุณจะได้รับวัตถุเสมอ! = null ในขณะที่ findOne ให้ null
Uwe Allner

คำตอบ:


137

TL; DR

T findOne(ID id)(ชื่อใน API เก่า) / Optional<T> findById(ID id)(ชื่อใน API ใหม่) ต้องอาศัยการEntityManager.find()ดำเนินการที่นิติบุคคลโหลดกระตือรือร้น

T getOne(ID id)ขึ้นอยู่กับการEntityManager.getReference()ดำเนินการที่นิติบุคคลโหลดขี้เกียจ ดังนั้นเพื่อให้แน่ใจว่าการโหลดเอนทิตีที่มีประสิทธิภาพต้องใช้วิธีการที่เรียกใช้

findOne()/findById()มีความชัดเจนและใช้งานง่ายกว่าgetOne()จริงๆ
ดังนั้นในสิ่งที่ดีที่สุดของกรณีโปรดปรานมากกว่าfindOne()/findById()getOne()


การเปลี่ยน API

อย่างน้อยก็จากที่2.0รุ่นที่มีการปรับเปลี่ยนSpring-Data-Jpa ก่อนหน้านี้มันถูกกำหนดในอินเตอร์เฟสเป็น:findOne()
CrudRepository

T findOne(ID primaryKey);

ตอนนี้findOne()วิธีการเดียวที่คุณจะพบCrudRepositoryคือวิธีใดที่กำหนดไว้ในQueryByExampleExecutorอินเทอร์เฟซเป็น:

<S extends T> Optional<S> findOne(Example<S> example);

ที่มีการนำมาใช้ในที่สุดโดยSimpleJpaRepositoryการใช้งานเริ่มต้นของCrudRepositoryอินเทอร์เฟซ
วิธีนี้เป็นแบบสอบถามโดยการค้นหาตัวอย่างและคุณไม่ต้องการแทนที่

ในความเป็นจริงวิธีการที่มีพฤติกรรมเดียวกันยังคงอยู่ใน API ใหม่ แต่ชื่อวิธีการนั้นได้เปลี่ยนไป
มันถูกเปลี่ยนชื่อจากfindOne()เป็นfindById()ในCrudRepositoryอินเทอร์เฟซ:

Optional<T> findById(ID id); 

Optionalตอนนี้ก็ส่งกลับ NullPointerExceptionซึ่งจะไม่เลวร้ายเพื่อป้องกันไม่ให้

ดังนั้นทางเลือกที่เกิดขึ้นจริงคือตอนนี้ระหว่างและ Optional<T> findById(ID id)T getOne(ID id)


สองวิธีที่แตกต่างที่ต้องพึ่งพาวิธีการดึงข้อมูล JPA EntityManager สองวิธี

1) Optional<T> findById(ID id)javadocระบุว่า:

ดึงข้อมูลเอนทิตี้ของ id

เมื่อเราพิจารณาถึงการนำไปปฏิบัติเราจะเห็นว่ามันต้องอาศัยEntityManager.find()การค้นคืน:

public Optional<T> findById(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);

    Class<T> domainType = getDomainClass();

    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }

    LockModeType type = metadata.getLockModeType();

    Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();

    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

และนี่em.find()คือEntityManagerวิธีการประกาศเป็น:

public <T> T find(Class<T> entityClass, Object primaryKey,
                  Map<String, Object> properties);

สถานะ javadoc:

ค้นหาตามคีย์หลักโดยใช้คุณสมบัติที่ระบุ

ดังนั้นการดึงเอนทิตีที่โหลดจึงดูเหมือนว่าจะเกิดขึ้น

2) ในขณะที่รัฐT getOne(ID id)javadoc (เน้นเป็นของฉัน):

ส่งคืนการอ้างอิงไปยังเอนทิตีที่มีตัวระบุที่กำหนด

อันที่จริงคำศัพท์อ้างอิงเป็นบอร์ดจริงๆและ JPA API ไม่ได้ระบุgetOne()วิธีการใด ๆ
ดังนั้นสิ่งที่ดีที่สุดที่จะทำเพื่อทำความเข้าใจในสิ่งที่เสื้อคลุมฤดูใบไม้ผลิจะมองหาการดำเนินการ:

@Override
public T getOne(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

นี่em.getReference()คือEntityManagerวิธีการประกาศเป็น:

public <T> T getReference(Class<T> entityClass,
                              Object primaryKey);

และโชคดีที่EntityManagerjavadoc กำหนดไว้ดีกว่าความตั้งใจของมัน (เน้นเป็นของฉัน):

ได้รับตัวอย่างซึ่งรัฐอาจจะเป็นความจริงอย่างเฉื่อยชา หากกรณีที่ร้องขอไม่ได้อยู่ในฐานข้อมูล EntityNotFoundException จะโยนเมื่อรัฐเช่นมีการเข้าถึงแรก (รันไทม์ผู้ให้บริการการเก็บรักษาได้รับอนุญาตให้โยน EntityNotFoundException เมื่อเรียกใช้ getReference) แอปพลิเคชันไม่ควรคาดหวังว่าสถานะอินสแตนซ์จะพร้อมใช้งานเมื่อไม่ได้ใช้งานเว้นแต่ว่าแอปพลิเคชันนั้นจะเปิดใช้งาน

ดังนั้นการเรียกใช้getOne()อาจส่งคืนเอนทิตีที่ดึงข้อมูลอย่างเกียจคร้าน
ที่นี่การดึงข้อมูลแบบขี้เกียจไม่ได้อ้างอิงถึงความสัมพันธ์ของเอนทิตี แต่เป็นเอนทิตีเอง

หมายความว่าถ้าเราเรียกใช้getOne()และจากนั้นบริบทการคงอยู่ก็ถูกปิดกิจการอาจไม่เคยโหลดและดังนั้นผลลัพธ์จึงไม่สามารถคาดเดาได้จริงๆ
ตัวอย่างเช่นหากวัตถุพร็อกซี่ต่อเนื่องกันคุณอาจได้รับการnullอ้างอิงว่าเป็นผลลัพธ์ที่ทำให้เป็นอนุกรมหรือหากมีการเรียกใช้เมธอดบนวัตถุพร็อกซีข้อยกเว้นLazyInitializationExceptionจะถูกโยนทิ้ง
ดังนั้นในสถานการณ์แบบนี้การโยนของEntityNotFoundExceptionนั่นคือเหตุผลหลักที่ใช้getOne()เพื่อจัดการกับอินสแตนซ์ที่ไม่มีอยู่ในฐานข้อมูลเนื่องจากสถานการณ์ข้อผิดพลาดอาจไม่เคยดำเนินการในขณะที่เอนทิตีไม่มีอยู่

ไม่ว่าในกรณีใดเพื่อให้แน่ใจว่าการโหลดนั้นคุณต้องจัดการเอนทิตีในขณะที่เปิดเซสชัน คุณสามารถทำได้โดยการเรียกใช้วิธีการใด ๆ ในเอนทิตี
หรือใช้findById(ID id)แทนทางเลือกที่ดีกว่า


ทำไม API ที่ไม่ชัดเจน?

เมื่อต้องการเสร็จสิ้นคำถามสองข้อสำหรับนักพัฒนา Spring-Data-JPA:

  • ทำไมไม่ได้มีเอกสารที่ชัดเจนสำหรับgetOne()? เอนทิตีที่ขี้เกียจไม่ได้มีรายละเอียดจริงๆ

  • ทำไมคุณต้องแนะนำgetOne()การห่อEM.getReference()?
    ทำไมไม่เพียงแค่ติดกับวิธีการห่อ: getReference()? วิธีการ EM นี้มีความเฉพาะเจาะจงมากในขณะเดียวกันgetOne() ก็ทำการประมวลผลอย่างง่าย


3
ฉันสับสนว่าทำไม getOne () ไม่ขว้าง EntityNotFoundException แต่ "EntityNotFoundException ของคุณถูกโยนออกมาเมื่อสถานะอินสแตนซ์ถูกเข้าถึงครั้งแรก" อธิบายแนวคิดของฉัน ขอบคุณ
TheCoder

บทสรุปของคำตอบนี้: getOne()ใช้การโหลดแบบขี้เกียจและโยนรายการEntityNotFoundExceptionถ้าไม่พบ findById()โหลดได้ทันทีและส่งคืนค่าว่างหากไม่พบ เนื่องจากมีบางสถานการณ์ที่คาดเดาไม่ได้กับ getOne () จึงขอแนะนำให้ใช้ findById () แทน
Janac Meena

124

ความแตกต่างพื้นฐานคือว่าgetOneขี้เกียจโหลดและfindOneไม่

ลองพิจารณาตัวอย่างต่อไปนี้:

public static String NON_EXISTING_ID = -1;
...
MyEntity getEnt = myEntityRepository.getOne(NON_EXISTING_ID);
MyEntity findEnt = myEntityRepository.findOne(NON_EXISTING_ID);

if(findEnt != null) {
     findEnt.getText(); // findEnt is null - this code is not executed
}

if(getEnt != null) {
     getEnt.getText(); // Throws exception - no data found, BUT getEnt is not null!!!
}

1
ไม่โหลดขี้เกียจหมายความว่ามันจะโหลดเฉพาะเมื่อจะใช้เอนทิตี ดังนั้นฉันคาดหวังว่า getEnt จะเป็นโมฆะและโค้ดภายในสองถ้าไม่ถูกเรียกใช้คุณช่วยอธิบายได้ไหม ขอบคุณ!
Doug

หากห่อไว้ในเว็บเซอร์วิส CompletableFuture <> ฉันพบว่าคุณจะต้องใช้ findOne () และ getOne () เพราะมันเป็นการใช้งานที่ขี้เกียจ
Fratt

76

1. ทำไมเมธอด getOne (id) จึงล้มเหลว

ดูในส่วนนี้ในเอกสาร คุณกำลังแทนที่ธุรกรรมที่มีอยู่แล้วอาจทำให้เกิดปัญหา อย่างไรก็ตามไม่มีข้อมูลเพิ่มเติมอันนี้ยากที่จะตอบ

2. เมื่อใดที่ฉันควรใช้เมธอด getOne (id)

หากไม่มีการขุดเข้าไปใน internals ของ Spring Data JPA ความแตกต่างนั้นดูเหมือนจะเป็นกลไกที่ใช้ในการดึงเอนทิตี

ถ้าคุณมองไปที่JavaDocสำหรับgetOne(ID)ภายใต้ดูเพิ่มเติม :

See Also:
EntityManager.getReference(Class, Object)

ดูเหมือนว่าวิธีนี้เพียงมอบหมายให้การดำเนินงานของผู้จัดการนิติบุคคล JPA

อย่างไรก็ตามเอกสารสำหรับfindOne(ID)ไม่ได้กล่าวถึงนี้

เบาะแสยังอยู่ในชื่อของที่เก็บ JpaRepositoryเป็น JPA เฉพาะและสามารถมอบสิทธิ์การโทรไปยังผู้จัดการกิจการได้หากจำเป็น CrudRepositoryเป็นผู้ไม่เชื่อเรื่องพระเจ้าของเทคโนโลยีการติดตาที่ใช้ ดูกร มันใช้เป็นอินเทอร์เฟซตัวทำเครื่องหมายสำหรับเทคโนโลยีการคงอยู่หลายอย่างเช่น JPA, Neo4Jเป็นต้น

ดังนั้นจึงมีไม่ได้จริงๆ 'แตกต่าง' ในสองวิธีสำหรับกรณีการใช้งานของคุณก็เพียงว่าเป็นทั่วไปมากขึ้นกว่าความเชี่ยวชาญมากขึ้นfindOne(ID) getOne(ID)สิ่งที่คุณใช้นั้นขึ้นอยู่กับคุณและโครงการของคุณ แต่ฉันจะติดกับตัวเองfindOne(ID)เพราะมันทำให้รหัสของคุณมีการใช้งานที่เฉพาะเจาะจงน้อยลงและเปิดประตูให้ย้ายไปสู่สิ่งต่าง ๆ เช่น MongoDB เป็นต้นในอนาคตโดยไม่ต้องปรับโครงสร้างมากเกินไป :)


ขอบคุณ Donovan มีความรู้สึกถึงคำตอบของคุณ
มานูเอลจอร์แดน

20
ฉันคิดว่ามันเป็นความเข้าใจผิดที่จะบอกว่าที่there's not really a 'difference' in the two methodsนี่เพราะมีความแตกต่างใหญ่ในวิธีการดึงข้อมูลและสิ่งที่คุณควรคาดหวังว่าวิธีการที่จะกลับมา คำตอบเพิ่มเติมโดย @davidxxx เน้นเรื่องนี้เป็นอย่างดีและฉันคิดว่าทุกคนที่ใช้ Spring Data JPA ควรตระหนักถึงสิ่งนี้ มิฉะนั้นอาจทำให้เกิดอาการปวดหัวได้เล็กน้อย
fridberg

16

getOneวิธีการผลตอบแทนเพียงการอ้างอิงจากฐานข้อมูล (โหลดขี้เกียจ) ดังนั้นโดยทั่วไปคุณอยู่นอกธุรกรรม ( Transactionalคุณได้รับการประกาศในระดับบริการจะไม่พิจารณา) และข้อผิดพลาดเกิดขึ้น


ดูเหมือน EntityManager.getReference (Class, Object) ส่งคืน "nothing" เนื่องจากเราอยู่ในขอบเขตธุรกรรมใหม่
มานูเอลจอร์แดน

2

ฉันพบว่ายากมากจากคำตอบข้างต้น จากมุมมองการแก้ไขข้อบกพร่องฉันใช้เวลาเกือบ 8 ชั่วโมงในการรู้ความผิดพลาด

ฉันมีการทดสอบสปริง + ไฮเบอร์เนต + รถดัน + โครงการ Mysql ต้องมีความชัดเจน.

ฉันมีเอนทิตีผู้ใช้, สมุดบัญชี คุณทำการคำนวณการทำแผนที่

หนังสือหลายเล่มถูกผูกไว้กับผู้ใช้หนึ่งคน แต่ใน UserServiceImpl ฉันพยายามค้นหามันโดย getOne (userId);

public UserDTO getById(int userId) throws Exception {

    final User user = userDao.getOne(userId);

    if (user == null) {
        throw new ServiceException("User not found", HttpStatus.NOT_FOUND);
    }
    userDto = mapEntityToDto.transformBO(user, UserDTO.class);

    return userDto;
}

ผลลัพธ์ที่เหลือคือ

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 1,
        "name": "TEST_ME",
        "bookList": null
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

รหัสด้านบนไม่สามารถดึงหนังสือที่ผู้ใช้อ่าน

รายการหนังสือนั้นว่างเสมอเนื่องจาก getOne (ID) หลังจากเปลี่ยนเป็น findOne (ID) ผลที่ได้คือ

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 0,
        "name": "Annama",
        "bookList": [
            {
                "id": 2,
                "book_no": "The karma of searching",
            }
        ]
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}


-1

ในขณะที่ spring.jpa.open-in-view เป็นจริงฉันไม่มีปัญหาใด ๆ กับ getOne แต่หลังจากตั้งค่าเป็นเท็จฉันได้รับ LazyInitializationException จากนั้นปัญหาได้รับการแก้ไขโดยแทนที่ด้วย findById
แม้ว่าจะมีวิธีแก้ไขปัญหาอื่นโดยไม่ต้องเปลี่ยนวิธี getOne และนั่นคือวิธี @ @Transactional ที่เรียก repository.getOne (id) ด้วยวิธีนี้การทำธุรกรรมจะมีอยู่และเซสชันจะไม่ถูกปิดในวิธีการของคุณและในขณะที่ใช้เอนทิตีจะไม่มี LazyInitializationException ใด ๆ


-2

ฉันมีปัญหาการทำความเข้าใจที่คล้ายกันว่าทำไม JpaRespository.getOne (id) ไม่ทำงานและเกิดข้อผิดพลาด

ฉันไปและเปลี่ยนเป็น JpaRespository.findById (id) ซึ่งต้องการให้คุณส่งคืนทางเลือก

นี่อาจเป็นความคิดเห็นแรกของฉันใน StackOverflow


น่าเสียดายที่สิ่งนี้ไม่ได้ให้และตอบคำถามหรือปรับปรุงคำตอบที่มีอยู่
JSTL

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