JPA 上手指南

JPA

版本:spring-boot-starter-data-jpa-2.7.2

[2022-08-09 18:18:03]

前言:

作为 MyBatis 的重度用户,当新项目用到新的 JPA 的持久化层技术时,完全没接触过而又没有拿来即用的参考文档时怎么办呢?下面的文章应该会对你有帮助

注:以下出现的单测均通过上级验证(建表语句见文末)

熟悉 MyBatis 的知道,MyBatis 是强大的,它是一款优秀的 ORM 框架,支持原生的 SQL 语句等等

JPA 听说过最出名的就是“不用写 sql 语句”,那么它是怎么做到的。

回顾起 mapper 文件,会自然想到,那它是怎么实现 ① mapper 的实体类和基类的一一映射关系、② Bean属性和表字段之间的映射、查询方法的…

跟着这个框架走可以帮助我们更快速地认识 JPA 的语法

1.实体类

过去需要继承一个基类,但是不适合测试,现在利用注解@Entity代替了

2.对象关系映射

可以像 MyBatis 一样用专门的配置文件 xml 配置,也可以用注解的方法配置 [从字段到属性] 之间的映射关系,也就是 jpa 中的 @Column

3.其它属性描述

注解@Id@GeneratedValue是告诉JPA主键和自动递增的信息,既可以加在get方法上也可以加在字段上,建议加在字段上

依赖:

spring-boot-starter-data-jpa
com.h2database.h2	# (创建基于内存的数据库/测试用)
lombok				# 缩写setter/getter用

1.取出查询实体

repository.findById(1).get()Optional<T> 里取出实体类

Optional<ProductCategory> byId = repository.findById(1);
ProductCategory productCategory = repository.findById(1).get();

2.正确的修改姿势

先查出来,然后对结果按需设置,相当于MyBatis的按需更新。

// [2022-08-05 10:02:05]
@Test
public void testUpdate() {
    ProductCategory productCategory = repository.findById(1).get();
    productCategory.setCategoryName("热销磅单///");
    repository.save(productCategory);
}
附:实体类的元素

以一张商品类别表举例

@Data
@AllArgsConstructor		// 构造对象用
@NoArgsConstructor		// 构造对象用
@Entity    // JPA
@Table(name="product_category")		// 绑定表,也方便查看映射表(非必填)如果命名严格按照驼峰的话
public class ProductCategory implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY) // 自动递增的字段
    @Column(name="category_id")		// (非必填)如果命名严格按照驼峰的话
    private Integer categoryId;

    @Column(name="category_type")
    private Integer categoryType;

    /**
     * 类目名字
     */	
    @Column(name="category_name")	// (非必填)
    private String categoryName;

    /**
     * create_time
     */
    @Column(name="create_time")		// (非必填)
    private Date createTime;

    /**
     * update_time
     */
    @Column(name="update_time")		// (非必填)
    private Date updateTime;

}

3.常用方法

1.count()
@Test
    public void testCount() {
        long count = repository.count();
    }
2.existById()
@Test
    public void testExistById() {
        boolean b = repository.existsById(2);
    }
3.saveAll()
@Test
    public void testSaveAll() {
        List<ProductCategory> list = mockList();
        repository.saveAll(list);
        List<ProductCategory> all = repository.findAll();
        all.forEach(System.out::println);
    }
4.mybatis没有的findAllById(List)
@Test
    public void testFindAllById() {
        Integer[] ints = {1,3,4,5,6,7};
        List<Integer> list = new ArrayList<>(Arrays.asList(ints));
        List<ProductCategory> allById = repository.findAllById(list);
        allById.forEach(System.out::println);
    }
4.+非主键字段的in查询

主键字段:category_id - findAllById()

非主键:category_type 也有对应的智能匹配生成

	repository
	/**
     * 非主键版 findAllById()
     *
     * @param typeIdList
     * @return java.util.List<com.imooc.domain.ProductCategory>
     */
    List<ProductCategory> findByCategoryTypeIn(List<Integer> typeIdList);
	Test
	/**
     * 测试根据非主键字段的 in 查询
     *
     */
    @Test
    public void testFindByCategoryTypeIn() {
        Integer[] ints = {1, 10};
        List<ProductCategory> byCategoryIn = repository.findByCategoryTypeIn(Arrays.asList(ints));
        System.out.println(byCategoryIn);
    }

