https://www.youtube.com/watch?v=CvDS6DltIno&list=PL41m5U3u3wwkS8BU0fIeRQwY3hK4VlFlX&index=2&t=13s
[!quote] JPA JPA 【
Java Persistence API】是一种规范,可以看作是 JDBC 的升级版。JPA 封装了 Hibernate,Hibernate 通过 ORM 将 Java 对象映射到表,再通过 SQL 来操作表
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>❤ 配置文件
hibernateddl-auto- none 不做任何操作
- validate 检查数据库结构是否与实体类一致,如果不一致则抛出异常,
ShardingSphere 使用 - create 应用启动会重新创建表
- create-drop 应用启动会重新创建表;在应用停止时,删除表
- update
yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-data
username: root
password: 13433026660
jpa:
open-in-view: false # 避免内存泄漏
hibernate:
ddl-auto: update # 自动检测实体类的变化,更新数据库表
show-sql: true # 在控制台显示sql语句
properties:
hibernate:
format_sql: true # 格式化sql语句
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # 生成的表名与实体名相同
# database: postgresql # 指定数据库
# database-platform: org.hibernate.dialect.PostgreSQLDialect # 指定数据库方言❤ Entity
💛 实体类注解
@Entity标记为实体@Table配置表name指定表名indexes指定索引@Index- columnList
- unique
- true 创建唯一约束,与普通索引在查询时没有性能差异,但是在写操作时由于需要检查,所以会比普通索引慢一点
- boolean
默认
@Comment注释(将展示在数据库中)
java
@Table(name = "Award",indexes = {
@Index(columnList = "awardKey"),
@Index(columnList = "awardConfig"),
// 复合索引,当查询条件同时有这两个值时,速度比单列索引快 30% - 50%
@Index(columnList = "awardKey, awardConfig"),
})
@Comment("奖品")💛 属性注解
@Id指定主键@GeneratedValue指定某个字段的值是如何生成的,如果显式指定,将覆盖该注解strategyGenerationType.AUTO由 JPA 自动选择,会生成后缀_SEQ的表GenerationType.IDENTITY让数据库自己自增,配置了数据分片中的主键策略也要选择这个GenerationType.SEQUENCE使用数据库的序列来生成主键值GenerationType.UUID根据 UUID 生成主键值
generator定义生成器的名字
GenericGenerator添加生成器name定义生成器的名字strategy生成器的全限定类名
@Column配置列name指定列名columnDefinition指定数据库字段的数据类型- JSON
- TEXT
- DATETIME DEFAULT CURRENT_TIMESTAMP 创建时间
- DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 更新时间
nullable- true
默认 - false 指定该字段不允许为空
- true
unique- true 指定该字段是唯一的
- false
默认
insertablefalse 表示在插入数据时,JPA 不会为这个字段提供值,而是由数据库来处理updatablefalse 表示在更新数据时,JPA 不会为这个字段提供值,而是由数据库来处理
@Enumerated- EnumType.ORDINAL 指定枚举值,在数据库中存储的形式为数字
- EnumType.STRING 指定枚举值,在数据库中存储的形式为 String
@Comment注释(将展示在数据库中)@Transient表示该字段不会持久化到 MongoDB 中
java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "tb_customer")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "no")
private Long no;
@Column(name = "cust_name")
private String custName;
@Column(name = "cust_address")
private String custAddress;
@Column(columnDefinition = "TEXT")
@Convert(converter = ListLongToJsonConverter.class)
private List<Long> awardIds; // 绑定的奖品集合
@Enumerated(EnumType.STRING)
private CustomerType type;
}@Convert给属性定义转换器,推荐- converter 指定具体的转换器类
java
/**
* 实体
*/
@Entity
@Table(name = "tb_customer")
public class Customer {
@Column(columnDefinition = "TEXT")
@Convert(converter = ListLongToJsonConverter.class)
private List<Long> awardIds; // 绑定的奖品集合
}
/**
* 转换器
*/
@Converter
public class ListLongToJsonConverter implements AttributeConverter<List<Long>, String> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<Long> longs) {
return objectMapper.writeValueAsString(longs);
}
@Override
public List<Long> convertToEntityAttribute(String dbData) {
return objectMapper.readValue(dbData, new TypeReference<List<Long>>() {});
}
}💛 方法注解
@PrePersist 可以在实体插入之前触发,不支持 JPQL
- 通常用于给字段进行初始化
@PreUpdate可以在实体更新之前触发,不支持 JPQL - 通常用于更新某些字段值
java
@Entity
public class Product {
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
public void setCreatedAt() {
this.createdAt = LocalDateTime.now();
}
@PreUpdate
public void setUpdatedAt() {
this.updatedAt = LocalDateTime.now();
}
}💛 生命周期回调
用以下注解注释方法即可
@PostLoad从数据库加载到内存后调用,JPQL 的查询可以生效@PrePersist实体被持久化之前调用@PostPersist实体被持久化之后调用@PreUpdate实体被更新之前调用,JPQL 的更新无法生效@PostUpdate实体被更新之后调用,JPQL 的更新无法生效@PreRemove实体被删除之前调用,JPQL 的更新无法生效,方法命名可以生效@PostRemove实体被删除之前调用,JPQL 的更新无法生效,方法命名可以生效
java
@PostLoad
public void postLoad() {
System.out.println("User.postLoad" + this);
}💛 技巧示例
自动赋值时间戳 :使用 columnDefinition 定义数据库数据类型
- 使用 Audit 监听器无法捕获 EntityManager、@Query 的操作
初始化默认值 :
java
// 默认初始化为 0
@Column(columnDefinition = "bigint default 0")
private Long raffleTimes; // 用户的抽奖次数动态初始化默认值 :
java
@Builder.Default
private Long activityOrderId = IdUtil.getSnowflakeNextId();使用 Java 方法动态生成默认值 :不推荐,写着玩儿的
- 定义注解
java
/**
* 为实体的字段提供默认的生成逻辑
*
* 示例:
* - @JpaDefaultValue(howToCreate = "cn.hutool.core.util.IdUtil.getSnowflakeNextId()")
* - private Long activityOrderId;
* - @JpaDefaultValue(howToCreate = "java.time.LocalDateTime.of(2000, 12, 31, 0, 0, 0)")
* - private LocalDateTime activityOrderEffectiveTime;
*/
@Target(ElementType.FIELD) // 只能应用于字段
@Retention(RetentionPolicy.RUNTIME) // 在运行时保留
public @interface JpaDefaultValue {
String howToCreate(); // 指定默认值生成方法
}- 注解类
java
public class JpaDefaultValueListener {
@PrePersist
@SneakyThrows
public void prePersist(Object entity) {
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
JpaDefaultValue annotation = field.getAnnotation(JpaDefaultValue.class);
if (annotation != null) {
field.setAccessible(true);
Object value = field.get(entity);
if (value == null) {
String howToCreate = annotation.howToCreate();
// 分离方法路径和参数部分
int paramStartIndex = howToCreate.indexOf('(');
int paramEndIndex = howToCreate.lastIndexOf(')');
if (paramStartIndex == -1 || paramEndIndex == -1) {
throw new IllegalArgumentException("howToCreate 格式错误,缺少括号");
}
// 提取方法路径和参数
String methodPath = howToCreate.substring(0, paramStartIndex).trim();
String paramString = howToCreate.substring(paramStartIndex + 1, paramEndIndex).trim();
// 分离类名和方法名
String[] parts = methodPath.split("\\.");
String className = String.join(".", Arrays.copyOf(parts, parts.length - 1));
String methodName = parts[parts.length - 1];
// 解析参数
Object[] params = parseParameters(paramString);
Class<?>[] paramTypes = Arrays.stream(params).map(this::getPrimitiveType).toArray(Class<?>[]::new);
// 获取类和方法
Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
// 调用方法生成值
Object generatedValue = method.invoke(null, params);
// 检查生成值的类型是否匹配
if (field.getType().isAssignableFrom(generatedValue.getClass())) {
field.set(entity, generatedValue);
} else {
throw new RuntimeException("生成器类型与属性类型不匹配");
}
}
}
}
}
/**
* 解析参数字符串为参数数组
*
* @param paramString 参数字符串(例如 "2000, 12, 31, 0, 0, 0")
* @return 转换后的参数数组
*/
private Object[] parseParameters(String paramString) {
if (paramString.isEmpty()) {
return new Object[0];
}
// 简单实现:根据逗号分隔并尝试解析常见类型(int, long, double, String 等)
String[] parts = paramString.split(",");
Object[] params = new Object[parts.length];
for (int i = 0; i < parts.length; i++) {
String part = parts[i].trim();
// 尝试解析为数字
if (part.matches("-?\\d+")) {
params[i] = Integer.parseInt(part);
} else if (part.matches("-?\\d+L")) {
params[i] = Long.parseLong(part.substring(0, part.length() - 1));
} else if (part.matches("-?\\d+\\.\\d+")) {
params[i] = Double.parseDouble(part);
} else if (part.matches("-?\\d+\\.\\d+f")) {
params[i] = Float.parseFloat(part.substring(0, part.length() - 1));
} else if (part.startsWith("\"") && part.endsWith("\"")) {
// 解析为字符串
params[i] = part.substring(1, part.length() - 1);
} else if (part.equalsIgnoreCase("true") || part.equalsIgnoreCase("false")) {
params[i] = Boolean.parseBoolean(part);
} else {
throw new IllegalArgumentException("无法解析参数: " + part);
}
}
return params;
}
/**
* 获取包装类型对应的基本类型
*
* @param obj 参数对象
* @return 基本类型(如果是包装类型则返回其基本类型,否则返回对象的实际类型)
*/
private Class<?> getPrimitiveType(Object obj) {
if (obj instanceof Integer) {
return int.class;
} else if (obj instanceof Long) {
return long.class;
} else if (obj instanceof Double) {
return double.class;
} else if (obj instanceof Float) {
return float.class;
} else if (obj instanceof Boolean) {
return boolean.class;
} else if (obj instanceof Character) {
return char.class;
} else if (obj instanceof Byte) {
return byte.class;
} else if (obj instanceof Short) {
return short.class;
}
return obj.getClass(); // 如果不是包装类型,直接返回实际类型
}
}- 使用
java
@Entity
@EntityListeners(JpaDefaultValueListener.class) // 添加注解处理器
@Table(name = "ActivityOrderFlow")
public class ActivityOrderFlow {
@JpaDefaultValue(howToCreate = "cn.hutool.core.util.IdUtil.getSnowflakeNextId()")
private Long activityOrderId;
@JpaDefaultValue(howToCreate = "java.time.LocalDateTime.of(2000, 12, 31, 0, 0, 0)")
private LocalDateTime activityOrderEffectiveTime;
}自定义 id 生成 :
java
public class SnowflakeIdGenerator implements IdentifierGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
return IdUtil.getSnowflakeNextId();
}
}java
@Id
@GeneratedValue(generator = "SnowflakeIdGenerator")
@GenericGenerator(name = "SnowflakeIdGenerator", strategy = "app.xlog.ggbond.persistent.util.SnowflakeIdGenerator")
private Long id;定义基类 :子类可以继承基类的属性(比如通用的 id、createTime、updateTime 都可以使用基类定义)
java
@MappedSuperclass
public class ShardingTableBaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(columnDefinition = "DATETIME DEFAULT CURRENT_TIMESTAMP", insertable = false, updatable = false)
private LocalDateTime createTime;
@Column(columnDefinition = "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", insertable = false, updatable = false)
private LocalDateTime updateTime;
}
@Entity
public class Award extends ShardingTableBaseEntity { …… }