如何在@HandleBeforeSave事件中获取旧实体值以确定属性是否已更改?

32

我正在尝试在@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缓存引起的。

有没有办法获取旧实体?

我需要这个来确定给定属性是否已更改。如果属性发生更改,则需要执行某些操作。


3
你是否已经找到了解决方案?我看Spring的工单仍然是开放状态。 - ALM
2
现在是2017年了,这方面有什么新消息吗?Spring团队似乎已经忽视了这个问题很长时间。 - Piotr
10个回答

14
如果您使用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
      }
   }
}

通过将它放入常规流程中,而不是使用面向方面/注入(@HandleBeforeSave)的东西,这对我起了作用。当我试图在一个复杂的项目中找到东西时,Spring已经让我的世界变得晕头转向了。谢谢你的回答。 - AlikElzin-kilaka

9

感谢Marcel Overdijk创建了这个问题单 -> https://jira.spring.io/browse/DATAREST-373

我看到了其他解决方法,想要贡献我的解决方法,因为我认为它很容易实现。

首先,在您的领域模型(例如Account)中设置一个瞬态标志:

@JsonIgnore
@Transient
private boolean passwordReset;

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

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

其次,在您的事件处理程序中检查标志:
@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"
}

5
您目前正在使用基于Hibernate的Spring-Data抽象。如果查找返回新值,则Spring-Data显然已将对象附加到Hibernate会话中。
我认为您有三个选择:
  1. 在当前会话刷新之前,在单独的会话/事务中获取对象。这很麻烦,需要非常微妙的配置。
  2. 在Spring附加新对象之前获取先前的版本。这是可行的。您可以在将对象交给存储库之前在服务层中执行此操作。但是,如果另一个具有相同类型和ID的对象已知于我们,则无法将对象保存到Hibernate会话。在这种情况下,请使用"merge"或"evict"。
  3. 此处所述,使用较低级别的Hibernate拦截器。正如您所看到的,"onFlushDirty"具有两个参数的值。请注意,Hibernate通常不会查询已经持久化实体的先前状态,而是在数据库中发出简单的更新(没有选择)。您可以通过在实体上配置select-before-update来强制选择。

我正在使用Spring Data REST,因此无法手动使用merge或evict,这些都是隐藏的。我能看到的只有@RepositoryEventHandler。 - Marcel Overdijk
我以前没有使用过spring-data-rest。从文档中,我了解到选项1和2可能会很困难。我认为仍然可以使用Spring配置来实现Hibernate拦截器的第3个选项。 - Maarten Winkels
谢谢Maarten,您知道如何将这样的拦截器与Spring Boot集成吗? - Marcel Overdijk
参见:https://dev59.com/zV8e5IYBdhLWcg3w4dek - Marcel Overdijk

3
创建以下内容并扩展你的实体:

创建以下内容并扩展你的实体:

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

是的,这个解决方案可行,因此您可以在PostUpdate方法中获取哪些字段已更改。但问题在于,由于我们不能在实体中定义任何服务/组件等,因此我们无法将其用于发送到其他地方,例如 :) - Adil Karaöz

2
我有这个需求,并且通过向实体添加一个暂态字段来解决它,修改setter方法以将先前的值存储在暂态字段中。由于json反序列化使用setter方法将rest数据映射到实体中,因此在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]

1

由于事件触发的位置,Spring Data Rest 无法并且可能永远无法做到这一点。如果您正在使用 Hibernate,则可以使用 Hibernate spi 事件和事件侦听器来实现此操作,您可以实现 PreUpdateEventListener,然后将您的类注册到 sessionFactory 中的 EventListenerRegistry 中。我创建了一个小型的 Spring 库来处理所有设置。

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事件的方法中。我还添加了静态实用程序函数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.
  }
}

Tyler - 你的库做得很好。完美运行。 - Raj

0

我曾经遇到过同样的问题,但是我想在REST存储库实现(Spring Data REST)的save(S entity)方法中使用旧实体。 我的解决方法是使用“干净”的实体管理器加载旧实体,然后使用它创建我的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);
    }

0

另一种使用模型的解决方案:

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

0
以下方法适用于我。如果不启动新线程,Hibernate会提供已经更新的版本。启动另一个线程是使用单独的JPA会话的一种方式。
@PreUpdate
Thread.start {
    if (entity instanceof MyEntity) {
        entity.previous = myEntityCrudRepository.findById(entity?.id).get()
    }
}.join()

如果有人需要更多背景信息,请告诉我。


-1

不知道你是否还在寻找答案,这可能有点“hacky”,但你可以使用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...
}

我正在使用这种方法获取相同的实体。 - Piotr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接