java 如何在@HandleBeforeSave 事件中获取旧实体值以确定属性是否已更改?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/25233089/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to get old entity value in @HandleBeforeSave event to determine if a property is changed or not?
提问by Marcel Overdijk
I'm trying to get the old entity in a @HandleBeforeSave
event.
我正在尝试在@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());
}
}
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.
如果我尝试使用该findOne
方法获取旧实体,但这会返回新事件。可能是因为当前会话中的 Hibernate/Repository 缓存。
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.
我需要这个来确定给定的属性是否被更改。如果属性发生变化,我需要执行一些操作。
采纳答案by Maarten Winkels
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.
您目前在休眠状态下使用 spring-data 抽象。如果 find 返回新值,则 spring-data 显然已经将对象附加到休眠会话。
I think you have three options:
我认为你有三个选择:
- Fetch the object in a separate session/transaction before the current season is flushed. This is awkward and requires very subtle configuration.
- 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. Usemerge
orevict
in that case. - 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.
- 在刷新当前季节之前,在单独的会话/事务中获取对象。这很尴尬,需要非常微妙的配置。
- 在 spring 附加新对象之前获取以前的版本。这是相当可行的。在将对象交给存储库之前,您可以在服务层中执行此操作。但是,
save
当另一个对象感染我们已知的相同类型和 id 时,您不能将其设为休眠会话。在这种情况下使用merge
或evict
。 - 使用此处所述的较低级别的休眠拦截器。如您所见,
onFlushDirty
这两个值都作为参数。但请注意,休眠通常不会查询您之前的状态,只需保存一个已经持久化的实体。取而代之的是在数据库中发出一个简单的更新(无选择)。您可以通过在实体上配置 select-before-update 来强制选择。
回答by Gabriel Bauman
If using Hibernate, you could simply detach the new version from the session and load the old version:
如果使用 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
}
}
}
回答by GedankenNebel
Thanks Marcel Overdijk, for creating the ticket -> https://jira.spring.io/browse/DATAREST-373
感谢 Marcel Overdijk 创建票证 -> 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:
其次,检查您的 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!
注意:对于此解决方案,您需要发送一个额外的resetPassword = true
参数!
For me, I′m sending a HTTP PATCH to my resource endpoint with the following request payload:
对我来说,我正在向我的资源端点发送一个 HTTP PATCH,其中包含以下请求负载:
{
"passwordReset": true,
"password": "someNewSecurePassword"
}
回答by a.alerta
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.
我正是有这个需求并解决了向实体添加一个瞬态字段以保留旧值,并修改 setter 方法以将先前值存储在瞬态字段中的问题。
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.
由于 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;
}
from application logs:
从应用程序日志:
2017-11-23 10:17:56,715 CompartmentRepositoryEventHandler - beforeSave begin
CompartmentEntity [status=ACTIVE, oldStatus=CREATED]
回答by AngelVel
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:
我遇到了同样的问题,但我希望旧实体在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);
}
回答by Tyler Eastman
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.
由于事件是从哪里触发的,Spring Data Rest 不能也很可能永远无法做到这一点。如果您正在使用 Hibernate,您可以使用 Hibernate spi 事件和事件侦听器来执行此操作,您可以实现 PreUpdateEventListener,然后使用 sessionFactory 中的 EventListenerRegistry 注册您的类。我创建了一个小型 spring 库来为您处理所有设置。
https://github.com/teastman/spring-data-hibernate-event
https://github.com/teastman/spring-data-hibernate-event
If you're using Spring Boot, the gist of it works like this, add the dependency:
如果您使用的是 Spring Boot,它的工作原理是这样的,添加依赖项:
<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 添加到任何方法中,其中第一个参数是您要侦听的实体,第二个参数是您要侦听的 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.
}
}
回答by Bas Kuis
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.
以下对我有用。在不启动新线程的情况下,休眠会话将提供已经更新的版本。启动另一个线程是拥有单独 JPA 会话的一种方式。
@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.
如果有人想要更多背景信息,请告诉我。
回答by aux
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"
}
回答by ArslanAnjum
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();
}
}
}
回答by Matt Rees
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 ...
不知道您是否还在寻找答案,这可能有点“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...
}