1.按照findByFieldNameBy(List<fieldType> list)的规则定义抽象方法

2.使用

相当于 MyBatis

	<!--item: 元素代词-->
    <select id="selectUserByPwdErrIn" parameterType="list" resultMap="UserResult">
        select * from t_user
        <where>
            <if test="timeList != null and timeList.size() > 1">
                pwd_err in
                <foreach collection="timeList" item="item" open="(" separator="," close=")">
                    #{item, jdbcType=VARCHAR}
                </foreach>
            </if>
        </where>
    </select>
5.Sort.by()排序
@Test
    public void testSort() {
        // 1.使用Sort的静态方法by() 定义Sort对象
        Sort orders = Sort.by(Sort.Direction.DESC, "categoryId");	// 第一个参数是Direction有ASC和DESC两个值 
        // 2.传入findAll()
        List<ProductCategory> all = repository.findAll(orders);
        System.out.println("测试jpa排序:");
        all.forEach(System.out::println);
    }
6.1.智能字段匹配

条件字段在方法名里体现和参数列表

// 1.定义方法时方法名和参数列表对应统一(Repository)

	/**
     * 根据商品类别查询
     *
     * @param categoryName
     * @return java.util.List<com.imooc.domain.ProductCategory>
     */
    List<ProductCategory> findByCategoryName(String categoryName);
	// 2.使用
	@Test
    public void testFindByCategoryName() {
        List<ProductCategory> byCategoryName = repository.findByCategoryName("女生最爱");
        byCategoryName.forEach(System.out::println);
    }

2

	/**
     * 根据商品类别查询
     *
     * @param categoryName
     * @param categoryType
     * @return java.util.List<com.imooc.domain.ProductCategory>
     */
    List<ProductCategory> findByCategoryNameAndCategoryType(String categoryName, Integer categoryType);

	@Test
    public void testFindByCategoryName() {
        List<ProductCategory> byCategoryName = repository.findByCategoryNameAndCategoryType("女生最爱", 2);
        byCategoryName.forEach(System.out::println);
    }
6.2.原生字段匹配sql
	/**
     * 原生写法的字段匹配
     *
     * @param categoryName
     * @return com.imooc.domain.ProductCategory
     */

    @Query(value="select * from product_category where category_name = ?1", nativeQuery = true)
    ProductCategory findByCategoryName(String categoryName);

	@Test
    public void testFindByCategoryType() {
        ProductCategory byCategoryName = repository.findByCategoryName("热销磅单///");
        System.out.println(byCategoryName);
    }

参考资料:

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

Example 63. Declare a native query at the query method using @Query

7.条件查询@Query

注解@Query自定义sql语句,nativeQuery = true表示使用原生sq

① ?索引的写法

@Query(value = "select * from product_category where " +
            "if(?1 is not null, category_id=?1,1=1) and " +
            "if(?2 is not null, category_type=?2,1=1) and " +
            "if(?3 != '' and category_name is not null, category_name like concat('%',?3, '%'), 1=1) and " +
            "if(?4 is not null, create_time=?4, 1=1) and " +
            "if (?5 is not null,update_time=?5,1=1) " +
            "order by category_id desc", nativeQuery = true)
    List<ProductCategory> find(Integer categoryId, Integer categoryType, String categoryName, Date createTime, Date updateTime);
    
 @Test
    public void testSelectIndex() {
        List<ProductCategory> categoryList = repository.find(null, null, "女生", null, null);
        categoryList.forEach(System.out::println);
    }

参考资料:SpringBoot | JPA基本查询及多条件查询,参数为空判断_Xyu_a的博客-CSDN博客_jpa参数为空

② :属性写法

