จะบังคับให้คอมมิตด้วยตนเองในวิธี @Transactional ได้อย่างไร [ทำซ้ำ]

ฉันใช้ Spring / Spring-data-JPA และพบว่าตัวเองจำเป็นต้องบังคับการกระทำในการทดสอบหน่วยด้วยตนเอง กรณีการใช้งานของฉันคือฉันกำลังทำการทดสอบแบบมัลติเธรดโดยที่ฉันต้องใช้ข้อมูลที่มีอยู่ก่อนที่เธรดจะถูกสร้างขึ้น

ขออภัย เนื่องจากการทดสอบดำเนินอยู่ในธุรกรรม @Transactional แม้แต่ flush ก็ไม่สามารถทำให้เธรดที่สร้างสามารถเข้าถึงได้

   @Transactional   
   public void testAddAttachment() throws Exception{
        final Contract c1 = contractDOD.getNewTransientContract(15);
        contractRepository.save(c1);

        // Need to commit the saveContract here, but don't know how!                
        em.getTransaction().commit();

        List<Thread> threads = new ArrayList<>();
        for( int i = 0; i < 5; i++){
            final int threadNumber = i; 
            Thread t =  new Thread( new Runnable() {
                @Override
                @Transactional
                public void run() {
                    try {
                        // do stuff here with c1

                        // sleep to ensure that the thread is not finished before another thread catches up
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
            threads.add(t);
            t.start();
        }

        // have to wait for all threads to complete
        for( Thread t : threads )
            t.join();

        // Need to validate test results.  Need to be within a transaction here
        Contract c2 = contractRepository.findOne(c1.getId());
    }

ฉันได้ลองใช้ตัวจัดการเอนทิตีแล้ว แต่ได้รับข้อความแสดงข้อผิดพลาดเมื่อฉัน:

org.springframework.dao.InvalidDataAccessApiUsageException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead; nested exception is java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:293)
    at org.springframework.orm.jpa.aspectj.JpaExceptionTranslatorAspect.ajc$afterThrowing$org_springframework_orm_jpa_aspectj_JpaExceptionTranslatorAspect$1$18a1ac9(JpaExceptionTranslatorAspect.aj:33)

มีวิธีใดในการทำธุรกรรมและดำเนินการต่อหรือไม่? ฉันไม่พบวิธีการใดๆ ที่อนุญาตให้ฉันโทร commit() ได้


person Eric B.    schedule 21.06.2014    source แหล่งที่มา
comment
คุณอาจค้นคว้าว่ามีวิธีที่จะให้เธรดที่สร้างมีส่วนร่วมในธุรกรรมหรือไม่ เพื่อที่พวกเขาจะได้เห็นผลลัพธ์ที่ไม่มีข้อผูกมัด   -  person Jim Garrison    schedule 21.06.2014
comment
ถ้าเมธอดเป็น @Transactional การส่งคืนจากเมธอดจะถือเป็นธุรกรรม แล้วทำไมไม่กลับจากวิธีการล่ะ?   -  person Raedwald    schedule 21.06.2014
comment
การทดสอบหน่วยตามแนวคิดไม่ควรเป็นธุรกรรม และสำหรับแบบจำลองของ Spring ก็ไม่สมเหตุสมผลเช่นกัน คุณควรดูการทดสอบการรวมโดยใช้ Spring TestContext ซึ่งมีเครื่องมือเพื่อช่วยในการทำธุรกรรม: docs.spring.io/spring/docs/3.2.x/spring-framework-reference/   -  person Matt Whipple    schedule 21.06.2014
comment
@JimGarrison จริงๆ แล้วจุดรวมของการทดสอบหน่วยของฉันคือการทดสอบธุรกรรมแบบขนานและตรวจสอบว่าไม่มีปัญหาการทำงานพร้อมกันในธุรกรรม   -  person Eric B.    schedule 22.06.2014
comment
@Raedwald ถ้าฉันกลับจากวิธีการฉันจะทำการทดสอบต่อไปได้อย่างไร ฉันต้องการคอมมิตก่อนที่เธรดของฉันจะวางไข่เนื่องจากเธรดใช้ข้อมูลที่สร้างขึ้นก่อนที่จะวางไข่   -  person Eric B.    schedule 22.06.2014
comment
อย่างไรก็ตาม หากคุณใช้งาน repository คุณจะมีเมธอด flush() บน JPARepository ของคุณ ดู docs.spring.io/spring-data/jpa/docs/current/api/org/   -  person Josh    schedule 22.08.2016


คำตอบ (3)


ฉันมีกรณีการใช้งานที่คล้ายกันระหว่างการทดสอบ Listener เหตุการณ์ไฮเบอร์เนตซึ่งจะถูกเรียกใช้เมื่อมีการคอมมิตเท่านั้น

วิธีแก้ไขคือห่อโค้ดให้คงอยู่ในวิธีอื่นที่มีคำอธิบายประกอบด้วย REQUIRES_NEW (ในคลาสอื่น) วิธีนี้ทำให้เกิดธุรกรรมใหม่และมีการออกฟลัช/คอมมิตเมื่อเมธอดส่งคืน

อุปกรณ์ Tx ต้องการ_ใหม่

โปรดทราบว่าสิ่งนี้อาจส่งผลต่อการทดสอบอื่นๆ ทั้งหมด! ดังนั้นเขียนไว้ตามนั้นหรือคุณต้องแน่ใจว่าคุณสามารถทำความสะอาดได้หลังจากการทดสอบทำงาน

person Martin Frey    schedule 21.06.2014
comment
ฉลาดหลักแหลม. ไม่ได้คิดเรื่องนั้นเลย ต้องแจกแจงวิธีการทดสอบของฉันนิดหน่อย ซึ่งฉันก็ไม่ค่อยตื่นเต้นนัก แต่ก็ได้ผลดี - person Eric B.; 22.06.2014
comment
@MartinFrey ทำไมในชั้นเรียนอื่น? - person Basemasta; 07.09.2014
comment
เนื่องจาก Spring ทำงานร่วมกับพรอกซีเพื่อให้ได้ฟีเจอร์นี้ (อื่นๆ มากมายเช่นกัน) คุณต้องใช้คลาสอื่นเพื่อให้ Spring สามารถทริกเกอร์ธุรกรรมได้ เมื่อคุณอยู่ในชั้นเรียนแล้ว คุณจะไม่ผ่านพรอกซีอีก - person Martin Frey; 07.09.2014
comment
นี่จะเป็นคลาสภายในได้ไหม? - person Gleeb; 11.05.2015
comment
อาจเป็นได้ตราบใดที่มันเป็นถั่วฤดูใบไม้ผลิ ไม่ได้ทดสอบแบบนี้ พยายามต่อไป :) - person Martin Frey; 11.05.2015
comment
คุณช่วยโพสต์ข้อความที่ตัดตอนมาจากรหัสได้ไหม? ทำตามที่คุณเป็นแต่มีเพียงธุรกรรมเดียวเท่านั้นที่เคยกระทำเมื่อสิ้นสุดการทดสอบ - person PragmaticProgrammer; 29.06.2018
comment
REQUIRES_NEWจะแก้ไขปัญหาอย่างไร อย่างที่คุณพูด มันแค่ระงับธุรกรรมหลักและไม่จำเป็นต้องยอมรับมัน (หลัก) หมายความว่าผู้ปกครองรอให้เด็กกลับมาแล้วการกระทำของผู้ปกครองเท่านั้นที่จะเกิดขึ้น? หากเป็นเช่นนั้น ก็ไม่ใช่วิธีแก้ปัญหา 100% เนื่องจากวิธีการทำงานของการร้อยด้าย มันแค่เพิ่มโอกาสในการหลีกเลี่ยงสภาพการแข่งขัน - person Aladin; 30.07.2020
comment
หากคุณมีธุรกรรมที่ซ้อนกัน ฉันคิดว่าประเด็นคือคุณไม่ต้องการระงับธุรกรรมภายนอก เช่น. ในกรณีสตรีมที่คุณต้องการเปิดไว้ - person html_programmer; 11.06.2021

ทำไมคุณไม่ใช้ TransactionTemplate ของ spring เพื่อควบคุมธุรกรรมโดยทางโปรแกรม? คุณยังสามารถปรับโครงสร้างโค้ดของคุณใหม่เพื่อให้ "บล็อกธุรกรรม" แต่ละอันมีวิธี @Transactional ของตัวเอง แต่เมื่อพิจารณาว่าเป็นการทดสอบ ฉันจะเลือกใช้การควบคุมธุรกรรมของคุณโดยทางโปรแกรม

โปรดทราบด้วยว่าคำอธิบายประกอบ @Transactional บน runnable ของคุณจะใช้งานไม่ได้ (เว้นแต่ว่าคุณกำลังใช้ spectj) เนื่องจาก runnables ไม่ได้รับการจัดการโดย Spring!

@RunWith(SpringJUnit4ClassRunner.class)
//other spring-test annotations; as your database context is dirty due to the committed transaction you might want to consider using @DirtiesContext
public class TransactionTemplateTest {

@Autowired
PlatformTransactionManager platformTransactionManager;

TransactionTemplate transactionTemplate;

@Before
public void setUp() throws Exception {
    transactionTemplate = new TransactionTemplate(platformTransactionManager);
}

@Test //note that there is no @Transactional configured for the method
public void test() throws InterruptedException {

    final Contract c1 = transactionTemplate.execute(new TransactionCallback<Contract>() {
        @Override
        public Contract doInTransaction(TransactionStatus status) {
            Contract c = contractDOD.getNewTransientContract(15);
            contractRepository.save(c);
            return c;
        }
    });

    ExecutorService executorService = Executors.newFixedThreadPool(5);

    for (int i = 0; i < 5; ++i) {
        executorService.execute(new Runnable() {
            @Override  //note that there is no @Transactional configured for the method
            public void run() {
                transactionTemplate.execute(new TransactionCallback<Object>() {
                    @Override
                    public Object doInTransaction(TransactionStatus status) {
                        // do whatever you want to do with c1
                        return null;
                    }
                });
            }
        });
    }

    executorService.shutdown();
    executorService.awaitTermination(10, TimeUnit.SECONDS);

    transactionTemplate.execute(new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            // validate test results in transaction
            return null;
        }
    });
}

}

