How to test spring batch job within @Transactional SpringBootTest test case?

早过忘川 提交于 2021-02-11 14:46:25

问题


I just can't seem to win today...

  1. Is there a way to read from a OneToMany relationship in a Spock SpringBootTest integration test, without annotating the test as @Transactional or adding the unrealistic spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true?
  2. OR, is there a way to launch a Spring-Batch Job from within a @Transactional test case?

Let me elaborate...

I'm trying to get a simple Spring Boot Integration test working for my Spring Batch reporting process, which reads from tangled web of DB2 tables and generates a series of change messages for interested systems. I'm using the Groovy Spock testing framework and an H2 in-memory database filled with a representative slice of my DB2 tables' data.

At the beginning of the test, I'm attempting to use every entity from a given Table to generate entries in a change-tracking table that drives my messaging.

setup:
List allExistingTestPeople = peopleRepository.findAll()
Collections.shuffle(allExistingTestPeople)
allExistingTestPeople?.each { Person person ->
    Nickname nicknames = person.nicknames
    nicknames?.each { Nickname nickname ->
        changeTrackingRepository.save(new Change(personId: person.id, nicknameId: nickname.id, status: NEW))
    }
}

Given these as my DB2 domain classes:

@Entity
@Table(name = "T_PERSON")
public class Person {

    @Id
    @Column(name = "P_ID")
    private Integer id;

    @Column(name = "P_NME")
    private String name;

    @OneToMany(targetEntity = Nickname.class, mappedBy = "person")
    private List<Nickname> nicknames;
}

@Entity
@Table(name = "T_NICKNAME")
public class Nickname{

    @EmbeddedId
    private PersonNicknamePK id;

    @Column(name = "N_NME")
    private String nickname;

    @ManyToOne(optional = false, targetEntity = Person.class)
    @JoinColumn(name = "P_ID", referencedColumnName="P_ID", insertable = false, updatable = false)
    private Person person;
}

@Embeddable
public class PersonNicknamePK implements Serializable {

    @Column(name="P_ID")
    private int personId;

    @Column(name="N_ID")
    private short nicknameId;
}

But I'm getting this LazyInitializationException, even though I'm reading from that OneToMany relationship within the context of a test case...

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.my.package.db2.model.Person.nicknames, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:161)
at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:350)

I came across the advice online to annotate my test case with the @Transactional annotation, which definitely got me a little further, allowing me to read from this OneToMany relationship. However, when I then attempt to launch the Spring Batch Job I'd like to test from my when clause:

@Transactional
def "Happy path test to validate I can generate a report of changes"() {
    setup:
    //... See above

    when:
    service.launchBatchJob()

    then:
    //... Messages are generated
} 

I'm getting the exception that a Spring Batch Job can't be launched from the context of a transaction! Even though I'm using an in-memory Job manager via ResourcelessTransactionManager and MapJobRepositoryFactoryBean, since this is just a short lived scheduled script I'm writing...

java.lang.IllegalStateException: Existing transaction detected in JobRepository. Please fix this and try again (e.g. remove @Transactional annotations from client).
    at org.springframework.batch.core.repository.support.AbstractJobRepositoryFactoryBean$1.invoke(AbstractJobRepositoryFactoryBean.java:177)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy125.createJobExecution(Unknown Source)
    at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:134)
    at com.my.package.service.MyService.launchBatchJob(MyService.java:30)

The only thing that seems to work so far is if I scrap the @Transactional annotation and instead add spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true to my application-test.properties file. BUT, this doesn't seem like a very good idea, because it's not realistic - if I add this, then even if there's a bug in my code due to a lazy-initialization-exception, I'd never see it in the tests.

Sorry for the novel, hoping someone can point me in the right direction :(


EDIT:

Also here's my In-memory Spring-Batch configuration, in which I've tried turning off the transaction validation. Unfortunately, while this gets me a little further, the Spring Batch partioner's autowired EntityManager is suddenly failing to run queries in the H2 database.

@Configuration
@EnableBatchProcessing
public class InMemoryBatchManagementConfig {

    @Bean
    public ResourcelessTransactionManager resourceslessTransactionManager() {
        ResourcelessTransactionManager resourcelessTransactionManager = new ResourcelessTransactionManager();
        resourcelessTransactionManager.setNestedTransactionAllowed(true);
        resourcelessTransactionManager.setValidateExistingTransaction(false);
        return resourcelessTransactionManager;
    }

    @Bean
    public MapJobRepositoryFactoryBean mapJobRepositoryFactory(ResourcelessTransactionManager txManager)
            throws Exception {
        MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean(txManager);
        factory.setValidateTransactionState(false);
        factory.afterPropertiesSet();
        return factory;
    }

    @Bean
    public JobRepository jobRepository(MapJobRepositoryFactoryBean factory) throws Exception {
        return factory.getObject();
    }

    @Bean
    public SimpleJobLauncher jobLauncher(JobRepository jobRepository) throws Exception {
        SimpleJobLauncher launcher = new SimpleJobLauncher();
        launcher.setJobRepository(jobRepository);
        launcher.afterPropertiesSet();
        return launcher;
    }

    @Bean
    public JobExplorer jobExplorer(MapJobRepositoryFactoryBean factory) {
        return new SimpleJobExplorer(factory.getJobInstanceDao(), factory.getJobExecutionDao(),
                factory.getStepExecutionDao(), factory.getExecutionContextDao());
    }

    @Bean
    public BatchConfigurer batchConfigurer(MapJobRepositoryFactoryBean mapJobRepositoryFactory,
                                           ResourcelessTransactionManager resourceslessTransactionManager,
                                           SimpleJobLauncher jobLauncher,
                                           JobExplorer jobExplorer) {
        return new BatchConfigurer() {
            @Override
            public JobRepository getJobRepository() throws Exception {
                return mapJobRepositoryFactory.getObject();
            }

            @Override
            public PlatformTransactionManager getTransactionManager() throws Exception {
                return resourceslessTransactionManager;
            }

            @Override
            public JobLauncher getJobLauncher() throws Exception {
                return jobLauncher;
            }

            @Override
            public JobExplorer getJobExplorer() throws Exception {
                return jobExplorer;
            }
        };
    }
}

回答1:


You can do transactions programmatically using TransactionTemplate to run only the "setup" inside a transaction (instead of having everything in @Transactional). Unfortunately this way the transaction will be committed and you will need to do some manual cleanup.

It can be autowired as any other bean:

    @Autowired
    private TransactionTemplate transactionTemplate;

...and it's used this way:

        transactionTemplate.execute((transactionStatus) -> {
            // ...setup...
            return null; // alternatively you can return some data out of the callback
        });



回答2:


This error happens because your code will be already executed in a transaction driven by Spring Batch. So running the job in the scope of a transaction is not correct. However, if you still want to disable the transaction validation done by the job repository, you can set the validateTransactionState to false, see AbstractJobRepositoryFactoryBean#setValidateTransactionState.

That said, running the job in a transaction is not the way to fix org.hibernate.LazyInitializationException. The property spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true is there for a reason, and if it works for you, I believe it is a better approach than running the entire job in a transaction (and btw, if I had to use a transaction for that, I would narrow its scope to the minimum (for example the step) and not the entire job).



来源:https://stackoverflow.com/questions/60610021/how-to-test-spring-batch-job-within-transactional-springboottest-test-case

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!