参数必须列全,逐个标记注解

	@Query(value = "select * from product_category where " +
            "if(:categoryId is not null, category_id=:categoryId,1=1) and " +
            "if(:categoryType is not null, category_type=:categoryType,1=1) and " +
            "if(:categoryName != '' and category_name is not null, category_name like                    				 concat('%',:categoryName, '%'), 1=1) and " +
            "if(:createTime is not null, create_time=:createTime, 1=1) and " +
            "if (:updateTime is not null,update_time=:updateTime,1=1) " +
            "order by category_id desc", nativeQuery = true)
    List<ProductCategory> find2(@Param("categoryId")Integer categoryId,
                                @Param("categoryType")Integer categoryType, 
                                @Param("categoryName")String categoryName, 
                                @Param("createTime")Date createTime, 
                                @Param("updateTime")Date updateTime);
	@Test
    public void testFind2() {
        List<ProductCategory> categoryList = repository.find2(null, null, "女生", null, null);
        categoryList.forEach(System.out::println);
    }

参考资料:

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

Example 66. Using named parameters

8.1.条件删除
    int deleteByCategoryId(Integer categoryId);

	@Test
    public void testdeleteByCategoryId() {
        repository.deleteByCategoryId(10);
    }
8.1.条件删除原生sql

注解依旧是@Query,加上注解@Modifying

	@Modifying
    @Query(value = "delete from product_category where category_id = ?1", nativeQuery=true)
    int deleteByXX(Integer categoryId);

	@Test
    public void testdeleteByXX() {
        int i = repository.deleteByXX(9);
    }
9.原生sql更新(save)

save方法必须先查询后save才能实现数据的更新

 	@Modifying
    @Query(value = "update product_category set category_type=?1,  category_name=?2, create_time=?3, 	update_time=?4 where category_id=?5", nativeQuery=true)
    int update(Integer categoryType, 
               String categoryName, 
               Date createTime, 
               Date updateTime, 
               Integer categoryId);

	@Test
    public void testupdate() {
        int i = repository.update(80, "电器", new Date(), new Date(), 8);
    }
10.分页
	@Test
    public void testFindAll() {
		// 构造Pagable对象
        Pageable pageable = PageRequest.of(0, 5);
		// 传入参数
        Page<ProductCategory> all1 = repository.findAll(pageable);
        System.out.println("分页结果:");
        all1.forEach(System.out::println);
    }

参考资料:spring data jpa分页5种方法_boss达人的博客-CSDN博客_jpa 分页

4.注解@DynamicUpdate

默认情况下,JPA是全字段插入的

Hibernate: insert into product_category (category_name, category_type, create_time, update_time) 
    values (?, ?, ?, ?)

[2022-08-09 16:04:23] 默认调用save()

此时如果检测不能为空字段没有设值时就会触发UnexpectedRollbackException

此时在实体类上打上注解@DynamicUpdate,则会动态更新,根据entity已有的值判空进行写入。

此时,如果建表时设置了on update CURRENT_TIMESTAMP或者自增的主键的规则,记录写入时也会自动触发MySQL的定义规则。

	/**
     * 测试 @DynamicUpdate
     * 只修改其中一个字段,不设置更新时间
     * */
    @Test
    public void testUpdate() {
        ProductCategory productCategory = repository.findById(1).get();
        productCategory.setCategoryName("热销磅单qaaa");
        repository.save(productCategory);
    }

	//
    import org.hibernate.annotations.DynamicUpdate;

    @Data
    @Entity    // JPA
    @Table(name="product_category")
    @DynamicUpdate  // 使用MySQL的自动更新
    public class ProductCategory implements Serializable {

    }

结果日志

Hibernate: update product_category set category_name=? where category_id=?

同理可得,@DynamicInsert动态取值插入

	/**
     * 测试 @DynamicInsert 
     * 不设主键(auto_increment)
     * 不设创建时间(default CURRENT_TIMESTAMP not null)
     * */
    @Test
    public void testSave1() {
        ProductCategory category = new ProductCategory();
        System.out.println("插入ing");
        // category.setCategoryId(16);
        category.setCategoryType(1);
        category.setCategoryName("热销磅单3.0");
        // category.setCreateTime(new Date());
        category.setUpdateTime(new Date());
        ProductCategory saved = repository.save(category);
        	
        Assertions.assertNotNull(saved);
        System.out.println("实际被插入的数据:" + saved);
    }
	
    @Table(name="product_category")
    @DynamicInsert  // 没有值的时候使用动态插入
    public class ProductCategory implements Serializable {
        //...
    }

