In my unit tests I want to persist some entities and test their retrieval from the database. They were not being saved and I figured out that when the test method was also annotated with #Transaction, anything that happened inside it did not get persisted, even though the method finished without an error.
I had previously encountered a LazyInitializationException when messing with a many-to-many lazy-loaded association and annotating the method with #Transaction seemed to fix the issue, that's why I have been using it.
What could be the cause why the entities don't get saved? There is no reason for the transaction to be rolled back, since it does not fail.
Code of related classes:
#Test
#Transactional
public void plainPersistence() throws NullParameterException {
User user = userHelper.createUser("User1", "password", null, null);
Assert.assertNotNull(userDAO.findByUsername("User1"));
}
userHelper:
#Service
public class UserHelper {
#Autowired
private UserDAO userDAO;
public User createUser(...) throws NullParameterException {
User newUser = new User(username, ...);
userDAO.save(newUser);
return newUser;
}
UserDAO's save() method subsequently calls save() on UserRepository:
#Repository
public interface UserRepository extends CrudRepository<User, Long> {
public User findByUsername(String username);
}
Since you're likely using Spring test, you should note that they are configured so that the default behaviour is to rollback the changes. To change this you should annotate your test classes with, if you are using Spring < 4.2
#TransactionConfiguration(defaultRollback = false)
otherwise, annotate the class with #Rollback(value = false)
Related
I have some uncatchable bug in my work.
For example, I have code that looks like this:
#Entity
public class Message {
#Id
#GeneratedValue(strategy = SEQUENCE, generator = "message_generator")
private long id;
private long massMessageId;
}
public class MessageDTO {
public final long id;
public final long massMessageId;
}
#Transactional
#Service
public class ExtendedMessageService {
private MessageService messageService;
public MessageDTO createMessage(MessageCreateDTO createDTO) {
var messageDTO = messageService.create();
return messageService.linkMassMessage(messageDTO.id, createDTO.massMessageId);
}
}
#Transactional
#Service
public class MessageService {
private final MessageRepository repository;
private final ObjectMapper mapper;
public MessageDTO create() {
var message = new Message();
var savedMessage = repository.save(message);
return mapper.map(savedMessage, MessageDTO.class);
}
public MessageDTO linkMassMessage(long messageId, long massMessageId) {
var message = repository.findById(messageId)
.orElseThrow(() -> new ObjectNotFoundException("Message with id " + id + " was not found"));
return mapper.map(repository.save(message.setMassMessageId(massMessageId)), MessageDTO.class);
}
}
What will happen in this situation? I have some bugs, when repository.findById(id) can't find entity and throws exception.
And i have no reason, why this bug is only on prod (i tried to repeat it on dev and nothing succeeded)
And when i try to find the reason of it, i get a question:
"Can i save entity and get it in one transaction in Spring?"
How saving works
repository.save() doesn't save anything to database, this method puts entity to the session (persistent context) in memory.
flush step — on this step actual SQL insert happens. It can be invoked manually repository.saveAndFlush(), repository.flush(). Hibernate can do flush in the background, before operations that can use saved to the database value, like JPQL statements.
Also flush happens when the end of #Transactional boundary is reached.
What can be an issue
You are using incorrect method. This method from the old version of Spring data and it doesn't perform search in the database. You have to use findById() method instead.
Hibernate: findById vs getbyId
The most simple way, if you want to use id after save — flush the data immediately.
Entity entity = new Entity(some_information);
repository.saveAndFlush(entity);
Entity findedEntity = repository.findById(entity.getId())
.orElseThrow(() -> new RuntimeException("Can't find id=" + entity.getId()));
Hibernate will not necessary perform SQL select to get findedEntity. It can get it from the session, if it happens in the same #Transactional boundaries.
So if the above code resides in the method with #Transaction SQL will not performed. if there is not #Transaction SQL will be performed.
About this question
"Can Spring or Hibernate find not flushed entity in transaction context? Or there are some other ways to do it?"
Hibernate can't find not flushed entity. if id is autogenerated, Hibernate needs to perform SQL INSERT (flush) to get the id from a database. Another option to set up an id manually. Probably in this case it will be possible to get an entity from the persistent context.
Suppose I have a user class:
#Entity
#Data
#Builder
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long userKey;
#Column(unique = true)
String someId;
String name;
}
And it's corresponding service
#Component
#Slf4j
public class UserService {
#Autowired
UserRepository repository;
#Transactional
public User createUserWithId(String name, String id) {
User userToAdd = User.builder()
.name(name)
.someId(id)
.build();
repository.save(userToAdd);
log.info("No issue in saving");
//some more code
return userToAdd;
}
}
As you can see that I have a unique constraint on someId field in User class but when I execute the method createUserWithId with a value in someId which is already present in DB, I'd expect to get an error on the line containing repository.save() and the code after it to not be executed. But the code after it is getting executed and I'm getting an exception at the end of the transactional block. My question is why this is happening and what are the exceptions which I would generally get when interacting with the repository object ( like in this case repository.save ) and which type of exceptions will I get at the end of transactional block ?
PS I am calling the UserService from inside a simple controller and I have created an empty UserRepository which just extends CrudRepository. Both of which I have left out from the question for brevity but let me know if adding them here would make sense.
EDIT 1: Adding user repository as per request in comments
#Repository
public interface UserRepository extends CrudRepository<User, Long> {
}
The error happens in the interceptor because right before committing the transaction, Hibernate needs to flush pending changes to the database. During that flush, the database exception happens. You can flush manually by calling saveAndFlush on the repository.
Uniqe error happens when duplicate .
spring has database error helper class that you can catch db exceptions on controller layer passed by #transactional to controller.
} catch (DataAccessException ex) {
or
} catch (DataIntegrityViolationException ex) {
in case the database connector has standard exception throw support.
in your case I think you missed
#Transactional(readOnly = false)
When we call
repository.save(obj);
hibernate kept this entity in memory, the Entity will persist at the of the method in the Transaction.
One more way to do such kind of operation, first you should try to fetch result on id
repository.findById(id)
And check whether it is null or not and accordingly perform save operation.
I am updating my application from Spring Boot 1.4.5 / Hibernate 4.3.5 to Spring Boot 2.0.9 / Hibernate 5.2.18 and code that used to work in the previous configuration is no longer working.
The scenario is as follows:
Start a transaction by entering a method annotated with #Transactional
Hydrate the entity
Change the entity
Make another query
Detect a problem. As a result of this problem, determine that changes should not persist.
Evict the entity
Exit the method / transaction
With Hibernate 4.3.5, calling entityManager.detach() would prevent the changes from being persisted. However, with Hibernate 5.2.18, I'm finding that changes are persisted even with this call. I have also tried to evict() from the session and I have tried to clear() all entities from the session (just to see what would happen).
So I ask - is it possible to discard entity changes in Hibernate 5.2.18 the way that I was able to do in Hibernate 4.3.5?
The relevant code is below...
#Entity
public class Agreement {
private Long agreementId;
private Integer agreementStateId;
#Id
#Column(name = "agreement_id")
public Long getAgreementId() {
return agreementId;
}
public void setAgreementId(Long agreementId) {
this.agreementId = agreementId;
}
#Basic
#Column(name = "agreement_state_id", nullable = false)
public Integer getAgreementStateId() {
return agreementStateId;
}
public void setAgreementStateId(Integer agreementStateId) {
this.agreementStateId = agreementStateId;
}
}
#Component
public class Repo1 {
#PersistenceContext(unitName = "rights")
private EntityManager entityManager;
public void evict(Object entity) {
entityManager.detach(entity);
}
public Agreement getAgreement(Long agreementId) {
// Code to get entity is here.
// Agreement with an agreementStateId of 5 is returned.
}
public void anotherQuery() {
// Code to make another query is here.
}
}
#Component
public class Service1 {
#Autowired
Repo1 repo;
#Transactional
public void doSomething() {
Agreement agreement = repo.getAgreement(1L);
// Change agreementStateId. Very simple for purposes of example.
agreement.setAgreementStateId(100);
// Make another query
repo.anotherQuery();
// Detect a problem here. Simplified for purposes of example.
if (agreement.getAgreementStateId() == 100) {
repo.evict(agreement);
}
}
}
I have found the problem and it has nothing to do with evict(). It turns out that an additional query was causing the session to flush prior to the evict() call.
In general, the application uses QueryDSL to make queries. Queries made in this way did not result in the session flushing prior to making a query. However in this case, the query was created via Session.createSQLQuery(). This uses the FlushMode already assigned to the session which was FlushMode.AUTO.
I was able to prevent the flush by calling setHibernateFlushMode(FlushMode.COMMIT) on the query prior to making the query. This causes the session FlushMode to temporarily change until after the query has been run. After that, the evict() call worked as expected.
For example I have
public interface CrudUserRepository extends JpaRepository<User, Integer> {
#Modifying
#Query(nativeQuery = true, value = "UPDATE users SET active=?2 WHERE id=?1")
void setActive(long id, boolean active);
}
In my test I invoking this method and then find by id.
#Autowired
CrudUserRepository crudUser;
#Transactional
#Test
public void setActiveTest(){
User user = getUser();
crudUser.save(user);
crudUser.setActive(user.getId(), true);
Optional<User> optUser = crudUser.findById(user.getId());
assert optUser.get().isActive(); //here my test falls
And when I perform crudUser.findById(user.getId()) my optUser contains that user doesn't active but actually he's active (I've tested that by setting #Rollback(false) and manually check in db).
How I can test that case?
Because crudUser.save will cache the entity. And when you call crudUser.findById, JPA just get the entity from the cache not from database. crudUser.setActive not update the entity.active in the cache. Change the code like bellow can fix it (clear the JPA cache) :
....
#PersistenceContext
EntityManager entityManager;
...
#Test
public void setActiveTest(){
...
// clear the cache and force crudUser.findById from database
entityManager.clear();
Optional<User> optUser = crudUser.findById(user.getId());
...
}
I have a question about xml-free configuration of Spring. Unfortunately it does not roll back my DB changes even if I mark a corresponding method with #Transactional annotation.
First of all I have a controller, which calls a class marked with #Transactional.
#RequestMapping(value="/Device", method=RequestMethod.POST, produces={"application/json"})
#ResponseStatus(HttpStatus.CREATED)
public #ResponseBody List<Device> addDevice(
#RequestBody Device deviceToAdd) {
List<Device> devices= deviceManager.addDevice(deviceToAdd);
return devices;
}
All controllers have an ExceptionResolver that catches DuplicateKeyException:
#ExceptionHandler(DuplicateKeyException.class)
public ResponseEntity<Result> handleBindException(DuplicateKeyException e) {
log.error(e.getMessage(), e);
Result result = new Result("this object already exists");
return new Response(result);
}
Here is my main transactional class which calls two DAO classes. The second one causes DuplicateKeyException, but the results of the first one are not deleted from DB.
#Transactional(propagation = Propagation.REQUIRED, rollbackFor=Exception.class)
public class DeviceManager {
#Autowired
DaoClass1 daoClass1;
#Autowired
DaoClass2 daoClass2;
public List<Device> addDevice(Device device){
Pocket pocket = daoClass1.addPocket(new Pocket());<--is not rolled back after Exception
device.setPocket(pocket);
List<Devices> addedDevices = daoClass2.addDevice(device); <--- causes exception
return devices;
}
I tried also to use #Transactional annotation both for the classes daoClass1 and daoClass2, but it did not change anything.
What could I have done wrong?