简介
JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体[对象持久化](https://baike.baidu.com/item/对象持久化/7316192)到数据库中。 ([来源:百度百科](https://baike.baidu.com/item/JPA/5660672))。总结一点就是对于简单的CRUD,只需要操作对应的实体类,无需编写SQL即可实现对应的功能,同时也支持自定义SQL,详情可查看[JPA官方操作手册](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#)。版本介绍
| 版本 | 备注 | |
|---|---|---|
| SpringBoot | 2.6.4 | |
| MySQL | 5.6.51 | |
| p6spy | 3.9.1 | 用于格式化sql文件,可以将占位符直接替换成参数值,将执行sql直接打印出来 | 
准备工作
测试表相关SQL
用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键id',
  `account` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账户名',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
  `gender` int(1) NULL DEFAULT NULL COMMENT '性别',
  `age` int(3) NULL DEFAULT NULL COMMENT '年龄',
  `create_user` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
  `create_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '创建时间',
  `update_user` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
  `update_time` timestamp(6) NOT NULL DEFAULT '0000-00-00 00:00:00.000000' COMMENT '更新时间',
  `delete_flag` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Compact;用户机构表
DROP TABLE IF EXISTS `user_org`;
CREATE TABLE `user_org`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键id',
  `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
  `org_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '机构代码',
  `org_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '机构名称',
  `create_user` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
  `create_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '创建时间',
  `update_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人',
  `update_time` timestamp(6) NOT NULL DEFAULT '0000-00-00 00:00:00.000000' COMMENT '更新时间',
  `delete_flag` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表机构表' ROW_FORMAT = Compact;引入依赖
<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/p6spy/p6spy -->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>编写配置文件
application.properties
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/demo-jpa
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=root
spring.datasource.password=rootspy.properties
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=sql -> %(sqlSingleLine)
appender=com.p6spy.engine.spy.appender.StdoutLogger通用审计日志基础信息配置
由于数据创建人、创建时间以及数据修改人、修改时间为公共字段,实现AuditorAware接口配合@CreateBy、@CreateDate、@LastModifiedBy、@LastModifiedDate即可字段插入对应的字段信息。
@Component
@EnableJpaAuditing
public class JpaAuditConfig implements AuditorAware<String> {
    /**
     * 可 用于 获取 当前操作人信息,根据实际项目做调整
     * @return
     */
    @Override
    public Optional<String> getCurrentAuditor() {
        // 当前测试为固定在,项目中根据实际情况调整即可
        String operatorId = "operateId";
        return Optional.of(operatorId);
    }自定义主键生成策略
由于多表新增时涉及到关联主键的问题,故表的主键需要先生成备用,新增数据时的主键生成逻辑需要调整:若新增的实体类已存在主键,则不重新生成。
jpa主键生成策略可参考:https://www.cnblogs.com/SummerinShire/p/7544897.html
public class CustomUUIDConfig extends UUIDGenerator {
    /**
     *
     * @param session
     * @param entityObj 当前实体类对象
     * @return
     * @throws HibernateException
     */
    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object entityObj) throws HibernateException {
        String entityIdField = this.getEntityIdField(entityObj);
        if (StrUtil.isBlank(entityIdField)) {
            throw new RuntimeException(" Entity must contains id annotation");
        }
        Object existId = ReflectUtil.getFieldValue(entityObj, entityIdField);
        if (ObjectUtil.isNotNull(existId)) {
            return (Serializable) existId;
        }
        return IdUtil.fastSimpleUUID();
    }
    /**
     * 获得实体类中id对应的字段名称
     * @param entityObj
     * @return
     */
    private String getEntityIdField(Object entityObj) {
        String idFieldName = null;
        Field[] fields = ReflectUtil.getFields(entityObj.getClass());
        for (Field field : fields) {
            if (AnnotationUtil.hasAnnotation(field, Id.class)) {
                idFieldName = field.getName();
                break;
            }
        }
        return idFieldName;
    }
}jpa 主键生成的4中策略
编写实体类
用户实体类
@Data
@Entity
@ToString
@Table(name = "user")
@Accessors(chain = true)
@EntityListeners(AuditingEntityListener.class)
public class UserPO implements Serializable {
    private static final long serialVersionUID = -228275281168696921L;
    /**
     * 主键id,使用自定义的主键生成策略
     */
    @Id
    @Column
    @GeneratedValue(generator = "my-uuid")
    @GenericGenerator(name = "my-uuid", strategy = "top.zhoujb.demo.jpa.config.CustomUUIDConfig")
    private String id;
    /**
     * 账户名
     */
    @Column
    private String account;
    /**
     * 密码
     */
    @Column
    private String password;
    /**
     * 名称
     */
    @Column
    private String userName;
    /**
     * 性别
     */
    @Column
    private Integer gender;
    /**
     * 年龄
     */
    @Column
    private Integer age;
    /**
     * 创建人
     */
    @Column
    @CreatedBy
    private String createUser;
    /**
     * 创建时间
     */
    @Column
    @CreatedDate
    private LocalDateTime createTime;
    /**
     * 更新人
     */
    @Column
    @LastModifiedBy
    private String updateUser;
    /**
     * 更新时间
     */
    @Column
    @LastModifiedDate
    private LocalDateTime updateTime;
    /**
     * 删除标识
     */
    @Column
    private String deleteFlag;
    /**
     * 机构代码
     */
    @Transient
    private String orgCode;
    /**
     * 机构名称
     */
    @Transient
    private String orgName;
}用户机构实体类
@Data
@Entity
@ToString
@Accessors(chain = true)
@Table(name = "user_org")
@EntityListeners(AuditingEntityListener.class)
public class UserOrgPO implements Serializable {
    private static final long serialVersionUID = 8618860155842190730L;
    /**
     * 主键id,使用自定义的主键生成策略
     */
    @Id
    @Column
    @GeneratedValue(generator = "my-uuid")
    @GenericGenerator(name = "my-uuid", strategy = "top.zhoujb.demo.jpa.config.CustomUUIDConfig")
    private String id;
    /**
     * 用户id
     */
    private String userId;
    /**
     * 机构代码
     */
    private String orgCode;
    /**
     * 机构名称
     */
    private String orgName;
    /**
     * 创建人
     */
    private String createUser;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
    /**
     * 更新人
     */
    private String updateUser;
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
    /**
     * 删除标识
     */
    private String deleteFlag;
}数据库操作接口
用户数据库访问接口
public interface UserDao extends JpaRepository<UserPO, String> {}用户机构数据库访问层接口
public interface UserOrgDao extends JpaRepository<UserOrgPO, String> {}测试案例
新增
单表新增
    @Test
    void insertOne() {
        String password = SecureUtil.md5("user");
        UserPO userPO = new UserPO()
                .setAccount("user")
                .setPassword(password)
                .setUserName("测试用户")
                .setAge(20)
                .setGender(1)
                .setDeleteFlag("0");
        this.userDao.save(userPO);
    }多表新增
    @Test
    void insertMultipleWithCustomId() {
        String password = SecureUtil.md5("user");
        List<UserPO> userList = new ArrayList<>();
        List<UserOrgPO> userOrgList = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            String userId = IdUtil.fastSimpleUUID();
            UserPO userPO = new UserPO()
                    .setId(userId)
                    .setAccount("user" + i + RandomUtil.randomInt())
                    .setPassword(password)
                    .setUserName("测试用户" + i + RandomUtil.randomInt())
                    .setAge(20 + i)
                    .setGender(i % 2)
                    .setDeleteFlag("0");
            userList.add(userPO);
            UserOrgPO userOrgPO = new UserOrgPO()
                    .setUserId(userId)
                    .setDeleteFlag("0")
                    .setOrgCode("orgCode" + i + RandomUtil.randomInt())
                    .setOrgName("orgName" + i + RandomUtil.randomInt());
            userOrgList.add(userOrgPO);
        }
        this.userDao.saveAllAndFlush(userList);
        this.userOrgDao.saveAllAndFlush(userOrgList);
    }修改
    @Test
    public void update() {
        String userId = "5d744def266146fc9b99d7f7b87883e4";
        Optional<UserPO> opt = this.userDao.findById(userId);
        UserPO userPO = new UserPO();
        if (opt.isPresent()) {
            BeanUtil.copyProperties(opt.get(), userPO);
        }
        userPO.setUserName("777").setAccount("user666");
        UserPO save = this.userDao.save(userPO);
        System.out.println(save.toString());
    }删除
    @Test
    public void deleteOne() {
        String userId = "2e78a777172b46e392cc2416ad0043b4";
        this.userDao.deleteById(userId);
    }
    @Test
    public void deleteMany() {
        List<String> idList = ListUtil.toList("f4394921d44243228fe15115fa4ea693", "ce99a184246a4af5ae5f3bbabf0dd807");
        this.userDao.deleteAllByIdInBatch(idList);
    }查询