日志:

插入ing
Hibernate: insert into product_category (category_name, category_type, update_time) values (?, ?, ?)
实际被插入的数据:ProductCategory(categoryId=21, categoryType=1, categoryName=热销磅单3.0, createTime=null, updateTime=Tue Aug 09 17:20:34 CST 2022)

执行时的日志中只选择性插入 3 个字段

具体实现了 MyBatis 里的哪种写法不得而知。

现象:为什么同时注解DynamicInsertDynamicUpdate没有起到插入时创建时间和更新时间都不用另外设值的效果

[2022-08-09 16:26:53] 两个都有注解,但是插入新纪录时,只设置(category_name, category_type)两个字段时,结果没有触发自动更新机制(update_time有设置on update CURRENT_TIMESTAMP

原因:

product_categoryDDL:

create table product_category
(
    category_id   int auto_increment
        primary key,
    category_name varchar(64)                         not null comment '类目名字',
    category_type int                                 not null comment '类目编号(唯一性索引)',
    create_time   timestamp default CURRENT_TIMESTAMP not null,
    update_time   timestamp                           not null on update CURRENT_TIMESTAMP
)
    comment '商品类目表';

create index product_category_category_type_uindex
    on product_category (category_type);

update_time与上面的create_time中间空了一大段,所以并没有设置默认值 default CURRENT_TIMESTAMP 的规则,只有定义更新时触发的规则。

所以 sql 语句在 insert into product_category (category_name, category_type) values (?, ?)时放数据库IDE里也执行不通,JPA 只能帮助执行原本能在数据库正常运行的 sql。

借此说明,DynamicInsertDynamicUpdate是各自为了动态插入和动态更新时服务的,加在一起并没有有什么加成的功效。

插入新纪录是,没有默认值、自增的 not null字段该设值还是乖乖设值。

5.不写入数据库

当我们测试时不想实际写入数据库时,这也是单元测试的理想效果,只测试逻辑代码,不对实际测试/生产数据造成改动。

springboot 原本就是将测试类设置为自动回滚的,所以默认情况下单测通过,数据库也不会发生实际的变化。

将上诉单测中的测试类头上的@Rollback(value = false) 注释即可

附:建表语句
create table product_category
(
    category_id   int auto_increment
        primary key,
    category_name varchar(64)                         not null comment '类目名字',
    category_type int                                 not null comment '类目编号(唯一性索引)',
    create_time   timestamp default CURRENT_TIMESTAMP not null,
    update_time   timestamp                           not null on update CURRENT_TIMESTAMP
)
    comment '商品类目表';

create index product_category_category_type_uindex
    on product_category (category_type);

INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (1, '热销磅单qaaa', 1, '2022-08-07 14:27:33', '2022-08-09 16:23:32');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (2, '女生最爱', 2, '2022-08-05 01:07:58', '2022-08-05 01:07:58');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (3, '类型4', 4, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (4, '类型5', 5, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (5, '类型6', 6, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (6, '类型7', 7, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (7, '类型8', 8, '2022-08-05 02:56:47', '2022-08-05 02:56:47');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (8, '电器', 80, '2022-08-07 14:27:33', '2022-08-07 14:27:33');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (13, '类型10', 10, '2022-08-07 14:10:14', '2022-08-07 14:10:14');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (14, '类型11', 11, '2022-08-07 14:10:14', '2022-08-07 14:10:14');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (17, '热销磅单2.0', 1, '2022-08-09 15:55:35', '2022-08-09 07:55:35');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (18, '热销磅单3.0', 1, '2022-08-09 16:12:31', '2022-08-09 08:12:32');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (19, '热销磅单3.0', 1, '2022-08-09 17:18:32', '2022-08-09 09:18:32');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (20, '热销磅单3.0', 1, '2022-08-09 17:19:06', '2022-08-09 09:19:06');
INSERT INTO dev.product_category (category_id, category_name, category_type, create_time, update_time) VALUES (21, '热销磅单3.0', 1, '2022-08-09 17:20:34', '2022-08-09 09:20:34');

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值