I have Java EE application with Hibernate. I want to implement a feature that every minute updates one of existing rows in database. I have following classes:
#Singleton
#Startup
public class TimerRunnerImpl implements TimerRunner {
#EJB
private WorkProcessor workProcessor;
private String jobId;
#Timeout
#AccessTimeout(value = 90, unit = TimeUnit.MINUTES)
#TransactionAttribute(value = TransactionAttributeType.NEVER)
public void doProcessing(Timer timer) {
jobId = workProcessor.doWork(jobId);
}
//other methods: startTimer, etc
}
#Stateless
public class WorkProcessorImpl implements WorkProcessor {
#EJB
private MyEntityDao myEntityDao;
#TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
#Override
public String doWork(String jobId) {
if (jobId == null) {
MyEntity myEntity = myEntityDao.oldestEntityToProcess();
String uuid = UUID.randomUUID().toString();
myEntity.setJobId(uuid);
myEntityDao.update(myEntity); // this invokes merge()
return uuid;
} else {
// line below can never find entity, although there is one in DB
MyEntity myEntity = myEntityDao.findByJobId(jobId);
myEntity.setSomeProperty("someValue");
// some other updates
myEntityDao.update(myEntity); // this invokes merge()
return jobId;
}
}
}
First run of doWork updates MyEntity with job ID. This is being persisted into database - I can query it manually from SQLDeveloper. Second run always fails to find entity by job ID. In case I try to retrieve it by entity_id in debug mode, the object retrieved from Entity Manager has job id with previous value.
This is not cache problem, I have tried on each run to evict all cache at the beginning and results are identical.
As far as I understand, transaction is around workProcessor.doWork(jobId). I find confirmation of this by the fact that when this method returns I can see changes in DB. But why does EntityManager keeps my unmodified object and returns it when I query for it?
Related
I have a simple Repository:
public interface ReviewRepository extends CrudRepository<ReviewEntity, Integer> {
#Transactional(readOnly = true)
List<ReviewEntity> findByProductId(int productId);
}
I want to test it using test containers I followed the procedures and wrote my test case:
public abstract class MySqlTestBase {
private static MySQLContainer database = new MySQLContainer("mysql:5.7.32");
static {
database.start();
}
#DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", database::getJdbcUrl);
registry.add("spring.datasource.username", database::getUsername);
registry.add("spring.datasource.password", database::getPassword);
}
}
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class PersistTests extends MySqlTestBase {
#Autowired
private ReviewRepository repository;
private ReviewEntity savedEntity;
#BeforeEach
void setupDb() {
repository.deleteAll();
ReviewEntity entity = new ReviewEntity(1, 2, "author1");
savedEntity = repository.save(entity);
assertEqualsReview(entity, savedEntity);
}
#Test
void update() {
savedEntity.setAuthor("author2");
repository.save(savedEntity);
ReviewEntity foundEntity = repository.findById(savedEntity.getId()).get();
assertEquals(1, (long)foundEntity.getVersion());
assertEquals("author2", foundEntity.getAuthor());
}
}
my ReviewEntity also is written like:
#Entity
public class ReviewEntity {
#Id #GeneratedValue
private int id;
#Version
private int version;
private int productId;
private int reviewId;
private String author;
public ReviewEntity(int productId, int reviewId, String author) {
this.productId = productId;
this.reviewId = reviewId;
this.author = author;
}
// setter and getter
}
When I run this test it fails at the assertEquals(1, (long)foundEntity.getVersion()); line with this message:
expected: <1> but was: <0>
Expected :1
Actual :0
But I update the ReviewEntity class and according to the documentation the #Version field should automatically increases but this not happens. what part of my test is wrong?
If you look at the default implementation of save method in CrudRepository interface in the SimpleJpaRepository class you will see save method is implemented like:
#Transactional
#Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "Entities must not be null!");
List<S> result = new ArrayList<S>();
for (S entity : entities) {
result.add(save(entity));
}
return result;
}
meaning it is marked with #Transactional with Required as its propagation level(it is default)
Required propagation works like this:
REQUIRED is the default propagation. Spring checks if there is an
active transaction, and if nothing exists, it creates a new one.
Otherwise, the business logic appends to the currently active
transaction
and for DataJpaTest annotation comment section says:
By default, tests annotated with #DataJpaTest are transactional and
roll back at the end of each test
So for method update in your test a transaction is going to be created and the save method in repository.save(savedEntity); is going to be appended to that transaction. meaning it is committed only if that transaction successfully committed and we now know that's not going to happen.
A workaround for this problem probably would be to annotate test class with #Transactional(propagation = NOT_SUPPORTED) to suspends the currently running transaction then for repository.save(savedEntity); a transaction is going to be created and committed at the end of save method and then you can proceed in your test.
I'm using:
Quarkus with JPA (javax)
Postgres 11 database
I have:
An Entity
#Entity
#Table(name = "MyEntityTable")
#NamedQuery(name = MyEntity.DOES_EXIST, query = "SELECT x FROM MyEntity x WHERE x.type = :type")
public class MyEntity {
public static final String DOES_EXIST = "MyEntity.DoesExists";
#Id
#SequenceGenerator(name = "myEntitySequence", allocationSize = 1)
#GeneratedValue(generator = myEntitySequence)
private long id;
#Column(name = type)
private String type;
}
A repository
#ApplicationScoped
#Transactional(Transactional.TxType.Supports)
public class MyEntityReporitory {
#Inject
EntityManager entityManager;
#Transactional(Transactional.TxType.Required)
public void persist(final MyEntity entity) {
entityManager.persist(entiy);
}
public boolean doesExist(final String type) {
final TypedQuery<MyEntity> query = entityManager
.createNamedQuery(MyEntity.DOES_EXIST, MyEntity.class)
.setParameter("type", type);
return query.getResultList().size() > 0;
}
}
A test with two variations
Variation 1
#QuarkusTest
#QuarkusTestResource(DatabaseResource.class) // used to set up a docker container with postgres db
public class MyEntityRepositoryTest {
private static final MyEntity ENTITY = entity();
#Inject
MyEntityRepository subject;
#Test
public void testDoesExist() {
subject.persist(ENTITY);
final boolean actual = subject.doesExist("type");
assertTrue(actual);
}
#Test
public void testDoesExist_notMatching() {
subject.persist(ENTITY);
final boolean actual = subject.doesExist("another_type");
assertFalse(actual);
}
private static MyEntity entity() {
final MyEntity result = new MyEntity();
result.setType("type")
return result;
}
}
When I execute this test class (both tests) I'm getting the following Exception on the second time the persist method is called:
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist com.mypackage.MyEntity
...
Variation 2
I removed the constant ENTITY from the test class, instead I'm calling now the entity() method inside the tests, like:
...
subject.persist(entity());
...
at both places. Now the Exeption is gone and everything is fine.
Question
Can someone explain to me, why this is the case (why variante 2 is working and variante 1 not)?
https://vladmihalcea.com/jpa-persist-and-merge/
The persist operation must be used only for new entities. From JPA perspective, an entity is new when it has never been associated with a database row, meaning that there is no table record in the database to match the entity in question.
testDoesExist executed, ENTITY saved to database and ENTITY.id set to 1
testDoesExist_notMatching executed and persist called on ENTITY shows the error beacuse it exists in the database, it has an id assigned
The simplest fix is to call entity() twice, as in you variation 2.
But don't forget that the records will exist after a test is run, and might affect your other test cases. You might want to consider cleaning up the data in an #After method or if you intend to use this entity in multiple test cases then put the perist code into a #BeforeClass method.
I'm writing some hql queries using the #Query annotation in a spring data jpa repository. I know that I can use the methods from the repository interface, but for learning purpose, I'm writing them explicitly.
Here is my Main class
#SpringBootApplication
public class Main implements CommandLineRunner {
#Autowired
PersonRepository personRepository;
public static void main( String[] args ) {
SpringApplication.run(Main.class, args);
}
/**
* if we delete the transactional annotation-> we get an exception
*/
#Override
#Transactional
public void run( String... args ) throws Exception {
saveOperation();
deleteOperationUsingHql();
}
private void saveOperation() {
Person p = new Person("jean", LocalDate.of(1977,12,12));
personRepository.save(p);
}
private void deleteOperationUsingHql() {
personRepository.deleteUsingHql(1L);
personRepository.flush();
Optional<Person> p = personRepository.findById(1L);
if (p.isPresent()){
System.out.println("still present");
}
}
}
My personRepository interface
public interface PersonRepository extends JpaRepository<Person, Long> {
#Query(value = "select p from Person p where p.id=?1")
List<Person> getById( Long id);
#Modifying
#Query(value = "delete from Person p where p.id=:id")
void deleteUsingHql( Long id );
}
The person class
#Entity
#Table(name = "Person")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private LocalDate date;
// constructors,getters...omitted for brievety
}
everything is running well, but for the deleteOperationUsingHql(), even If I deleted the person from the database and even if I flush the modification to the database, the person with id=1 is still returned by the findById(1L) method. What should I do for making the findById(1L) returning an empty Optional.
My second question is about the #Transactional annotation, I know how it works in details, but I don't know why if we delete it, We get the following exception
Caused by: javax.persistence.TransactionRequiredException: Executing
an update/delete query
Could someone explains why I'm getting this exception when #Transactional is removed.
even If I deleted the person from the database and even if I flush the modification to the database, the person with id=1 is still returned by the findById(1L) method
That's normal, because you use a query to delete the person, instead of actually using the repository (and thus the EntityManager) delete method. Queries bypass the session cache completely, so Hibernate has no idea that this person has been deleted, and returns the instance in its cache. Solution: don't use a query. Alternate solution, clear the cache after deleting (for example by setting the clearAutomaticallyflag of the Modifying annotation to true).
Could someone explains why I'm getting this exception when #Transactional is removed.
Because when #Transactional is removed, there is no transaction being started by SPring before executing the method, and as you can see from the error message, delete queries must be executed inside a transaction.
I faced with a very strange behavior in my web app with spring 3 and hibernate-core 3.5.1-Final.
For simplicity i provide my code..
if(ripid!=null){ //Parameter
Appuntamento apDaRip = appuntamentoService.findById(ripid);
if(apDaRip.getIdpadre()!=null){
apDaRip.setNota("RIPROGRAMMATO n."+ripid.toString()+"\n"+apDaRip.getNota());
apDaRip.setIdpadre(apDaRip.getIdpadre());
}else{
apDaRip.setNota("RIPROGRAMMATO n."+ripid.toString()+"\n"+apDaRip.getNota());
apDaRip.setIdpadre(ripid);
}
try{
apDaRip.setOrarioinizio(null);
apDaRip.setDurata(null);
//apDaRip.setIdappuntamento(null);
}catch(Exception e){e.printStackTrace();}
map.put("appuntamento", apDaRip);
}
di = datiintranetService.findById(DatiintranetService.PASS_X_INTERVENTI);
map.put("passinterventi", di.getBoolean());
The idea behind is to use some data of an object "Appuntamento" for produce a new one.
So i'm going to change some value and before send the object to my view (jsp) i fetch other data by calling findbyid. This cause an update to the Appuntamento object... Off course i don't want this behavior. Someone can have an explanation of this?
Edit-1
Here's the Dao
#Transactional
public class DatiintranetService {
private DatiintranetDAO datiintranetDAO;
public void setDatiintranetDAO(DatiintranetDAO datiintranetDAO) {
this.datiintranetDAO = datiintranetDAO;
}
public DatiintranetDAO getDatiintranetDAO() {
return datiintranetDAO;
}
public Datiintranet findById(Integer id) {
return datiintranetDAO.findById(id);
}
}
and For Appuntamento class I provide to you a snapshot
#Entity
#Table(name = "appuntamento", schema = "public")
public class Appuntamento implements java.io.Serializable {
#Id
#SequenceGenerator(name="appuntamentoID", sequenceName="appuntamento_idappuntamento_seq",allocationSize =1)
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="appuntamentoID")
#Column(name = "idappuntamento", unique = true, nullable = false)
public Integer getIdappuntamento() {
return this.idappuntamento;
}
}
Edit-2
IF i move thoese two row above the if statement no update occur.
di = datiintranetService.findById(DatiintranetService.PASS_X_INTERVENTI);
map.put("passinterventi", di.getBoolean());
If you query for an entity and change the entity, the default behavior is to persist those changes via an update to the database. This is usually what you want to happen, but obviously not in all cases.
If you want to avoid the update, you need to detach the entity by calling session.evict(apDaRip) where session is a reference to the hibernate session (see Session.evict()). You probably want to evict the entity right after you get it (immediately following the call to findById).
I'm trying to get the old entity in a #HandleBeforeSave event.
#Component
#RepositoryEventHandler(Customer.class)
public class CustomerEventHandler {
private CustomerRepository customerRepository;
#Autowired
public CustomerEventHandler(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
#HandleBeforeSave
public void handleBeforeSave(Customer customer) {
System.out.println("handleBeforeSave :: customer.id = " + customer.getId());
System.out.println("handleBeforeSave :: new customer.name = " + customer.getName());
Customer old = customerRepository.findOne(customer.getId());
System.out.println("handleBeforeSave :: new customer.name = " + customer.getName());
System.out.println("handleBeforeSave :: old customer.name = " + old.getName());
}
}
In the event I try to get the old entity using the findOne method but this return the new event. Probably because of Hibernate/Repository caching in the current session.
Is there a way to get the old entity?
I need this to determine if a given property is changed or not. In case the property is changes I need to perform some action.
If using Hibernate, you could simply detach the new version from the session and load the old version:
#RepositoryEventHandler
#Component
public class PersonEventHandler {
#PersistenceContext
private EntityManager entityManager;
#HandleBeforeSave
public void handlePersonSave(Person newPerson) {
entityManager.detach(newPerson);
Person currentPerson = personRepository.findOne(newPerson.getId());
if (!newPerson.getName().equals(currentPerson.getName)) {
//react on name change
}
}
}
Thanks Marcel Overdijk, for creating the ticket -> https://jira.spring.io/browse/DATAREST-373
I saw the other workarounds for this issue and want to contribute my workaround as well, cause I think it´s quite simple to implement.
First, set a transient flag in your domain model (e.g. Account):
#JsonIgnore
#Transient
private boolean passwordReset;
#JsonIgnore
public boolean isPasswordReset() {
return passwordReset;
}
#JsonProperty
public void setPasswordReset(boolean passwordReset) {
this.passwordReset = passwordReset;
}
Second, check the flag in your EventHandler:
#Component
#RepositoryEventHandler
public class AccountRepositoryEventHandler {
#Resource
private PasswordEncoder passwordEncoder;
#HandleBeforeSave
public void onResetPassword(Account account) {
if (account.isPasswordReset()) {
account.setPassword(encodePassword(account.getPassword()));
}
}
private String encodePassword(String plainPassword) {
return passwordEncoder.encode(plainPassword);
}
}
Note: For this solution you need to send an additionally resetPassword = true parameter!
For me, I´m sending a HTTP PATCH to my resource endpoint with the following request payload:
{
"passwordReset": true,
"password": "someNewSecurePassword"
}
You're currently using a spring-data abstraction over hibernate.
If the find returns the new values, spring-data has apparently already attached the object to the hibernate session.
I think you have three options:
Fetch the object in a separate session/transaction before the current season is flushed. This is awkward and requires very subtle configuration.
Fetch the previous version before spring attached the new object. This is quite doable. You could do it in the service layer before handing the object to the repository. You can, however not save an object too an hibernate session when another infect with the same type and id it's known to our. Use merge or evict in that case.
Use a lower level hibernate interceptor as described here. As you see the onFlushDirty has both values as parameters. Take note though, that hibernate normally does not query for previous state of you simply save an already persisted entity. In stead a simple update is issued in the db (no select). You can force the select by configuring select-before-update on your entity.
Create following and extend your entities with it:
#MappedSuperclass
public class OEntity<T> {
#Transient
T originalObj;
#Transient
public T getOriginalObj(){
return this.originalObj;
}
#PostLoad
public void onLoad(){
ObjectMapper mapper = new ObjectMapper();
try {
String serialized = mapper.writeValueAsString(this);
this.originalObj = (T) mapper.readValue(serialized, this.getClass());
} catch (Exception e) {
e.printStackTrace();
}
}
}
I had exactly this need and resolved adding a transient field to the entity to keep the old value, and modifying the setter method to store the previous value in the transient field.
Since json deserializing uses setter methods to map rest data to the entity, in the RepositoryEventHandler I will check the transient field to track changes.
#Column(name="STATUS")
private FundStatus status;
#JsonIgnore
private transient FundStatus oldStatus;
public FundStatus getStatus() {
return status;
}
public FundStatus getOldStatus() {
return this.oldStatus;
}
public void setStatus(FundStatus status) {
this.oldStatus = this.status;
this.status = status;
}
from application logs:
2017-11-23 10:17:56,715 CompartmentRepositoryEventHandler - beforeSave begin
CompartmentEntity [status=ACTIVE, oldStatus=CREATED]
Spring Data Rest can't and likely won't ever be able to do this due to where the events are fired from. If you're using Hibernate you can use Hibernate spi events and event listeners to do this, you can implement PreUpdateEventListener and then register your class with the EventListenerRegistry in the sessionFactory. I created a small spring library to handle all of the setup for you.
https://github.com/teastman/spring-data-hibernate-event
If you're using Spring Boot, the gist of it works like this, add the dependency:
<dependency>
<groupId>io.github.teastman</groupId>
<artifactId>spring-data-hibernate-event</artifactId>
<version>1.0.0</version>
</dependency>
Then add the annotation #HibernateEventListener to any method where the first parameter is the entity you want to listen to, and the second parameter is the Hibernate event that you want to listen for. I've also added the static util function getPropertyIndex to more easily get access to the specific property you want to check, but you can also just look at the raw Hibernate event.
#HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
int index = getPropertyIndex(event, "name");
if (event.getOldState()[index] != event.getState()[index]) {
// The name changed.
}
}
Just another solution using model:
public class Customer {
#JsonIgnore
private String name;
#JsonIgnore
#Transient
private String newName;
public void setName(String name){
this.name = name;
}
#JsonProperty("name")
public void setNewName(String newName){
this.newName = newName;
}
#JsonProperty
public void getName(String name){
return name;
}
public void getNewName(String newName){
return newName;
}
}
Alternative to consider. Might be reasonable if you need some special handling for this use-case then treat it separately. Do not allow direct property writing on the object. Create a separate endpoint with a custom controller to rename customer.
Example request:
POST /customers/{id}/identity
{
"name": "New name"
}
I had the same problem, but I wanted the old entity available in the save(S entity) method of a REST repository implementation (Spring Data REST).
What I did was to load the old entity using a 'clean' entity manager from which I create my QueryDSL query:
#Override
#Transactional
public <S extends Entity> S save(S entity) {
EntityManager cleanEM = entityManager.getEntityManagerFactory().createEntityManager();
JPAQuery<AccessControl> query = new JPAQuery<AccessControl>(cleanEM);
//here do what I need with the query which can retrieve all old values
cleanEM.close();
return super.save(entity);
}
The following worked for me. Without starting a new thread the hibernate session will provide the already updated version. Starting another thread is a way to have a separate JPA session.
#PreUpdate
Thread.start {
if (entity instanceof MyEntity) {
entity.previous = myEntityCrudRepository.findById(entity?.id).get()
}
}.join()
Just let me know if anybody would like more context.
Don't know if you're still after an answer, and this is probably a bit 'hacky', but you could form a query with an EntityManager and fetch the object that way ...
#Autowired
EntityManager em;
#HandleBeforeSave
public void handleBeforeSave(Customer obj) {
Query q = em.createQuery("SELECT a FROM CustomerRepository a WHERE a.id=" + obj.getId());
Customer ret = q.getSingleResult();
// ret should contain the 'before' object...
}