I had a database table that was storing 3rd party passwords in plain text. I am now updating the table to store the passwords encrypted. To handle this, I have an @EntityListener
class that does the encryption and decryption post-load and pre-persist/update. Now, I'm trying to write code to encrypt all of the passwords that are currently in the database.
I wrote the migration function to load everything that hasn't been migrated and save it again (so that the entity listener could run). The entity listener won't actually be invoked unless Hibernate thinks that the object is dirty, so I decided to evict the entity from the current session, thinking that would be the easiest way to run the conversion. Here's what I have:
public class Migrator {
@Autowired
EntityManager entityManager;
@Autowired
MyRepository myRepo;
@Async
public void migrateAll() {
List<MyEntity> toBeMigrated = myRepo.getUnencrypted();
for (MyEntity eachEntity : toBeMigrated) {
attemptEncryption(eachEntity);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void attemptEncryption(MyEntity entity) {
Session session = (Session) entityManager.getDelegate();
session.evict(entity);
myRepo.save(entity);
}
}
(Not shown: a simple web controller that calls the Migrator)
I added the @Transactional
annotation to attemptEncryption()
so that each record migrated would be saved. I didn't want a failure on a random row causing a rollback of the entire operation; as soon as something is successfully saved, I want that transaction committed.
Since this takes a while to run, I wanted to send back the HTTP response and run migrateAll()
in a separate thread. It worked fine without multithreading, but once I added the @Async
annotation, I ended up with org.hibernate.SessionException: Session is closed!
exceptions (stacktrace showed that the exception was thrown when trying to evict the entity in attemptEncryption()
). I assume that moving the work to another thread is the cause, but I've used @Async
in other parts of my application without any issues. The only major difference between this code and my other async code is that I'm evicting entities from the session; I don't do that anywhere else. Also, the database loads entities just fine with myRepo.getUnencrypted()
. I feel like this should fail if I were doing something wrong with Hibernate.
Questions
- Why is my session closed when I go to evict the entity?
- Is there a better way to make my entity dirty so that Hibernate will run the pre-persist listener and flush to the DB?
Research
- (Loading objects as detached) Field Level Encryption with Spring Data JPA and JPA EntityListener - I tried adding
@Transactional(propagation = Propagation.NOT_SUPPORTED)
togetUnencrypted()
method, but nothing changed. - (Sessions in @Async method) https://stackoverflow.com/a/29271930/2047962 - I'm already using a
@Transactional
annotation as prescribed. - (Forcing an update on a "clean" entity) https://stackoverflow.com/a/37255257/2047962 - Can't use Persistable. I don't want this entity to always look dirty, I just want it to be dirty for this one method.
- Force update in Hibernate - Used this code in
attemptEncryption()