Skip to content

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>

❤ 配置文件

  • hibernate
    • ddl-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 指定某个字段的值是如何生成的,如果显式指定,将覆盖该注解
    • strategy
      • GenerationType.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 指定该字段不允许为空
    • unique
      • true 指定该字段是唯一的
      • false 默认
    • insertable false 表示在插入数据时,JPA 不会为这个字段提供值,而是由数据库来处理
    • updatable false 表示在更新数据时,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 { …… }