#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. Не делайте свой метод доступным только для чтения или не делайте его транзакционным, но это приведет к загрязнению вашей базы данных. Поскольку записи остаются после теста.