添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
打酱油的书签  ·  邮件报 535 5.7.0 Error: ...·  11 月前    · 
乐观的签字笔  ·  Java ...·  1 年前    · 
听话的板栗  ·  ts中类型为 "string" ...·  1 年前    · 
JPA快速上手指南—入门篇

JPA快速上手指南—入门篇

一、前言

当今市面上有两款比较优秀的ORM框架,就是众所周知的MyBatis和JPA,由于受到一些大厂的影响,国内的MyBatis的市场占有率非常的高,MyBatis一向以灵活著称,而在国外使用JPA的项目更高,那么两者谁更加好用呢?这是一个没有答案的问题,要根据项目人员对哪种技术掌握的熟练程度以及业务场景来进行选择。

MyBatis也可以使用注解进行开发,但是如果做复杂查询xml文件依然是最好的选择,但xml文件不利于维护,维护工作比较繁重,今天我们来介绍另一种ORM框架——JPA。

1.1 什么是JPA

其实JPA并不是一种框架而是一种ORM规范。

1.2 什么是Hibernate

Hibernate是实现了JPA规范的一种ORM框架,但是,JPA规范的实现仅仅是Hibernate的一部分.

1.3 什么是Spring Data JPA

Spring Data JPA为Java Persistence API(JPA)提供了实现。它简化了通过JPA访问数据库的开发工作,提供了很多CRUD的快捷操作,还提供了如分页、排序、复杂查询、自定义查询(JPQL)等功能,Spring Data JPA底层也是依赖于Hibernate来实现的,Spring Data JPA拥有标准化、简单易用、面向对象等优势,并且Spring将EntityManager 的创建与销毁、事务管理等代码抽取出来,并由Spring统一进行管理。

二、环境搭建

本文将通过一个SpringBoot项目进行说明。

maven依赖
 1<parent>
 2  <groupId>org.springframework.boot</groupId>
 3  <artifactId>spring-boot-starter-parent</artifactId>
 4  <version>2.2.1.RELEASE</version>
 5</parent>
 6<dependencies>
 7  <!-- Spring Boot Web -->
 8  <dependency>
 9    <groupId>org.springframework.boot</groupId>
10    <artifactId>spring-boot-starter-web</artifactId>
11  </dependency>
13  <dependency>
14    <groupId>org.springframework.boot</groupId>
15    <artifactId>spring-boot-starter-test</artifactId>
16  </dependency>
18  <!-- lombok -->
19  <dependency>
20    <groupId>org.projectlombok</groupId>
21    <artifactId>lombok</artifactId>
22    <version>1.18.12</version>
23    <scope>provided</scope>
24  </dependency>
26  <!-- jpa -->
27  <dependency>
28    <groupId>org.springframework.boot</groupId>
29    <artifactId>spring-boot-starter-data-jpa</artifactId>
30  </dependency>
32  <!-- spring-boot-starter-jdbc -->
33  <dependency>
34    <groupId>org.springframework.boot</groupId>
35    <artifactId>spring-boot-starter-jdbc</artifactId>
36  </dependency>
38  <!-- mysql-connector-java -->
39  <dependency>
40    <groupId>mysql</groupId>
41    <artifactId>mysql-connector-java</artifactId>
42    <version>8.0.21</version>
43    <scope>runtime</scope>
44  </dependency>
46  <!-- fastJson -->
47  <dependency>
48    <groupId>com.alibaba</groupId>
49    <artifactId>fastjson</artifactId>
50    <version>1.2.66</version>
51  </dependency>
52</dependencies>
SpringBoot配置
 1spring:
 2  application:
 3    #应用名称
 4    name: spring-data-jpa
 5  datasource:
 6    #你的数据库密码
 7    password: 123456
 8    #你的数据库地址
 9    url: jdbc:mysql://localhost:3306/spring_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true
10    #数据库用户名
11    username: root
12    #数据库驱动名称
13    driver-class-name: com.mysql.cj.jdbc.Driver #配置MySQL的驱动程序类
14    #指定连接池类型
15    type: com.zaxxer.hikari.HikariDataSource
16    #数据库连接池的配置
17    hikari:
18      #客户端等待连接池连接的最大毫秒数
19      connection-timeout: 30000
20      #连接池中维护的最小空闲连接数
21      minimum-idle: 4
22      #最大池大小
23      maximum-pool-size: 8
24      #允许连接池在连接池中空闲的最长时间(毫秒)
25      idle-timeout: 30000
26      #池中连接关闭的最长生命周期(毫秒)
27      max-lifetime: 45000
28      #从池返回的连接的默认自动提交行为(默认为true)
29      auto-commit: true
30      #连接池的名称
31      pool-name: SpringDataJPAHikariCP
32  #jpa相关配置
33  jpa:
34    hibernate:
35      #DDL:用于定义数据库的三层结构,包括外模式、概念模式、内模式及其相互之间的映像,定义数据的完整性,安全控制等约束
36      ddl-auto: none #什么也不做
37      #其他可选值
38      #create: 每次运行应用程序时,都会重新创建表,所以,数据都会丢失
39      #create-drop:每次运行程序时会创建表结构,然后程序结束时清空数据
40      #update: 每次运行程序没有表时会创建表,如果对象改变会更新表结构,原有数据不会清除,只会更新
41      #validate: 运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错
42    #打印执行的sql及参数
43    show-sql: true
44    # 关闭懒加载配置,否则会报错
45    open-in-view: false
46    properties:
47      hibernate:
48        #输出sql语句
49        show_sql: true
50        #格式化输出的sql,否则会一行显示
51        format_sql: true
52server:
53  #指定服务端口号
54  port: 8008
数据库脚本

