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

时间:2014-08-10 21:17:08

标签: java hibernate spring-data-rest

我正试图在@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缓存。

有没有办法获得旧实体?

我需要这个来确定给定的属性是否被更改。如果属性是更改,我需要执行一些操作。

10 个答案:

答案 0 :(得分:12)

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

答案 1 :(得分:8)

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

答案 2 :(得分:2)

您目前正在使用基于休眠的spring-data抽象。 如果find返回新值,则spring-data显然已经将对象附加到hibernate会话。

我认为你有三种选择:

  1. 在刷新当前季节之前,在单独的会话/事务中获取对象。这很尴尬,需要非常精细的配置。
  2. 在弹簧附加新对象之前获取以前的版本。这是非常可行的。在将对象交给存储库之前,您可以在服务层中执行此操作。但是,当另一个感染具有相同类型并且我们已知的ID时,您可以save一个对象也不是一个休眠会话。在这种情况下,请使用mergeevict
  3. 使用较低级别的hibernate拦截器,如here所述。如您所见,onFlushDirty将两个值都作为参数。请注意,hibernate通常不会查询您之前的状态只是保存已经存在的实体。而不是在db中发出简单的更新(没有选择)。您可以通过在实体上配置select-before-update来强制执行选择。

答案 3 :(得分:1)

我确实有这个需求并解决了向实体添加瞬态字段以保留旧值,并修改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]

答案 4 :(得分: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"
}

答案 5 :(得分:0)

使用它创建以下内容并扩展您的实体:

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

答案 6 :(得分:0)

我遇到了同样的问题,但是我希望在REST存储库实现(Spring Data REST)的let llFromDistance = function(latitude, longitude, distance, bearing) { // taken from: https://stackoverflow.com/a/46410871/13549 // distance in KM, bearing in degrees const R = 6378.1; // Radius of the Earth const brng = bearing * Math.PI / 180; // Convert bearing to radian let lat = latitude * Math.PI / 180; // Current coords to radians let lon = longitude * Math.PI / 180; // Do the math magic lat = Math.asin(Math.sin(lat) * Math.cos(distance / R) + Math.cos(lat) * Math.sin(distance / R) * Math.cos(brng)); lon += Math.atan2(Math.sin(brng) * Math.sin(distance / R) * Math.cos(lat), Math.cos(distance / R) - Math.sin(lat) * Math.sin(lat)); // Coords back to degrees and return return [(lat * 180 / Math.PI), (lon * 180 / Math.PI)]; } console.log(llFromDistance(19.0659115, 72.8574557, Math.sqrt(2)*10, 135))方法中可以使用旧实体。 我要做的是使用“干净的”实体管理器加载旧的实体,从中创建我的QueryDSL查询:

save(S entity)

答案 7 :(得分:0)

由于发生事件的地点,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事件。我还添加了静态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.
  }
}

答案 8 :(得分:0)

以下内容对我有用。无需启动新线程,休眠会话将提供已经更新的版本。启动另一个线程是进行单独的JPA会话的一种方式。

@PreUpdate

Thread.start {
    if (entity instanceof MyEntity) {
        entity.previous = myEntityCrudRepository.findById(entity?.id).get()
    }
}.join()

请让我知道是否有人需要更多背景信息。

答案 9 :(得分:-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...
}