I am using Spring boot and trying to set transactions to work properly.
This is my main class:
#Configuration
#EnableJpaRepositories
#EnableAutoConfiguration
#EnableTransactionManagement
#ComponentScan(basePackages = "com.aa.bb")
public class WebApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebApplication.class, args);
}
}
This is my service:
#Service
#Path("/users")
public class UserRestServices {
#Autowired
private UserDao userDao;
#Autowired
private LogRecordDao logRecordDao;
#POST
#Transactional(rollbackFor = Exception.class)
public String saveUser(User user) {
userDao.create(user);
logUser(user);
return "SUCCESS";
}
private void logUser(User user) {
LogRecord log = new LogRecord();
log.setClassName(this.getClass().getSimpleName());
log.setText("User was created " + user.getName());
logRecordDao.create(log);
}
}
The create user works well as it should.
The logUser in the logRecordDao is implemented for exception:
public void create(LogRecord entity) {
Query query = sessionFactory.getCurrentSession().createSQLQuery("UPDATE AAA SET lll = 3");
query.executeUpdate();
}
There is no such table therefore I expect a rollback.
There isn't any.
The user is saved even though an exception occured:
Log:
Hibernate:
insert
into
users
(name, userName)
values
(?, ?)
Hibernate:
UPDATE
AAA
SET
lll = 3
2015-09-12 13:35:28.645 WARN 34974 --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1146, SQLState: 42S02
2015-09-12 13:35:28.646 ERROR 34974 --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : Table 'db.AAA' doesn't exist
I tried to set #EnableTransactionManagement(mode = AdviceMode.PROXY)
didn't work.
Any ideas?
Thanks,
id
Related
I want to test a transactional method. I added the #transactional over test method but I got an exception IllegalStateException: Failed to retrieve PlatformTransactionManager for #Transactional test. I don't know how to activate the transaction. I tried to add TestTransaction.start() in the first line of the test body but it throws an exception that the transaction is not active. I don't know what's wrong with my test.
If I remove classes in #SpringbootTest the test randomly throws NoSuchBeanException for my repositories.
My test method:
#ExtendWith(SpringExtension.class)
#DataJpaTest
#AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
class PersistenceHelperTest {
#Autowired
private ReturnRepository returnRepository;
#SpyBean
private PersistenceHelper persistenceHelper;
#Test
void removeFromDbOnWriteExceptionForNotWritableFile(#TempDir Path tempDir) {
Path testFile = tempDir.resolve("testFile.txt");
try {
String content = "This is a test";
testFile.toFile().createNewFile();
boolean writable = testFile.toFile().setWritable(false);
assertTrue("File should not be writable", writable);
persistenceHelper.saveToFileAndDB(content.getBytes(), testFile.toFile().getAbsolutePath(), returnFile, returnRepository);
fail();
} catch (Exception e) {
assertTrue("Should be instance of IOException", e instanceof IOException);
assertTrue("Should exists", Files.exists(testFile));
assertSame(0, returnRepository.findAll().size());
}
}
The class under the test:
#Component
public class PersistenceHelper {
#Transactional(rollbackFor = {IOException.class}, propagation = Propagation.REQUIRES_NEW)
public <T extends BaseEntity> void saveToFileAndDB(byte[] fileContent, String fileAbsolutePath, T entity, JpaRepository<T, Long> jpaRepository) throws IOException {
FileTransactionListener transactionListener = new FileTransactionListener(new FileDeleter(), fileAbsolutePath);
TransactionSynchronizationManager.registerSynchronization(transactionListener);
jpaRepository.save(entity);
FileUtil.writeToFile(fileAbsolutePath, fileContent);
}
}
my Springboot class:
#SpringBootApplication
#EnableTransactionManagement
#EnableJpaRepositories("packageAddress")
public class MyApplication {
#Transactional annotation on a method inside the Application class is not rolling back insertions. However, #Transactional annotation on the service class(EmpService.java) method("insertEmp(Emp emp)") is working as expected.
Could someone please let me know why #Transactional working differently?
Spring Boot version - 2.1.3.RELEASE with the h2 database.
Please let me know if any additional information required.
#SpringBootApplication
#ComponentScan("org.saheb")
#EnableJpaRepositories("org.saheb.repo")
#EntityScan("org.saheb.vo")
#EnableTransactionManagement
public class SpringJpaTransactionApplication implements CommandLineRunner {
#Autowired
private EmpService empService;
public static void main(String[] args) {
SpringApplication.run(SpringJpaTransactionApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
insertSingleBatch();
}
#Transactional(rollbackFor=RuntimeException.class, propagation=Propagation.REQUIRES_NEW)
public void insertSingleBatch() {
try {
Set<Emp> empSet = new LinkedHashSet<>();
Dept dept = new Dept();
dept.setDeptNo(10);
empSet.add(new Emp("abc", "abc", dept));
empSet.add(new Emp("xyz", "xyz", dept));
empSet.add(new Emp("def", "def", dept));
empSet.add(new Emp("pqrstu", "pqr", dept));// This will fail as max character allowed in 5 and should rollback all the insertion. But, first three records are getting saved in h2 database.
empService.insertEmp(empSet);
} catch (RuntimeException e) {
System.out.println("Exception in batch1.." + e.getMessage());
}
}
}
#Service
public class EmpService {
#Autowired
private EmpRepository empRepository;
//#Transactional(rollbackFor=RuntimeException.class, propagation=Propagation.REQUIRES_NEW)//This is working as expected as all the insertions are rolling back after failure of 4th insertion
public void insertEmp(Set<Emp> empSet) {
System.out.println("Inside insert");
for (Emp temp : empSet) {
Emp temp2 =empRepository.save(temp);
System.out.println("inserted-->"+temp2.getFirstName());
}
}
}
You are "self-invocation" a #Transactional method from the same bean which will not work .This behaviour is well explained in the docs at here and here (Search the keyword "self-invocation")
You can simply move the #Transactional method to another bean.Then inject this bean to its client bean and invoke this #Transactional method.
Or use the TransactionTemplate to execute it within a transaction:
#Autowired
private TransactionTemplate txTemplate;
#Override
public void run(String... args) throws Exception {
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txTemplate.execute(status->{
insertSingleBatch();
return null;
});
}
Please note that TransactionTemplate will ignore the setting on the #Transactional and you have to configure it programatically.
I need to create a multitenanacy application with ability to switch between schemas inside my java-code (not based on a user request).
I've read articles:
https://fizzylogic.nl/2016/01/24/make-your-spring-boot-application-multi-tenant-aware-in-2-steps/
http://www.greggbolinger.com/tenant-per-schema-with-spring-boot/
Solution works fine, when the schema is passed in Rest-request.
However I need to implement the following logic:
public void compare(String originalSchema, String secondSchema){
TenantContext.setCurrentTenant(originalSchema);
List<MyObject> originalData = myRepository.findData();
TenantContext.setCurrentTenant(secondSchema);
List<MyObject> migratedData = myRepository.findData();
}
The point is, that connection is not switched, when I manually set up TenenantContext. MultiTenantConnectionProviderImpl.getConnection is invoked only on the first call to my repository.
#Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
#Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = getAnyConnection();
try {
connection.createStatement().execute( "ALTER SESSION SET CURRENT_SCHEMA = " + tenantIdentifier );
}
catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",e);
}
return connection;
}
}
Is it possible to force switching sessions?
Found a hard-coded solution.
#Service
public class DatabaseSessionManager {
#PersistenceUnit
private EntityManagerFactory entityManagerFactory;
public void bindSession() {
if (!TransactionSynchronizationManager.hasResource(entityManagerFactory)) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager));
}
}
public void unbindSession() {
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager
.unbindResource(entityManagerFactory);
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
}
}
Each block, loading data in a new tenantContext should execute the following:
databaseSessionManager.unbindSession();
TenantContext.setCurrentTenant(schema);
databaseSessionManager.bindSession();
//execute selects
Well, you need it
public interface Service {
List<MyObject> myObjects();
}
#Service
#Transactional(propagation = Propagation.REQUIRES_NEW)
public class ServiceImpl implements Service {
#Autowired
private MyRepository myRepository;
#Override
public List<MyObject> myObjects() {
return myRepository.findData();
}
}
#Service
public class AnotherService() {
#Autowired
private Service service;
public void compare(String originalSchema, String secondSchema){
TenantContext.setCurrentTenant(originalSchema);
List<MyObject> originalData = service.myObjects();
TenantContext.setCurrentTenant(secondSchema);
List<MyObject> migratedData = service.myObjects();
}
}
Try using
spring.jpa.open-in-view=false
in your application.properties file.
More info on this
What is this spring.jpa.open-in-view=true property in Spring Boot?
Hope this helps..
I am able to run a Rest Controller PUT method that uses a Autowired #Service as expected via the Spring Boot Application. The same Autowiring is failing while trying to perform a Spring JUnit Test. I have tried reading through multiple threads with similar issues. I made sure I am NOT creating the #Service through the "new" keyword and I tried Context Configuration and other methods .. but all seems to be in vain. I am not sure where I am going wrong.
My Spring Boot Application class -
#SpringBootApplication
#ComponentScan({
"com.initech.myapp.*"
})
public class IngestionServerApplication {
private static final Log logger = LogFactory.getLog(IngestionServerApplication.class);
public static void main(String[] args) {
SpringApplication.run(IngestionServerApplication.class, args);
logger.info("Ingestion Server Application started...");
}
}
Rest Controller Class -
package com.initech.myapp.ingestion.controller;
#RestController
public class IngestionController extends BaseRestController {
private static final Log logger = LogFactory.getLog(IngestionController.class);
// This variable is getting "null" autowiring if invoked
// via Spring Unit Testing framework while it is injected fine via
// Spring Boot app invocation.
#Autowired
public IngestionPayloadProcessor payloadProcessor;
#RequestMapping(path = "/ingest", method = RequestMethod.PUT,
consumes = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
},
produces = {
MediaType.APPLICATION_JSON_VALUE
})
public IngestionSuccessResponse ingest(#RequestHeader(value = "authToken", required = true) String authToken,
#RequestBody String jsonBody) throws Exception {
IngestionPayload ingestionPayload = new IngestionPayload();
ingestionPayload.setAuthToken(authToken);
ingestionPayload.setJsonBody(jsonBody);
IngestionSuccessResponse ingestionSuccessResponse = payloadProcessor.process(ingestionPayload);
return ingestionSuccessResponse;
}
}
Service Class
package com.initech.myapp.ingestion.app.service;
#Service
#ImportResource({"spring.xml"})
public class IngestionPayloadProcessor {
private static final Log logger = LogFactory.getLog(IngestionPayloadProcessor.class);
#Resource(name = "kafkaConfig")
private Properties kafkaConfig;
#Value("${kakfaTopic}")
private String kakfaTopic;
public IngestionSuccessResponse process(IngestionPayload ingestionPayload) throws Exception {
try {
IngestionSuccessResponse ingestionSuccessResponse = buildSuccessResponse(ingestionPayload);
return ingestionSuccessResponse;
}
catch (IllegalStateException e)
{
logger.error("Encountered exception while dropping message in Kafka... " + e.getMessage());
throw e;
}
}
}
private buildSuccessResponse() { ... }
Spring Unit Testing
#RunWith(SpringRunner.class)
#ContextConfiguration(locations = "classpath*:/spring.xml")
#WebMvcTest(IngestionServerApplication.class)
public class IngestionServerApplicationTests {
#Autowired
private MockMvc mockMvc;
#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(
new IngestionServiceController())
.build();
}
#Test
public void testIngestService() throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("authToken","safdafio12312asdfs23");
RequestBuilder requestBuilder = put("/ingest").content("{'testKey' : 'testVal'}").accept(MediaType.APPLICATION_JSON).headers(httpHeaders);
this.mockMvc.perform(requestBuilder).andExpect(status().isOk());
}
}
Error Logs
2016-08-10 19:24:36.500 DEBUG 7505 --- [ main] m.m.a.RequestResponseBodyMethodProcessor : Read [class java.lang.String] as "application/json" with [org.springframework.http.converter.StringHttpMessageConverter#49aa766b]
2016-08-10 19:24:36.510 DEBUG 7505 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : Resolving exception from handler [public com.initech.myapp.ingestion.model.IngestionSuccessResponse com.initech.myapp.ingestion.app.controller.myappIngestionServiceController.ingest(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String) throws java.lang.Exception]: java.lang.NullPointerException
2016-08-10 19:24:36.512 DEBUG 7505 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : Invoking #ExceptionHandler method: public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> com.initech.myapp.base.controller.BaseRestController.handleException(java.lang.Exception,javax.servlet.http.HttpServletRequest)
This is the error handler...
2016-08-10 19:24:36.514 INFO 7505 --- [ main] p.d.i.a.c.myappIngestionServiceController : > handleNoResultException
2016-08-10 19:24:36.574 DEBUG 7505 --- [ main] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Written [{status=500, authToken=6acb1a5c-2ced-4690-95b3-eb7957c7c28a, error=null}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#50d3bf39]
java.lang.AssertionError: Status
Expected :200
Actual :500
Note that I have debugged through the test and I can see that the NullPointer exception is thrown at the below line in the Rest Controller Class as the payloadProcessor object is null.
IngestionSuccessResponse ingestionSuccessResponse = payloadProcessor.process(ingestionPayload);
=====
Unit Test with Mock objects: (This is working as expected)
#RunWith(SpringRunner.class)
#ActiveProfiles("dev")
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureMockMvc
public class IngestionServerUnitTests {
#Autowired
private MockMvc mockMvc;
#MockBean
private IngestionPayloadProcessor processor;
// I was able to get this to work by removing the setUp() method
// that was originally in my code. It was trying to build a new instance
// of the REST controller and then run the "perform" on top of it
// which was causing the test to fail I assume!
/*#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(
new IngestionServiceController())
.build();
}*/
#Test
public void testIngestService() throws Exception {
IngestionSuccessResponse ingestionSuccessResponse = new IngestionSuccessResponse();
ingestionSuccessResponse.setStatusCode("200");
ingestionSuccessResponse.setRequestId("6acb1a5c-2ced-4690-95b3-eb7957c7c28a");
ingestionSuccessResponse.setReceivedTimestamp("2016-08-09T19:43:30.02234312");
given(this.processor.process(anyObject())).willReturn(ingestionSuccessResponse);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization","5e18685c95b34690");
RequestBuilder requestBuilder = put("/ingest").content("<test>test data</test>").accept(MediaType.APPLICATION_JSON).headers(httpHeaders);
this.mockMvc.perform(requestBuilder).andExpect(status().isOk());
}
}
When you specify #WebMvcTest there are only certain components of your application that are added to the ApplicationContext. The annotation is actually a composition of a bunch of other annotations as described in the docs: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html
Based on this your IngestionPayloadProcessor doesn't get instantiated as a bean, and shouldn't as you are telling the test to only run tests for the web layer. What you need to do is specify a #MockBeanfor the IngestionPayloadProcessor within the test and then define a mock for the method that the controller is calling.
#RunWith(SpringRunner.class)
#WebMvcTest(IngestionServerApplication.class)
public class IngestionServerApplicationTests {
#Autowired
private MockMvc mockMvc;
#MockBean
private IngestionPayloadProcessor processor;
#Test
public void testIngestService() throws Exception {
given(this.processor.process(anyObject())).willReturn(new InjestionSuccessResponse());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("authToken","safdafio12312asdfs23");
RequestBuilder requestBuilder = put("/ingest").content("{'testKey' : 'testVal'}").accept(MediaType.APPLICATION_JSON).headers(httpHeaders);
this.mockMvc.perform(requestBuilder).andExpect(status().isOk());
}
}
Details on the new features of Spring Boot 1.4 testing are here: https://spring.io/blog/2016/04/15/testing-improvements-in-spring-boot-1-4
* Update based on comments *
Realized you could just auto configure the MockMvc and not need to use the TestRestTemplate. I haven't tested this but it should work.
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class IngestionServerApplicationTests {
#Autowired
private MockMvc mockMvc;
#Test
public void testIngestService() throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("authToken","safdafio12312asdfs23");
RequestBuilder requestBuilder = put("/ingest").content("{'testKey' : 'testVal'}").accept(MediaType.APPLICATION_JSON).headers(httpHeaders);
this.mockMvc.perform(requestBuilder).andExpect(status().isOk());
}
}
I'm trying to execute sample of transaction code. I throw checked exception from transactional method and set rollbackFor parameter for the class of this exception. However this transaction doesn't rollback. So my question is what may be the reason of that behaviour. Here is a sample of my code:
MyService:
#Service
#Transactional
#EnableTransactionManagement
public class MyService implements IMyService {
#Autowired
private IMyDao myDao;
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = BreakException.class)
public void test() throws BreakException {
myDao.updateMethodA("64", "'N'", "'2015.01.01/0008661/0008'");
if(true) throw new BreakException();
myDao.updateMethodB("15", "'N'", "'2015.01.01/0008661/0008'");
}
}
MyDao:
#Repository
public class MyDao implements IMyDao {
#Override
#Transactional(propagation=Propagation.MANDATORY)
public void updateOperationA(String x, String y, String z) {
String sql = "UPDATE ...."; //masked for example
jdbcTemplate.update(sql);
}
#Override
#Transactional(propagation=Propagation.MANDATORY, rollbackFor = Exception.class)
public void updateOperationB(String saldoPo, String saldo_status, String unikalne_id) throws BreakException {
String sql = "UPDATE ...."; //masked for example
//if(true) throw new BreakException();
jdbcTemplate.update(sql);
}
}
1.) So when I call test() on MyService the update from method updateMethodA is not rolledback after BreakException() is thrown.
2.) BreakException is my class that extends Exception
3.) When I throw BreakException from updateOperationB(commented in example) it does rollback correctly.
4.) When I change BreakException to extend TransactionException it works as expected so it seems the problem is only when I extend Exception.