这里简单介绍一下表和表之间的关系,一个Banner对应多个BannerItem,属于一对多关系,一个Goods商品可以属于多个商品分类,一个商品分类下面会有多个商品,属于多对多关系,需要用第三张表goods_category_relation来进行组织。

 1CREATE TABLE `goods`  (
 2  `id` bigint(20) NOT NULL COMMENT '主键',
 3  `name` varchar(512) NOT NULL COMMENT '商品名称',
 4  `description` varchar(2048) NULL COMMENT '商品描述',
 5  `detail` text NULL COMMENT '商品详情',
 6  `price` decimal(10, 4) NOT NULL COMMENT '商品价格',
 7  `img_url` varchar(2048) NULL COMMENT '商品图片,以逗号分割',
 8  `create_time` datetime NOT NULL COMMENT '创建时间',
 9  `modify_time` datetime NOT NULL COMMENT '变更时间',
10  `deleted` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除',
11  `enable` tinyint(2) NOT NULL DEFAULT 1 COMMENT '是否上架',
12  `creator` bigint NULL COMMENT '创建人',
13  PRIMARY KEY (`id`)
14) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
16CREATE TABLE `goods_category`  (
17  `id` bigint(20) NOT NULL COMMENT '主键',
18  `parent_id` bigint(20) NOT NULL COMMENT '父级分类',
19  `name` varchar(512) NOT NULL COMMENT '商品分类名称',
20  `level` tinyint(2) NOT NULL COMMENT '分类层级',
21  `description` varchar(2048) NULL COMMENT '分类描述',
22  `img_url` varchar(2048) NULL COMMENT '商品分类图标',
23  `create_time` datetime NOT NULL COMMENT '创建时间',
24  `modify_time` datetime NOT NULL COMMENT '变更时间',
25  `deleted` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除',
26  `enable` tinyint(2) NOT NULL DEFAULT 1 COMMENT '是否上架',
27  `creator` bigint NULL COMMENT '创建人',
28  PRIMARY KEY (`id`)
29) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';
31CREATE TABLE `goods_category_relation`  (
32  `id` bigint(20) NOT NULL COMMENT '主键',
33  `goods_id` bigint(20) NOT NULL COMMENT '商品id',
34  `category_id` bigint(20) NOT NULL COMMENT '商品分类id',
35  PRIMARY KEY (`id`)
36) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类关系表';
38CREATE TABLE `banner`  (
39  `id` bigint(20) NOT NULL COMMENT '主键',
40  `name` varchar(512) NOT NULL COMMENT 'banner名称',
41  `description` varchar(2048) NULL COMMENT 'banner描述',
42  `create_time` datetime NOT NULL COMMENT '创建时间',
43  `modify_time` datetime NOT NULL COMMENT '变更时间',
44  `deleted` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除',
45  `enable` tinyint(2) NOT NULL DEFAULT 1 COMMENT '是否上架',
46  `creator` bigint NULL COMMENT '创建人',
47  PRIMARY KEY (`id`)
48) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='banner信息表';
50CREATE TABLE `banner_item`  (
51  `id` bigint(20) NOT NULL COMMENT '主键',
52  `banner_id` bigint(20) NOT NULL COMMENT 'banner Id',
53  `name` varchar(512) NOT NULL COMMENT 'banner子项名称',
54  `description` varchar(2048) NULL COMMENT 'banner子项描述',
55  `img_url` varchar(2048) NOT NULL COMMENT 'banner图片',
56  `type` tinyint(2) NOT NULL COMMENT '类型:0商品,1:分类',
57  `jump_url` varchar(2048) NOT NULL COMMENT '跳转地址',
58  `create_time` datetime NOT NULL COMMENT '创建时间',
59  `modify_time` datetime NOT NULL COMMENT '变更时间',
60  `deleted` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除',
61  `enable` tinyint(2) NOT NULL DEFAULT 1 COMMENT '是否上架',
62  `creator` bigint NULL COMMENT '创建人',
63  PRIMARY KEY (`id`)
64) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='banner子项信息表';
初始化数据
 1-- banner表
 2INSERT INTO `spring_test`.`banner`(`id`, `name`, `description`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (1, '首页banner', '首页顶部展示banner', '2021-03-26 17:10:02', '2021-03-26 17:10:05', 0, 1, 1);
 4-- banner item表
 5INSERT INTO `spring_test`.`banner_item`(`id`, `banner_id`, `name`, `description`, `img_url`, `type`, `jump_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (1, 1, '子banner1', '这是一个banner', 'a.jpg', 1, 'http://www.baidu.com', '2021-03-26 17:39:57', '2021-03-26 17:40:01', 0, 1, 1);
 6INSERT INTO `spring_test`.`banner_item`(`id`, `banner_id`, `name`, `description`, `img_url`, `type`, `jump_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (2, 1, '子banner2', '这是一个banner', 'b.jpg', 1, 'http://www.baidu.com', '2021-03-26 17:39:57', '2021-03-26 17:40:01', 0, 1, 1);
 7INSERT INTO `spring_test`.`banner_item`(`id`, `banner_id`, `name`, `description`, `img_url`, `type`, `jump_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (3, 1, '子banner3', '这是一个banner', 'c.jpg', 1, 'http://www.baidu.com', '2021-03-26 17:39:57', '2021-03-26 17:40:01', 0, 1, 1);
 9-- goods表
10INSERT INTO `spring_test`.`goods`(`id`, `name`, `description`, `detail`, `price`, `img_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (1, 'MacBook', '512G+16G', 'I9处理器', 23000.0000, 'a.jpg', '2021-03-28 09:04:23', '2021-03-28 09:04:26', 0, 1, NULL);
11INSERT INTO `spring_test`.`goods`(`id`, `name`, `description`, `detail`, `price`, `img_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (2, 'MacBook', '256G+8G', 'I5处理器', 10000.0000, 'b.jpg', '2021-03-28 09:05:08', '2021-03-28 09:05:12', 0, 1, NULL);
12INSERT INTO `spring_test`.`goods`(`id`, `name`, `description`, `detail`, `price`, `img_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (3, 'MacBook', '128G+8G', 'I7处理器', 10000.0000, 'b.jpg', '2021-03-28 09:05:08', '2021-03-28 09:05:12', 0, 1, NULL);
14-- goods category表
15INSERT INTO `spring_test`.`goods_category`(`id`, `parent_id`, `name`, `level`, `description`, `img_url`, `create_time`, `modify_time`, `deleted`, `enable`, `creator`) VALUES (1, 0, '电子产品', 0, '电子产品分类', 'a.jpg', '2021-03-29 15:10:54', '2021-03-29 15:11:01', 0, 1, 1);
17-- 商品和商品分类关联关系表
18INSERT INTO `spring_test`.`goods_category_relation`(`id`, `goods_id`, `category_id`) VALUES (1, 1, 1);
19INSERT INTO `spring_test`.`goods_category_relation`(`id`, `goods_id`, `category_id`) VALUES (2, 2, 1);
20INSERT INTO `spring_test`.`goods_category_relation`(`id`, `goods_id`, `category_id`) VALUES (3, 3, 1);

三、JPA基本操作

JPA提供了数据表相关的增删改查方法,其中最重要的就是查询,因为查询是最为灵活多变的操作,业务开发当中大部分的SQL也是围绕查询展开的,下面主要针对查询来进行介绍。

3.1 实体类的编写

创建Banner对应的实体类,通过实体类来映射数据库中的字段。

 1@Entity
 2@Getter
 3@Setter
 4public class Banner {
 5    @Id
 6    @GeneratedValue(strategy = GenerationType.IDENTITY)
 7    private Long id;
 8    private String name;
 9    private String description;
10    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
11    private Date createTime;
12    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
13    private Date modifyTime;
14    private Integer deleted;
15    private Integer enable;
16    private Long creator;
代码详解

@Entity:标志为一个JPA的实体类

@Getter、@Setter:表示设置实体类属性的Getter、Setter方法。

@Id注解:标志数据库表的主键字段。

@GeneratedValue(strategy = GenerationType.IDENTITY):指定主键的生成策略,当前指定的策略为主键自增,由数据库进行生成。

除了主键自增之外,其他主键生成策略:

TABLE:使用一个特定的数据库表去保存主键。

SEQUENCE:根据底层数据库序列生成主键。

AUTO:由程序控制,是GenerationType 的默认值

补充说明

@Column:实体和数据表列不同名的时候才有用,如数据库字段banner_id,实体类字段id,这时就要使用@Column注解来标志数据库字段名称@Column(name="banner_id")

其他属性:

name:数据库表中的字段名称

nullable:该字段是否允许为null,默认是true。

unique:字段值是否唯一,默认是false

length:字段的大小,仅仅对String类型的字段有效。

以上的限制,都是在JPA层面进行拦截限制,而不是在数据库层面。这个注解中还有很多属性,可以自行了解。

@Basic:标志是数据库表的列,这个注解会默认加载字段上,如果数据表没有这个字段将会报错。

@Transient:如果想忽略实体类的某一个字段时,可以使用这个注解,JPA就不会将这个字段作为数据表的列。

1@Transient
2private String extData;
使用IDEA自动生成实体类

除了手动编写之外,IDEA也提供了自动生成实体类的功能,在View菜单中选择Persistence选项。


选择By Database Schema

选要生成实体类的数据表和生成实体类的位置


生成成功,具体实体类的细节根据需求进行调整。




3.2 Repository的编写

JPA中,我们只需要定义一个接口就可以轻松地操作数据库,这个接口要继承JpaRepository接口,这里需要指定两个泛型。

1public interface BannerRepository extends JpaRepository<Banner, Long> {

第一个泛型是实体类的类型,第二个泛型是主键的类型。

JpaRepository继承关系

继承关系如下,JpaRepository继承自PagingAndSortingRepository,PagingAndSortingRepository则继承自CrudRepository,CrudRepository继承自Repository接口,扩展了Repository的接口,并添加了一些其他的功能。



查看Repository源码,可以看到Repository接口中没有任何的实现,但是这个接口是JPA的核心接口,实现了该接口的类,都会被Spring容器识别为一个Repository Bean,放到IOC容器中,进而可以定义一些满足一定规范的方法。

CrudRepository实现了一组CRUD的相关方法,PagingAndSortingRepository实现了排序和分页的相关功能,我们可以自定义一些自定义的Repository,作为通用的Repository,具体选择哪个父接口,要根据业务需求来进行选择,如:有时可能不想暴露一些接口,如可能不想对外暴露删除的方法,这时可以把想要的方法复制到Repository中,来避免暴露不必要的方法。


另一种定义repository的方式

通过@RepositoryDefinition注解来指定实体类的类型和主键的类型。

1@RepositoryDefinition(domainClass = Banner.class, idClass = Long.class)
2public interface BannerRepositoryDefinition {

这样的方式和之前定义方式是一样的,但是并不常用,在一般的业务开发中,通常是继承JpaRepository就足够了。

3.3 JPA简单条件查询

JPA要求 我们按照一定的规则进行命名就可以进行一些简单的查询,下面我们开始编写第一个JPA方法。

我们可以定义如下的方法,按照字面上的理解就是,通过查询Banner通过Id,参数是bannerId,也可以通过其他字段来进行查询,IDE会给出提示,我们仅仅需要编写一个这样的方法JPA就会自动的完成查询工作,当前方法中返回的Java8中的Optional对象,也可以直接返回Banner实体类,在一些业务场景下,返回的结果集可能是多个实体对象,这时要用List来进行接收。

 1public interface BannerRepository extends JpaRepository<Banner, Long> {
 3    /**
 4     * 查询banner 通过bannerId
 5     *
 6     * @param bannerId bannerId
 7     * @return banner 详细信息
 8     */
 9    Optional<Banner> findBannerById(Long bannerId);
第一个查询方法单元测试验证
1@Test
2public void testFindBannerById() {
4  Optional<Banner> bannerInfo = bannerRepository.findBannerById(1L);
6  log.info("banner exists: {}, banner info: {}", bannerInfo.isPresent(),JSON.toJSONString(bannerInfo.get()));

日志输出结果执行成功,打印了执行的sql,输出了打印查询结果实体类信息。

 1Hibernate: 
 2    select
 3        banner0_.id as id1_0_,
 4        banner0_.create_time as create_t2_0_,
 5        banner0_.creator as creator3_0_,
 6        banner0_.deleted as deleted4_0_,
 7        banner0_.description as descript5_0_,
 8        banner0_.enable as enable6_0_,
 9        banner0_.modify_time as modify_t7_0_,
10        banner0_.name as name8_0_ 
11    from
12        banner banner0_ 
13    where
14        banner0_.id=?
152021-03-27 23:31:57.409  INFO 39303 --- [main] com.wzy.service.JpaBannerTest : banner exists: true, banner info: {"createTime":1616749802000,"creator":1,"deleted":0,"description":"首页顶部展示banner","enable":1,"id":1,"modifyTime":1616749805000,"name":"首页banner"}

简单查询方法,只要符合JPA规范就不需要写SQL语句,简单条件查询:查询方法 find | read | get |query | stream开头,它们都是同义词没有任何区别。

删除方法如delete | remove开头的方法也没有区别。

3.4 多条件查询

如果通过多个条件进行查询,如通过名称和描述进行查询,JPA也支持这种查询方式,我们只需要将条件用and进行连接,这里的条件的属性名称与个数要与参数的位置和个数一一对应。

1Banner findBannerByNameAndDescription(String name, String description);

单元测试验证:

1@Test
2public void testFindBannerByNameAndDescription() {
3        Banner bannerInfo = bannerRepository.findBannerByNameAndDescription("首页banner", "首页顶部展示banner");
5        log.info("banner info: {}", JSON.toJSONString(bannerInfo));

执行结果,测试通过,输出了banner信息

 1Hibernate: 
 2    select
 3        banner0_.id as id1_0_,
 4        banner0_.create_time as create_t2_0_,
 5        banner0_.creator as creator3_0_,
 6        banner0_.deleted as deleted4_0_,
 7        banner0_.description as descript5_0_,
 8        banner0_.enable as enable6_0_,
 9        banner0_.modify_time as modify_t7_0_,
10        banner0_.name as name8_0_ 
11    from
12        banner banner0_ 
13    where
14        banner0_.name=? 
15        and banner0_.description=?
162021-03-28 08:33:50.571  INFO 50973 --- [main] com.wzy.service.JpaBannerTest: banner info: {"createTime":1616749802000,"creator":1,"deleted":0,"description":"首页顶部展示banner","enable":1,"id":1,"modifyTime":1616749805000,"name":"首页banner"}
创建Goods相关实体类、Repository与之前的步骤相同

Goods实体类

 1@Entity
 2@Getter
 3@Setter
 4@NoArgsConstructor
 5@ToString
 6public class Goods {
 7    @Id
 8    @GeneratedValue(strategy = GenerationType.IDENTITY)
 9    private Long id;
10    private String name;
11    private String description;
12    private String detail;
13    private BigDecimal price;
14    private String imgUrl;
15    private Date createTime;
16    private Date modifyTime;
17    private Integer deleted;
18    private Integer enable;
19    private Long creator;

GoodsRepository

1public interface GoodsRepository extends JpaRepository<Goods, Long> {

JPA几乎实现了MySQL所有的查询关键字,第一个查询是通过Equals关键字来查询名称相同和价格相等的商品,第二个查询是通过GreaterThanEqual关键字来查询名称相同价格大于等于给定价格的商品。

 1/**
 2* 查询商品列表通过名称和价格
 4List<Goods> findByNameAndPriceEquals(String name, BigDecimal price);
 7/**
 8* 查询商品列表并且价格大于等于指定的价格
10List<Goods> findByNameAndPriceGreaterThanEqual(String name, BigDecimal price);
单元测试验证

测试第一个查询。

1@Test
2public void testFindByNameAndPriceEquals() {
3  log.info("Query price equals goods info: {}",
4           JSON.toJSONString(goodsRepository.findByNameAndPriceEquals("MacBook",BigDecimal.valueOf(10000))));

运行单元测试通过,打印JPA执行SQL和查询出的数据。

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    where
17        goods0_.name=? 
18        and goods0_.price=?
192021-03-28 14:10:13.582  INFO 53283 --- [main] com.wzy.service.JpaGoodsTest : Query price equals goods info: [{"createTime":1616893508000,"deleted":0,"description":"256G+8G","detail":"I5处理器","enable":1,"id":2,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}]

测试第二个查询

1@Test
2public void testFindByNameAndPriceGreaterThanEqual() {
3  log.info("Query price greater than or equal to goods info: {}",
4           JSON.toJSONString(goodsRepository.findByNameAndPriceGreaterThanEqual("MacBook", BigDecimal.valueOf(20000))));

执行单元测试,打印JPA执行SQL和查询出的数据。

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    where
17        goods0_.name=? 
18        and goods0_.price>=?
192021-03-28 14:20:38.799  INFO 53526 --- [main] com.wzy.service.JpaGoodsTest : Query price greater than or equal to goods info: [{"createTime":1616893463000,"deleted":0,"description":"512G+16G","detail":"I9处理器","enable":1,"id":1,"imgUrl":"a.jpg","modifyTime":1616893466000,"name":"MacBook","price":23000.0000}]

3.5 排序与分页

JPA还提供了对排序和分页的支持,只需要在查询方法的入参中加入Sort对象作为入参,就可以实现排序功能,实现分页功能需要将Pageable作为入参就可以实现分页功能。

JPA实现排序

我们只需要将Sort对象作为入参。

1List<Goods> findAll(Sort sort);

单元测试验证,可以使用Sort.by静态方法来快速构建Sort对象,并且我们可以传入多个Sort对象,如下的操作的排序规则是先按照价格降序排列,再按照id升序排列。

1@Test
2public void testSortFindAll() {
3  log.info("{}", JSON.toJSONString(goodsRepository.findAll(Sort.by(Sort.Order.desc("price"), (Sort.Order.asc("id"))))));

执行成功,日志输出如下,可以看到打印的结果,是按照价格降序排列,再按照id升序排列,如果价格相同则按照id升序排列

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    order by
17        goods0_.price desc,
18        goods0_.id asc
192021-03-29 13:58:25.441  INFO 73745 --- [           main] com.wzy.service.JpaGoodsTest             : [{"createTime":1616893463000,"deleted":0,"description":"512G+16G","detail":"I9处理器","enable":1,"id":1,"imgUrl":"a.jpg","modifyTime":1616893466000,"name":"MacBook","price":23000.0000},{"createTime":1616893508000,"deleted":0,"description":"256G+8G","detail":"I5处理器","enable":1,"id":2,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000},{"createTime":1616893508000,"deleted":0,"description":"128G+8G","detail":"I7处理器","enable":1,"id":3,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}]
JPA实现分页

在JPA中,为开发者提供了非常方便的分页功能,在查询方法的入参中加入分页参数Pageable,通过构建分页参数就可以轻松的完成分页查询,下面定义了返回的分页对象Page并且可以指定泛型Goods实体类。

1/**
2* JPA实现分页
4Page<Goods> findAll(Pageable pageable);

编写单元测试,如下:我们创建了一个Pageable对象,指定了第0页,每页有1条记录,指定了排序方式和之前一样也是Sort对象,按照价格升序排列,当然你可以指定更多的排序规则,将Pageable传递进方法中,会为我们返回一个Page对象,下方打印了Page对象的相关属性。

 1@Test
 2public void testPageableFindAll() {
 3    Pageable pageable = PageRequest.of(0, 1,
 4    Sort.by(Sort.Order.asc("price")));
 5    Page<Goods> goodsPage = goodsRepository.findAll(pageable);
 6    log.info("{}", JSON.toJSONString(goodsPage));
 8    log.info("总页数:{}", goodsPage.getTotalPages());
 9    log.info("记录总数:{}", goodsPage.getTotalElements());
10    log.info("当前页索引(第几页):{}", goodsPage.getNumber());
11    log.info("查询的结果信息:{}", goodsPage.getContent());
12    log.info("当前页上的元素数量:{}", goodsPage.getNumberOfElements());
13    log.info("请求页的Pageable:{}", goodsPage.getPageable());
14    log.info("页大小:{}", goodsPage.getSize());
15    log.info("页的排序参数:{}", goodsPage.getSort());
16    log.info("页面上是否有内容:{}", goodsPage.hasContent());
17    log.info("是否有下一页:{}", goodsPage.hasNext());
18    log.info("是否有上一页:{}", goodsPage.hasPrevious());
19    log.info("当前页是否是最后一个:{}", goodsPage.isLast());
20    log.info("下一页的 Pageable:{}", goodsPage.nextPageable());
21    log.info("下一页的 Pageable:{}", goodsPage.previousPageable());

单元测试执行成功,根据执行结果,我们可以看出,这个方法执行了两个SQL,一个是计算记录的总数,另一个是对数据进行排序和分页。

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    order by
17        goods0_.price asc limit ?
18Hibernate: 
19    select
20        count(goods0_.id) as col_0_0_ 
21    from
22        goods goods0_
232021-03-29 14:17:44.093  INFO 74234 --- [           main] com.wzy.service.JpaGoodsTest             : {"content":[{"createTime":1616893508000,"deleted":0,"description":"256G+8G","detail":"I5处理器","enable":1,"id":2,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}],"empty":false,"first":true,"last":false,"number":0,"numberOfElements":1,"pageable":{"offset":0,"pageNumber":0,"pageSize":1,"paged":true,"sort":{"empty":false,"sorted":true,"unsorted":false},"unpaged":false},"size":1,"sort":{"$ref":"$.pageable.sort"},"totalElements":3,"totalPages":3}
242021-03-29 14:17:44.093  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 总页数3
252021-03-29 14:17:44.093  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 记录总数3
262021-03-29 14:17:44.093  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 当前页索引第几页):0
272021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 查询的结果信息[Goods(id=2, name=MacBook, description=256G+8G, detail=I5处理器, price=10000.0000, imgUrl=b.jpg, createTime=2021-03-28 09:05:08.0, modifyTime=2021-03-28 09:05:12.0, deleted=0, enable=1, creator=null)]
282021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 当前页上的元素数量1
292021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 请求页的PageablePage request [number: 0, size 1, sort: price: ASC]
302021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 页大小1
312021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 页的排序参数price: ASC
322021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 页面上是否有内容true
332021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 是否有下一页true
342021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 是否有上一页false
352021-03-29 14:17:44.094  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 当前页是否是最后一个false
362021-03-29 14:17:44.095  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 下一页的 PageablePage request [number: 1, size 1, sort: price: ASC]
372021-03-29 14:17:44.095  INFO 74234 --- [main] com.wzy.service.JpaGoodsTest: 下一页的 PageableINSTANCE

四、JPA复杂查询

4.1 自定义JPQL

如果JPA规范定义的查询关键字不能满足需求的话,就可以使用@Query自定义查询的JPQL。

第一个自定义简单查询

查询最大id的商品信息,nativeQuery属性表示,是否使用原生SQL,目前使用的JPQL并不是原生的,该属性默认是false,默认可以不写就是使用JPQL查询,这里要注意的是查询使用的表名是实体类,而不是数据表。

1/**
2  * 查询id值最大的商品信息
3  */
4@Query(value = "SELECT g from Goods g WHERE id = (SELECT max(id) FROM Goods)", nativeQuery = false)
5Goods getMaxIdGoods();

单元测试验证

1@Test
2public void testGetMaxIdGoods() {
3  log.info("{}", JSON.toJSONString(goodsRepository.getMaxIdGoods()));

执行成功打印日志,执行结果正确

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    where
17        goods0_.id=(
18            select
19                max(goods1_.id) 
20            from
21                goods goods1_
22        )
232021-03-30 09:10:35.733  INFO 86726 --- [           main] com.wzy.service.JpaGoodsTest             : {"createTime":1616893508000,"deleted":0,"description":"128G+8G","detail":"I7处理器","enable":1,"id":3,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}

上面我们展示的查询方法是没有参数的,我们下面要演示如何在自定义的JPQL中传递参数。

接收参数的第一种方式

第一种传递参数的方式:通过索引进行传递,要注意需要从1开始,在JPQL中使用?索引位置来进行参数传递,如下查询是为了查找名称等于当前名称,且价格大于指定价格的商品列表。

1@Query("SELECT g FROM Goods g WHERE name=?1 and price >= ?2")
2List<Goods> findGoodsByFirstParam(String name, BigDecimal price);

单元测试验证

1@Test
2public void testFindGoodsByFirstParam() {
3  log.info("{}", JSON.toJSONString(goodsRepository.findGoodsByFirstParam("MacBook", BigDecimal.valueOf(10000))));

执行成功打印结果:

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    where
17        goods0_.name=? 
18        and goods0_.price>=?
192021-03-30 09:15:48.358  INFO 86846 --- [           main] com.wzy.service.JpaGoodsTest             : [{"createTime":1616893463000,"deleted":0,"description":"512G+16G","detail":"I9处理器","enable":1,"id":1,"imgUrl":"a.jpg","modifyTime":1616893466000,"name":"MacBook","price":23000.0000},{"createTime":1616893508000,"deleted":0,"description":"256G+8G","detail":"I5处理器","enable":1,"id":2,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000},{"createTime":1616893508000,"deleted":0,"description":"128G+8G","detail":"I7处理器","enable":1,"id":3,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}]
接收参数的第二种方式

第二种传递参数的方式:通过参数名来进行传递,通过@Param注解来指定参数名称,在JPQL中使用:参数名的方式。

1@Query("SELECT g FROM Goods g WHERE name= :name and price >= :price")
2List<Goods> findGoodsBySecondParam(@Param("name") String name, @Param("price") BigDecimal price);

单元测试验证

1@Test
2public void testFindGoodsBySecondParam() {
3    log.info("{}", JSON.toJSONString(goodsRepository.findGoodsBySecondParam("MacBook",  BigDecimal.valueOf(10000))));

执行成功打印结果:

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    where
17        goods0_.name=? 
18        and goods0_.price>=?
192021-03-28 22:39:37.085  INFO 64464 --- [           main] com.wzy.service.JpaGoodsTest             : [{"createTime":1616893463000,"deleted":0,"description":"512G+16G","detail":"I9处理器","enable":1,"id":1,"imgUrl":"a.jpg","modifyTime":1616893466000,"name":"MacBook","price":23000.0000},{"createTime":1616893508000,"deleted":0,"description":"256G+8G","detail":"I5处理器","enable":1,"id":2,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}]
只查询某几个字段

如果我们想仅仅查询出某几个字段,可以自定义一个仅包含指定字段的构造函数,如我们只想查询商品名称和价格,那么就可以定义只包含两个字段的构造函数。

1public Goods(String name, BigDecimal price) {
2        this.name = name;
3        this.price = price;

我们可以编写如下的JPQL查询,使用new的方式在JPQL查询中构造实体类,这是JPQL的特性。

1@Query("SELECT new Goods (g.name, g.price) FROM Goods g")
2List<Goods> getGoodsNameAndPriceInfo();

单元测试验证

1@Test
2public void testGetGoodsNameAndPriceInfo() {
3        log.info("{}", JSON.toJSONString(goodsRepository.getGoodsNameAndPriceInfo()));

执行成功,打印信息如下,可以看出仅仅打印出名称和价格两个属性。

1Hibernate: 
2    select
3        goods0_.name as col_0_0_,
4        goods0_.price as col_1_0_ 
5    from
6        goods goods0_
72021-03-28 22:53:02.951  INFO 64768 --- [           main] com.wzy.service.JpaGoodsTest             : [{"name":"MacBook","price":23000.0000},{"name":"MacBook","price":10000.0000}]
编写JPQL视图

在IDEA中,在你编写的JPQL上使用alt+Enter快捷键,选择这个Edit Spring Data QL Fragment。


在这个视图上编写JPQL将更加方便。

4.2 简化JPA更新操作

在JPA中,默认提供了一个save方法用来保存或更新数据,但是这个方法的缺点是会更新所有字段的值,我们在单元测试中进行验证。

首先在GoodsRepository中添加根据id查询商品方法

1Goods findGoodsById(Long goodsId);

编写单元测试,首先先将id为1的商品查询出来,并且将其价格更新为30000。

1@Test
2public void testSaveUpdateGoods() {
3    Goods updateGoods = goodsRepository.findGoodsById(1L);
4  updateGoods.setPrice(BigDecimal.valueOf(30000));
5  //包含了一次查询用来确定是插入还是更新
6  goodsRepository.save(updateGoods);

测试用例执行成功,修改数据生效。


我们可以看到控制台打印的SQL,第一条SQL是我们调用findGoodsById方法所产生的,第二条SQL是JPA为了判断当前调用save方法是用来更新还是插入产生的,第三条SQL语句是用来更新记录的,我们可以看到更新了所有的字段信息,这样性能显然是低下的。

 1Hibernate: 
 2    select
 3        goods0_.id as id1_2_,
 4        goods0_.create_time as create_t2_2_,
 5        goods0_.creator as creator3_2_,
 6        goods0_.deleted as deleted4_2_,
 7        goods0_.description as descript5_2_,
 8        goods0_.detail as detail6_2_,
 9        goods0_.enable as enable7_2_,
10        goods0_.img_url as img_url8_2_,
11        goods0_.modify_time as modify_t9_2_,
12        goods0_.name as name10_2_,
13        goods0_.price as price11_2_ 
14    from
15        goods goods0_ 
16    where
17        goods0_.id=?
18Hibernate: 
19    select
20        goods0_.id as id1_2_0_,
21        goods0_.create_time as create_t2_2_0_,
22        goods0_.creator as creator3_2_0_,
23        goods0_.deleted as deleted4_2_0_,
24        goods0_.description as descript5_2_0_,
25        goods0_.detail as detail6_2_0_,
26        goods0_.enable as enable7_2_0_,
27        goods0_.img_url as img_url8_2_0_,
28        goods0_.modify_time as modify_t9_2_0_,
29        goods0_.name as name10_2_0_,
30        goods0_.price as price11_2_0_ 
31    from
32        goods goods0_ 
33    where
34        goods0_.id=?
35Hibernate: 
36    update
37        goods 
38    set
39        create_time=?,
40        creator=?,
41        deleted=?,
42        description=?,
43        detail=?,
44        enable=?,
45        img_url=?,
46        modify_time=?,
47        name=?,
48        price=? 
49    where
50        id=?

我们可以通过JPQL的方式来自定义更新只更新我们想更新的字段即可,就能更加高效的执行更新操作,定义方法如下,首先使用@Query来编写更新的JPQL,使用@Modifying注解来标志是一个更新操作,JPA中的其他查询方法默认都是只读事务,我们需要使用@Transactional注解来标志当前方法不是只读事务,readOnly属性可以不写默认就是非只读事务。

1@Query("UPDATE Goods g SET g.price = :price WHERE id = :goodsId")
2@Modifying
3@Transactional(readOnly = false)
4int updateGoodsPrice(@Param("goodsId") Long goodsId,
5                        @Param("price") BigDecimal price);

我们编写测试用例来进行验证,将id为1的商品的价格更新回23000。

1@Test
2public void testUpdateGoodsPrice() {
3        goodsRepository.updateGoodsPrice(1L, BigDecimal.valueOf(23000));

单元测试执行成功,打印更新商品信息的SQL,可以看到非常简洁的SQL,并且仅仅更新了price字段。

1Hibernate: 
2    update
3        goods 
4    set
5        price=? 
6    where
7        id=?

数据库数据更新成功



4.3 JPA原生查询

之前我们使用的自定义查询都是使用JPQL来进行查询,下面将演示使用原生SQL来进行查询,如下查询所有的商品信息,前面已经介绍过,nativeQuery用来标志当前查询是一条原生SQL。

1@Query(value = "SELECT * FROM goods", nativeQuery = true)
2List<Goods> findAllNativeQuery();

那么如果我们想要拿到原生查询的部分结果,那么我们就需要使用Map来进行接收。

1@Query(value = "SELECT name, price FROM goods", nativeQuery = true)
2List<Map<String, Object>> getGoodsNameAndPriceNativeQuery();

单元测试验证

1@Test
2public void testFindAllNativeQuery() {
3        log.info("{}", JSON.toJSONString(goodsRepository.findAllNativeQuery()));

执行成功,输出SQL语句和查询结果

1Hibernate: 
2    SELECT
4    FROM
5        goods
62021-03-28 23:06:51.708  INFO 65074 --- [           main] com.wzy.service.JpaGoodsTest             : [{"createTime":1616893463000,"deleted":0,"description":"512G+16G","detail":"I9处理器","enable":1,"id":1,"imgUrl":"a.jpg","modifyTime":1616893466000,"name":"MacBook","price":23000.0000},{"createTime":1616893508000,"deleted":0,"description":"256G+8G","detail":"I5处理器","enable":1,"id":2,"imgUrl":"b.jpg","modifyTime":1616893512000,"name":"MacBook","price":10000.0000}]

编写单元测试

1@Test
2public void testGetGoodsNameAndPriceNativeQuery() {
3        log.info("{}", JSON.toJSONString(goodsRepository.getGoodsNameAndPriceNativeQuery()));

执行成功,输出SQL语句和查询结果

1Hibernate: 
2    SELECT
3        name,
4        price 
5    FROM
6        goods
72021-03-28 23:11:22.381  INFO 65183 --- [           main] com.wzy.service.JpaGoodsTest             : [{"name":"MacBook","price":23000.0000},{"price":10000.0000,"name":"MacBook"}]

4.4 一对多关系映射

前面我们介绍了,JPA提供的基本查询方法、JPQL自定义查询方法、原生SQL自定义查询方法,接下来我们要介绍如何进行一对多关系映射和多对多关系映射,要通过配置导航属性来实现,一个Banner对应多个BannerItem,属于一对多关系,一个Goods商品可以属于多个商品分类,一个商品分类下面会有多个商品,属于多对多关系。

在某些情况下,我们需要查询Banner对应的所有BannerItem信息,或者查询分类下的所有商品,一个商品属于哪些商品分类等需求,我们就需要来配置导航属性来实现这样的功能。

首先同样我们先定义其他未定义的实体类。

BannerItem实体类

 1@Entity
 2@Getter
 3@Setter
 4public class BannerItem {
 5    @Id
 6    @GeneratedValue(strategy = GenerationType.IDENTITY)
 7    private Long id;
 8    private Long bannerId;
 9    private String name;
10    private String description;
11    private String imgUrl;
12    private Integer type;
13    private String jumpUrl;
14    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
15    private Date createTime;
16    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
17    private Date modifyTime;
18    private Integer deleted;
19    private Integer enable;
20    private Long creator;

GoodsCategory实体类

 1@Entity
 2@Getter
 3@Setter
 4public class GoodsCategory {
 5    @Id
 6    @GeneratedValue(strategy = GenerationType.IDENTITY)
 7    private Long id;
 8    private Long parentId;
 9    private String name;
10    private Integer level;
11    private String description;
12    private String imgUrl;
13    private Date createTime;
14    private Date modifyTime;
15    private Integer deleted;
16    private Integer enable;
17    private Long creator;

根据上面的分析,我们在查询Banner的时候一般来说要查询出它对应的所有BannerItem,实现方式如下,在Banner实体类中配置@OneToMany注解来指定一对多关系,fetch用来指定当前查询是懒加载还是急加载,该属性默认是懒加载方式,懒加载在这里的含义是在查询是Banner的信息会被直接查询出来,而BannerItem的信息用得到的时候才会加载,这里设置为急加载,否则在单元测试中因懒加载原因,打印JSON字符串无法解析bannerItemList对象,@JoinColumn注解指定的是要关联的“多”的那张表的实体类属性,这里要注意是实体类属性而不是数据库字段。

1@OneToMany(fetch = FetchType.EAGER)
2@JoinColumn(name = "bannerId")
3private List<BannerItem> bannerItemList;

单元测试验证,直接查询出所有的banner信息。

1@Test
2public void testOneToManyQuery() {
3  List<Banner> allBannerInfo = bannerRepository.findAll();
5  log.info("{}", JSON.toJSONString(allBannerInfo));

单元测试执行成功,输出查询结果,可以看出所有的Banner对应的BannerItem都被成功查询出来。

 1Hibernate: 
 2    select
 3        banner0_.id as id1_0_,
 4        banner0_.create_time as create_t2_0_,
 5        banner0_.creator as creator3_0_,
 6        banner0_.deleted as deleted4_0_,
 7        banner0_.description as descript5_0_,
 8        banner0_.enable as enable6_0_,
 9        banner0_.modify_time as modify_t7_0_,
10        banner0_.name as name8_0_ 
11    from
12        banner banner0_
13Hibernate: 
14    select
15        banneritem0_.banner_id as banner_i2_1_0_,
16        banneritem0_.id as id1_1_0_,
17        banneritem0_.id as id1_1_1_,
18        banneritem0_.banner_id as banner_i2_1_1_,
19        banneritem0_.create_time as create_t3_1_1_,
20        banneritem0_.creator as creator4_1_1_,
21        banneritem0_.deleted as deleted5_1_1_,
22        banneritem0_.description as descript6_1_1_,
23        banneritem0_.enable as enable7_1_1_,
24        banneritem0_.img_url as img_url8_1_1_,
25        banneritem0_.jump_url as jump_url9_1_1_,
26        banneritem0_.modify_time as modify_10_1_1_,
27        banneritem0_.name as name11_1_1_,
28        banneritem0_.type as type12_1_1_ 
29    from
30        banner_item banneritem0_ 
31    where
32        banneritem0_.banner_id=?
332021-03-29 14:52:04.106  INFO 75010 --- [           main] com.wzy.service.JpaBannerTest            : [{"bannerItemList":[{"bannerId":1,"createTime":1616751597000,"creator":1,"deleted":0,"description":"这是一个banner","enable":1,"id":1,"imgUrl":"a.jpg","jumpUrl":"http://www.baidu.com","modifyTime":1616751601000,"name":"子banner1","type":1},{"bannerId":1,"createTime":1616751597000,"creator":1,"deleted":0,"description":"这是一个banner","enable":1,"id":2,"imgUrl":"b.jpg","jumpUrl":"http://www.baidu.com","modifyTime":1616751601000,"name":"子banner2","type":1},{"bannerId":1,"createTime":1616751597000,"creator":1,"deleted":0,"description":"这是一个banner","enable":1,"id":3,"imgUrl":"c.jpg","jumpUrl":"http://www.baidu.com","modifyTime":1616751601000,"name":"子banner3","type":1}],"createTime":1616749802000,"creator":1,"deleted":0,"description":"首页顶部展示banner","enable":1,"id":1,"modifyTime":1616749805000,"name":"首页banner"}]

4.5 多对多关系映射

下面我们来配置多对多的关系映射,我们需要在查询商品分类时同时查询出分类下的所有商品,我们在GoodsCategory实体类中进行多对多配置,使用@ManyToMany进行多对多配置,使用急加载的方式,使用@JoinTable注解来指定多对多的第三张表,name属性指定数据表的名称,joinColumn属性指定的是当前实体在第三张表中的关联字段名称,inverseJoinColumns属性指定的是要关联的另一张表的关联字段的名称。

1@ManyToMany(fetch = FetchType.EAGER)
2@JoinTable(name = "goods_category_relation", joinColumns = @JoinColumn(name = "category_id"),
3inverseJoinColumns = @JoinColumn(name = "goods_id"))
4private List<Goods> goodsList;

首先定义GoodsCategoryRepository

1public interface GoodsCategoryRepository extends JpaRepository<GoodsCategory, Long> {

在单元测试用引入GoodsCategoryRepository

1@Autowired
2private GoodsCategoryRepository goodsCategoryRepository;

编写单元测试进行验证:

1@Test
2public void testGetGoodsCategoryManyToMany() {
3    List<GoodsCategory> goodsCategoryList = goodsCategoryRepository.findAll();
5    log.info("{}", JSON.toJSONString(goodsCategoryList));

单元测试执行成功,并打印相关日志,可以看到商品分类下的商品也一起查询出来了。

 1Hibernate: 
 2    select
 3        goodscateg0_.id as id1_4_,
 4        goodscateg0_.create_time as create_t2_4_,
 5        goodscateg0_.creator as creator3_4_,
 6        goodscateg0_.deleted as deleted4_4_,
 7        goodscateg0_.description as descript5_4_,
 8        goodscateg0_.enable as enable6_4_,
 9        goodscateg0_.img_url as img_url7_4_,
10        goodscateg0_.level as level8_4_,
11        goodscateg0_.modify_time as modify_t9_4_,
12        goodscateg0_.name as name10_4_,
13        goodscateg0_.parent_id as parent_11_4_ 
14    from
15        goods_category goodscateg0_
16Hibernate: 
17    select
18        goodslist0_.category_id as category1_3_0_,
19        goodslist0_.goods_id as goods_id2_3_0_,
20        goods1_.id as id1_2_1_,
21        goods1_.create_time as create_t2_2_1_,
22        goods1_.creator as creator3_2_1_,
23        goods1_.deleted as deleted4_2_1_,
24        goods1_.description as descript5_2_1_,
25        goods1_.detail as detail6_2_1_,
26        goods1_.enable as enable7_2_1_,
27        goods1_.img_url as img_url8_2_1_,
28        goods1_.modify_time as modify_t9_2_1_,
29        goods1_.name as name10_2_1_,
30        goods1_.price as price11_2_1_ 
31    from
32        goods_category_relation goodslist0_ 
33    inner join