31

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.

Dherik
  • 17,757
  • 11
  • 115
  • 164
Marcel Overdijk
  • 11,041
  • 17
  • 71
  • 110

10 Answers10

14

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
      }
   }
}
Gabriel Bauman
  • 2,270
  • 1
  • 22
  • 36
  • 2
    This did not work for me. See https://jira.spring.io/browse/DATAREST-373?focusedCommentId=127082&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-127082 for a further explanation. – ALM Jun 03 '16 at 22:25
  • Worked for me by putting it in the regular flow, without the aspect-oriented/injection (`@HandleBeforeSave`) stuff that makes my world dizzy already as it is with Spring when I try to find stuff in a complex project. Thanks for the answer – AlikElzin-kilaka Jun 26 '23 at 11:56
9

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"
}
GedankenNebel
  • 2,437
  • 5
  • 29
  • 39
5

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:

  1. Fetch the object in a separate session/transaction before the current season is flushed. This is awkward and requires very subtle configuration.
  2. 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.
  3. 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.
Maarten Winkels
  • 2,407
  • 16
  • 15
  • I'm using Spring Data REST so I cannot use merge or evict manually, that's all hidden. All I can use I can see is the @RepositoryEventHandler's. – Marcel Overdijk Aug 11 '14 at 07:11
  • I have not used spring-data-rest before. From the doc, I understand that options 1 and 2 might be difficult. I think 3 should still be possible using spring configuration for hibernate interceptor. – Maarten Winkels Aug 12 '14 at 01:53
  • Thanks Maarten, do you how to integrate such an interceptor with Spring Boot? – Marcel Overdijk Aug 13 '14 at 09:57
  • See also: http://stackoverflow.com/questions/25283767/how-to-use-spring-managed-hibernate-interceptors-in-spring-boot – Marcel Overdijk Aug 14 '14 at 13:46
3

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();
        }
    }
}
ArslanAnjum
  • 1,674
  • 2
  • 17
  • 31
  • yeah, this solution is working, thus you can grab which fields are changed in the PostUpdate method. But the problem is that hence we can't define any service/component etc. in entities, we can't use it in order to send it somewhere for example :) – Adil Karaöz Dec 02 '21 at 11:13
1

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]
a.alerta
  • 11
  • 1
1

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.
  }
}
Tyler Eastman
  • 366
  • 2
  • 5
0

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"
}
aux
  • 1,589
  • 12
  • 20
0

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);
    }
AngelVel
  • 11
  • 2
0

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.

Bas Kuis
  • 748
  • 1
  • 7
  • 20
-1

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...
}
Matt Rees
  • 17
  • 1