简单查询
    @Test
    public void findOne() {
        String userId = "a1f704b9600548e6a71e6d694e04698a";
        Optional<UserPO> byId = this.userDao.findById(userId);
        if (byId.isPresent()) {
            System.out.println(byId.get().toString());
        } else {
            System.out.println("no data");
        }
    }
    @Test
    public void findAll() {
        List<UserPO> all = this.userDao.findAll();
        System.out.println(all.size());
    }普通单表分页
自定义分页方法,参数为实体类时,在UserDao接口中添加如下代码
    /**
     * 分页查询数据
     * @param params
     * @param pageRequest
     * @return
     */
    @Query(nativeQuery=true, value = "select * from user where account like %:#{#user.account}%")
    Page<UserPO> selectByPage(@Param("user") UserPO params, Pageable pageRequest);测试代码
    @Test
    public void findByPage() {
        int page = 1;
        int size = 5;
        Sort genderSort = Sort.by(Sort.Direction.ASC, "gender");
        Pageable pageRequest = PageRequest.of(page, size, genderSort);
        UserPO params = new UserPO().setAccount("user");
        Page<UserPO> poPage = this.userDao.selectByPage(params, pageRequest);
        System.out.println(poPage.getContent().size());
    }多表联合分页
多表分页,参数为实体类时,在UserDao接口中添加如下代码。注意:此时需要自己重写countQuery,否则会导致sql执行报错或者统计结果错误等问题。
    /**
     * 分页查询数据
     * @param params
     * @param pageRequest
     * @return
     */
    @Query(nativeQuery = true,
            value = "select u.*,uo.org_code,uo.org_name from user u join user_org uo on uo.user_id = u.id where u.account like %:#{#user.account}%"
            , countQuery = "select count(*) from user u join user_org uo on uo.user_id = u.id where u.account like %:#{#user.account}%"
    )
    Page<UserPO> selectUseAndOrgByPage(@Param("user") UserPO params, Pageable pageRequest);测试代码
    @Test
    public void findByPageWithMultipleTable() {
        int page = 1;
        int size = 5;
        Sort genderSort = Sort.by(Sort.Direction.ASC, "gender");
        Pageable pageRequest = PageRequest.of(page, size, genderSort);
        UserPO params = new UserPO().setAccount("user");
        Page<UserPO> poPage = this.userDao.selectUseAndOrgByPage(params, pageRequest);
        List<UserPO> content = poPage.getContent();
        if (CollUtil.isNotEmpty(content)) {
            content.stream().forEach(uo -> System.out.println(uo.toString()));
        }
    }总结