person Pieter    schedule 21.06.2014
comment
ขอบคุณสำหรับความคิด เคยพิจารณาแล้ว แต่ดูเหมือนว่าจะมีงานมาก/ต้องใช้ทักษะมากเกินไปสำหรับปัญหาง่ายๆ สันนิษฐานว่าจะต้องมีบางสิ่งที่ง่ายกว่านี้มาก แบ่งมันเป็นวิธีแยกกันด้วย Propagation.REQUIRES_NEW สำเร็จแค่นั้น (ดูคำตอบของ @ MartinFrey) - person Eric B.; 22.06.2014
comment
สำหรับกรณีของฉันมันใช้งานได้กับ transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); เท่านั้น ดูโค้ดที่นี่ - person Grigory Kislin; 26.09.2017

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

ในบางกรณี (ขึ้นอยู่กับประเภทของแอปพลิเคชัน) วิธีที่ดีที่สุดในการใช้ธุรกรรมในการทดสอบ Spring คือการปิดการใช้งาน @Transactional เกี่ยวกับวิธีการทดสอบ ทำไม เนื่องจาก @Transactional อาจนำไปสู่การทดสอบผลบวกลวงหลายครั้ง คุณสามารถดูบทความตัวอย่างนี้เพื่อดูรายละเอียด . ในกรณีเช่นนี้ TransactionTemplate เหมาะอย่างยิ่งสำหรับการควบคุมขอบเขตธุรกรรมเมื่อเราต้องการการควบคุมนั้น

