I'm trying to set up a simple Spring Boot project with a JPA repository on top of a read-only data source, and I would like to propagate the read-only flag as hint to the underlying JDBC driver for performance optimizations, as per Spring Data JPA Reference.
The repository implementation looks as follows:
public interface MyReadOnlyRepository extends JpaRepository<String, String> {}
This is the configuration class:
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "readOnlyEntityManagerFactory",
transactionManagerRef = "readOnlyTransactionManager",
basePackageClasses = ReadOnlyDbConfig.class)
public class ReadOnlyDbConfig {
#Bean(name = "readOnlyDataSource")
#ConfigurationProperties(prefix = "spring.datasource.ro")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = "readOnlyEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("readOnlyDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages(MyEntity.class)
.persistenceUnit("readOnly")
.build();
}
#Bean(name = "readOnlyTransactionManager")
public PlatformTransactionManager transactionManager(
#Qualifier("readOnlyEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
and the relevant YML properties:
spring:
datasource:
ro:
url: jdbc:postgresql://...
username: ...
password: ...
driver-class-name: org.postgresql.Driver
default-read-only: true
To test it, I have written a simple test which runs against a dockerised PostgresSQL instance:
#SpringBootTest(classes = TestApplication.class)
#RunWith(SpringRunner.class)
#ContextConfiguration(initializers = { PostgresInDockerInitializer.class })
public class MyReadOnlyStoreTest {
#Inject
private MyReadOnlyRepository repo;
#Test(expected = RuntimeException.class)
public void testIsReadOnly() {
repo.save("test");
}
}
I have called the save just for the sake of testing whether the flag is set: I appreciate the read-only flag is only meant as a hint for the JDBC driver, not as a protection mechanism against write operations. The test throws an exception, as expected ("cannot execute INSERT in a read-only transaction").
My problem is that, as soon as a transaction completes successfully, the JPA transaction manager resets the connection read-only flag to false. So, for example, this test fails:
#Test(expected = RuntimeException.class)
public void testIsReadOnly() {
repo.findAll(); // <-- on commit, the transaction manager resets the read-only flag to false
repo.save("test");
}
Note that the same happens if the repository is annotated with #Transactional(readOnly=true) as per the Spring Data JPA Reference.
Could somebody please explain why the transaction manager does so? Is there an easy/better way to avoid the flag being reset, other than setting the transaction status to rollback-only?
Thanks for your help.
Related
I have an already working Jpa Repository exposing a native query method
myPrimary.datasource.jdbc-url=jdbc:sqlserver://host:1433;databaseName=dbName
myPrimary.datasource.username=user
myPrimary.datasource.password=pwd
#Configuration
#EnableJpaRepositories(
entityManagerFactoryRef = "myPrimaryEntityManagerFactory",
transactionManagerRef = "myPrimaryTransactionManager",
basePackages = {"myPrimaryPackage"}
)
#EnableTransactionManagement
public class PrimaryDaoConfig {
#Primary
#Bean(name = "myPrimaryDataSource")
#ConfigurationProperties(prefix = "myPrimary.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Primary
#Bean(name = "myPrimaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, #Qualifier("myPrimaryDataSource") DataSource dataSource) {
return builder.dataSource(dataSource).packages("myPrimaryPackage").persistenceUnit("myPrimary").build();
}
#Primary
#Bean(name = "myPrimaryTransactionManager")
public PlatformTransactionManager transactionManager(#Qualifier("myPrimaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
#Repository
public interface MyPrimaryRepository<T extends MyPrimaryEntity, ID extends MyPrimaryId> extends org.springframework.data.repository.Repository<T, ID> {
String MY_CUSTOM_NATIVE_QUERY = "...";
#Query(
value = MY_CUSTOM_NATIVE_QUERY,
nativeQuery = true
)
List<MyPrimaryEntity> findAll();
}
#RunWith(SpringRunner.class)
#ActiveProfiles("dev")
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class RepositoryTest {
#Autowired
private MyPrimaryRepository myPrimaryRepository;
#Test
public void findAll() {
assertNotNull(myPrimaryRepository);
List<MyPrimaryEntity> entities = myPrimaryRepository.findAll();
assertEquals(332, entities.size());
}
}
I'd like to use it in Spring IntegrationFlows.
From the docs I found, there's a way to pass an EntityManagerFactory like this
#Bean
public IntegrationFlow dbInboundFlow() throws Exception {
return IntegrationFlows
.from(Jpa
.inboundAdapter(myPrimaryEntityManagerFactory)
.nativeQuery(MyPrimaryRepository.MY_CUSTOM_NATIVE_QUERY)
.entityClass(MyPrimaryEntity.class),
e -> e.poller(Pollers.fixedDelay(10000))
)
.handle(jobLaunchingMessageHandler())
.channel("nullChannel")
.get();
}
My unit test is successful, but when trying to pass the entityMangerFactory in the IntegrationFlows, I get the
Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
error.
I tried to add
spring.jpa.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect
but no luck either.
So is there a way to pass a repository instead of the entityManagerFactory ?
For now I only can answer that if you have already a JPA repository, you should use a generic method invocation MessageSource instead of that Jpa.inboundChannelAdapter(). The last one technically does for us whatever repository does but more in messaging manner.
The issue with dialect not clear for me: looks like you use the same. The dialect if not set explicitly is derived from the JpaVendorAdapter. The HibernateJpaConfiguration should do a stuff for us on the matter. Not clear if #DataJpaTest does for us everything what we needed.
Nothing to do with Spring Integration though. I'd like to see some simple project from you to play with locally on my side.
I want to do some DB related actions in service method. Initialy it looks like this:
#Override
#Transactional
public void addDirectory(Directory directory) {
//some cheks here
directoryRepo.save(directory);
rsdhUtilsService.createPhysTable(directory);
}
Firs method directoryRepo.save(directory); is just simple JPA save action, second one rsdhUtilsService.createPhysTable(directory); is JDBCTemplate stored procedure call from it's own service. The problem is: if any exceptions accures within JPA or SimpleJdbcCall action, transaction will rollback and nothig related to JPA won't be persited, but if exception occures only within JPA action, result of SimpleJdbcCall won't be affected by transaction rollback.
To illustrate this behaviour I've remove JAP action, mark #Transactional as (readOnly = true) and moved all JDBCTemplate related logic from another service to current one.
#Service
public class DirectoriesServiceImpl implements DirectoriesService {
private final DirectoryRepo directoryRepo;
private final MapSQLParamUtils sqlParamUtils;
private final JdbcTemplate jdbcTemplate;
#Autowired
public DirectoriesServiceImpl(DirectoryRepo directoryRepo, MapSQLParamUtils sqlParamUtils, JdbcTemplate jdbcTemplate) {
this.directoryRepo = directoryRepo;
this.sqlParamUtils = sqlParamUtils;
this.jdbcTemplate = jdbcTemplate;
}
#Override
#Transactional(readOnly = true)
public void addDirectory(Directory directory) {
directoryRepo.save(directory);
new SimpleJdbcCall(jdbcTemplate).withSchemaName("RSDH_DICT").withCatalogName("UTL_DICT")
.withFunctionName("create_dict")
.executeFunction(String.class, sqlParamUtils.getMapSqlParamForCreatePhysTable(directory));
}
}
As a result #Transactional annotation is ignored and I can see new records persisted in DB.
I've got only one DataSource configured via application.properties, and here is how JDBCTemlate configured
#Component
class MapSQLParamUtils {
private final DataSource dataSource;
#Autowired
MapSQLParamUtils(DataSource dataSource) {
this.dataSource = dataSource;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource);
}
}
So my questions are: why do #Transactional ignored by SimpleJdbcCall and how to configure JPA and JDBCTemlate to use same transaction manager.
UPDATE:
This is how I use this service in controller
#RestController
#RequestMapping(value = "/api/v1/directories")
public class DirectoriesRESTControllerV1 {
private final DirectoriesService directoriesService;
#Autowired
public DirectoriesRESTControllerV1(DirectoriesService directoriesService) {
this.directoriesService = directoriesService;
}
#PostMapping
#PreAuthorize("hasPermission('DIRECTORIES_USER', 'W')")
public ResponseEntity createDirectory(#NotNull #RequestBody DirectoryRequestDTO createDirectoryRequestDTO) {
Directory directoryFromRequest = ServiceUtils.convertDtoToEntity(createDirectoryRequestDTO);
directoriesService.addDirectory(directoryFromRequest);
return ResponseEntity.noContent().build();
}
}
As mentioned earlier, the problem here is that JPA does not execute sql queries at once repository methods called. To enforce it you can use explicit entityManager.flush():
#Autowired
private javax.persistence.EntityManager entityManager;
...
#Override
#Transactional(readOnly = true)
public void addDirectory(Directory directory) {
directoryRepo.save(directory);
entityManager.flush();
new SimpleJdbcCall(jdbcTemplate).withSchemaName("RSDH_DICT").withCatalogName("UTL_DICT")
.withFunctionName("create_dict")
.executeFunction(String.class, sqlParamUtils.getMapSqlParamForCreatePhysTable(directory));
}
To see real SQL queries by hibernate you can enable option show_sql, in case if your application is spring-boot, this configuration enables it:
spring.jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging.level:
org.hibernate.SQL: DEBUG
Regarding transaction manager. In case if entityManager flush is not enough, you may need the composite transaction manager, that handles both JPA and DataSource. Spring data commons has ChainedTransactionManager. Note: you should be careful with it. I used it this way in my project:
#Bean(BEAN_CONTROLLER_TX)
public PlatformTransactionManager controllerTransactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
#Bean(BEAN_ANALYTICS_TX)
public PlatformTransactionManager analyticsTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* Chained both 2 transaction managers.
*
* #return chained transaction manager for controller datasource and analytics datasource
*/
#Primary
#Bean
public PlatformTransactionManager transactionManager(
#Qualifier(BEAN_CONTROLLER_TX) PlatformTransactionManager controllerTransactionManager,
#Qualifier(BEAN_ANALYTICS_TX) PlatformTransactionManager analyticsTransactionManager) {
return new ChainedTransactionManager(controllerTransactionManager, analyticsTransactionManager);
}
Please try this :
#Transactional(rollbackFor = Exception.class)
public void addDirectory(Directory directory){
#Transactional only rolls back transactions for unchecked exceptions. For checked exceptions and their subclasses, it commits data. So although an exception is raised here, because it's a checked exception, Spring ignores it and commits the data to the database.
So if you throw an Exception or a subclass of it, always use the above with the #Transactional annotation to tell Spring to roll back transactions if a checked exception occurs.
It's very simple, just use the following with #Transactional:
#Transactional(rollbackFor = Exception.class)
In our application, we have a common database called central and every customer will have their own database with exactly the same set of tables. Each customer's database might be hosted on our own server or the customer's server based on the requirement of the customer organization.
To handle this multi-tenant requirement, we're extending the AbstractRoutingDataSource from Spring JPA and overriding the determineTargetDataSource() method to create a new DataSource and establish a new connection on the fly based on the incoming customerCode. We also use a simple DatabaseContextHolder class to store the current datasource context in a ThreadLocal variable. Our solution is similar to what is describe in this article.
Let's say in a single request, we'll need to update some data in both the central database and the customer's database as following.
public void createNewEmployeeAccount(EmployeeData employee) {
DatabaseContextHolder.setDatabaseContext("central");
// Code to save a user account for logging in to the system in the central database
DatabaseContextHolder.setDatabaseContext(employee.getCustomerCode());
// Code to save user details like Name, Designation, etc. in the customer's database
}
This code would only work if determineTargetDataSource() is called every time just before any SQL queries gets executed so that we can switch the DataSource dynamically half way through our method.
However, from this Stackoverflow question, it seems like determineTargetDataSource() is only called once for each HttpRequest when a DataSource is being retrieved for the very first time in that request.
I'd be very grateful if you can give me some insights into when AbstractRoutingDataSource.determineTargetDataSource() actually gets called. Besides, if you've dealt with a similar multi-tenant scenario before, I'd love to hear your opinion on how I should deal with the updating of multiple DataSource in a single request.
We found a working solution, which is a mix of static data source settings for our central database and dynamic data source settings for our customer's database.
In essence, we know exactly which table comes from which database. Hence, we were able to separate our #Entity classes into 2 different packages as following.
com.ft.model
-- central
-- UserAccount.java
-- UserAccountRepo.java
-- customer
-- UserProfile.java
-- UserProfileRepo.java
Subsequently, we created two #Configuration classes to set up the data source setting for each package. For our central database, we use static settings as following.
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager",
basePackages = { "com.ft.model.central" }
)
public class CentralDatabaseConfiguration {
#Primary
#Bean(name = "dataSource")
public DataSource dataSource() {
return DataSourceBuilder.create(this.getClass().getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
#Primary
#Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, #Qualifier("dataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.central")
.persistenceUnit("central")
.build();
}
#Primary
#Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager (#Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
For the #Entity in the customer package, we set up dynamic data source resolver using the following #Configuration.
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "customerEntityManagerFactory",
transactionManagerRef = "customerTransactionManager",
basePackages = { "com.ft.model.customer" }
)
public class CustomerDatabaseConfiguration {
#Bean(name = "customerDataSource")
public DataSource dataSource() {
return new MultitenantDataSourceResolver();
}
#Bean(name = "customerEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, #Qualifier("customerDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.customer")
.persistenceUnit("customer")
.build();
}
#Bean(name = "customerTransactionManager")
public PlatformTransactionManager transactionManager(#Qualifier("customerEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
In the MultitenantDataSourceResolver class, we plan to maintain a Map of the created DataSource using customerCode as key. From each incoming request, we will get the customerCode and inject it into our MultitenantDataSourceResolver to get the correct DataSource within the determineTargetDataSource() method.
public class MultitenantDataSourceResolver extends AbstractRoutingDataSource {
#Autowired
private Provider<CustomerWrapper> customerWrapper;
private static final Map<String, DataSource> dsCache = new HashMap<String, DataSource>();
#Override
protected Object determineCurrentLookupKey() {
try {
return customerWrapper.get().getCustomerCode();
} catch (Exception ex) {
return null;
}
}
#Override
protected DataSource determineTargetDataSource() {
String customerCode = (String) this.determineCurrentLookupKey();
if (customerCode == null)
return MultitenantDataSourceResolver.getDefaultDataSource();
else {
DataSource dataSource = dsCache.get(customerCode);
if (dataSource == null)
dataSource = this.buildDataSourceForCustomer();
return dataSource;
}
}
private synchronized DataSource buildDataSourceForCustomer() {
CustomerWrapper wrapper = customerWrapper.get();
if (dsCache.containsKey(wrapper.getCustomerCode()))
return dsCache.get(wrapper.getCustomerCode() );
else {
DataSource dataSource = DataSourceBuilder.create(MultitenantDataSourceResolver.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url(wrapper.getJdbcUrl())
.username(wrapper.getDbUsername())
.password(wrapper.getDbPassword())
.build();
dsCache.put(wrapper.getCustomerCode(), dataSource);
return dataSource;
}
}
private static DataSource getDefaultDataSource() {
return DataSourceBuilder.create(CustomerDatabaseConfiguration.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
}
The CustomerWrapper is a #RequestScope object whose values will be populated on each request by the #Controller. We use java.inject.Provider to inject it into our MultitenantDataSourceResolver.
Lastly, even though, logically, we will never save anything using the default DataSource because all requests will always contain a customerCode, at startup time, there is no customerCode available. Hence, we still need to provide a valid default DataSource. Otherwise, the application will not be able to start.
If you have any comments or a better solution, please let me know.
I use:
Spring data (4.x)
HikariCP
Hibernate (I use EntityManager)
Consider the following repositories:
public interface TestModelRepository extends JpaRepository<TestModel, Long> {
}
public interface TestModelRepository2 extends JpaRepository<TestModel2, Long> {
}
and the following service:
#Service
static class Svc {
#Autowired
private TestModelRepository modelRepository;
#Autowired
private TestModelRepository2 modelRepository2;
#Transactional
public void insertWithException() {
assertThat(TransactionAspectSupport.currentTransactionStatus()).isNotNull();
modelRepository.save(new TestModel("any"));
modelRepository2.save(new TestModel2("unique"));
modelRepository2.save(new TestModel2("unique"));
}
}
The second save in repository 2 throws DataIntegrityViolationException because the provided value is not unique. Transaction should rollback everything in this method as it is annotated with #Transactional, hovewer it does not.
TestModel and one of TestModel2 are persisted. Actually they are persisted to the database just after each save() call, so the values are inserted into the database even if the #Transactional method did not complete yet (I verified it by placing a breakpoint and logging into the database). It looks to me like autocommit is set to true, hovewer I set it to false (in HikariCP config).
Here is my java-based configuration (fragments):
#Bean
PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
#Bean
JpaVendorAdapter vendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setDatabase(Database.MYSQL);
adapter.setDatabasePlatform(MySQL5Dialect.class.getName());
return adapter;
}
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(dataSource);
entityManagerFactory.setJpaVendorAdapter(vendorAdapter);
entityManagerFactory.setPackagesToScan(packagesToScan);
entityManagerFactory.setJpaProperties(properties);
Main question: why the transaction does not rollback everything?
Additional questions:
when the data should be commited to the database? When the transaction is commited or anytime?
is the connection kept during the whole transaction, or can it be returned to the pool in the middle of transaction and then another connection is requested if needed?
Spring Data doesn't require #Transactional on repositories, what are the transaction parameters then (propagation and isolation)?
You may have autocommit on the connection or in the db config. I also noticed you're using mysql. Make sure your schema and tables are InnoDB not MyISAM
We are updating an old Spring application to use java-config instead of XML.
The application runs fine during unit tests, but when deployed under Wildfly it seems that the transactions are inactive and the entity manager is never closed : we don't see inserts/updates being sent to the DB, and despite loggers org.springframework.transaction and
org.springframework.orm.jpa are set to DEBUG we are not getting traces of transaction begin/end.
We are using Wildfly 9.0.2 with wildfly BOM (=> Hibernate 4.3.10) and Spring 4.3.7.
We have two application modules (.war) and a persistence module (.jar) shared between them.
The persistence module holds the JpaConfig.java and DaoConfig.java configuration classes :
#Configuration
#EnableTransactionManagement(proxyTargetClass = true)
public class JpaConfig {
#Bean(destroyMethod = "close")
public EntityManagerFactory entityManagerFactory(DataSource datasource, JpaVendorAdapter jpaVendorAdapter) {
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(datasource);
entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter);
entityManagerFactory.setJpaProperties(jpaProperties());
entityManagerFactory.setJpaDialect(new HibernateJpaDialect());
entityManagerFactory.setPackagesToScan("my.package.for.entities");
entityManagerFactory.setPersistenceUnitName("my-pu");
entityManagerFactory.afterPropertiesSet();
return entityManagerFactory.getObject();
}
#Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
#Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
#Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setDatabase(Database.MYSQL);
adapter.setGenerateDdl(false);
adapter.setShowSql(LOG.isDebugEnabled());
return adapter;
}
#Bean
public DataSource dataSource() {
return new JndiDataSourceLookup().getDataSource("java:/appDS");
}
protected Properties jpaProperties() {
Properties props = new Properties();
props.setProperty("hibernate.hibernate.dialect", MySQL5InnoDBDialect.class.getName());
props.setProperty("hibernate.show_sql", LOG.isDebugEnabled() ? "true" : "false");
props.setProperty("hibernate.format_sql", "false");
return props;
}
}
#Configuration
#ComponentScan("my.package.for.repositories")
#EnableTransactionManagement(proxyTargetClass = true)
public class DaoConfig {
...
}
We've been trying multiple variations of the above (returning LocalContainerEntityManagerFactoryBean directly instead of calling afterPropertiesSet + getObject and returning the EntityManager, with and without a persistence.xml in META-INF, with and without a "Dependencies" manifest entry, being less redundant between Configuration classes, ...), without success.
Both WARs have their own configuration classes, all of which import JpaConfig.java and are annotated with #EnableTransactionManagement, such as :
#Configuration
#Import({ SecurityConfig.class, ServicesConfig.class, ControllerConfig.java, JpaConfig.class, DaoConfig.class })
public class RootConfig {
...
}
#Configuration
#EnableWebMvc
#EnableTransactionManagement(proxyTargetClass = true)
#ComponentScan("com.my.controller")
public class ControllerConfig extends WebMvcConfigurerAdapter {
...
}
#Configuration
#EnableWebSecurity
#PropertySource("classpath:security.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
#Configuration
#EnableAspectJAutoProxy
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ComponentScan("com.my.services")
#Import(DaoConfig.class)
...
}
All controllers are annotated with #Transactional, so I would expect Spring to create a new transaction whenever an endpoint is called, and flush and close the EM + commit the transaction when the methods return. But this doesn't happen : here is an extract of our logs :
INFO [RequestProcessingTimeInterceptor] [Start call] POST http://server/web/api/rest/catalog/2
INFO [stdout] Hibernate: select .... from CATALOG catalog0_ where catalog0_.id=?
INFO [CatalogServiceImpl] Updating catalog #2...
INFO [CatalogServiceImpl] Catalog #2 updated !
INFO [RequestProcessingTimeInterceptor] [Call took 23ms] POST http://server/web/api/rest/catalog/2
There should be an update statement somewhere.
Am I missing something obvious ?
We finally solved this issue.
Actually, the above configuration is correct. Transactions are properly created. The reason why the changes were not flushed to the DB is that someone (mistakenly) added a #Immutable annotation to the entity.
Hibernate doesn't log any warning, and doesn't throw, when an #Immutable entity is updated. So in case you are having the same problem... check the annotations on the entity.
In case any Hibernate maintainer finds this answer : it'd be nice that hibernate logs a warning by default when an immutable entity is updated. That would make it easier to spot such coding errors.