Как получить значение старого объекта в событии @HandleBeforeSave, чтобы определить, изменилось ли свойство?

Я пытаюсь получить старую сущность в событии @HandleBeforeSave.

@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());
    }
}

В случае, если я пытаюсь получить старую сущность, используя метод findOne но это возвращает новое событие. Вероятно, из-за кэширования Hibernate/Repository в текущем сеансе.

Есть ли способ получить старую сущность?

Мне нужно это, чтобы определить, изменилось ли данное свойство или нет. В случае изменения свойства мне нужно выполнить какое-то действие.

Ответ 1

В настоящее время вы используете абстракцию spring -data через спящий режим. Если find возвращает новые значения, spring -data, по-видимому, уже привязал объект к сеансу спящего режима.

Я думаю, у вас есть три варианта:

  • Извлеките объект в отдельный сеанс/транзакцию до того, как текущий сезон будет сброшен. Это неудобно и требует очень тонкой конфигурации.
  • Приобретите предыдущую версию до spring прикрепить новый объект. Это вполне выполнимо. Вы можете сделать это на уровне сервиса перед передачей объекта в репозиторий. Вы можете, однако, не save объект также спящий сеанс, когда другой зараженный с тем же типом и идентификатором он известен нашему. Используйте merge или evict в этом случае.
  • Используйте перехватчик спящего режима более низкого уровня, как описано здесь. Как вы видите, onFlushDirty имеет оба значения в качестве параметров. Обратите внимание, что, что hibernate обычно не запрашивает предыдущее состояние, вы просто сохраняете уже сохраненный объект. Вместо простого обновления выдается в db (нет выбора). Вы можете принудительно выбрать выбор, настроив выбор-до-обновления на свой объект.

Ответ 2

Если вы используете Hibernate, вы можете просто отсоединить новую версию от сеанса и загрузить старую версию:

@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
      }
   }
}

Ответ 3

Спасибо Marcel Overdijk за создание билета → https://jira.spring.io/browse/DATAREST-373

Я видел другие обходные пути для этой проблемы и хочу также внести свой вклад в обходной путь, потому что я думаю, что это довольно просто реализовать.

Сначала установите флаг переходного процесса в своей модели домена (например, учетная запись):

@JsonIgnore
@Transient
private boolean passwordReset;

@JsonIgnore
public boolean isPasswordReset() {
    return passwordReset;
}

@JsonProperty
public void setPasswordReset(boolean passwordReset) {
    this.passwordReset = passwordReset;
}

Во-вторых, проверьте флаг в 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);
    }

}

Примечание. Для этого решения вам необходимо отправить дополнительно параметр resetPassword = true!

Для меня я отправляю HTTP-PATCH в мою конечную точку ресурса со следующей полезной нагрузкой запроса:

{
    "passwordReset": true,
    "password": "someNewSecurePassword"
}

Ответ 4

У меня была именно эта необходимость и я решил добавить переходное поле к сущности, чтобы сохранить старое значение, и модифицировать метод setter для хранения предыдущего значения в поле переходного процесса.

Так как json deserializing использует методы setter для отображения данных остатка в сущности, в RepositoryEventHandler я проверю переходное поле для отслеживания изменений.

@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;
}

из журналов приложений:

2017-11-23 10:17:56,715 CompartmentRepositoryEventHandler - beforeSave begin
CompartmentEntity [status=ACTIVE, oldStatus=CREATED]

Ответ 5

Еще одно решение с использованием модели:

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;
  }

}

Альтернатива для рассмотрения. Может быть разумным, если вам нужна специальная обработка для этого случая использования, а затем обработайте его отдельно. Не допускайте прямой записи свойств объекта. Создайте отдельную конечную точку с настраиваемым контроллером для переименования клиента.

Пример запроса:

POST /customers/{id}/identity

{
  "name": "New name"
}

Ответ 6

Создайте следующие и расширьте свои сущности:

@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();
        }
    }
}

Ответ 7

У меня была та же проблема, но я хотел, чтобы старая сущность была доступна в методе save(S entity) реализации репозитория REST (Spring Data REST). Я загрузил старую сущность, используя "чистый" диспетчер сущностей, из которого я создал запрос QueryDSL:

    @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);
    }

Ответ 8

Spring Data Rest не может и, вероятно, никогда не сможет этого сделать из-за того, откуда происходят события. Если вы используете Hibernate, вы можете использовать для этого события spi и прослушиватели событий Hibernate, вы можете реализовать PreUpdateEventListener, а затем зарегистрировать свой класс в EventListenerRegistry в sessionFactory. Я создал небольшую библиотеку пружин, чтобы обработать все настройки для вас.

https://github.com/teastman/spring-data-hibernate-event

Если вы используете Spring Boot, суть его работает следующим образом, добавьте зависимость:

<dependency>
  <groupId>io.github.teastman</groupId>
  <artifactId>spring-data-hibernate-event</artifactId>
  <version>1.0.0</version>
</dependency>

Затем добавьте аннотацию @HibernateEventListener в любой метод, где первый параметр - это объект, который вы хотите прослушать, а второй параметр - событие Hibernate, которое вы хотите прослушать. Я также добавил статическую функцию util getPropertyIndex, чтобы упростить получение доступа к определенному свойству, которое вы хотите проверить, но вы также можете просто посмотреть на необработанное событие Hibernate.

@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
  int index = getPropertyIndex(event, "name");
  if (event.getOldState()[index] != event.getState()[index]) {
    // The name changed.
  }
}

Ответ 9

Самый простой способ, который я нашел, это клонировать сущность в новую сущность.

@HandleBeforeSave
public void handleBeforeSave(Customer customer) {

    Customer old = Customer.clone(customer);

    System.out.println("handleBeforeSave :: customer.id = " + customer.getId());
    System.out.println("handleBeforeSave :: new customer.name = " + customer.getName());

    System.out.println("handleBeforeSave :: new customer.name = " + customer.getName());
    System.out.println("handleBeforeSave :: old customer.name = " + old.getName());
}

Метод клонирования может быть простым:

class Customer {

    public static Customer clone(Customer customer) {
        return new Customer(customer.getName());
    }

}

Ответ 10

Не знаю, остаетесь ли вы после ответа, и это, вероятно, немного "взломано", но вы можете сформировать запрос с EntityManager и получить объект таким образом...

@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...
}