person G. Demecki    schedule 23.10.2014
comment
สวัสดี ฉันสงสัยว่าสิ่งนี้จะเกิดขึ้นหรือไม่: เมื่อสร้างการทดสอบการรวมในคำสั่งที่บันทึกออบเจ็กต์ ขอแนะนำให้ล้างตัวจัดการเอนทิตีเพื่อหลีกเลี่ยงผลลบลวง กล่าวคือ เพื่อหลีกเลี่ยงการทดสอบที่ทำงานได้ดีแต่การดำเนินการของใครจะล้มเหลว เมื่อดำเนินการในการผลิต ที่จริงแล้ว การทดสอบอาจทำงานได้ดีเพียงเพราะว่าแคชระดับแรกไม่ได้ถูกล้าง และไม่มีการเขียนใดเข้าถึงฐานข้อมูล เพื่อหลีกเลี่ยงการทดสอบการรวมเชิงลบที่ผิดพลาด ให้ใช้ฟลัชอย่างชัดเจนในตัวทดสอบ - person Stephane; 08.10.2015
comment
@StephaneEybert IMO เพียงแค่ล้าง EntityManager ก็เหมือนกับแฮ็ค :-) แต่มันขึ้นอยู่กับความต้องการและประเภทของการทดสอบที่คุณกำลังทำ สำหรับการทดสอบการรวมจริง (ที่ควรทำงานเหมือนกับที่ใช้งานจริงทุกประการ) คำตอบคือ: ห้ามใช้ @Transactional รอบการทดสอบอย่างแน่นอน แต่ก็มีข้อเสียอยู่เช่นกัน: คุณต้องตั้งค่าฐานข้อมูลให้อยู่ในสถานะที่รู้จักก่อนการทดสอบทุกครั้ง - person G. Demecki; 09.10.2015