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

Posted

技术标签:

【中文标题】如何在@HandleBeforeSave 事件中获取旧实体值以确定属性是不是已更改?【英文标题】:How to get old entity value in @HandleBeforeSave event to determine if a property is changed or not?如何在@HandleBeforeSave 事件中获取旧实体值以确定属性是否已更改? 【发布时间】:2014-10-03 16:19:22 【问题描述】:

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

有没有办法获取旧实体?

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

【问题讨论】:

您有没有为此找到解决方案?我看春票还在开。 现在是 2017 年……有什么消息吗? Spring 团队似乎多年来一直忽略这个问题 【参考方案1】:

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

我认为你有三个选择:

    在当前季节刷新之前,在单独的会话/事务中获取对象。这很尴尬,需要非常微妙的配置。 在 spring 附加新对象之前获取以前的版本。这是相当可行的。您可以在将对象交给存储库之前在服务层执行此操作。但是,您不能save 一个对象也不能成为休眠会话,因为我们知道另一个具有相同类型和 id 的感染。在这种情况下使用mergeevict。 如here 所述,使用较低级别的休眠拦截器。如您所见,onFlushDirty 具有两个值作为参数。但请注意,休眠通常不会查询您以前的状态,只需保存一个已经持久化的实体。相反,在数据库中发布了一个简单的更新(无选择)。您可以通过在实体上配置 select-before-update 来强制选择。

【讨论】:

我正在使用 Spring Data REST,所以我不能手动使用合并或逐出,这都是隐藏的。我能看到的只有@RepositoryEventHandler's。 我之前没有使用过spring-data-rest。从文档中,我了解到选项 1 和 2 可能很困难。我认为 3 应该仍然可以使用休眠拦截器的弹簧配置。 谢谢 Maarten,请问如何将这样的拦截器与 Spring Boot 集成? 另见:***.com/questions/25283767/…【参考方案2】:

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

【讨论】:

【参考方案3】:

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

【讨论】:

这对我不起作用。更多解释请参见jira.spring.io/browse/…。【参考方案4】:

不知道您是否还在寻找答案,这可能有点“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...

【讨论】:

我使用这种方法得到了相同的实体。【参考方案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();
        
    

【讨论】:

是的,这个解决方案有效,因此您可以在 PostUpdate 方法中获取更改了哪些字段。但问题是,因此我们不能在实体中定义任何服务/组件等,我们不能使用它来将它发送到某个地方例如:)【参考方案7】:

我正是有这个需求,并解决了向实体添加一个瞬态字段以保留旧值,并修改 setter 方法以将先前的值存储在瞬态字段中。

由于 json 反序列化使用 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]

【讨论】:

【参考方案8】:

我遇到了同样的问题,但我希望旧实体在 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);
    

【讨论】:

【参考方案9】:

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

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.
  

【讨论】:

Tyler - 与您的图书馆合作愉快。只是工作。【参考方案10】:

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

@PreUpdate

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

如果有人想要更多上下文,请告诉我。

【讨论】:

以上是关于如何在@HandleBeforeSave 事件中获取旧实体值以确定属性是不是已更改?的主要内容,如果未能解决你的问题,请参考以下文章

C ++:我在一种方法中获得了一个迭代器,如何在另一种方法中通过迭代器修改原始列表?

input标签中value属性存入了一个对象,如何在js中获去这个对象的属性

React - 在表格行上触发点击事件并在另一个页面中显示所选行的详细信息

vue监听键盘回车事件

Vue组件收到数据后如何更新

确保我在 laravel 5 中获得了正确的外键语法