注意事项
| 说明 | |
|---|---|
| 查询参数为实体类时参数书写方式 | 格式: :#{#实体类参数名.实体类属性名}。示例:实体类参数名称为:params,其中有name属性,则在sql中应为::#{#params.name} | 
| 多表联合查询注意事项 | 多表联合分页查询时需要统计sql | 
jpa 的关键字使用
| 关键字 | 案例 | 执行的SQL片段 | 
|---|---|---|
| Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 | 
| And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 | 
| Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 | 
| Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 | 
| Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 | 
| LessThan | findByAgeLessThan | … where x.age < ?1 | 
| LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 | 
| GreaterThan | findByAgeGreaterThan | … where x.age > ?1 | 
| GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 | 
| After | findByStartDateAfter | … where x.startDate > ?1 | 
| Before | findByStartDateBefore | … where x.startDate < ?1 | 
| IsNull, Null | findByAge(Is)Null | … where x.age is null | 
| IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null | 
| Like | findByFirstnameLike | … where x.firstname like ?1 | 
| NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 | 
| StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) | 
| EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) | 
| Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) | 
| OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc | 
| Not | findByLastnameNot | … where x.lastname <> ?1 | 
| In | findByAgeIn(Collection ages) | … where x.age in ?1 | 
| NotIn | findByAgeNotIn(Collection ages) | … where x.age not in ?1 | 
| TRUE | findByActiveTrue() | … where x.active = true | 
| FALSE | findByActiveFalse() | … where x.active = false | 
| IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) | 
来源:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation