Почему Spring пытается ВСТАВИТЬ данные внутри другой транзакции?

#postgresql #hibernate #spring-boot #spring-data-jpa #spring-transactions

#postgresql #спящий режим #spring-boot #spring-data-jpa #spring-транзакции

Вопрос:

Я обнаружил некоторое странное поведение в объявленных транзакциях PostgreSQL и Spring, когда пытался написать простой тест. Давайте посмотрим на это. Предположим, у нас есть два таких объекта JPA:

 @Entity
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private long id;

    @Column
    private String name;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "foo")
    private List<Bar> bars = new ArrayList<>();

    public void addBar(Bar bar){
       bars.add(bar);
       bar.setFoo(this);
    }

    //getters,setters,equals and hashCode omitted
}

@Entity
public class Bar {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private long id;

    @Column
    private String name;

    @ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    @JoinColumn(name = "foo_id")
    private Foo foo;

    //getters,setters,equals and hashCode omitted
}
 

Также у нас есть тривиальный репозиторий:

 public interface FooRepo extends JpaRepository<Foo,Long> {}  
 

И это тест:

 @RunWith(SpringRunner.class)
@SpringBootTest
public class FooRepoTest {

    @Autowired
    private FooRepo fooRepo;

    @Before
    public void setUp() throws Exception {

        Bar bar = new Bar();
        bar.setName("bar");

        Foo foo = new Foo();
        foo.setName("foo");

        foo.addBar(bar);

        fooRepo.saveAndFlush(foo);
    }

    @Test
    @Transactional(readOnly = true)
    public void testGet(){
        Foo foo = fooRepo.findOne(1L);
        assertThat(foo).isNotNull();
        assertThat(foo.getName()).isEqualTo("foo");
        assertThat(foo.getBars().get(0)).isNotNull();
        assertThat(foo.getBars().get(0).getName()).isEqualTo("bar");
    }

}
 

Application.properties:

 #spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL; DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false
#spring.datasource.driverClassName=org.h2.Driver

spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver

spring.jpa.database=postgresql

spring.jpa.generate-ddl=true
spring.jpa.hibernate.use-new-id-generator-mappings=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
 

pom.xml:

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   


<groupId>com.example</groupId>
<artifactId>transact-issue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>transact-issue</name>
<description>Demo project for Spring Boot</description>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>LATEST</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>LATEST</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    </dependencies>

    <build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
 

After all I run the test and got:

         Hibernate: insert into foo (name, id) values (?, ?)
    2016-10-15 23:59:30.128  WARN 4764 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 25006
    2016-10-15 23:59:30.128 ERROR 4764 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: could not perform INSERT in read-only transaction 
    2016-10-15 23:59:30.128  INFO 4764 --- [           main] o.h.e.j.b.internal.AbstractBatchImpl     : HHH000010: On release of batch it still contained JDBC statements
    2016-10-15 23:59:30.144  INFO 4764 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test context [DefaultTestContext@f8c1ddd testClass = FooRepoTest, testInstance = com.example.repo.FooRepoTest@60704c, testMethod = testGet@FooRepoTest, testException = org.springframework.orm.jpa.JpaSystemException: could not execute statement; nested exception is org.hibernate.exception.GenericJDBCException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@70be0a2b testClass = FooRepoTest, locations = '{}', classes = '{class com.example.TransactIssueApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.SpringBootTestContextCustomizer@527740a2, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@370736d9, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2b552920], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]].
    Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.262 sec <<< FAILURE! - in com.example.repo.FooRepoTest
    testGet(com.example.repo.FooRepoTest)  Time elapsed: 0.063 sec  <<< ERROR!
    org.springframework.orm.jpa.JpaSystemException: could not execute statement; nested exception is org.hibernate.exception.GenericJDBCException: could not execute statement
        at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2103)
        at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1836)
        at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:257)
        at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:512)
        at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:388)
        at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:334)
        at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
        at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:45)
        at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2897)
        at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3397)
        at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:89)
        at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:582)
        at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:456)
        at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337)
        at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39)
        at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1282)
        at org.hibernate.jpa.spi.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:1300)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298)
        at com.sun.proxy.$Proxy68.flush(Unknown Source)
        at org.springframework.data.jpa.repository.support.SimpleJpaRepository.flush(SimpleJpaRepository.java:553)
        at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:521)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:503)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:488)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:460)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
        at com.sun.proxy.$Proxy72.saveAndFlush(Unknown Source)
        at com.example.repo.FooRepoTest.setUp(FooRepoTest.java:34)
 

Здесь основная проблема:
ERROR: could not perform INSERT in read-only transaction (it's my own translation from my native language)

Для меня это звучит странно, потому что я не вставляю никаких данных в свой testGet метод. Я пометил это как @Transactional(readOnly = true) из-за того, что я использую коллекцию отложенной инициализации.

Если бы я не пометил это так, я бы получил сообщение об ошибке следующим образом:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.entities.Foo.bars, could not initialize proxy - no Session

Если я пометил это как @Transactional или запускаю FooRepoTest.setUp в одиночку, я не получу никаких ошибок.

Теперь я не понимаю такого поведения, потому что я знаю, что когда я вызываю fooRepo.saveAndFlush(foo) , он должен выполняться в своей собственной транзакции, поскольку я нашел следующий код в org.springframework.data.jpa.repository.support.SimpleJpaRepository :

 @Transactional
public <S extends T> S saveAndFlush(S entity) {
    Object result = this.save(entity);
    this.flush();
    return result;
}
 

Похоже, что в этом случае Spring пытается объединить две транзакции или сделать что-то подобное.

И, наконец, еще одно уведомление. ЕСЛИ я поменяю DB с PostgreSQL на HB2 в режиме PostgreSQL, я больше не вижу никаких ошибок.

Может кто-нибудь объяснить эту магию выше?

Комментарии:

1. Нет, это не так… Ваш @Before метод выполняется в той же транзакции и перед выполнением запроса состояние синхронизируется с базой данных. Из-за того, что доступно только для чтения, это невозможно сделать.

2. Вы имеете в виду, что мой @Before метод @Transactional(readOnly = true) также выполняется в? Почему это происходит? Хотя я не отмечал это.

3. Это выполняется как часть вашего тестового выполнения… Он выполняется в той же транзакции, что и ваш метод тестирования, так что любые изменения могут быть отменены после метода тестирования. Если бы это было не так, ваш тест загрязнил бы базу данных. Также @Before перед каждым методом выполняется @Test метод. Итак, если у вас есть 5 @Test методов, ваш @Before будет запущен 5 раз.

4. ОК. Спасибо за ваше объяснение. Но могу ли я как-то повлиять на это? Я имею в виду, что я хочу, чтобы каждое выполнение setUp метода было в его собственной транзакции и не было откат.

5. Не делайте свой метод доступным только для чтения или не делайте его транзакционным, но это приведет к загрязнению вашей базы данных. Поскольку записи остаются после теста.