Spring Data JPA 从入门到精通~@EntityListeners注解示例

被@Prepersist注解的方法 ,完成save之前的操作。
被@Preupdate注解的方法 ,完成update之前的操作。
被@PreRemove注解的方法 ,完成remove之前的操作。
被@Postpersist注解的方法 ,完成save之后的操作。
被@Postupdate注解的方法 ,完成update之后的操作。
被@PostRemovet注解的方法 ,完成remove之后的操作。

 This page will provide JPA @EntityListeners example with callbacks @PrePersist, @PostPersist, @PostLoad, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove. JPA @EntityListeners is used on entity or mapped superclass at class level. JPA provides callback methods for saving, fetching, updating and removing data from database. Here we will use JPA EntityManager to interact with database.

JPA @EntityListeners

@EntityListeners annotation specifies the callback listener classes . This annotation can be used for an entity or mapped superclass. 
1. To configure single callback listener class, we can do as follows.

@EntityListeners(UserListener.class)
public class User {} 

2. To configure multiple callback listener classes, we can do as follows.

@EntityListeners({UserListener1.class, UserListener2.class})
public class User { } 

JPA Callbacks Method

JPA provides callback methods to listen saving, fetching, updating and removing data from database. These callback methods annotated in a listener bean class must have return type void and accept one argument

@PrePersist: The method annotated with @PrePersist in listener bean class is called before persisting data by entity manager persist() method. 

@PostPersist: The method annotated with @PostPersist is called after persisting data. 

@PostLoad: The method annotated with @PostLoad is called after fetching data using entity manager find() method in persistence context or refreshing it with database by using refresh() method. If the entity instance is already loaded in persistence context, then calling of find() method will not call @PostLoad

@PreUpdate: The method annotated with @PreUpdate in listener bean class is called before updating data. 

@PostUpdate: It is called after updating data. 

@PreRemove: The method annotated with @PreRemove in listener bean class is called before removing data by using entity manager remove() method. 

@PostRemove: It is called after removing data.

Database Schema

For the demo we are using a table with following schema created in MySQL. 
Table: user

