I am just trying to understand the transaction propagation in spring: using jpa, postgres, validators and web starter in my project. Propogation REQUIRED says:
when one of these logical transactions is rolled back, all the logical transactions of the current physical transaction are rolled back.
but when I am throwing exception in insertUserKumar() it should not persist data in database as per documentation but its getting persisted.
my properties are below defined
logging.level.sql=debug
spring.datasource.generate_unique_name=false
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=dummy
spring.datasource.password=dummy
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
spring.datasource.hikari.auto-commit=false
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
some will say you might have missed #EnableTransactionManagement I tried it with or without that. but two records are getting persisted into database. Can anyone help me?
#Slf4j
#Component
#RequiredArgsConstructor
public class AppRunner implements CommandLineRunner {
private final UserRepository userRepository;
#Override
public void run(String... args) throws Exception {
insertUserBunty();// this transaction should fail but its getting persisted
}
#Transactional(propagation = Propagation.REQUIRED)
void insertUserBunty() {
User bunty = new User(null, "Bunty");
userRepository.save(bunty);
insertUserKumar();
}
#Transactional(propagation = Propagation.REQUIRED)
void insertUserKumar() {
User kumar = new User(null, "Kumar");
userRepository.save(kumar);
throw new RuntimeException("This will rollback both insert Bunty and Kumar");
}
}
The reason is that #Transactional does not work when you call the method from another method in the class. The reason being the way Spring handles the transactionality features. It is basically handled by a Proxy of your class and thus #Transactional annotation only has an effect when the method is called by a method outside your class. Check details at:
https://medium.com/javarevisited/spring-transactional-mistakes-everyone-did-31418e5a6d6b
Related
I want to test a non-transactional method in the service layer when inner methods are transactional separately. I want to know what will happen if some method throws an exception. Does it properly roll back or not? I also tried rollbackFor but it did not help. Any suggestions?
Spring v-2.5.1
app.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=create-drop
server.error.include-message=always
server.error.include-binding-errors=always
Controller
#PostMapping("/test")
public void test(){
service.test();
}
Service
#Service
public class UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private PasswordEncoder passwordEncoder;
public void test() {
User user1 = new User();
String encodedPassword = passwordEncoder.encode("123");
user1.setUsername("username");
user1.setPassword(encodedPassword);
user1.setFirst_name("name1");
user1.setLast_name("family1");
save(user1);
User update = update(user1.getUsername());
throw new RuntimeException();// I expect Spring rollback all data for save and update methods
//but it seems data has been committed before an exception occur.
}
#Transactional
public void save(User user) {
if (userExists(user.getUsername()))
throw new ApiRequestException("Username has already taken!");
else {
User user1 = new User();
String encodedPassword = passwordEncoder.encode(user.getPassword());
user1.setUsername(user.getUsername());
user1.setPassword(encodedPassword);
user1.setFirst_name(user.getFirst_name());
user1.setLast_name(user.getLast_name());
user1.setCreate_date(user.getCreate_date());
user1.setModified_date(user.getModified_date());
user1.setCompany(user.getCompany());
user1.setAddresses(user.getAddresses());
userRepository.save(user1);
// throw new RuntimeException(); Similarly I expect rollback here.
}
}
#Transactional
public User update(String username) {
User userFromDb = userRepository.findByUsername(username);
userFromDb.setFirst_name("new username");
userFromDb.setLast_name("new lastname");
return userRepository.save(userFromDb);
}
}
Due to way spring proxying works by default, you can not call a "proxied" method from within the instance.
Consider that when you put #Transactional annotation on a method, Spring makes a proxy of that class and the proxy is where the transaction begin/commit/rollback gets handled. The proxy calls the actual class instance. Spring hides this from you.
But given that, if you have a method on the class instance (test()), that calls another method on itself (this.save()), that call doesn't goes through the proxy, and so there is no "#Transactional" proxy. There is nothing to do the rollback when the RuntimeException occurs.
There are ways to change the how Spring does the proxying which would allow this to work, but it has changed over the years. There are various alternatives. One way is to create create separate classes. Perhaps UserServiceHelper. The UserServiceHelper contains #Transactional methods that get called by UserService. This same issue occurs when different #Transactional isolations and propagations are needed.
Related answers and info:
Spring #Transaction method call by the method within the same class, does not work?
Does Spring #Transactional attribute work on a private method?
Spring Transaction Doesn't Rollback
Your example code is not very clear as to what you are trying to do. Often, #Transactional would be put on the service class and apply to all public methods (like test()).
To begin with, I'm not sure if you understand what a transaction represents. If there's a unit of work that contains several functionalities to be executed, and you either want them to completely fail or completely pass - that's when you use a transaction.
So in the case of test() method - why should underlying mechanism (in this case Hibernate) care about rolling back something that happened within save() and update() when the test() itself isn't a transaction?
Sorry for asking more than answering, but as far as I can see - you need to annotate test() with the annotation, and not the individual methods. Or you can annotate them all.
I need to make a #Scheduled method that has a list of schemas and for each schema, deletes rows from 2 tables.
#Scheduled(fixedDelay = 10000)
public void scheduleFixedDelayTask() {
List<String> presentSchemas = getPresentSchemas();
for (String schema : presentSchemas) {
deleteFromCustomerTables(schema);
}
}
I've defined deleteFromCustomerTables as #Transactional(propagation = Propagation.REQUIRES_NEW) and inside it i use the EntityManager to delete rows from 2 tables.
In order to make it work i need to add #Transactional to scheduleFixedDelayTask, otherwise i recive a TransactionRequiredException.
My problem is that i do not want the whole scheduler to be #Transactional, if something goes wrong in one schema i do not want to do a rollback of all schemas.
I've also tried without #Transactional and with :
Session session = entityManager.unwrap(Session.class);
Transaction t = session.beginTransaction();
//exec delete
t.commit();
session.close();
But i still recieve TransactionRequiredException.
Do you have a solution?
You have to be sure that you configured the transaction manager something like this:
#Configuration
#EnableTransactionManagement
public class TransactionConfig {
#Bean
#Primary
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager();
}
}
You have to be sure that method deleteFromCustomerTables is not in the same component, something like this:
#Component
class Component1 {
void scheduleFixedDelayTask(){...}
}
#Component
class Component2 {
void deleteFromCustomerTables(){...}
}
But at a very high level, Spring creates proxies for classes that
declare #Transactional on the class itself or on members. The proxy is
mostly invisible at runtime. It provides a way for Spring to inject
behaviors before, after, or around method calls into the object being
proxied. Transaction management is just one example of the behaviors
that can be hooked in. Security checks are another. And you can
provide your own, too, for things like logging. So when you annotate a
method with #Transactional, Spring dynamically creates a proxy that
implements the same interface(s) as the class you're annotating. And
when clients make calls into your object, the calls are intercepted
and the behaviors injected via the proxy mechanism.
Spring boot, postgres, spring jpa.
Have a service, which is trying to store changes across multiple repositories:
class Service {
#Transactional
public void doStuff() {
repo1.delete(...);
repo2.saveAll(...);
repo1.save(...);
}
}
This operation requires to be rolled back if anything fails.
Here I struck into two things:
If I add a throw RuntimeException somewhere in the middle of that method, all things before it don't get rolled back.
In regular flow I get
Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction
My configuration is:
#Configuration
#EnableTransactionManager
#EntityScan
#EnableJpaRepositories
public class DataConfig {}
Also trying to use the TransactionTemplate bean with it's execute method. Manage to overcome the first issue, but still fail with the second one.
You can try by adding #Transactional(rollbackFor = Exception.class) this will rollback even if you have an exception in your code
class Service {
#Transactional(rollbackFor = Exception.class)
public void doStuff() {
repo1.delete(...);
repo2.saveAll(...);
repo1.save(...);
}
}
#Service
#Transactional
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private RoleRepository roleRepository;
#Transactional
public void save(String name) {
method1(name);
method2(name);
}
public void method1(String name){
userRepository.save(name)
}
public void method2(String name) {
roleRepository.save(name);// Error
}
}
public interface UserService {
void save(String name) throws Exception;
}
#PostMapping("/save")
public void save() throws Exception {
userService.save("SomeThing");
}
I have 2 methods in the save method. As you can see, the method2 has an error.
I run the program, the first method prints to the database. However, if the second method is incorrect, I want the rollback process to fall and the first method not to be written to the database. How can I do it?
I tried to use #Transaction annotation on method1 and method2 but it error continued. And i tried Propagation.REQUIRES_NEW, and Propagation.REQUIRED, TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); never changed.
This is totally logical what you are trying to do. Just rollingback when one part of your flow fails is normal.
I'll give more background to make sure you have everything:
#Transactional only rollback, by default, when a RuntimeException is thrown. You can customize this. Check the doc, it is very easy to find out. Be careful, make sure you use org.springframework.transaction.annotation.Transactional. You have more options with this one.
Understanding propagation is key. By default, the propagation of #Transactional is Required, which means it will either joins an existing transaction, or creates a new one. Requires_New always create a new one. In your case, Propagation.REQUIRED is the good one.
Once you are in a Transactional method, adding #Transactional on other method inside your class and make inner call won't have any effect. The transaction starts when you enter in your method from the outside of your class. Once you in your class, the others annotations won't affect the runtime.
Three things/questions to consider:
What kind of error is throwing the RoleRepository.save(...) ?
What is the context ? Is it a test ? Is it a running application ? Could you provide more code ?
As said in one comment, read this: Spring Transaction Management with Hibernate and MySQL, Global and Local. You could have some issues with that as well...
I want to test hibernate session's save() method using spring testing framework.
#Test method is :
#Test
#Transactional
public void testSave() {
User expected = createUser();
getGenericDao().currentSession().save(expected);
User actual = getUser(generatedId);
assertUsersEqual(expected,actual);
}
I want to flush user into database. I want my user to be in database after this method
getGenericDao().currentSession().save(expected);
Then I want to go to database using spring data framework and fetch this saved user by next line:
User actual = getUser(generatedId);
I tried to use hibernate flush method like:
currentSession().setFlushMode(MANUAL);
//do saving here
currentSession().flush();
It does not flush my user into database!
However if I don't make use of #Transactional spring annotation and save my user in programmatic spring transaction I achieve what I want. Unfortunately then user saved into db
is not rollbacked as there is no spring #Transactional. Therefore my test method changes the db and behaviour of subsequent test methods.
So I need to flush my user into db inside test method(not at the end) and at the end of test method rollback all changes to db.
UPDATE suggestion to prepare method as follows:
#Transactional
public void doSave(User user){
getGenericDao().currentSession().save(user);
}
And call doSave inside testSave is doing nothing. Still I have no user in db after executing this method. I set breakpoint and check my db from command-line.
UPDATE Thanks very much for response. The problem is that method flush() does not put my user into database.I tried Isolation.READ_UNCOMMITTED and it does not put my user into database. I can achieve what I want but only if I turn off spring transaction on #Test method and do saving in programmatic transaction. BUT then #Test method is not rolled back leaving saved user for subsequent #Test methods. Here #Test method to save user is not as dangerous as #Test method to delete user, because it is not rolled back. So there must spring transactional support for #Test method with which I can't anyway put my user(or delete) into db. Actually user is put(or deleted) into db only after #Test method ends and transaction for #Test method is comitted. So I want to save my User into db in the middle of #Test method and roll back it at the end of #Test method
Thank you!
Finally I stuck to the following solution:
First, my #Test methods are not running within spring #Transactional support. See this article to know how dangerous it may be. Next, instead of using #Repository beans inside #Test methods I autowire #Service beans which use #Transactional annotation.
The miracle is that #Test method like this
#Test
#Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testSave() {
Answer created = createAnswer();
Long generatedId = answerService.save(created);
//at this moment answer is already in db
Answer actual=getAnswerById(generatedId);
... }
puts my Answer object into database (just after answerService.save(created);) and method getAnswerById goes to DB and extracts it to check if save was correct.
To eliminate changes made to database in #Test method I recreate database by JdbcTestUtils.executeSqlScript
Have a look here with warning about #Transactional tests (Spring Pitfalls: Transactional tests considered harmful). I've used #org.springframework.test.context.jdbc.Sql to re-populate DB in my service tests and #Transactional for controllers.
ConstraintViolationException for controller update test with invalid data have been thrown only when transaction is committed. So I've found 3 options:
2.1 Annotate test with #Commit or with #Transactional(propagation = Propagation.NEVER). Be aware of DB change.
2.2 Use TestTransaction
Code:
TestTransaction.flagForCommit();
TestTransaction.end();
2.3 Use TransactionTemplate
Code:
#Autowired
private PlatformTransactionManager platformTransactionManager;
#Test(expected = Exception.class)
public void testUpdate() throws Exception {
TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
String json = ...
transactionTemplate.execute(ts -> {
try {
mockMvc.perform(put(REST_URL + USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isOk());
...
} catch (Exception e) {
e.printStackTrace();
}
return null;
});
You should also take care of the importedpackage :
in my case I imported
import javax.transaction.Transactional;
instead of
import org.springframework.transaction.annotation.Transactional;
If flush is not working then it very much depend on your database isolation level.
Isolation is one of the ACID properties of database, that defines how/when the changes made by one operation become visible to other concurrent operations.
I believe your isolation level is set to Read Committed or Repeatable Read.