CREATE TABLE `user` (
	`id` INT(11) NOT NULL,
	`name` VARCHAR(255) NULL DEFAULT NULL,
	PRIMARY KEY (`id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB; 

Gradle File

Find the gradle file. 
build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'
archivesBaseName = 'HibernateJPA'
version = '1' 
repositories {
    mavenCentral()
}
dependencies {
    compile 'org.hibernate:hibernate-entitymanager:5.0.7.Final'
    compile 'mysql:mysql-connector-java:5.1.31'
} 

Create Listener Class

Find the listener class which consist callback methods annotated with @PrePersist, @PostPersist, @PostLoad, @PreUpdate, @PostUpdate, @PreRemove and @PostRemove. 
UserListener.java

package com.concretepage;
import javax.persistence.PostLoad;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
public class UserListener {
	@PrePersist
	public void userPrePersist(User ob) {
		System.out.println("Listening User Pre Persist : " + ob.getName());
	}
	@PostPersist
	public void userPostPersist(User ob) {
		System.out.println("Listening User Post Persist : " + ob.getName());
	}
	@PostLoad
	public void userPostLoad(User ob) {
		System.out.println("Listening User Post Load : " + ob.getName());
	}	
	@PreUpdate
	public void userPreUpdate(User ob) {
		System.out.println("Listening User Pre Update : " + ob.getName());
	}
	@PostUpdate
	public void userPostUpdate(User ob) {
		System.out.println("Listening User Post Update : " + ob.getName());
	}
	@PreRemove
	public void userPreRemove(User ob) {
		System.out.println("Listening User Pre Remove : " + ob.getName());
	}
	@PostRemove
	public void userPostRemove(User ob) {
		System.out.println("Listening User Post Remove : " + ob.getName());
	}
} 

Create Entity annotated with @EntityListeners

Now find the entity annotated with @EntityListeners
User.java

package com.concretepage;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@EntityListeners(UserListener.class)
@Table(name="user")
public class User {
	@Id
	@Column(name="id")
	private int id;
	@Column(name="name")
	private String name;
	public User() {}
	public User(int id, String name) {
		this.id = id;
		this.name = name;
	}
	public int getId() {
		return id;
	}	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
} 

persistence.xml

Find the persistence.xml file.

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="com.concretepage">
        <description>JPA Demo</description>
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/concretepage"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value=""/>
        </properties>
    </persistence-unit>
</persistence>

Run Application

First find the JPA utility singleton class that will provide the instance of EntityManager
JPAUtility.java

package com.concretepage;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class JPAUtility {
 	private static final EntityManagerFactory emFactory;
	static {
		   emFactory = Persistence.createEntityManagerFactory("com.concretepage");
	}
	public static EntityManager getEntityManager(){
		return emFactory.createEntityManager();
	}
	public static void close(){
		emFactory.close();
	}
} 

Find the class to test the application. 
JPAListenerDemo.java

package com.concretepage;
import javax.persistence.EntityManager;
public class JPAListenerDemo {
	public static void main(String[] args) {
		EntityManager entityManager = JPAUtility.getEntityManager();	
		entityManager.getTransaction().begin();
		//persist user
		User user = new User(1, "Mahesh");
		entityManager.persist(user);
		entityManager.getTransaction().commit();
		//refresh user
		entityManager.refresh(user);
		//update user
		entityManager.getTransaction().begin();				
		user.setName("Krishna");
		entityManager.getTransaction().commit();
		//remove user
		entityManager.getTransaction().begin();				
		entityManager.remove(user);
		entityManager.getTransaction().commit();		
		entityManager.close();
		JPAUtility.close();		
	}
} 

Find the output.

Listening User Pre Persist : Mahesh
Listening User Post Persist : Mahesh
Listening User Post Load : Mahesh
Listening User Pre Update : Krishna
Listening User Post Update : Krishna
Listening User Pre Remove : Krishna
Listening User Post Remove : Krishna 

<think> 根据用户的问题,我们需要明确两点: 1. `@LastModifiedDate` 是 Spring Data JPA 提供的注解,用于自动记录实体最后修改的时间。 2. `@Version` 是 JPA 规范中的注解,用于实现乐观锁机制,通常是一个数值类型(如 Long、Integer)字段。 问题核心:这两个注解能否同时使用在同一个字段上? 分析: - `@Version` 要求字段类型必须是数值类型(如 Long, Integer, int, long)或时间类型(java.util.Date, java.sql.Timestamp)?实际上,JPA 规范中 `@Version` 支持的字段类型包括: - `int`, `Integer` - `long`, `Long` - `short`, `Short` - `java.sql.Timestamp` - `java.util.Date`(但注意,使用 `java.util.Date` 可能会遇到时区问题,通常不推荐) - `@LastModifiedDate` 通常用于时间类型的字段(如 `java.time.LocalDateTime`, `java.util.Date` 等)。 因此,从类型上看,如果一个字段是时间类型,那么它理论上可以同时使用 `@Version` 和 `@LastModifiedDate` 注解吗? 然而,我们需要考虑它们的用途: - `@Version`:用于乐观锁,每次更新操作时,这个字段的值会自动递增(或更新为当前时间戳,如果字段是时间类型的话)。在更新操作时,JPA 会检查版本号是否匹配,以防止并发冲突。 - `@LastModifiedDate`:用于记录最后修改时间,通常由审计功能(通过 `@EntityListeners` 和 `AuditingEntityListener`)自动设置,在实体更新时自动设置为当前时间。 但是,同时使用这两个注解在同一个字段上会产生冲突,因为: 1. 它们各自有不同的更新机制: - `@Version` 由 JPA 提供者在执行更新操作时自动管理(通常是递增或设置为当前时间戳,取决于字段类型)。 - `@LastModifiedDate` 由 Spring Data 的审计机制(通过事件监听器)在更新前设置。 2. 执行顺序:审计监听器(设置 `@LastModifiedDate`)通常发生在 JPA 的更新操作之前,但是 `@Version` 的更新是在持久化上下文(persistence context)同步到数据库时发生的。如果同时使用,可能会导致不可预测的行为,例如: - 审计监听器先设置一个时间戳。 - 然后 JPA 在更新时可能会再次修改这个字段(因为它是版本字段,JPA 会将其视为乐观锁字段,需要更新)。 - 这样就会导致两次修改,而且可能覆盖。 另外,从实际使用角度: - 如果将时间类型字段作为 `@Version`,那么每次更新时,JPA 会将其更新为当前时间戳(这是 JPA 提供者的实现行为)。此时,它已经起到了记录最后修改时间的作用。那么再使用 `@LastModifiedDate` 就是多余的。 - 反过来,如果使用 `@LastModifiedDate` 来记录时间,那么它并不能提供乐观锁的功能,因为乐观锁需要版本号(数值递增或时间戳)来检查并发修改。 因此,结论如下: **Spring Data JPA 不允许在同一字段上同时使用 `@Version` 和 `@LastModifiedDate` 注解**。 原因: 1. **功能冲突**:`@Version` 要求字段由 JPA 提供者管理(用于乐观锁),而 `@LastModifiedDate` 要求字段由 Spring Data 的审计机制管理(用于记录时间)。二者更新机制不同,同时使用会导致更新行为冲突。 2. **数据类型限制**:虽然 `@Version` 支持时间类型字段(如 `java.sql.Timestamp`),但此时该字段已被用于乐观锁,不应再被其他机制修改。 替代方案: - 如果需要同时使用乐观锁和记录最后修改时间,应该使用两个独立的字段: - 一个字段用 `@Version` 注解(可以是数值类型,也可以是时间类型,但通常数值类型更常见)。 - 另一个字段用 `@LastModifiedDate` 注解(时间类型)。 示例代码: ```java import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.LocalDateTime; @Entity @EntityListeners(AuditingEntityListener.class) // 启用审计 public class MyEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 乐观锁字段:使用数值类型 @Version private Long version; // 最后修改时间字段 @LastModifiedDate private LocalDateTime lastModifiedDate; // 其他字段... } ``` 或者,如果你希望用时间戳作为乐观锁字段(不常用,但可行): ```java @Entity public class MyEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 使用时间戳作为乐观锁字段(同时记录了最后修改时间) @Version private LocalDateTime version; // 此时不再需要@LastModifiedDate // 注意:这样就不再需要单独的@LastModifiedDate字段,因为version字段已经记录了更新时间 } ``` 总结: - 不要混合使用 `@Version` 和 `@LastModifiedDate` 在同一字段上。 - 根据需求选择: - 方案1:使用两个字段分别管理版本(乐观锁)和最后修改时间(推荐,职责分离)。 - 方案2:只使用一个 `@Version` 字段(时间类型)同时作为乐观锁和修改时间(但注意,这种方式下,乐观锁的检查依赖于时间戳,可能存在精度问题,如并发更新在同一毫秒内发生,可能导致锁失效)。 注意事项: - 使用 `@LastModifiedDate` 需要启用审计功能(在配置类上使用 `@EnableJpaAuditing`,并在实体类上添加 `@EntityListeners(AuditingEntityListener.class)`)[^1]。 - 使用 `@Version` 时,注意乐观锁的异常处理(如 `OptimisticLockException`)[^3]。 引用说明: - 关于 `@Version` 和乐观锁的实现,参考引用[3]。 - 关于 Spring Data JPA 的基本使用和实体映射,参考引用[2]。 - 关于审计功能(包括 `@LastModifiedDate`)的使用,参考 Spring Data JPA 官方文档(引用[1]中提到的框架功能)[^1]。 §§相关问题§§ 1. 如何正确配置Spring Data JPA的审计功能(`@CreatedDate`, `@LastModifiedDate`)? 2. 使用数值类型和时间戳类型作为乐观锁字段(`@Version`)各有什么优缺点? 3. 在Spring Data JPA中如何处理乐观锁失败(`OptimisticLockException`)? </think> ### Spring Data JPA 中 `@LastModifiedDate` 和 `@Version` 能否同时使用在同一字段? **结论:不建议同时使用在同一字段上**。这两个注解的设计目的和实现机制存在冲突,混合使用可能导致数据不一致或不可预测的行为[^3][^5]。以下是详细分析: --- ### 一、两个注解的核心差异 1. **`@Version`**(JPA规范注解) - **作用**: 实现乐观锁机制,避免并发更新冲突[^3] - **数据类型要求**: 必须是数值类型(`Long`, `Integer`)或时间戳(`java.sql.Timestamp`) - **更新机制**: 每次更新时自动递增(数值类型)或更新为当前时间(时间戳类型) - **典型用例**: ```java @Version private Long version; // 数值类型 ``` 2. **`@LastModifiedDate`**(Spring Data JPA扩展注解) - **作用**: 自动记录实体最后修改时间 - **数据类型要求**: 时间类型(`LocalDateTime`, `Date`等) - **更新机制**: 通过审计监听器(`AuditingEntityListener`)在事务提交前更新 - **典型用例**: ```java @LastModifiedDate private LocalDateTime lastModifiedAt; ``` --- ### 二、冲突原因分析 1. **更新机制冲突** - `@Version` 由JPA提供者(如Hibernate)在**SQL执行阶段**自动更新 - `@LastModifiedDate` 由Spring审计机制在**实体监听阶段**更新 两者更新时机不同,可能导致最终值被覆盖(如审计值被JPA乐观锁重置)[^3][^5]。 2. **数据类型不兼容** - `@Version` 支持时间戳类型时,行为是**覆盖为当前数据库时间**(如`CURRENT_TIMESTAMP`) - `@LastModifiedDate` 期望保留精确的应用程序层时间戳 混合使用可能导致时间精度丢失(例如数据库时间与系统时间差问题)[^4]。 3. **乐观锁失效风险** 如果同时使用: ```java @Version @LastModifiedDate private LocalDateTime conflictField; // 错误用法! ``` - 审计监听器先修改字段值 → JPA检测到版本变化 → 触发乐观锁更新 → **导致版本号与实际修改时间分离** - 可能引发`OptimisticLockException`误报[^3]。 --- ### 三、正确实践方案 #### ✅ 方案1:分离字段(推荐) ```java @Entity @EntityListeners(AuditingEntityListener.class) // 启用审计 public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Version private Long version; // 纯乐观锁字段(数值类型) @LastModifiedDate private LocalDateTime lastModifiedAt; // 纯审计时间字段 } ``` **优势**: - 职责分离,避免逻辑冲突 - 兼容所有JPA提供者(Hibernate/EclipseLink等)[^5] #### ✅ 方案2:单一注解替代 若需用时间戳实现乐观锁: ```java @Version private LocalDateTime version; // 同时承担乐观锁和修改时间 ``` **注意**: - 需在数据库配置默认值`CURRENT_TIMESTAMP` - 放弃`@LastModifiedDate`的审计功能,由JPA全权管理时间戳[^4] --- ### 四、配置审计的完整示例 ```java // 1. 启用JPA审计(配置类) @Configuration @EnableJpaAuditing public class JpaConfig {} // 2. 实体类 @Entity @EntityListeners(AuditingEntityListener.class) public class Order { @Id private UUID id; @Version private Integer version; // 乐观锁 @LastModifiedDate private Instant updatedAt; // 修改时间 } // 3. 更新时代码自动生效 orderRepository.save(order); // 自动更新 version 和 updatedAt ``` --- ### 关键注意事项 1. **时间精度问题** 数据库时间戳精度可能低于Java(如MySQL毫秒级),建议用`Instant`或`LocalDateTime`[^4]。 2. **并发更新处理** 始终捕获`OptimisticLockException`并设计重试机制[^3]。 3. **审计字段不可更新** 添加`@Column(updatable = false)`避免手动修改: ```java @LastModifiedDate @Column(updatable = false) private LocalDateTime lastModifiedAt; ``` > **总结**:虽然技术上可能强制将两个注解用于同一时间戳字段,但会破坏数据一致性。优先采用**分离字段方案**,兼顾安全性与功能性[^1][^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值