JPA一篇从入门到实战
Hibernate JPA 快速入门
1、ORM 的介绍
什么是 ORM?
ORM(Object-Relational Mapping) 表示对象关系映射。在面向对象的软件开发中,通过ORM,就可以把对象映射到关系型数据库中。只要有一套程序能够做到建立对象与数据库的关联,操作对象就可以直接操作数据库数据,就可以说这套程序实现了ORM对象关系映射。
简单的说:ORM就是建立实体类和数据库表之间的关系,从而达到操作实体类就相当于操作数据库表的目的(就是说ORM会把MySQL表映射成一个Java对象,使开发人员可以关注Java程序)。
为什么要使用 ORM?
当实现一个应用程序时(不使用O/R Mapping),我们可能会写特别多数据访问层的代码(各种各样的DAO类),从数据库增删改查等操作,而这些代码都是重复的。但使用ORM框架则会大大减少重复性代码。对象关系映射(Object Relational Mapping,简称ORM),主要实现程序对象到关系数据库数据的映射。
优点:
1:提高开发效率,降低开发成本 (减少了DAO类的操作)
2:使开发更加对象化 (直接在实体类domain中来映射关系)
3:可移植
4:可以很方便地引入数据缓存之类的附加功能
缺点:
1:自动化进行关系数据库的映射需要消耗系统性能。消耗的性能可以忽略不记
2:在处理多表联查、where条件复杂之类的查询时,ORM的语法会变得复杂。(这才是最致命的缺点)
常见ORM思想的框架:JDBC、Hibernate、MyBatis、TopLink、JPA(对ORM框架的再一次封装,这里是一套规范)
JDBC:其实JDBC是最原生的API,支持连接并操作各种关系型数据库,也就是说可以用JDBC完成ORM思想的程序编写,如一些有ORM思想的框架底层都调用JDBC,所有这个JDBC我对其理解为ORM思想
Hibernate:这个框架就不用多说了,完全使用ORM思想,不过现在直接使用hibernate的少了,大多都是在JPA的封装上调用此框架
MyBatis:用过Mybatis都知道,这个框架可以手动写SQL语句,也可以完成对象关系映射,其实严格上说Mybatis不完全是一个ORM框架,包括后面介绍JPA的供应商就不支持Mybatis
2、Hibernate JPA简介
认识 hibernate
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将 POJO与数据库表建立映射关系,是一个全自动的 orm 框架,hibernate 可以自动生成 SQL 语句,自动执行,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。
认识 JPA
JPA的全称:Java Persistence API, 即Java 持久化API,是SUN公司推出的一套基于ORM的规范,内部是由一系列的接口和抽象类构成。
JPA通过JDK 5.0注解描述 对象-关系表 的映射关系,并将运行期的实体对象持久化到数据库中。
优势
- 标准化
JPA 是 JCP 组织发布的 Java EE 标准之一,因此任何声称符合 JPA 标准的框架都遵循同样的架构,提供相同的访问API,这保证了基于JPA开发的企业应用能够经过少量的修改就能够在不同的JPA框架下运行。 - 容器级特性的支持
JPA框架中支持大数据集、事务、并发等容器级事务,这使得 JPA 超越了简单持久化框架的局限,在企业应用发挥更大的作用。 - 简单方便
JPA的主要目标之一就是提供更加简单的编程模型:在JPA框架下创建实体和创建Java 类一样简单,没有任何的约束和限制,只需要使用 javax.persistence.Entity 进行注释,JPA的框架和接口也都非常简单,没有太多特别的规则和设计模式的要求,开发者可以很容易的掌握。JPA基于非侵入式原则设计,因此可以很容易的和其它框架或者容器集成 - 查询能力
JPA的查询语言是面向对象而非面向数据库的,它以面向对象的自然语法构造查询语句,可以看成是Hibernate HQL的等价物。JPA定义了独特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一种扩展,它是针对实体的一种查询语言,操作对象是实体,而不是关系数据库的表,而且能够支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能够提供的高级查询特性,甚至还能够支持子查询。 - 高级特性
JPA 中能够支持面向对象的高级特性,如类之间的继承、多态和类之间的复杂关系,这样的支持能够让开发者最大限度的使用面向对象的模型设计企业应用,而不需要自行处理这些特性在关系数据库的持久化。
JPA 与 hibernate 关系
JPA规范本质上就是一种ORM规范,注意不是ORM框架,因为JPA并未提供ORM实现,它只是定义了一些规范,提供了一些编程的API接口,但具体实现则由服务厂商来提供实现
JPA示意图:
Java代码
|
⬇
JPA规范(SUM公司定义的,Java持久化规范,内部由一系列接口和抽象类组成)
|
⬇
Hibernate/TopLink...其他ORM框架(它们都是实现了JPA规范)
|
⬇
JDBC规范(SUM公司制定)
|
⬇
MySQL/Oracle驱动
|
⬇
MySQL/Oracle数据库
JPA 和 Hibernate 的关系就像 JDBC 和 JDBC 驱动的关系,JPA是规范,Hibernate除了作为ORM框架之外,它也是一种JPA实现。
JPA 能取代 Hibernate 吗?正如同问 JDBC规范可以驱动底层数据库吗?答案是否定的,如果使用JPA规范进行数据库操作,底层需要 hibernate 作为其实现类完成数据持久化工作。
3、搭建开发环境(IDEA)
直接使用IDEA创建JPA项目:https://blog.csdn.net/jellily12/article/details/89304345
咱们使用Maven方式创建即可
1、使用 IDEA 创建 maven 工程(可能会缺少src/test/resources目录,手动创建和设置即可)
├─JPA
│ JPA.iml
│ pom.xml
└─src
├─main
│ ├─java
│ └─resources
└─test
├─java
└─resources
2、导入pom坐标
<properties>
<!--锁定 jdk 版本为 1.8-->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- hibernate对jpa的支持包 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.1.Final</version>
</dependency>
<!-- Mysql驱动 -->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!-- junit4单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
</dependencies>
3、建立 /resources/META-INF/persistence.xml 配置文件
-
路径:配置到类路径(resources)下的 META-INF 的文件夹下,文件名:persistence.xml
-
备注:由于主要在 src/test/ 下做测试,所以建议在 src/test/resources/ 下也建立 META-INF/persistence.xml
-
IDEA有persistence模板:setting=》file and code Template=》JPA==》Deployment descriptors=》persistenceXX.xml
persistence.xml 内容:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<!-- name:持久化单元名称,transaction-type:持久化单元事务类型(JTA:分布式事务管理,RESOURCE_LOCAL:本地事务管理) -->
<persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
<!--jpa的实现方式,配置JPA服务提供商 -->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!-- 可配可不配,如果配置了顺序不能错,必须在provider之后-->
<!--<class>com.caochenlei.hibernate.jpa.Customer</class>-->
<!--可选配置:配置jpa实现方的配置信息-->
<properties>
<!-- 数据库信息配置:数据库驱动、数据库地址、数据库账户、数据库密码 -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
<property name="hibernate.connection.url" value="jdbc:mysql://127.0.0.1:3306/hibernate_jpa"/>
<property name="hibernate.connection.username" value="root"/>
<property name="hibernate.connection.password" value="password"/>
<!-- 配置JPA服务提供商可选参数 -->
<property name="hibernate.show_sql" value="true" /><!-- 自动显示sql -->
<property name="hibernate.format_sql" value="true"/><!-- 格式化sql -->
<!-- 自动创建数据库表:
none :不会创建表
create : 程序运行时创建数据库表(如果有表,先删除表再创建)
update :程序运行时创建表(如果有表,不会创建表)
create-drop : 每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
validate : 每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。
-->
<property name="hibernate.hbm2ddl.auto" value="update" />
</properties>
</persistence-unit>
</persistence>
4、编写实体类和数据库表的映射配置(目的是达到操作实体类,就相当于操作数据库表)
/**
* 客户的实体类
* 配置映射关系
* 1.实体类和表的映射关系
* @Entity:声明实体类
* @Table:配置实体类和表的映射关系
* name : 配置数据库表的名称
* 2.实体类中属性和表中字段的映射关系
*/
@Data
@Entity
@Table(name = "tb_customer")
public class Customer {
/**
* @Id:声明主键的配置
* @GeneratedValue:配置主键的生成策略
* strategy:
* GenerationType.IDENTITY :自增,mysql
* 底层数据库必须支持自动增长(底层数据库支持的自动增长方式,对id自增)
* GenerationType.SEQUENCE : 序列,oracle
* 底层数据库必须支持序列
* GenerationType.TABLE : jpa提供的一种机制,通过一张数据库表的形式帮助我们完成主键自增
* GenerationType.AUTO : 由程序自动的帮助我们选择主键生成策略
* @Column:配置属性和字段的映射关系
* name:数据库表中字段的名称
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "customer_id")
private Long Id; //客户的主键
@Column(name = "customer_name")
private String name;//客户名称
@Column(name="customer_age")
private int age;//客户年龄
@Column(name="customer_sex")
private boolean sex;//客户性别
@Column(name="customer_phone")
private String phone;//客户的联系方式
@Column(name="customer_address")
private String address;//客户地址
}
对应的数据库表信息:
Field | Type | Comment |
---|---|---|
customer_id | bigint not null auto_increment | 客户编号(主键) |
customer_name | varchar(255) | 客户名称(公司名称) |
customer_age | integer | 客户年龄 |
bicustomer_sex | bit | 客户性别 |
customer_phone | varchar(255) | 客户联系电话 |
customer_address | varchar(255) | 客户联系地址 |
6、测试保存操作的执行
/**
* 测试jpa的保存
* 案例:保存一个客户到数据库中
* Jpa的操作步骤
* 1.加载配置文件创建工厂(实体管理器工厂)对象
* 2.通过实体管理器工厂获取实体管理器
* 3.获取事务对象,开启事务
* 4.完成增删改查操作
* 5.提交事务(回滚事务)
* 6.释放资源
*/
@Test
public void testSave() {
//1.加载配置文件创建工厂(实体管理器工厂)对象
EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
//2.通过实体管理器工厂获取实体管理器
EntityManager em = factory.createEntityManager();
//3.获取事务对象,开启事务
EntityTransaction tx = em.getTransaction();
tx.begin();
//4.完成增删改查操作:保存一个客户到数据库中
Customer customer = new Customer();
customer.setName("Sam");
customer.setAddress("Beijing");
//保存操作
em.persist(customer);
//5.提交事务
tx.commit();
//6.释放资源
em.close();
factory.close();
}
7、查看日志
Hibernate:
create table tb_customer (
customer_id bigint not null auto_increment,
customer_address varchar(255),
customer_age integer,
customer_name varchar(255),
customer_phone varchar(255),
customer_sex bit,
primary key (customer_id)
) engine=InnoDB
Hibernate:
insert
into
tb_customer
(customer_address, customer_age, customer_name, customer_phone, customer_sex)
values
(?, ?, ?, ?, ?)
8、这里可以看到为我们自动生成了SQL语句,因为xml 里这里设置的是 update,所以有表的前提下,不会再生成表
<!-- 自动创建数据库表:
none :不会创建表
create : 程序运行时创建数据库表(如果有表,先删除表再创建)
update :程序运行时创建表(如果有表,不会创建表)
create-drop : 每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
validate : 每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。
-->
<property name="hibernate.hbm2ddl.auto" value="update" />
9、错误处理(暂无)
4、JPA 完整配置文件
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<!--
持久化单元:
name:持久化单元名称
transaction-type:持久化单元事务类型
- JTA:分布式事务管理
- RESOURCE_LOCAL:本地事务管理
-->
<persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
<!--jpa的实现方式,配置JPA服务提供商 -->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!-- 可配可不配,如果配置了顺序不能错,必须在provider之后-->
<!--<class>com.caochenlei.hibernate.jpa.Customer</class>-->
<!--
配置二级缓存时候使用的模式,可配置值有:
- ALL:所有的实体类都被缓存
- NONE:所有的实体类都不被缓存
- ENABLE_SELECTIVE:标识@Cacheable(true)注解的实体类将被缓存
- DISABLE_SELECTIVE;缓存除标识@Cacheable(false)以外的所有实体类
- UNSPECIFIED:默认值,JPA 产品默认值将被使用
-->
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<!--可选配置:配置jpa实现方的配置信息-->
<properties>
<!-- 数据库信息配置:数据库驱动、数据库地址、数据库账户、数据库密码 -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
<property name="hibernate.connection.url" value="jdbc:mysql://127.0.0.1:3306/hibernate_jpa"/>
<property name="hibernate.connection.username" value="root"/>
<property name="hibernate.connection.password" value="password"/>
<!-- 数据库信息配置:数据库驱动、数据库地址、数据库账户、数据库密码 -->
<!--<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/hibernate_jpa"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="password"/>-->
<!-- 配置JPA服务提供商可选参数 -->
<!-- 自动显示sql -->
<property name="hibernate.show_sql" value="true" />
<!-- 格式化sql -->
<property name="hibernate.format_sql" value="true"/>
<!-- 自动创建数据库表:
none :不会创建表
create : 程序运行时创建数据库表(如果有表,先删除表再创建)
update :程序运行时创建表(如果有表,不会创建表)
create-drop : 每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
validate : 每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。
-->
<property name="hibernate.hbm2ddl.auto" value="update" />
<!-- 建表使用MyISAM,默认是InnoDB,下列是MySQL5 和 MySQL8 两种方言引擎设置-->
<!-- InnoDB引擎:org.hibernate.dialect.MySQL5InnoDBDialect、org.hibernate.dialect.MySQL8InnoDBDialect -->
<!-- MyISAM引擎:org.hibernate.dialect.MySQL5Dialect、org.hibernate.dialect.MySQL5Dialect -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
<!-- Scan for annotated classes and Hibernate mapping XML files (可不配置)-->
<!--<property name="hibernate.archive.autodetection" value="class, hbm"/>-->
<!-- 二级缓存相关 -->
<!-- 开启二级缓存 -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<!-- 配置二级缓存处理类 -->
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
<!-- 开启查询缓存,entityManager.find查询可以不配置,如果使用JPQL或SQL查询需要开启该配置 -->
<property name="hibernate.cache.use_query_cache" value="true"/>
<!-- 指定缓存配置文件位置,如果默认在resources下可不配置 -->
<property name="hibernate.cache.provider_configuration" value="classpath:ehcache.xml"/>
<!-- *****************************连接池的配置***************************** -->
<!-- Hibernate JPA整合C3P0数据库连接池 -->
<property name="hibernate.connection.provider_class"
value="org.hibernate.connection.C3P0ConnectionProvider" />
<!-- 数据库连接池的最小连接数 -->
<property name="c3p0.min_size" value="5" />
<!-- 数据库连接池的最大连接数 -->
<property name="c3p0.max_size" value="30" />
<!-- 最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
<property name="c3p0.maxIdleTime" value="60" />
<!-- 获得连接的超时时间,如果超过这个时间,会抛出异常,单位毫秒 -->
<property name="c3p0.timeout" value="1800" />
<!-- 最大的PreparedStatement的数量 -->
<property name="c3p0.max_statements" value="50" />
<!-- 每隔120秒检查连接池里的空闲连接,单位是秒 -->
<property name="c3p0.idle_test_period" value="120" />
<!-- 当连接池里面的连接用完的时候,C3P0一下获取的新的连接数 -->
<property name="c3p0.acquire_increment" value="1" />
<!-- 是否每次都验证连接是否可用 -->
<property name="c3p0.validate" value="false" />
<!-- Druid连接池配置 -->
<property name="hibernate.connection.provider_class"
value="com.alibaba.druid.support.hibernate.DruidConnectionProvider" />
<property name="url"
value="jdbc:mysql://192.168.1.200:3306/SSH_Data?useSSL=false&allowPublicKeyRetrieval=true" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="driverClassName"
value="com.mysql.cj.jdbc.Driver" />
<property name="filters" value="stat" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
<property name="asyncInit" value="true" />
</properties>
</persistence-unit>
</persistence>
5、JPA 核心API对象
Persistence / EntityManagerFactory / EntityManager / EntityTransaction
Hibernate JPA
的操作步骤:
1. 加载配置文件创建实体管理器工厂
2. 根据实体管理器工厂,创建实体管理器
3. 创建事务对象,开启事务
4. 完成增删改查操作
5. 提交事务(回滚事务)
6. 释放资源
Persistence对象(创建实体管理器工厂)
/**
* Persisitence.createEntityMnagerFactory(持久化单元名称)
* 静态方法(根据持久化单元名称创建实体管理器工厂)
**/
// 1.加载配置文件创建工厂(实体管理器工厂)对象
EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
Persistence
对象主要作用是用于获取EntityManagerFactory
对象的 。通过调用该类的createEntityManagerFactory
静态方法,根据配置文件中持久化单元名称创建EntityManagerFactory
。
EntityManagerFactory(
EntityManagerFactory
接口主要用来创建EntityManager
实例)
/**
* entityManagerFactory.createEntityManager():获取EntityManager对象
* 内部维护的很多的内容:
* 1.内部维护了数据库信息
* 2.维护了缓存信息
* 3.维护了所有的实体管理器对象
* 4.在创建EntityManagerFactory的过程中会根据配置创建数据库表
* EntityManagerFactory的创建过程比较浪费资源
* 特点:线程安全的对象,多个线程访问同一个EntityManagerFactory不会有线程安全问题
* 如何解决EntityManagerFactory的创建过程浪费资源(耗时)的问题?
* 思路:创建一个公共的EntityManagerFactory的对象
* 静态代码块的形式创建EntityManagerFactory
**/
// 2.通过实体管理器工厂获取实体管理器
EntityManager em = factory.createEntityManager();
EntityManager
/**
* EntityManager:实体类管理器
* 获取事务对象: getTransaction()
* 保存数据: presist()
* 更新数据: merge()
* 删除数据: remove()
* 根据id查询: find()/getRefrence()
* 清空缓存: clear()
* 强制立即写入数据库: flush()
* 重新加载缓存的对象: refresh()
* ......(还有挺多方法可以自己慢慢琢磨)
**/
// 3.获取事务对象,然后可以开启事务、提交事务、回滚事务
EntityTransaction tx = em.getTransaction();
在 JPA 规范中, EntityManager
是完成持久化操作的核心对象。实体类作为普通 java对象,只有在调用 EntityManager
将其持久化后才会变成持久化对象。EntityManager
对象在一组实体类与底层数据源之间进行 O/R 映射的管理。它可以用来管理和更新 Entity Bean, 根椐主键查找 Entity Bean
, 还可以通过JPQL语句查询实体。我们可以通过调用EntityManager
的方法完成获取事务,以及持久化数据库的操作
EntityTransaction
/**
* EntityTransaction: 事务对象
* 开启事务: begin()
* 提交事务: commit()
* 回滚事务: rollback()
**/
// 开启事务
tx.begin();
在 JPA 规范中, EntityTransaction
是完成事务操作的核心对象,对于EntityTransaction
在我们的 java 代码中承接的功能比较简单
6、JPA 实现CRUD操作
保存操作
/**
* 运行之前,修改hibernate.hbm2ddl.auto=create
* 保存操作
*/
@Test
public void testSave() {
// 获取实体管理器工厂
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
// 获取实体管理器
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 获取事务
EntityTransaction transaction = entityManager.getTransaction();
// 开启事务
transaction.begin();
// 创建实体对象并保存
Customer customer1 = new Customer();
customer1.setName("张三");
customer1.setAge(20);
customer1.setSex(true);
customer1.setPhone("13018882888");
customer1.setAddress("北京");
entityManager.persist(customer1);
Customer customer2 = new Customer();
customer2.setName("李四");
customer2.setAge(18);
customer2.setSex(false);
customer2.setPhone("13533333555");
customer2.setAddress("广州");
entityManager.persist(customer2);
Customer customer3 = new Customer();
customer3.setName("王五");
customer3.setAge(28);
customer3.setSex(true);
customer3.setPhone("13012345678");
customer3.setAddress("深圳");
entityManager.persist(customer3);
// 提交事务
transaction.commit();
// 释放资源
entityManager.close();
entityManagerFactory.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: insert into c_customer (c_address, c_age, c_name, c_phone, c_sex) values (?, ?, ?, ?, ?)
Hibernate: insert into c_customer (c_address, c_age, c_name, c_phone, c_sex) values (?, ?, ?, ?, ?)
Hibernate: insert into c_customer (c_address, c_age, c_name, c_phone, c_sex) values (?, ?, ?, ?, ?)
根据 id 查询操作
find
方法实现(立即查询)
/**
* 运行之前,修改hibernate.hbm2ddl.auto=update
* 立即查询操作
*/
@Test
public void testQuery1() {
// 获取实体管理器工厂
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
// 获取实体管理器
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 获取事务
EntityTransaction transaction = entityManager.getTransaction();
// 开启事务
transaction.begin();
// 查询实体并输出
Customer customer = entityManager.find(Customer.class, 2L);
System.out.println(customer);
// 提交事务
transaction.commit();
// 释放资源
entityManager.close();
entityManagerFactory.close();
}
实际发送的SQL语句就是:select * from customer where id = 2
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_0_, customer0_.c_address as c_addres2_0_0_, customer0_.c_age as c_age3_0_0_, customer0_.c_name as c_name4_0_0_, customer0_.c_phone as c_phone5_0_0_, customer0_.c_sex as c_sex6_0_0_ from c_customer customer0_ where customer0_.id=?
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=广州)
getReference
方法实现(懒加载查询)(推荐)
/**
* 运行之前,修改hibernate.hbm2ddl.auto=update
* 延迟查询操作(查询结果对象的时候,才会发送查询的sql语句)
*/
@Test
public void testQuery2() {
// 获取实体管理器工厂
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
// 获取实体管理器
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 获取事务
EntityTransaction transaction = entityManager.getTransaction();
// 开启事务
transaction.begin();
// 查询实体并输出
Customer customer = entityManager.getReference(Customer.class, 2L);
System.out.println(customer);
// 提交事务
transaction.commit();
// 释放资源
entityManager.close();
entityManagerFactory.close();
}
日志发现与 find 查询没有区别:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_0_, customer0_.c_address as c_addres2_0_0_, customer0_.c_age as c_age3_0_0_, customer0_.c_name as c_name4_0_0_, customer0_.c_phone as c_phone5_0_0_, customer0_.c_sex as c_sex6_0_0_ from c_customer customer0_ where customer0_.id=?
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=广州)
可以注释打印对象在对比试试://System.out.println(customer)
;
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
可以发现,如果没有使用对象的话,是没有调用查询SQL语句的
find
与getReference
查询对比:
find
实现的查询时,是会在调用 find 方法时,立即发送SQL语句查询数据库的操作getReference
是一种延迟加载策略的操作,调用getReference方法不会立即发送sql语句查询数据库,当调用查询结果对象的时候,才会发送查询的sql语句(实际就是动态代理)
更新操作
/**
* 运行之前,修改hibernate.hbm2ddl.auto=update
* 更新操作
*/
@Test
public void testUpdate() {
// 获取实体管理器工厂
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
// 获取实体管理器
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 获取事务
EntityTransaction transaction = entityManager.getTransaction();
// 开启事务
transaction.begin();
// 查询实体并更新
Customer customer = entityManager.find(Customer.class, 2L);
customer.setAddress("上海");
entityManager.merge(customer);
// 提交事务
transaction.commit();
// 释放资源
entityManager.close();
entityManagerFactory.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_0_, customer0_.c_address as c_addres2_0_0_, customer0_.c_age as c_age3_0_0_, customer0_.c_name as c_name4_0_0_, customer0_.c_phone as c_phone5_0_0_, customer0_.c_sex as c_sex6_0_0_ from c_customer customer0_ where customer0_.id=?
Hibernate: update c_customer set c_address=?, c_age=?, c_name=?, c_phone=?, c_sex=? where id=?
删除操作
/**
* 运行之前,修改hibernate.hbm2ddl.auto=update
* 删除操作
*/
@Test
public void testDelete() {
// 获取实体管理器工厂
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("myJpa");
// 获取实体管理器
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 获取事务
EntityTransaction transaction = entityManager.getTransaction();
// 开启事务
transaction.begin();
// 查询实体并删除
Customer customer = entityManager.find(Customer.class, 1L);
entityManager.remove(customer);
// 提交事务
transaction.commit();
// 释放资源
entityManager.close();
entityManagerFactory.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_0_, customer0_.c_address as c_addres2_0_0_, customer0_.c_age as c_age3_0_0_, customer0_.c_name as c_name4_0_0_, customer0_.c_phone as c_phone5_0_0_, customer0_.c_sex as c_sex6_0_0_ from c_customer customer0_ where customer0_.id=?
Hibernate: delete from c_customer where id=?
7、JPA 连接工厂工具类
为什么要抽取 JpaUtil 工具类?
由于EntityManagerFactory
是一个线程安全的对象(即多个线程访问同一个EntityManagerFactory
对象不会有线程安全问题),并且EntityManagerFactory
的创建极其浪费资源,所以在使用JPA编程时,我们可以对EntityManagerFactory
的创建进行优化,只需要做到一个工程只存在一个EntityManagerFactory
即可。
解决思路是通过静态代码的形式创建 EntityManagerFactory
JpaUtil.java
工具类
/**
* 解决实体管理器工厂的浪费资源和耗时问题
* 通过静态代码块的形式,当程序第一次访问此工具类时,创建一个公共的实体管理器工厂对象
*
* 第一次访问getEntityManager方法:经过静态代码块创建一个factory对象,再调用方法创建一个EntityManager对象
* 第二次方法getEntityManager方法:直接通过一个已经创建好的factory对象,创建EntityManager对象
*/
public class JpaUtils {
private static EntityManagerFactory factory;
static {
//1.加载配置文件,创建entityManagerFactory
factory = Persistence.createEntityManagerFactory("myJpa");
}
/**
* 获取EntityManager对象
*/
public static EntityManager getEntityManager() {
return factory.createEntityManager();
}
}
- 测试工具类
public class JpaTest {
@Test
public void testSave() {
// 1.通过工具类获取实体类管理器
EntityManager em = JpaUtils.getEntityManager();
// 2.获取事务对象
EntityTransaction tx = em.getTransaction();
// 开启事务
tx.begin();
//3.完成增删改查操作:保存一个客户到数据库中
Customer customer = new Customer();
customer.setName("Yolo");
customer.setAddress("BeiJing");
//保存操作
em.persist(customer);
//4.提交事务
tx.commit();
//5.释放资源
em.close();
//因为工厂是公共的,不能关闭,不然其他线程将无法获取
//factory.close();
}
}
Hibernate JPA 复杂查询
1、JPQL 语句
JPQL全称:Java Persistence Query Language
在 EJB2.0
中引入的 EJB
查询语言 (EJB QL
),Java
持久化查询语言 (JPQL
) 是一种可移植的查询语言,旨在以面向对象表达式语言的表达式,将 SQL
语法和简单查询语义绑定在一起,使用这种语言编写的查询是可移植的,可以被编译成所有主流数据库服务器上的 SQL
。
在使用 JPQL
的时候整体的处理效果与SQL
的语法形式是几乎相同的,而后在JPA
执行的时候会将 JPQL
编译为传统的关系型数据库支持的SQL
语句。其特征与原生 SQL
语句类似,并且完全面向对象,通过类名和属性访问,而不是表名和表的属性。
1.1、简单查询
/**
* 查询全部
* jqpl:from Customer
* sql:select * from c_customer
*/
@Test
public void testFindAll() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 2.查询全部:创建Query查询对象,query对象才是执行jqpl的对象
Query query = entityManager.createQuery("from Customer");
// 3.发送查询,并封装结果集
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
1.2、别名查询
/**
* 别名查询
* jqpl:from Customer c
* jqpl: select c from Customer c
* sql:select * from c_customer c
*/
@Test
public void testFindAll2() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 查询所有客户,采用链式调用
List<Customer> list1 = entityManager.createQuery("from Customer c").getResultList();
list1.forEach(System.out::println);
// 查询所有客户,采用链式调用
List<Customer> list2 = entityManager.createQuery("select c from Customer c").getResultList();
list2.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
1.3、排序查询
/**
* 排序查询
* jqpl:from Customer order by Id desc
* sql:select * from c_customer order by Id desc
*/
@Test
public void testOder() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 采用链式调用,默认情况(升序)
List<Customer> list1 = entityManager.createQuery("from Customer order by Id").getResultList();
list1.forEach(System.out::println);
// 采用链式调用,升序情况
List<Customer> list2 = entityManager.createQuery("from Customer order by Id asc").getResultList();
list2.forEach(System.out::println);
// 采用链式调用,降序情况
List<Customer> list3 = entityManager.createQuery("from Customer order by Id desc").getResultList();
list3.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ order by customer0_.id
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ order by customer0_.id asc
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ order by customer0_.id desc
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
1.4、条件查询
/**
* 条件查询
* jqpl:from Customer where sex = ?1 and name like ?2
* sql:SELECT * FROM c_customer where c_sex = ?1 and c_name like ?2
* jqpl:from Customer where sex = :aaa and name like :bbb
* sql:SELECT * FROM c_customer where c_sex = :aaa and c_name like :bbb
*/
@Test
public void testWhere() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 条件查询:按参数位置绑定
Query query1 = entityManager.createQuery("from Customer where sex = ?1 and name like ?2");
query1.setParameter(1, true);
query1.setParameter(2, "张%");
List<Customer> list1 = query1.getResultList();
list1.forEach(System.out::println);
// 条件查询:按参数名称绑定
Query query2 = entityManager.createQuery("from Customer where sex = :aaa and name like :bbb");
query2.setParameter("aaa", true);
query2.setParameter("bbb", "张%");
List<Customer> list2 = query2.getResultList();
list2.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where customer0_.c_sex=? and (customer0_.c_name like ?)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where customer0_.c_sex=? and (customer0_.c_name like ?)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
1.5、投影查询
/**
* 投影查询
* 1.单个字段查询
* jqpl:select c.name from Customer c
* sql:SELECT * FROM c_customer order by Id desc
* 2.多个字段查询
* jpql:select c.name,c.age from Customer c
* jpql:select new Customer(c.name,c.age) from Customer c
* sql:SELECT * FROM c_customer order by Id desc
*/
@Test
public void testFieldName() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 查询所有客户名称:单个字段查询
Query query1 = entityManager.createQuery("select c.name from Customer c");
List<Object> list1 = query1.getResultList();
list1.forEach(System.out::println);
// 查询所有客户名称、客户年龄:多个字段查询,封装到数组中
Query query2 = entityManager.createQuery("select c.name,c.age from Customer c");
List<Object[]> list2 = query2.getResultList();
list2.forEach(x-> System.out.println(Arrays.toString(x)));
// 查询所有客户名称、客户来源:多个字段查询,封装到对象中
// 请在Customer.java添加以下两个构造方法,否则会执行失败
// public Customer() 和 public Customer(String name, String age)
Query query3 = entityManager.createQuery("select new Customer(c.name,c.age) from Customer c");
List<Customer> list3 = query3.getResultList();
list3.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.c_name as col_0_0_ from c_customer customer0_
李四
王五
张三
李四
王五
张三
李四
王五
张三
李四
王五
Hibernate: select customer0_.c_name as col_0_0_, customer0_.c_age as col_1_0_ from c_customer customer0_
[李四, 20]
[王五, 20]
[张三, 20]
[李四, 28]
[王五, 0]
[张三, 20]
[李四, 18]
[王五, 28]
[张三, 20]
[李四, 18]
[王五, 28]
Hibernate: select customer0_.c_name as col_0_0_, customer0_.c_age as col_1_0_ from c_customer customer0_
Customer(Id=null, name=李四, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=张三, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=李四, age=28, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=null, name=张三, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=李四, age=18, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=28, sex=false, phone=null, address=null)
Customer(Id=null, name=张三, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=李四, age=18, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=28, sex=false, phone=null, address=null)
1.6、分页查询
/**
* 分页查询
* jqpl : from Customer
* sql:select * from c_customer limit 2,5
*/
@Test
public void testLimit() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 分页查询
Query query = entityManager.createQuery("from Customer");
// 起始索引、每页查询的条数
query.setFirstResult(2).setMaxResults(5);
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ limit ?, ?
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
1.7、分组查询
/**
* 分组查询
* jqpl : select count(*) from Customer
* sql:select count(*) from c_customer
*
* jqpl : select sex,count(*) from Customer group by sex
* sql:select c_sex,count(*) from c_customer group by c_sex
*/
@Test
public void testGroupBy() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 聚合函数:count(),max(),min(),avg(),sum()
Object object2 = entityManager.createQuery("select count(*) from Customer").getSingleResult();
System.out.println(object2);
// 分组统计:
List<Object[]> list = entityManager.createQuery("select sex,count(*) from Customer group by sex").getResultList();
list.forEach(x->System.out.println(Arrays.toString(x)));
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select count(*) as col_0_0_ from c_customer customer0_
11
Hibernate: select customer0_.c_sex as col_0_0_, count(*) as col_1_0_ from c_customer customer0_ group by customer0_.c_sex
[false, 4]
[true, 7]
1.8、多表查询
@Test
public void testXXJoin() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 内连接
List<Object[]> list1 = entityManager.createQuery("from Customer c inner join c.linkMans").getResultList();
list1.forEach(x->System.out.println(Arrays.toString(x)));
// 迫切内连接(hibernate独有,将另一个对象的数据封装到该对象中)
List<Customer> list2 = entityManager.createQuery("select distinct c from Customer c inner join fetch c.linkMans").getResultList();
list2.forEach(System.out::println);
// 左外连接
List<Object[]> list3 = entityManager.createQuery("from Customer c left outer join c.linkMans").getResultList();
list3.forEach(x->System.out.println(Arrays.toString(x)));
// 迫切左外连接(hibernate独有,将另一个对象的数据封装到该对象中)
List<Customer> list4 = entityManager.createQuery("select distinct c from Customer c left outer join fetch c.linkMans").getResultList();
list4.forEach(System.out::println);
// 右外连接
List<Object[]> list5 = entityManager.createQuery("from Customer c right outer join c.linkMans").getResultList();
list5.forEach(x->System.out.println(Arrays.toString(x)));
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
2、原生SQL查询
有些时候,JPQL使用不当会导致转化成的sql并不如理想或者特定场合需要sql优化,还是得用到原生SQL查询的。EntityManager
对象的createNativeQuery
方法,可以实现十分复杂的查询,但是需要对查询结果进行实体映射处理,并且不能跨数据库。
-
获得原生SQL查询对象:
Query createNativeQuery(String var1, Class var2);
-
与
JPQL
对比只需要把查询对象方法由createQuery()
替换成createNativeQuery()
即可(传入值变成原生SQL和类对象)
public class CustomerJPASQLTest {
/**
* 查询全部 sql:select * from c_customer
*/
@Test
public void testFindAll() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 2.查询全部:创建Query查询对象,query对象才是执行jqpl的对象
Query query = entityManager.createNativeQuery("select * from c_customer",Customer.class);
// 3.发送查询,并封装结果集
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
/**
* 排序查询 sql:select * from c_customer order by Id desc
*/
@Test
public void testOder() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 采用链式调用,默认情况(升序)
List<Customer> list1 = entityManager.createNativeQuery("select * from c_customer order by Id",Customer.class).getResultList();
list1.forEach(System.out::println);
// 采用链式调用,升序情况
List<Customer> list2 = entityManager.createNativeQuery("select * from c_customer order by Id asc ",Customer.class).getResultList();
list2.forEach(System.out::println);
// 采用链式调用,降序情况
List<Customer> list3 = entityManager.createNativeQuery("select * from c_customer order by Id desc",Customer.class).getResultList();
list3.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
/**
* 条件查询
* sql:SELECT * FROM c_customer where c_sex = ?1 and c_name like ?2
* sql:SELECT * FROM c_customer where c_sex = :aaa and c_name like :bbb
*/
@Test
public void testWhere() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 条件查询:按参数位置绑定
Query query1 = entityManager.createNativeQuery("SELECT * FROM c_customer where c_sex = ?1 and c_name like ?2",Customer.class);
query1.setParameter(1, true);
query1.setParameter(2, "张%");
List<Customer> list1 = query1.getResultList();
list1.forEach(System.out::println);
// 条件查询:按参数名称绑定
Query query2 = entityManager.createNativeQuery("SELECT * FROM c_customer where c_sex = :aaa and c_name like :bbb",Customer.class);
query2.setParameter("aaa", true);
query2.setParameter("bbb", "张%");
List<Customer> list2 = query2.getResultList();
list2.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
/**
* 分页查询 sql:select * from c_customer limit 2,5
*/
@Test
public void testLimit() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 分页查询
Query query = entityManager.createNativeQuery("select * from c_customer limit 2,5",Customer.class);
// 起始索引、每页查询的条数
// query.setFirstResult(2).setMaxResults(5);
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
/**
* 分组查询
* sql:select count(*) from c_customer
* sql:select c_sex,count(*) from c_customer group by c_sex
*/
@Test
public void testGroupBy() {
// 1.获取entityManager对象 && 开启事务
EntityManager entityManager = JpaUtils.getEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 聚合函数:count(),max(),min(),avg(),sum()
Object object2 = entityManager.createNativeQuery("select count(*) from c_customer").getSingleResult();
System.out.println(object2);
// 分组统计:
List<Object[]> list = entityManager.createNativeQuery("select c_sex,count(*) from c_customer group by c_sex").getResultList();
list.forEach(x->System.out.println(Arrays.toString(x)));
// 4.提交事务 && 释放资源
transaction.commit();
entityManager.close();
}
}
查看日志(随意抽取一条):可以发现SQL语句与JPQL生成的还是有些不一样
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select * from c_customer
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
3、Criteria 查询
JPA Criteria API:https://www.cnblogs.com/xingqi/p/3929386.html
通过JPA
的Criteria API
实现:
- 1.EntityManager获取CriteriaBuilder
- 2.CriteriaBuilder创建CriteriaQuery
- 3.CriteriaQuery指定要查询的表,得到Root,Root代表要查询的表
- 4.CriteriaBuilder创建条件Predicate,Predicate相对于SQL的where条件,多个Predicate可以进行与、或操作。
- 5.通过EntityManager创建TypedQuery
- 6.TypedQuery执行查询,返回结果
基本对象的构建详细步骤:
- 1:通过
EntityManager.getCriteriaBuilder()
或EntityManagerFactory.getCriteriaBuilder()
方法可以得到CriteriaBuilder
对象 - 2:通过调用
CriteriaBuilder.createQuery()
或createTupleQuery()
方法可以获得CriteriaQuery
实例 - 3:通过调用
CriteriaQuery.from()
方法可以获得Root实例
过滤条件:
- 1:过滤条件会被应用到SQL语句的FROM子句中。在Criteria查询中,查询条件通过
Predicate
或Expression
实例应用到CriteriaQuery
对象上 - 2:这些条件使用
CriteriaQuery.where()
方法应用到CriteriaQuery
对象上 - 3:
CriteriaBuilder
也作为Predicate
实例的工厂,通过调用CriteriaBuilder的条件方法(equal,notEqual, gt, ge,lt, le,between,like等)创建Predicate
对象。 - 4:复合的
Predicate
语句可以使用CriteriaBuilder
的and, or, andnot
方法构建
3.1、查询全部
/**
* 查询全部(简单写法)
* sql:select * from c_customer
*/
@Test
public void testFindAll() {
// 获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
// 1.EntityManager获取CriteriaBuilder
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 2.CriteriaBuilder创建CriteriaQuery
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
// 3.CriteriaQuery指定要查询的表,得到Root,Root代表要查询的表
criteriaQuery.from(Customer.class);
TypedQuery<Customer> query = entityManager.createQuery(criteriaQuery);
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 释放资源
entityManager.close();
}
/**
* 查询全部(完整写法)
* sql:select * from c_customer
*/
@Test
public void testFindAll2() {
// 获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
// 1.EntityManager获取CriteriaBuilder
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 2.CriteriaBuilder创建CriteriaQuery
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
// 3.CriteriaQuery指定要查询的表,得到Root,Root代表要查询的表
Root<Customer> from = criteriaQuery.from(Customer.class);
// 这一步骤可以省略,返回的依旧是CriteriaQuery对象
CriteriaQuery<Customer> select = criteriaQuery.select(from);
// 如果上一步省略就传入criteriaQuery
TypedQuery<Customer> query = entityManager.createQuery(select);
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 释放资源
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=13532534566, address=成都)
Customer(Id=7, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=10, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=12, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
3.2、投影查询
获取 CriteriaQuery
实例有两种方式:
CriteriaBuilder.createQuery()
:返回CriteriaQuery<T>
CriteriaBuilder.createTupleQuery()
:返回CriteriaQuery<Tuple>
区别:
createQuery()
: 主要用来查询与实体类字段对应的SQLcreateTupleQuery()
:可以查询实体类字段以外的字段数据(如:count、sum、max、min、avg 等)group by用的较多,一般配合criteriaQuery.multiselect()
方法较多
/**
* 投影查询 — criteriaBuilder.createQuery(Customer.class)
* sql:select c_name, c_age from c_customer
* 注意:
* 实体类中必须要有该构造方法:public Customer(String name, int age) 不然会报错
* 也可以用另一个方式:
* criteriaBuilder.createTupleQuery() //创建查询,返回元组类型,Tuple包含多个TupleElements
*/
@Test
public void testFindAllMultiSelect() {
// 获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
// 1.CriteriaBuilder 安全查询创建工厂
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 2.CriteriaQuery 安全查询主语句
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
// 3.Root 定义查询的From子句中能出现的类型
Root<Customer> from = criteriaQuery.from(Customer.class);
// 必须要有该构造方法才行:public Customer(String name, int age),不然会报错
criteriaQuery.multiselect(from.get("name"), from.get("age"));
TypedQuery<Customer> query = entityManager.createQuery(criteriaQuery);
List<Customer> list = query.getResultList();
list.forEach(System.out::println);
// 释放资源
entityManager.close();
}
/**
* 投影查询 — criteriaBuilder.createTupleQuery()
* sql:select c_name, c_age from c_customer
* 注意:
* 使用:返回元组(Tuple)的查询,不会报错
*/
@Test
public void testFindAllMultiSelect2() {
// 获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
// 1.CriteriaBuilder 安全查询创建工厂
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 2.CriteriaQuery 安全查询主语句
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
// 3.Root 定义查询的From子句中能出现的类型
Root<Customer> from = criteriaQuery.from(Customer.class);
// 需要给字段取别名,否则无法通过tuple.get(field)获取数据
criteriaQuery.multiselect(from.get("name").alias("name"), from.get("age").alias("age"));
TypedQuery<Tuple> query = entityManager.createQuery(criteriaQuery);
List<Tuple> list = query.getResultList();
// name = list.get(0).get(0); age = list.get(0).get(1)
list.forEach(x-> System.out.println(x.get("name")+"、"+x.get("age")));
// 释放资源
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.c_name as col_0_0_, customer0_.c_age as col_1_0_ from c_customer customer0_
Customer(Id=null, name=李四, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=张三, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=李四, age=28, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=0, sex=false, phone=null, address=null)
Customer(Id=null, name=张三, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=李四, age=18, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=28, sex=false, phone=null, address=null)
Customer(Id=null, name=张三, age=20, sex=false, phone=null, address=null)
Customer(Id=null, name=李四, age=18, sex=false, phone=null, address=null)
Customer(Id=null, name=王五, age=28, sex=false, phone=null, address=null)
Hibernate: select customer0_.c_name as col_0_0_, customer0_.c_age as col_1_0_ from c_customer customer0_
李四、20
王五、20
张三、20
李四、28
王五、0
张三、20
李四、18
王五、28
张三、20
李四、18
王五、28
3.3、条件查询
主要示范方法:
单条件查询
/**
* 条件查询——单条件
* Predicate 过滤条件
* sql:select * from c_customer where c_name = '李四'
*/
@Test
public void testFindWhereEQ() {
// 获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
Root<Customer> from = criteriaQuery.from(Customer.class);
criteriaQuery.select(from);// 定义查询,该行可以省略
// 4.Predicate 或 Predicate[] 过滤条件
Predicate predicate = criteriaBuilder.equal(from.get("name"), "李四");
criteriaQuery.where(predicate);
// 采用链式调用
entityManager.createQuery(criteriaQuery).getResultList().forEach(System.out::println);
// 释放资源
entityManager.close();
}
查看日志:
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where customer0_.c_name=?
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
多条件查询
conjunction、disjunction 和 Predicate[] 对比:
criteriaBuilder.conjunction()
// 逻辑与(and)criteriaBuilder.disjunction()
// 逻辑或(or)Predicate[]
:// // 逻辑与(and)
conjunction 与 Predicate[] 区别:
Predicate[]
正常把数组内所有条件用and拼接,而conjunction
会在where 后面自动加上 1=1 然后再拼接条件
/**
* 条件查询——多条件(and,equals,lt)
* Predicate[] 多个过滤条件(个人推荐)
* sql:select * from c_customer where c_name = '李四' and c_age < 20
*/
@Test
public void testFindWhereEqGt() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
Root<Customer> from = criteriaQuery.from(Customer.class);
// 4.Predicate[] 过滤条件,设置一个查询条件集合
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.equal(from.get("name"), "李四"));
predicates.add(criteriaBuilder.lt(from.get("age"), 20));
criteriaQuery.where(predicates.toArray(new Predicate[]{}));// new Predicate[]{}=》new Predicate[predicates.size()]
// 采用链式调用
entityManager.createQuery(criteriaQuery).getResultList().forEach(System.out::println);
// 释放资源
entityManager.close();
}
/**
* 条件查询——多条件(like,and,equal,lt)
* Predicate 多个过滤条件 —— criteriaBuilder.conjunction() //逻辑与
* —— criteriaBuilder.disjunction() //逻辑或
* sql:select * from c_customer where 1=1 and c_name like '李%' and c_name = '李四' and c_age < 20
*/
@Test
public void testFindWhereEqGt2() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
Root<Customer> from = criteriaQuery.from(Customer.class);
// 4.Predicate[] 过滤条件,设置一个查询条件集合
Predicate predicate = criteriaBuilder.conjunction();
// 过滤条件— like:and c_name like 李%
predicate= criteriaBuilder.and(predicate, criteriaBuilder.like(from.get("name"), "李%"));
// 过滤条件— equal:and c_name like 李% and c_c_name = '李四'
predicate= criteriaBuilder.and(predicate, criteriaBuilder.equal(from.get("name"), "李四"));
// 过滤条件— lt:and c_name like 李% and c_c_name = '李四' and c_age < 20
predicate= criteriaBuilder.and(predicate, criteriaBuilder.lt(from.get("age"), 20));
criteriaQuery.where(predicate);
// 采用链式调用
entityManager.createQuery(criteriaQuery).getResultList().forEach(System.out::println);
// 释放资源
entityManager.close();
}
/**
* 条件查询——多条件(not in,between)
* Predicate 多个过滤条件 —— criteriaBuilder.conjunction() //逻辑与
* —— criteriaBuilder.disjunction() //逻辑或
* sql:select * from c_customer where 1=1 and c_age not in (18 , 20) and id between 5 and 9
*/
@Test
public void testFindWhereNotInBetween() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
Root<Customer> from = criteriaQuery.from(Customer.class);
// 4.Predicate 过滤多条件
Predicate predicate = criteriaBuilder.conjunction();
// 过滤条件— in:c_age not in (18 , 20)
predicate= criteriaBuilder.and(predicate, from.get("age").in(Arrays.asList(18, 20)).not());
// 过滤条件— in:c_age not in (18 , 20) and id between 5 and 9
predicate= criteriaBuilder.and(predicate, criteriaBuilder.between(from.get("Id"), 5,9));
criteriaQuery.where(predicate);
// 采用链式调用
entityManager.createQuery(criteriaQuery).getResultList().forEach(System.out::println);
// 释放资源
entityManager.close();
}
查看日志:(条件查询——多条件(and,equals,lt))
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where customer0_.c_name=? and customer0_.c_age<20
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
查看日志:(条件查询——多条件(like,and,equal,lt))
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where 1=1 and (customer0_.c_name like ?) and customer0_.c_name=? and customer0_.c_age<20
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=11, name=李四, age=18, sex=false, phone=13533333555, address=广州)
查看日志:(条件查询——多条件(not in,between))
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where 1=1 and (customer0_.c_age not in (18 , 20)) and (customer0_.id between 5 and 9)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=13532534566, address=成都)
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
3.4、排序查询
可以设置单个order和多个order
/**
* 排序查询
* sql:select * from c_customer where 1=1 and c_age in (18 , 28) and id between 2 and 4 order by id desc
*/
@Test
public void testFindWhereInBetweenOderBy() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
Root<Customer> from = criteriaQuery.from(Customer.class);
// 4.Predicate 或 Predicate[] 过滤条件
Predicate age = from.get("age").in(18, 28);
Predicate id = criteriaBuilder.between(from.get("Id"), 2, 10);
criteriaQuery.where(Arrays.asList(age, id).toArray(new Predicate[]{}));
// 排序(降序)
Order idOrder = criteriaBuilder.desc(from.get("Id"));
criteriaQuery.orderBy(idOrder);
// 可以设置多个 order。先按Id降序,然后按age升序排序
// criteriaQuery.orderBy(criteriaBuilder.desc(from.get("Id")),criteriaBuilder.asc(from.get("age")));
// 采用链式调用
entityManager.createQuery(criteriaQuery).getResultList().forEach(System.out::println);
// 释放资源
entityManager.close();
}
查看日志:
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where (customer0_.c_age in (18 , 28)) and (customer0_.id between 2 and 10) order by customer0_.id desc
Customer(Id=9, name=王五, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=8, name=李四, age=18, sex=false, phone=13533333555, address=广州)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
3.5、分页查询
/**
* 分页查询
* sql:select * from c_customer where 1=1 and id between 2 and 10 limit 0,5
*/
@Test
public void testFindWhereBetween() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
Root<Customer> from = criteriaQuery.from(Customer.class);
// 4.Predicate 或 Predicate[] 过滤条件
Predicate id = criteriaBuilder.between(from.get("Id"), 2, 10);
criteriaQuery.where(Arrays.asList(id).toArray(new Predicate[]{}));
// 采用链式调用,分页查询
entityManager.createQuery(criteriaQuery)
.setFirstResult(0) // 起始索引
.setMaxResults(5) // 每页查询的条数
.getResultList()
.forEach(System.out::println);
// 释放资源
entityManager.close();
}
查看日志:
Hibernate: select customer0_.id as id1_0_, customer0_.c_address as c_addres2_0_, customer0_.c_age as c_age3_0_, customer0_.c_name as c_name4_0_, customer0_.c_phone as c_phone5_0_, customer0_.c_sex as c_sex6_0_ from c_customer customer0_ where customer0_.id between 2 and 10 limit ?
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
Customer(Id=3, name=王五, age=20, sex=true, phone=13012345678, address=深圳)
Customer(Id=4, name=张三, age=20, sex=true, phone=13018882888, address=北京)
Customer(Id=5, name=李四, age=28, sex=true, phone=13012345678, address=深圳)
Customer(Id=6, name=王五, age=0, sex=false, phone=13532534566, address=成都)
3.6、分组查询
使用 group by
需要注意事项:
- 创建查询用元组类型:
criteriaBuilder.createTupleQuery()
- 设置多个选择结果:
criteriaQuery.multiselect()
- 给字段设置别名(这不是必须的,为了获取方便可以设置)
/**
* 分组查询
* sql:select c_name,count(c_name),max(c_age),min(c_age),sum(c_age),avg(c_age) from c_customer group by c_name
*/
@Test
public void testFindGroupBy() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
Root<Customer> from = criteriaQuery.from(Customer.class);
// .alias("name") 取别名
criteriaQuery.multiselect(
from.get("name").alias("name"),
criteriaBuilder.count(from.get("name")).alias("count"),
criteriaBuilder.max(from.get("age")).alias("max"),
criteriaBuilder.min(from.get("age")).alias("min"),
criteriaBuilder.sum(from.get("age")).alias("sum"),
criteriaBuilder.avg(from.get("age")).alias("avg"));
criteriaQuery.groupBy(from.get("name"));
// criteriaQuery.having(criteriaBuilder.disjunction());
// 采用链式调用
entityManager.createQuery(criteriaQuery).getResultList()
.forEach(x-> System.out.println(
x.get("name") + "、" +
x.get("count") + "、" +
x.get("max") + "、" +
x.get("min") + "、" +
x.get("sum") + "、" +
x.get("avg"))
);
// 释放资源
entityManager.close();
}
查看日志:
Hibernate: select customer0_.c_name as col_0_0_, count(customer0_.c_name) as col_1_0_, max(customer0_.c_age) as col_2_0_, min(customer0_.c_age) as col_3_0_, sum(customer0_.c_age) as col_4_0_, avg(customer0_.c_age) as col_5_0_ from c_customer customer0_ group by customer0_.c_name
张三、3、20、20、60、20.0
李四、4、28、18、84、21.0
王五、4、28、0、76、19.0
3.7、多表查询
建议先参考多对一教程的实体类测试,或者也可以直接使用下方给定的实体类
/**
* 多方/外键表
* 本实例采用的是:单向多对一。所以只需要配置多方即可
*/
@Data
@Entity
@Table(name = "t_many")
public class Many {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "many_id")
private Long manyId;
@Column(name = "many_name")
private String manyName;
@ManyToOne
@JoinColumn(name="one_id")
private One one;
}
/**
* 一方/主键表
* 本实例采用的是:单向多对一。所以只需要配置多方即可
*/
@Data
@Entity
@Table(name = "t_one")
public class One {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "one_id")
private Long oneId;
@Column(name = "one_type")
private String oneType;
}
测试代码:
/**
* 多表关联:单向多对一
* 数据插入
*/
@Test
public void testSaveJoinTable() {
// 1.获取entityManager对象
EntityManager entityManager = JpaUtils.getEntityManager();
// 开启事务
entityManager.getTransaction().begin();
// 给One插入一条数据(主键表)
One one = new One();
one.setOneType("初中");
entityManager.persist(one);
// 给Many插入三条数据(外键表)
Many manyA = new Many();
manyA.setManyName("初一");
manyA.setOne(one);
entityManager.persist(manyA);
Many manyB = new Many();
manyB.setManyName("初二");
manyB.setOne(one);
entityManager.persist(manyB);
Many manyC = new Many();
manyC.setManyName("初三");
manyC.setOne(one);
entityManager.persist(manyC);
// 提交事务
entityManager.getTransaction().commit();
}
/**
* Root代表需要查询的表(这里表示的是Many表)
* Join代表连接查询(表),通过root对象获取(Join代表的是关联表One表)
* 创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
* JoinType.INNER:内连接, JoinType.LEFT: 左外连接, JoinType.RIGHT:右外连接
* 使用多表关联后,Join 就相当有了 root 的功能,可以join.get("对象属性名称")
* 注意!注意!注意:
* Root:代表的是Many表,所以通过root.get()只能获取Many表的属性或字段.
* join:代表的是One表,虽然是关联查询,但它只代表关联的One表,只能获取One的属性
* 如果root/join获取自己表以外的属性或字段会报如下错:
* Unable to locate Attribute with the the given name [categoryType] on this ManagedType[XXX]
* 如果有第三张表关联使用join.join()往下传递即可。
*/
@Test
public void testFindJoinTable() {
// 1.获取entityManager对象 && 这里就省略事务了
EntityManager entityManager = JpaUtils.getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Many> criteriaQuery = criteriaBuilder.createQuery(Many.class);
// 重点:root只能代表Many表,join只能代表One表。
Root<Many> root = criteriaQuery.from(Many.class);
Join<Many, One> join = root.join("one", JoinType.LEFT);
Path<Object> type = join.get("oneType");
Path<Object> manyName = root.get("manyName");
Predicate p1 = criteriaBuilder.equal(type, "初中");
Predicate p2 = criteriaBuilder.equal(manyName, "初一");
Predicate predicate = criteriaBuilder.and(p1, p2);
criteriaQuery.where(predicate);
TypedQuery<Many> query = entityManager.createQuery(criteriaQuery);
List<Many> resultList = query.getResultList();
resultList.forEach(System.out::println);
}
查看日志:
Hibernate:
select
many0_.many_id as many_id1_3_,
many0_.many_name as many_nam2_3_,
many0_.one_id as one_id3_3_
from
t_many many0_
left outer join
t_one one1_
on many0_.one_id=one1_.one_id
where
one1_.one_type=?
and many0_.many_name=?
Hibernate:
select
one0_.one_id as one_id1_4_0_,
one0_.one_type as one_type2_4_0_
from
t_one one0_
where
one0_.one_id=?
Many(manyId=1, manyName=初一, one=One(oneId=1, oneType=初中))
Hibernate JPA 注解分析
1、常用注解
@Entity(必须)
应用于实体类,并且使用默认的ORM规则,即class名对应数据库表中表名,class字段名即表中的字段名。
如想改变这种默认的ORM规则,就要使用@Table来改变class名与数据库中表名的映射规则,@Column来改变class中字段名与db中表的字段名的映射规则.
元数据属性说明:
- name(可选):表名
@Table
Table用来定义entity主表的name,catalog,schema等属性。
元数据属性说明:
name
(可选):表名catalog
(可选):对应关系数据库中的catalogschema
(可选):对应关系数据库中的schemaUniqueConstraints
(可选):定义一个UniqueConstraint数组,指定需要建唯一约束的列
@Entity与@Table对比
比较@Entity(name = "实体名")与@Entity @Table(name = "表名")
我们在使用 JPA 的时候,需要做数据表和实体类的映射,@Entity注解的实体类,默认的实体名是非限定类名,对实体名首字母小写后得到表名,例如
@Entity
public class Comment
实体名:Comment,表名:comment,但实际项目中,有的表名以tb_开头,那就映射不到了,那么就需要把表名写出来,有两种方式可以写:
1、@Entity(name = "实体名"),一般把实体名写成跟表名相同,相当于写出了表名
2、@Entity + @Table(name = "表名")
那么哪种方式好呢?粗略一看,应该是上面一种好,但是应用到JPQL语句时,未必。
例如:
@Entity(name = "tb_comment")
public class Comment{}
那么
@Query("update tb_comment c set c.author=?1 where c.id=?2")
这是JPQL语句,一不小心就把tb_comment认为是表名,后面的c.xxx,就会去表中找字段,如果字段名跟实体的属性名不一样,就出错了。
如果写成下面这样的
@Entity
@Table(name = "tb_comment")
public class Comment{}
那么
@Query("update Comment c set c.author=?1 where c.id=?2")
可读性好,Comment一看就是实体类名,后面的c.xxx就会去类中找属性名,但实体名不能用JPQL的保留字。
结论:
- @Entity(name = "实体名")会降低JPQL语句的可读性,一般实体名不会跟JPQL保留字重复。
- @Entity + @Table(name = "表名") 可读性强,更容易看出来面向对象编程思想,但要避免实体名与JPQL保留字重复。
@Id(必须)
@Id定义了映射到数据库表的主键的属性,一个实体只能有一个属性被映射为主键.置于getXxxx()前.
@GerneratedValue
与@Id一同使用,定义主键的生成策略,通过strategy
属性指定。
元数据属性说明:
name
:GenerationType.AUTO
:JPA自动选择合适的策略,是默认选项GenerationType.IDENTITY
: 采用数据库ID自增长的方式生成主键值,一般用于MySQL数据库,Oracle不支持这种方式GenerationType.SEQUENCE
:通过序列产生主键,通过@SequenceGenerator注解指定序列名,MySql不支持这种方式GenerationType.TABLE
:通过表产生主键,会生成一张表模拟序列产生主键,该策略通用性强易于数据库的移植,效率低
@Column
应用于实体类属性,可以指定数据库表字段的名字和其他属性。
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Column {
String name() default "";
boolean unique() default false;
boolean nullable() default true;
boolean insertable() default true;
boolean updatable() default true;
String columnDefinition() default "";
String table() default "";
int length() default 255;
int precision() default 0;
int scale() default 0;
}
元数据属性说明:
- name:表示数据库表中该字段的名称,默认情形属性名称一致
- unique:表示该字段是否是唯一标识,默认为false
- nullable:表示该字段是否允许为null,默认为true
- insertable:表示在ORM框架执行插入操作时,该字段是否应出现INSETRT语句中,默认为true
- updateable:表示在ORM框架执行更新操作时,该字段是否应该出现在UPDATE语句中,默认为
true
。对于一经创建就不能更改的字段,该属性非常有用,比如email
属性。(insertable和updatable属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。) - columnDefinition:表示该字段在数据库中的实际类型。通常ORM框架可以根据属性类型自动判断数据库中字段的类型,但是依然有些例外:date类型无法确定数据库中字段类型究竟是DATE、TIME还是TIMESTAMP。此外,String 的默认映射类型为VARCHAR,如果希望将String类型映射到特定数据库的 **BLOB **或 TEXT 字段类型,则需要进行设置
- table:定义了包含当前字段的表名
- length:表示该字段的大小,仅对String类型的字段有效
- precision:表示精度,当字段类型为double时,
precision
表示数值的总长度 - scale:表示精度,当字段类型为double时,
scale
表示小数点所占的位数
@Basic
应用于实体类属性,表明该字段是要映射到数据库表,@Entity标注的实体类,所有属性默认为@Basic。
元数据属性说明:
-
fetch
:属性的读取策略,有EAGER
和LAZY
两种取值,分别表示主动抓取和延迟抓取,默认为EAGER
FetchType.LAZY
:延迟加载FetchType.EAGER
:立即加载
-
optional
:表示该属性是否可以为null
,默认值为true
。
@Basic(fetch =FetchType.LAZY)标注某属性时,表示只有调用Hibernate对象的该属性的get方法时,才会从数据库表中查找对应该属性的字段值
@Transient
作用在类属性上,与@Basic作用相反,表明该属性不需要持久化,JPA映射数据时忽略该属性;
@Temporal
作用在类属性上,用来设置Date类型的属性映射到数据库时的精度
当我们使用到java.util包中的时间日期类型,则需要此注释来说明转化成java.util包中的类型。
@Temporal(TemporalType.DATE)
:映射为日期 // date (只有日期)@Temporal(TemporalType.TIME)
:映射为日期 // time (是有时间)@Temporal(TemporalType.TIMESTAMP)
:映射为日期 // date time (日期+时间)
2、其他注解
@OrderBy:在加载数据的时候可以为其指定顺序。
@Enumerated:(很重要)
使用此注解映射枚举字段,以String类型存入数据库
注入数据库的类型有两种:EnumType.ORDINAL(Interger)、EnumType.STRING(String)
@Embedded、@Embeddable:
当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表
@Embeddable:注解在类上,表示此类是可以被其他类嵌套
@Embedded:注解在属性上,表示嵌套被@Embeddable注解的同类型类
@ElementCollection:集合映射
@CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy:(很重要)
表示字段为创建时间字段(insert自动设置)、创建用户字段(insert自动设置)、最后修改时间字段(update自定设置)、最后修改用户字段(update自定设置)
用法:
1、@EntityListeners(AuditingEntityListener.class):申明实体类并加注解
2、@EnableJpaAuditing:在启动类中加此注解
3、在实体类中属性中加上面四种注解
4、自定义添加用户
5.@MappedSuperclass:(很重要)
实现将实体类的多个属性分别封装到不同的非实体类中
注解的类将不是完整的实体类,不会映射到数据库表,但其属性将映射到子类的数据库字段
注解的类不能再标注@Entity或@Table注解,也无需实现序列化接口
注解的类继承另一个实体类 或 标注@MappedSuperclass类,他可使用@AttributeOverride 或 @AttributeOverrides注解重定义其父类属性映射到数据库表中字段。
.......还有很多注解,可以查看官网API,用到了再学
可以参考:
https://blog.csdn.net/yswKnight/article/details/79257372
https://blog.csdn.net/yiyelanxin/article/details/100107335
Hibernate JPA 主键策略
Hibernate JPA 生成主键主要通过:@Id 和 @GeneratedValue 实现,其生成规则由@GeneratedValue设定
@GeneratedValue的源代码:
@Target({METHOD,FIELD})
@Retention(RUNTIME)
public @interface GeneratedValue{
GenerationType strategy() default AUTO;
String generator() default "";
}
其中GenerationType枚举属性:
public enum GenerationType{
TABLE,
SEQUENCE,
IDENTITY,
AUTO
}
**Hibernate JPA的4种主键策略 分别为:AUTO、Identity、Sequence、Table **
剩下的都是hibernate
自己的策略,包括我们常用的 native、uuid7n、assigned、sequence
-
AUTO
:JPA自动选择合适的策略,是默认选项 -
IDENTITY
:采用数据库 ID自增长的方式来自增主键字段,Oracle 不支持这种方式 -
SEQUENCE
:通过序列产生主键,通过@SequenceGenerator
注解指定序列名,MySql 不支持这种方式 -
TABLE
:通过一张数据库表的形式帮助我们完成主键自增
1、AUTO 策略
默认的配置。如果不指定主键生成策略,默认为AUTO。
设置自动主键策略,在save对象的时候可以自己设置主键Id值,也可以不填写.
示例(任选其一都可以):
// 方式一:如果AUTO可以不用配置@GeneratedValue,默认就是AUTO设置
@Id
private Long Id;
// 方式二:默认配置@GeneratedValue,strategy属性default AUTO.
@Id
@GeneratedValue
private Long Id;
// 方式三:完整配置
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long Id;
注意:
1、如果是mysql数据库,一定要将主键列设置成自增长的,否则使用AUTO策略的时候,会报错:
org.hibernate.exception.GenericJDBCException: Field 'id' doesn't have a default value
2、如果是oracle数据库,那么会使用hibernate_sequence
,这个名称是固定的,不能更改。
2、Identity 策略
主键则由数据库自动维护,底层数据库必须支持自动增长(对id自增,MySQL支持,Oracle不支持)
示例:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
备注:设置了主键自增的话建议在保存对象是就不要设置主键Id值了,会报错。如果先手动设置值可以先注释@GeneratedValue
3、Sequence 策略
1、@SequenceGenerator 注解的使用
该策略一般会 @GeneratedValue
与@SequenceGenerator
注解同时使用.
GenerationType.SEQUENCE
:在某些数据库中不支持主键自增长。如Oracle
, 其提供了一种叫做序列(sequence)的机制生成主键。该策略不足之处正好与TABLE
相反,由于只有部分数据库(Oracle、PostgreSQL、DB2)支持序列对象。所以该策略一般不应用其他数据库。该策略一般与@SequenceGenerator注解一起使用,该注解指定了生成主键的序列,然后JPA
会根据注解内容创建一个序列(或使用一个现有序列)如果不指定序列,则使用厂商提供的默认序列生成器:Hibernate
默认提供序列名称为HIBERNATE_SEQUENCE
。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_sequence")
@SequenceGenerator(name="id_sequence", initialValue=8, allocationSize=1, sequenceName="ID_SEQUENCE")
private int id;
@SequenceGenerator
注解的定义(用来指定序列的相关信息)
@Repeatable(SequenceGenerators.class)
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface SequenceGenerator {
String name();
String sequenceName() default "";
String catalog() default "";
String schema() default "";
int initialValue() default 1;
int allocationSize() default 50;
}
- name:序列生成器的名称,会在
@GeneratedValue
中进行引用 - sequenceName:表示生成策略用到的数据库序列名称
- initialValue:主键的初始值,默认为0
- allocationSize:主键每次增长值的大小,默认为50
属性 | 说明 |
---|---|
name | 该属性是必须设置的属性,序列生成器的名称,会在@GeneratedValue 中进行引用 |
sequenceName | 实体标识所使用的数据库序列号的名称。该属性是可选的,如果我们没有为该属性设置值,OpenJPA 框架将自动创建名为 OPENJPA_SEQUENCE的序列号。如果一个 OpenJPA 容器中管理的多个实体都选择使用序列号机制生成实体标识,而且实体类中都没有指定标识字段的sequenceName属性,那么这些实体将会共享系统提供的默认名为 OPENJPA_SEQUENCE的序列号。这可能引起实体类编号的不连续。我们可以用下面的这个简单例子说明这种情况:假设 OpenJPA 容器中存在两个实体类 Dog 和 Fish,它们的实体标识字段都是数值型,并且都选择使用序列号生成实体标识,但是实体类中并没有提供sequenceName属性值。当我们首先持久化一个 Dog 对象时,它的实体标识将会是 1,紧接着我们持久化一个 Fish 对象,它的实体标识就是 2,依次类推。 |
initialValue | 该属性设置所使用序列号的起始值。默认为0 |
allocationSize | 一些数据库的序列化机制允许预先分配序列号,比如 Oracle,这种预先分配机制可以一次性生成多个序列号,然后放在 cache 中,数据库用户获取的序列号是从序列号 cache 中获取的,这样就避免了在每一次数据库用户获取序列号的时候都要重新生成序列号。allocationSize属性设置的就是一次预先分配序列号的数目,默认情况下allocationSize属性的值是 50。 |
注意:如果底层数据库不执行序列,会报错:org.hibernate.MappingException: org.hibernate.dialect.MySQLDialect does not support sequences
4、Table 策略
主要使用 @TableGenerator注解
GenerationType.TABLE
:使用一张特殊的数据库表,保存插入记录的时,需要的主键值。
有时候为了不依赖于数据库的具体实现,在不同数据库之间更好的移植,可以在数据库中新建序列表来生成主键,序列表一般包含两个字段:第一个字段引用不同的关系表(表名),第二个字段是该关系表的最大序号。这样,只需要一张序列就可以用于多张表的主键生成。
TableGenerator
注解的定义
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface TableGenerator {
String name();
String table() default "";
String catalog() default "";
String schema() default "";
String pkColumnName() default "";
String valueColumnName() default "";
String pkColumnValue() default "";
int initialValue() default 0;
int allocationSize() default 50;
UniqueConstraint[] uniqueConstraints() default {};
}
其中属性说明:
属性名 | 解释 |
---|---|
name | 对应GeneratedValue的generator属性值。通过俩者将其互相关联 |
allocationSize | 分配大小,指主键增长步长。这里指定为1,则意思是主键每次增长为1。 |
table | 对应第三方主键生成表名[代表JPA将使用哪个第三方表来主键值得计算] |
pkColumnName | 指定第三方表中对应的某个列名 |
pkColumnValue | 指定第三方表中对应的某个列的值,某个列指 pkColumnName属性中的指定的列名 |
valueColumnName | 指定生成的列名[对应第三方表的另外一个列值] |
1.先创建保存主键的数据表,并插入初始数据
-- 创建表
create table tb_generator (
id BIGINT NOT NULL,
pk_name varchar(50) not null,
pk_value int(50) not null
) engine = innodb;
-- 插入一条信息,也可以不插入,jpa会自动创建
INSERT INTO tb_generator (id, pk_name, pk_value) VALUES (1, 'User_ID', 100);
-- tb_generator表初始数据
mysql> select * from tb_generator;
+----+---------+----------+
| id | pk_name | pk_value |
+----+---------+----------+
| 1 | User_ID | 102 |
+----+---------+----------+
1 row in set (0.04 sec)
注意:这个表可以给无数的表作为主键表,现在只插入了一数据,这一条数据只是为一个表做主键而已。需要为其他表作为主键表著需要插入一行数据即可。需要保证table、pkColumnName、valueColumnName三个属性值相同就可以了
2.使用GenerationType.TABLE
主键策略:
@Data
@Entity
public class User {
@Id
@TableGenerator(
name = "id_generator", // 定义一个主键生成器的名称,GeneratedValue会引用
table = "tb_generator", // 表示表生成策略所持久化的表名
pkColumnName = "pk_name", // 在持久化表中该主键生成策略所对应键值的名称(列)
pkColumnValue = "pk_value", // 表示在持久化表中该生成策略所对应的主键(列)
valueColumnName = "User_ID", // 主键操作的内容字段(pkColumnValue列的值)
allocationSize = 100) // 每次增长的步长
@GeneratedValue(strategy = GenerationType.TABLE, generator = "id_generator")
private Long id;
private String username;
private String password;
}
3.测试代码:
@Test
public void testId(){
//1.加载配置文件创建工厂(实体管理器工厂)对象
EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
//2.通过实体管理器工厂获取实体管理器
EntityManager entityManager = factory.createEntityManager();
//3.获取事务对象,开启事务
EntityTransaction tx = entityManager.getTransaction(); //获取事务对象
tx.begin();
// 保存数据,按照TABLE主键策略生成主键
entityManager.persist(new User());
entityManager.persist(new User());
//5.提交事务、6.释放资源
tx.commit();
entityManager.close();
factory.close();
}
4.查看日志:
Hibernate: select tbl.pk_value from tb_generator tbl where tbl.pk_name=? for update
Hibernate: update tb_generator set pk_value=? where pk_value=? and pk_name=?
Hibernate: select tbl.pk_value from tb_generator tbl where tbl.pk_name=? for update
Hibernate: update tb_generator set pk_value=? where pk_value=? and pk_name=?
Hibernate: insert into User (password, username, id) values (?, ?, ?)
Hibernate: insert into User (password, username, id) values (?, ?, ?)
5.查看数据库表信息:
mysql> use hibernate_jpa;
Database changed
mysql> select * from tb_generator;
+----+---------+----------+
| id | pk_name | pk_value |
+----+---------+----------+
| 1 | User_ID | 102 |
+----+---------+----------+
1 row in set (0.04 sec)
mysql> select * from user;
+-----+----------+----------+
| id | password | username |
+-----+----------+----------+
| 101 | NULL | NULL |
| 102 | NULL | NULL |
+-----+----------+----------+
2 rows in set (0.04 sec)
5、Hibernate 自定义主键
@GenericGenerator 注解的使用(hibernate自定义主键生成器,可以直接引用内置主键策略)
该策略一般会 @GeneratedValue
与@GenericGenerator
注解同时使用.
Hibernate
同时也对JPA进行了扩展,可以在GeneratedValue
中指定generator
,然后用@GenericGenerator
指定策略来维护主键.
简单示例:
@Id
@GeneratedValue(generator = "myGenerator")
@GenericGenerator(name = "myGenerator", strategy = "uuid")
private Long Id;
@GenericGenerator
注解的定义(用来指定序列的相关信息)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(GenericGenerators.class)
public @interface GenericGenerator {
String name();// @GeneratedValue中的generator值
String strategy();// 生成器名称
Parameter[] parameters() default {};// 生成器的参数
}
- name:指定生成器名称。会在
@GeneratedValue
中进行引用 - strategy:指定具体生成器的类名。
- parameters:得到
strategy
指定的具体生成器所用到的参数(可选参数)
通过查看hibernate的源码查看内置主键策略,可以发现strategy
有14个内置主键策略选项:
public DefaultIdentifierGeneratorFactory() {
this.register("uuid2", UUIDGenerator.class);
this.register("guid", GUIDGenerator.class);
this.register("uuid", UUIDHexGenerator.class);
this.register("uuid.hex", UUIDHexGenerator.class);
this.register("assigned", Assigned.class);
this.register("identity", IdentityGenerator.class);
this.register("select", SelectGenerator.class);
this.register("sequence", SequenceStyleGenerator.class);
this.register("seqhilo", SequenceHiLoGenerator.class);
this.register("increment", IncrementGenerator.class);
this.register("foreign", ForeignGenerator.class);
this.register("sequence-identity", SequenceIdentityGenerator.class);
this.register("enhanced-sequence", SequenceStyleGenerator.class);
this.register("enhanced-table", TableGenerator.class);
}
列出几个Hibernate
比较常用的生成策略:
- native:对于
Oracle
采用Sequence
方式,对于MySQL
和SQL Server
采用identity
(自增主键生成机制),native
就是将主键的生成工作交由数据库完成,hibernate
不管 - uuid:采用128位的uuid算法生成主键,uuid被编码为一个32位16进制数字的字符串。占用空间大(字符串类型)。
- assigned:在插入数据的时候主键由程序处理(即程序员手动指定)这是元素没有指定时的默认生成策略。等同
AUTO
- identity:使用
SQL Server
和MySQL
自增字段,Oracle
不支持主键自增,要设定sequence
(MySQL 和 SQL Server 中很常用)。 等同于JPA
的INDENTITY
。 - increment:插入数据的时候
hibernate
会给主键添加一个自增的主键,但是一个hibernate
实例就维护一个计数器,所以在多个实例运行的时候不能使用这个方法。
个人用uuid
策略较多,用在Oracle
数据库的主键生成上,它会根据内部程序计算出32位长度的唯一id.
@Id
@GeneratedValue(generator = "systemUUID")// 使用了generator默认可以不指定strategy
@GenericGenerator(name = "systemUUID", strategy = "uuid")
private Long Id;
// 如果不用此注解生成,也可以用代码手动生成UUID
customer.setId(UUID.randomUUID().toString());
hibernate—14个内置主键策略详解:
内置主键策略
- uuid2
IdentifierGenerator 的实现类是 UUIDGenerator,具体由 UUIDGenerationStrategy 策略负责生成,它有两个实现 StandardRandomStrategy 和 CustomVersionOneStrategy,他们都是使用j ava.util.UUID 的 api 生成主键的。
StandardRandomStrategy 最终由 UUID.randomUUID(); 生成;
CustomVersionOneStrategy 则采用版本号与位运算通过构造函数 new UUID(mostSignificantBits,leastSignificantBits); 生成。
特点是:不需要和数据库交互,可根据RFC4122定义的5中变量控制具体的生成策略(因为符合RFC4122定义,所以避免了警告信息)
- guid
IdentifierGenerator 的实现类是 GUIDGenerator,通过 session.getFactory().getDialect().getSelectGUIDString(); 获得各个数据库中的标示字符串.
MySQL 用 select uuid();
Oracle 用 return "select rawtohex(sys_guid()) from dual";
特点是:需要和数据库进行一次查询才能生成。数据库全局唯一。
- uuid,uuid.hex
uuid和uuid.hex 两个一个东西。IdentifierGenerator的实现类是UUIDHexGenerator,通过StringBuffer(36).append(format(getIP())).append(sep).append(format(getJVM())).append(sep).append(format(getHiTime())).append(sep).append(format(getLoTime())).append(sep).append(format(getCount()))生成。
特点:不需要和数据库交互,全网唯一。
- hilo
IdentifierGenerator 的实现类 TableHiLoGenerator,逻辑较为复杂,通过高低位酸腐生成,但是需要给定表和列作为高值的源。加上本地的地位计算所得。
特点:需要和数据库交互,全数据库唯一,与guid不同的是,在标识符的单个源必须被多个插入访问时可以避免拥堵。
- assigned
IdentifierGenerator 的实现类 Assigned,没有生成逻辑,如果为空就抛出异常。
特点:不需要和数据库交互,自己管理主键生成,显示的指定id。
- identity
IdentityGenerator 并没有直接实现 IdentifierGenerator,而是扩展了AbstractPostInsertGenerator,并实现PostInsertIdentifierGenerator。
而 PostInsertIdentifierGenerator 实现了 IdentifierGenerator,通过IdentifierGeneratorHelper类生成。
这个比较特殊,它返回是个常量 "POST_INSERT_INDICATOR",指在数据库插入后时生成,然后返回数据库生成的id;
还有个常量 "SHORT_CIRCUIT_INDICATOR",是用外键ForeignGenerator时使用的。
特点:需要和数据库交互,数据插入后返回(反查)id,同一列唯一。
- select
SelectGenerator 扩展了 AbstractPostInsertGenerator 实现了 Configurable 接口,而 AbstractPostInsertGenerator 实现了 PostInsertIdentifierGenerator。所以具有和identity类似的行为,有数据库触发器生成。
特点:需要和数据库交互。
- sequence
SequenceGenerator 实现了 PersistentIdentifierGenerator 接口,和 Configurable 接口。
PersistentIdentifierGenerator 接口扩展 IdentifierGenerator 接口,通过不同的数据库,获取不同的取值语句 dialect.getSequenceNextValString(sequenceName); 然后进行查询,缓存到IntegralDataTypeHolder中,通过 generateHolder( session ).makeValue(); 获得。
特点:需要和数据库交互(但不是每次都是)。sequence唯一。
- seqhilo
seqhilo,扩展了 SequenceGenerator, 处理逻辑和 hilo 相同,值不过是使用一个具名的数据库序列来生成高值部分。
特点:需要和数据库交互,全数据库唯一,与guid不同的是,在标识符的单个源必须被多个插入访问时可以避免拥堵。
- increment
IdentifierGenerator 的实现类 IncrementGenerator,并实现了 Configurable 接口。数据库启动时查询表的最大主键列支,并通过 IntegralDataTypeHolder 缓存。插入一条,它自加一。
特点:仅需要首次访问数据库。
- foreign
IdentifierGenerator 的实现类 ForeignGenerator,通过给定的 entityName 和 propertyName 查询获得值。
特点:需要和数据库访问。
6、主键对数据库的支持
数据库类型 | 支持的id策略 |
---|---|
MySQL | GenerationType.TABLE GenerationType.AUTO GenerationType.IDENTITY 不支持GenerationType.SEQUENCE |
Oracle | strategy=GenerationType.AUTO GenerationType.SEQUENCE GenerationType.TABLE 不支持GenerationType.IDENTITY |
postgreSQL | GenerationType.TABLE GenerationType.AUTO GenerationType.IDENTITY GenerationType.SEQUENCE 都支持 |
kingbase | GenerationType.TABLE GenerationType.AUTO GenerationType.IDENTITY GenerationType.SEQUENCE 都支持 |
7、表主键用uuid还是数字
主键选择 | 优点 | 缺点 |
---|---|---|
自增主键 | 1、数据存储空间小 2、查询效率高 |
1、如果数据量过大,会超出自增长的值范围 2、分布式存储的表操作,尤其是在合并的时候操作复杂 3、安全性低,因为是有规律的,如果恶意扒取用户信息会很容易,如果是单据编号使用,竞争对手会容易查询出货量 |
UUID主键 | 1、出现重复的机会少 2、适合大量数据的插入和更新操作,尤其是在高并发和分布式环境下 3、安全性较高 |
1、存储空间大(16 byte),因此它将会占用更多的磁盘空间, MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求 2、性能降低,对MySQL索引不利: 如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。 |
适用场景
1、项目是单机版的,并且数据量比较大(百万级)时,用自增长的,此时最好能考虑下安全性,做些安全措施
2、项目是单机版的,并且数据量没那么大,对速度和存储要求不高时,用UUID
3、项目是分布式的,那么首选UUID,分布式一般对速度和存储要求不高
4、项目是分布式的,并且数据量达到千万级别可更高时,对速度和存储有要求时,可以用自增长。
参考:https://blog.csdn.net/weixin_38446891/article/details/109813272
Hibernate JPA 缓存配置
1、一级缓存
一级缓存指的是EntityManager
级的缓存,对于这样的缓存几乎是一直存在的,也就是说只要用户进行JPA
的操作,那么就永远都会存在有一级缓存。
1、使用fnd() 执行两次查询,并且查询的ID信息都一样:
@Test
public void testFindByIDCache(){
EntityManager entityManager = JpaUtils.getEntityManager();
Customer customer1 = entityManager.find(Customer.class, 2L);
System.err.println(customer1);
System.err.println("--------------华丽的分割线-------------------");
Customer customer2 = entityManager.find(Customer.class, 2L);
System.err.println(customer2);
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
--------------华丽的分割线-------------------
// 可以发现第二次查询的时候可以发送SQL语句
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
可以发现这个时候真正执行的是一次数据库查询,也就是说在同一个EntityManager
对象操作之中如果使用 find()查询,那么同一条数据默认情况下只会査询一次,查询完成之后会将这个数据进行缓存。
2、如果说现在在查询过程之中发生了内容的变更呢?如:第一次查询出来的对象现在要求进行一些修改。
@Test
public void testFindCache2(){
EntityManager entityManager = JpaUtils.getEntityManager();
Customer customer1 = entityManager.find(Customer.class, 2L);
customer1.setName("李四2");// 第一次查询的数据发生改变
System.err.println(customer1);
System.err.println("--------------华丽的分割线-------------------");
Customer customer2 = entityManager.find(Customer.class, 2L);
System.err.println(customer2);
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四2, age=20, sex=false, phone=13533333555, address=上海)
--------------华丽的分割线-------------------
// 可以发现第二次查询的时候可以发送SQL语句
Customer(Id=2, name=李四2, age=20, sex=false, phone=13533333555, address=上海)
第一个数据读取进来之后由于发生了改变,肯定和原始数据库的内容不一样了,那么此时通过第二次查询获得的结果可以发现,依然只是查询了一次,并且在ID不改变的情况下,会发生相同ID的查询依然引用已有的缓存对象.
3、如果现在想内容进行更新,需要采用刷新的模式完成。entityManager.refresh(对象)
@Test
public void testFindRefresh(){
EntityManager entityManager = JpaUtils.getEntityManager();
Customer customer1 = entityManager.find(Customer.class, 2L);
customer1.setName("李四2");// 第一次查询的数据发生改变
System.err.println(customer1);
entityManager.refresh(customer1);// 重新加载
System.err.println("--------------华丽的分割线-------------------");
Customer customer2 = entityManager.find(Customer.class, 2L);
System.err.println(customer2);
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四2, age=20, sex=false, phone=13533333555, address=上海)
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
--------------华丽的分割线-------------------
Customer(Id=2, name=李四, age=20, sex=false, phone=13533333555, address=上海)
此时同样操作发现重复查询了两次,主要是因为使用了refresh()
明确表示该缓存对象需要重新加载。在JPA
中有一个非常重要的概念:JPA
的对象状态,在JPA
里面一共有四种对象状态.
JPA 对象状态
- New:瞬时对象,尚未有id,还未和
Persistence Context
建立关联的对象 - Managed:持久态对象,有id值,与
Persistence Context
建立了关联的对象 - Datached:游离态对象,有id值,没有和
Persistence Context
建立关联的对象 - Removed:删除的对象,有id值,尚未和
Persistence Context
有关联,但是已经准备好从数据库中删除
示例说明:
@Test
public void testFindAdd(){
Customer customer = new Customer();// 临时状态
customer.setName("小刘");
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(customer);// 持久状态
entityManager.remove(customer);// 删除状态
entityManager.getTransaction().commit();
entityManager.close();
System.err.println(selectCustomer);// 游离状态
}
1.临时状态(transient):瞬时状态
- 没有与entityManager发生关系,没有被持久化,不处于entityManager中的对象
2持久化状态(persistent):托管状态
- 与entityManager发生关系,已经被持久化,加入到entityManager的一级缓存中的对象
3.删除状态(removed)
-
调用了entityManager.remove(domain对象),对象有关联的ID,并且在entityManager管理下
-
调用remove方法后已经计划删除,事物提交后才会被真正从数据库删除
4.游离状态(detached):脱管状态
- 对象和entityManager解除关系
4、观察在持久态下的数据更新(重点)
@Test
public void testFindEdit(){
EntityManager entityManager = JpaUtils.getEntityManager();
// 持久态
Customer customer = entityManager.find(Customer.class, 2L);
entityManager.getTransaction().begin();// 开启事务
customer.setName("李四四");// 持久化数据发生改变
entityManager.getTransaction().commit();// 提交更新事务
entityManager.close();
}
查看日志:
Hibernate: ### entityManager.find(Customer.class, 2L)
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Hibernate: ### 更新了持久态中的属性内容,并且提交了事务
update
c_customer
set
c_address=?,
c_age=?,
c_name=?,
c_phone=?,
c_sex=?
where
id=?
然后查看数据库这条数据:
mysql> select * from c_customer where id = 2;
+----+-----------+-------+--------+-------------+-------+
| id | c_address | c_age | c_name | c_phone | c_sex |
+----+-----------+-------+--------+-------------+-------+
| 2 | 上海 | 20 | 李四四 | 13533333555 | 0 |
+----+-----------+-------+--------+-------------+-------+
1 row in set (0.03 sec)
发现这条数据c_name发生了改变,但是我们并没有执行更新方法,可以得出结论:持久态话下的数据发生改变的话,只要在事务中并且提交了事务更新,那么默认就相当于执行了更新数据库操作
5、一个新的对象默认情况下属于瞬时态,瞬时态也可以持久化
@Test
public void testFindAdd(){
Customer customer = new Customer();
customer.setName("小刘");
customer.setAge(94);
customer.setSex(true);
customer.setPhone("13588880000");
customer.setAddress("广州");
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
entityManager.persist(customer);// 数据持久化,自动可以获取ID
entityManager.getTransaction().commit();// 提交更新事务
System.err.println("-----新增数据的ID是:" + customer.getId());
// 将刚刚保存对象信息根据ID查询出来
Customer selectCustomer = entityManager.find(Customer.class, customer.getId());
System.err.println(selectCustomer);
entityManager.close();
}
查看日志:
Hibernate:
insert
into
c_customer
(c_address, c_age, c_name, c_phone, c_sex)
values
(?, ?, ?, ?, ?)
-----新增数据的ID是:13
Customer(Id=13, name=小刘, age=94, sex=true, phone=13588880000, address=广州)
由于现在使用了persisit()
方法将瞬时态对象变为了持久态,所以这个对象就会被缓存起来,那么再执行一次查询的时候就不会重复发出查询命令,而是直接使用缓存中的数据.
6、如果每新增一个数据都被缓存起来,当批量新增时,缓存所占用的空间就会出现严重的不足。最好的做法是进行强制性的保存以及清空
@Test
public void testFindFlushAndClear(){
Customer customer = new Customer();
customer.setName("小刘");
customer.setAge(94);
customer.setSex(true);
customer.setPhone("13588880000");
customer.setAddress("广州");
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
entityManager.persist(customer);// 数据持久化,自动可以获取ID
entityManager.flush(); // 强制立即写入数据库
entityManager.clear(); // 清空缓存
entityManager.getTransaction().commit();// 提交更新事务
System.err.println("-----新增数据的ID是:" + customer.getId());
// 将刚刚保存对象信息根据ID查询出来
Customer selectCustomer = entityManager.find(Customer.class, customer.getId());
System.err.println(selectCustomer);
entityManager.close();
}
查看日志:
Hibernate:
insert
into
c_customer
(c_address, c_age, c_name, c_phone, c_sex)
values
(?, ?, ?, ?, ?)
-----新增数据的ID是:14
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=14, name=小刘, age=94, sex=true, phone=13588880000, address=广州)
由于现在清楚了缓存,所以此时的数据再次查询的时候就需要重新发出查询指令,当然重新查询之后也就意味这一个对象重新被缓存了
所以在真实进行数据的批量增加时,我们应该适当加上强制写入和清空缓存.(例如每新增10条数据后就执行一次
flush
和clear
方法)
7、删除数据(游离状态)
@Test
public void testDelete(){
EntityManager entityManager = JpaUtils.getEntityManager();
Customer customer = entityManager.find(Customer.class, 15L);
entityManager.getTransaction().begin();// 开启事务
entityManager.remove(customer); // 删除数据
customer.setName("小明");
entityManager.getTransaction().commit();// 提交事务
entityManager.close();
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Hibernate:
delete
from
c_customer
where
id=?
删除的数据就属于游离态了,所以此时已无法实现试据的持久态管理了
2、二级缓存
一级缓存时针对与EntityManager
的缓存处理,并且永久存在,而二级缓存指的是针对于多个EntityManager
实现的缓存处理,但是二级缓存默认是没有开启的.
1、建立两个不同的EntityManager
进行数据查询
@Test
public void testFind(){
EntityManager entityManagerA = JpaUtils.getEntityManager();
System.err.println(entityManagerA.find(Customer.class, 2L));
entityManagerA.close();
System.err.println("--------------华丽的分割线-------------------");
EntityManager entityManagerB = JpaUtils.getEntityManager();
System.err.println(entityManagerB.find(Customer.class, 2L));
entityManagerB.close();
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
--------------华丽的分割线-------------------
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
可以发现不同的Seeson(EntityManager)查询同一条数据的时候依然发出了两次查询指令,所以此时表示JPA中没有开启二级缓存
2、二级缓存一般用第三方组件Redis、Ehache,这里用Ehcache
为例,pox.xml引入Ehcache
依赖
<!--添加Hibernate-Ehcache包,版本号与hibernate一样 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>5.4.1.Final</version>
</dependency>
<!--Ehcache-core 包 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
3、在resources
目录下新建一个ehcache.xml
文件。如果在加载时未找到/ehcache.xml
资源或出现问题,则将使用默认配置。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
- user.home – 用户主目录
- user.dir – 用户当前工作目录
- java.io.tmpdir – 默认临时文件路径
- 或自定义一个本地磁盘路径例如:/home、./tmpdir/Tmp_EhCache
-->
<!-- 磁盘缓存位置 -->
<diskStore path="./tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="259200"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="cloud_user"
eternal="false"
maxElementsInMemory="5000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="1800"
memoryStoreEvictionPolicy="LRU"/>
<!--
defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
-->
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
</ehcache>
4、在JPA配置文件persistence.xml
中增加二级缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<!-- 持久化单元:持久化单元事务类型,RESOURCE_LOCAL:本地事务管理 -->
<persistence-unit name="myJpa" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!--
配置二级缓存时候使用的模式,可配置值有:
- ALL:所有的实体类都被缓存
- NONE:所有的实体类都不被缓存
- ENABLE_SELECTIVE:标识@Cacheable(true)注解的实体类将被缓存
- DISABLE_SELECTIVE;缓存除标识@Cacheable(false)以外的所有实体类
- UNSPECIFIED:默认值,JPA 产品默认值将被使用
-->
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<!--可选配置:配置jpa实现方的配置信息-->
<properties>
<!-- 数据库信息配置:数据库驱动、数据库地址、数据库账户、数据库密码 -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
<property name="hibernate.connection.url" value="jdbc:mysql://127.0.0.1:3306/hibernate_jpa"/>
<property name="hibernate.connection.username" value="root"/>
<property name="hibernate.connection.password" value="password"/>
<property name="hibernate.show_sql" value="true" /><!-- 自动显示sql -->
<property name="hibernate.format_sql" value="true"/><!-- 格式化sql -->
<!-- 自动创建数据库表:none、create、update、create-drop、validate -->
<property name="hibernate.hbm2ddl.auto" value="update" />
<!-- 二级缓存相关 -->
<!-- 开启二级缓存 -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<!-- 配置二级缓存处理类 -->
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
<!-- 开启查询缓存,entityManager.find查询可以不配置,如果使用JPQL或SQL查询需要开启该配置 -->
<property name="hibernate.cache.use_query_cache" value="true"/>
<!-- 指定缓存配置文件位置,如果默认在resources下可不配置 -->
<property name="hibernate.cache.provider_configuration" value="classpath:ehcache.xml"/>
</properties>
</persistence-unit>
</persistence>
5、其中ENABLE_SELECTIVE
模式为实体类上配置 @Cacheable(true)
的才会进行生效
@Cacheable(true)
@Entity
public class Customer {}
/***************另一种用法(没有特殊需求只用上面配置即可)******************/
/**
* 如果只配置@Cacheable(true)的话是用默认defaultCache配置,
* 想用自定义名称的缓存配置,需要增加@Cache注解,例如想使用<cache name="cloud_user"../> 缓存配置
* 增加注解 @Cache(usage = CacheConcurrencyStrategy.READ_WRITE,region="指定的cache")
**/
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE,region="cloud_user")
@Cacheable(true)
@Entity
public class Customer {}
usage
提供缓存对象的事务隔离机制有如下几种:
NONE、READ_ONLY、 NONSTRICT_READ_WRITE、READ_WRITE、TRANSACTIONAL
- ehcache不支持transaction事务机制,但其他三种可以使用:
- read-only:无需修改, 那么就可以对其进行只读 缓存,注意,在此策略下,如果直接修改数据库,即使能够看到前台显示效果,但是将对象修改至cache中会报error,cache不会发生作用。另:删除记录会报错,因为不能在read-only模式的对象从cache中删除
- read-write:需要更新数据,那么使用读/写缓存 比较合适,前提:数据库不可以为serializable transaction isolation level(序列化事务隔离级别)
- nonstrice-read-write:只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离,那么比较适合使用非严格读/写缓存策略。
6、再次测试Test代码:
@Test
public void testFind(){
EntityManager entityManagerA = JpaUtils.getEntityManager();
System.err.println(entityManagerA.find(Customer.class, 2L));
entityManagerA.close();
System.err.println("--------------华丽的分割线-------------------");
EntityManager entityManagerB = JpaUtils.getEntityManager();
System.err.println(entityManagerB.find(Customer.class, 2L));
entityManagerB.close();
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
--------------华丽的分割线-------------------
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
可以发现第二次查询没有发送查询语句了
3、查询缓存
在进行数据査询的时候使用的都直接是EntityManager
接口提供的查询方法,这种查询方式可以帮助用户直接进行缓存处理,而如果说现在采用的是Query
查询,则对于缓存的配置就不生效了
由于在二级缓存中已经配置了如下配置,我们先注释掉下面的配置开始测试:
<property name="hibernate.cache.use_query_cache" value="true"/>
1、观察默认情况下Query
查询处理
@Test
public void testFindQuery(){
String jpql = "from Customer where Id = :Id";
EntityManager entityManagerA = JpaUtils.getEntityManager();
TypedQuery<Customer> queryA = entityManagerA.createQuery(jpql, Customer.class);
queryA.setParameter("Id", 2L);
System.err.println(queryA.getSingleResult());
entityManagerA.close();
System.err.println("--------------华丽的分割线-------------------");
EntityManager entityManagerB = JpaUtils.getEntityManager();
TypedQuery<Customer> queryB = entityManagerB.createQuery(jpql, Customer.class);
queryB.setParameter("Id", 2L);
System.err.println(queryB.getSingleResult());
entityManagerB.close();
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_,
customer0_.c_address as c_addres2_0_,
customer0_.c_age as c_age3_0_,
customer0_.c_name as c_name4_0_,
customer0_.c_phone as c_phone5_0_,
customer0_.c_sex as c_sex6_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
--------------华丽的分割线-------------------
Hibernate:
select
customer0_.id as id1_0_,
customer0_.c_address as c_addres2_0_,
customer0_.c_age as c_age3_0_,
customer0_.c_name as c_name4_0_,
customer0_.c_phone as c_phone5_0_,
customer0_.c_sex as c_sex6_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
发现默认情况下即便开启了二级缓存,对于Query查询也是无效的,所以还是需要开启查询缓存配置
2、开启查询缓存配置(还原注释掉的配置),使用QueryHints
操作
@Test
public void testFindQuery2(){
String jpql = "from Customer where Id = :Id";
EntityManager entityManagerA = JpaUtils.getEntityManager();
TypedQuery<Customer> queryA = entityManagerA.createQuery(jpql, Customer.class);
queryA.setHint(QueryHints.HINT_CACHEABLE,true);
queryA.setParameter("Id", 2L);
System.err.println(queryA.getSingleResult());
entityManagerA.close();
System.err.println("--------------华丽的分割线-------------------");
EntityManager entityManagerB = JpaUtils.getEntityManager();
TypedQuery<Customer> queryB = entityManagerB.createQuery(jpql, Customer.class);
queryB.setHint(QueryHints.HINT_CACHEABLE,true);
queryB.setParameter("Id", 2L);
System.err.println(queryB.getSingleResult());
entityManagerB.close();
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_,
customer0_.c_address as c_addres2_0_,
customer0_.c_age as c_age3_0_,
customer0_.c_name as c_name4_0_,
customer0_.c_phone as c_phone5_0_,
customer0_.c_sex as c_sex6_0_
from
c_customer customer0_
where
customer0_.id=?
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
--------------华丽的分割线-------------------
Customer(Id=2, name=李四四, age=20, sex=false, phone=13533333555, address=上海)
可以发现只发送了一次查询操作
Hibernate JPA 锁机制
当数据库并发访问的时候为了保证操作的统一性,那么往往会对并发数据的访问做出限制,例如:只允许一个会话
处理,这样的机制就称为锁机制,而在JPA
之中也支持锁机制的处理,而JPA
支持两类锁:
- 悲观锁:假设数据的访问一直都存在有并发,所以悲观锁一直都会存在,主要依靠的是数据库的锁机制.
- 乐观锁:假设不会进行并发访问(不会出现同时的数据更新处理)主要是依靠算法来实现的,就是设置一个版本号,通过版本号来判断当前的
Session
能否进行更新.
在JPA
里面专门提供有一个锁的处理模式:javax.persistence.LockModeType;
1、悲观锁(Pessimistic)
悲观锁认为用户的并发访问会一直发生,并且在整个的处理之中悲观锁一定会采用锁的机制,对一个事务内的操作数据进行锁定,这样其他的事务就无法进行该数据的更新操作了。
在悲观锁中定义了如下几种处理模式:
- PESSIMISTIEC_READ:只要事务读实体,实体管理器就锁定实体,直到事务完成锁才会解开,当你想使用重复语义查询数据时使用这种锁模式,换句话说,当你想确保数据在连续读取期间不被修改,这种锁模式不会阻碍其他事务读取数据。
- PESSIMISTIC_WRITE:只要事务更新实体,实体管理器就会锁定实体,这种锁模式强制尝试修改实体数据的事务串行化,当多个并发更新事务出现更新失败几率较高时使用这种锁模式。
- PESSIMISTIC_FORCE_INCREMENT:当事务读实体时,实体管理器就锁定实体,当事务结束时会增加实体的版本属性,即使实体没有修改。
- NONE:不适用锁
1、使用悲观锁编写测试类
/**
* 悲观锁
*/
@Test
public void testFindPessimisticLock(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入悲观锁
entityManager.find(Customer.class, 2L, LockModeType.PESSIMISTIC_WRITE);
entityManager.getTransaction().rollback();// 可以回滚或提交
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=? for update
可以发现查询语句最后跟上了:for update
(代表我对这条数据进行锁定了,在我没有提交或回滚事务之前,其他线程不能对该数据进行修改)
2、模仿两个线程来进行锁处理
模拟场景:
- 线程A查询数据,并给这条数据加上写入悲观锁,然后对数据进行修改,在事务提交之前休眠20秒
- 线程B查询同一条数据,并给这条数据加上写入悲观锁,然后对数据进行修改,不休眠直接提交事务
线程A:TestPessimisticA.java
public class TestPessimisticA {
/**
* 悲观锁
*/
public static void main(String[] args) throws Exception {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入悲观锁
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.PESSIMISTIC_WRITE);
customer.setName("悲观锁修改A");
TimeUnit.SECONDS.sleep(20);// 休眠20秒
entityManager.getTransaction().commit();// 可以提交或回滚
entityManager.close();
}
线程B:TestPessimisticB.java
public class TestPessimisticB {
/**
* 悲观锁
*/
public static void main(String[] args) {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入悲观锁
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.PESSIMISTIC_WRITE);
customer.setName("悲观锁修改B");
entityManager.getTransaction().commit();// 可以提交或回滚
entityManager.close();
}
操作步骤和结论:
- 1、先执行线程A,然后里面去执行线程B。
- 2、会出现线程A执行完数据修改后提交事务前,休眠20秒,控制台停住了。
- 3、线程B虽然没有休眠,可是也依旧停住了,因为在等待线程A操作完该数据。
- 4、等待线程A操作完数据提交了事务,线程B也立马提交了事务。
- 5、最终这条数据经历了先被A修改,然后立马又被线程B修改了。
查看日志(从日志上什么也看不出来,线程A和B输出日志一摸一样,需要模拟时看控制台停滞与停止状态)
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_
from
c_customer customer0_
where
customer0_.id=? for update
Hibernate:
update
c_customer
set
c_address=?,
c_age=?,
c_name=?,
c_phone=?,
c_sex=?
where
id=?
这个必须要亲自模拟多次才能理解清楚,可以在线程B加上控制台输出
2、乐观锁(Optimistic)
JPA
最早的时候所提供的锁机制就是乐观锁,乐观锁:假设没有多个事务修改同一条数据的情况,而且乐观锁最大的差别就是需要对数据表上增加一个表示数据版本的编号。
对于乐观锁有如下几种锁的处理模式:
- OPTIMISTIC:它和READ锁模式相同,
JPA 2.0
仍然支持READ
模式,但明确指出在新应用程序中推荐使用OPTIMISTIC
- OPTIMISTIC_FORCE_INCREMENT:它和WRITE锁模式相同,
JPA 2.0
仍然支持WRITE
锁模式,但明确的指出在新应用程序中推荐使用OPTIMISTIC_FORCE_INCREMENT
1、修改数据库脚本:增加一个版本号字段:c_version
(如果JPA
设置自动检测修改表的话可以忽略这一步)
alter table c_customer add column c_version bigint default 0;
2、修改实体类,增加版本号字段
@Version
@Column(name = "c_version",columnDefinition="bigint default 0")
private Long version;
3、采用乐观锁操作,编写程序类
/**
* 乐观锁
*/
@Test
public void testFindOptimisticLock(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入乐观锁
Customer customer = entityManager.find(Customer.class, 2L, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
customer.setName("李四四");
entityManager.getTransaction().commit();// 可以回滚或提交
}
查看日志:
Hibernate:
select
customer0_.id as id1_0_0_,
customer0_.c_address as c_addres2_0_0_,
customer0_.c_age as c_age3_0_0_,
customer0_.c_name as c_name4_0_0_,
customer0_.c_phone as c_phone5_0_0_,
customer0_.c_sex as c_sex6_0_0_,
customer0_.c_version as c_versio7_0_0_
from
c_customer customer0_
where
customer0_.id=?
Hibernate:
update
c_customer
set
c_version=?
where
id=?
and c_version=?
会发现每一次在进行更新的时候会出现版本号的修改操作
4、模拟多用户并发访问
模拟场景(与悲观锁场景一样):
- 线程A查询数据,并给这条数据加上写入乐观锁,然后对数据进行修改,在事务提交之前休眠20秒
- 线程B查询同一条数据,并给这条数据加上写入悲观锁,然后对数据进行修改,不休眠直接提交事务
线程A:TestOptimisticA
public class TestOptimisticA {
/**
* 乐观锁
* @param args
*/
public static void main(String[] args) throws Exception {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入乐观锁
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
customer.setName("乐观锁修改A");
TimeUnit.SECONDS.sleep(20);// 休眠20秒
entityManager.getTransaction().commit();// 可以提交或回滚
entityManager.close();
}
线程B:TestOptimisticB
/**
* 乐观锁
* @param args
*/
public static void main(String[] args) {
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 加上写入乐观锁
System.err.println("查询数据--------");
Customer customer = entityManager.find(Customer.class, 14L, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
customer.setName("乐观锁修改B");
entityManager.getTransaction().commit();// 可以提交或回滚
System.err.println("修改线程B的数据完成");
entityManager.close();
}
操作步骤和结论:
- 1、先执行线程A,然后里面去执行线程B
- 2、由于乐观锁不是对一条数据的锁定,等于现在第二个事务(线程B)会先实现数据的更新
- 3、但是由于第一个事务先启动,所以他读取到版本号和它更新时候的版本号肯定是不同的
- 4、所以在线程A提交事务的时候会报如下错误:
Caused by: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [domain.Customer#14]
....
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [domain.Customer#14]
乐观锁是一种基于数据算法的锁的处理机制,乐观锁特点就是在于你的项目之中不存在多个用户更新同一数据的情况。如果一直存在并发更新同一数据的话,那么一定采用悲观锁
Hibernate JPA 关联关系
关联关系从整体上分为单向关联和双向关联:
- 单向关联:只需从一端访问另一端,如教师Teacher可访问学生Student,则Teacher实体需要包含类型为Student的属性
- 双向关联:两端均可互相访问,如教师Teacher可访问学生Student,学生Student也可访问教师Teacher,两个实体均需要包含类型为对方的属性
1、一对一关联映射
1.1、单向一对一
- 单向1-1:需要在控制关系的一方实体中使用注解 @OneToOne、@JoinColumn 标注类型为对方的属性。如Person端为控制关系的一方,只需要在Person控制方加这两个注解即可。反端既不用配置属性字段也不用配置注解
单向一对一是关联关系映射中最简单的一种,简单地说就是可以从关联的一方去查询另一方,却不能反向查询。我们用下面的例子来举例说明,清单 1 中的 Person 实体类和清单 2 中的 Address 类就是这种单向的一对一关系,我们可以查询一个 Person 的对应的 Address 的内容,但是我们却不能由一个 Address 的值去查询这个值对应的 Person
person_address(外键表):(address_id,street,city,country,person_id)
person(主键表):(person_id,username)
清单 1. 单向一对一关系的拥有端
@Data
@Entity
@Table(name = "t_person")
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "person_id")
private Long personId;
private String username;
/**
* @JoinColumn
* name: person_address表外键字段名(数据库字段名)
* referencedColumnName: person表主键字段(数据库字段名)
*/
@OneToOne
@JoinColumn(name = "address_id",referencedColumnName = "address_id")
private PersonAddress personAddress;
}
清单 2. 单向一对一关系的被拥有端(反端)
@Data
@Entity
@Table(name = "t_person_address")
public class PersonAddress implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "address_id")
private Long addressId;
private String country;
private String city;
}
执行代码测试:
/**
* 单向一对一查询
*/
@Test
public void testOneToOne(){
EntityManager entityManager = JpaUtils.getEntityManager();
// 开启事务
entityManager.getTransaction().begin();
// 保存地址数据
PersonAddress address = new PersonAddress();
address.setCountry("中国");
address.setCity("广州");
entityManager.persist(address);
// 保存用户信息
Person person = new Person();
person.setUsername("Sam");
person.setPersonAddress(address);
entityManager.persist(person);
// 提交更新事务
entityManager.getTransaction().commit();
// 查询拥有端(外键表端),先清理缓存
entityManager.clear();
System.out.println(entityManager.find(Person.class,person.getPersonId()));
entityManager.close();
}
查看日志1(日志分为两段,1为建表):
Hibernate:
create table t_person (
person_id bigint not null auto_increment,
username varchar(255),
address_id bigint,
primary key (person_id)
) engine=InnoDB
Hibernate:
create table t_person_address (
address_id bigint not null auto_increment,
city varchar(255),
country varchar(255),
primary key (address_id)
) engine=InnoDB
Hibernate:
alter table t_person
add constraint FKcep21ttdy3yuo1f56giyatasf
foreign key (address_id)
references t_person_address (address_id)
查看日志2(日志分为两段,2为数据插入与查询):
Hibernate:
insert
into
t_person_address
(city, country)
values
(?, ?)
Hibernate:
insert
into
t_person
(address_id, username)
values
(?, ?)
// 省略查询语句
Person(personId=1, username=Sam, address=PersonAddress(addressId=1, country=中国, city=广州))
1.2、双向一对一
清单 3. 单向一对一关系的拥有端
@Data
@Entity
@Table(name = "t_person_address")
public class PersonAddress implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "address_id")
private Long addressId;
private String country;
private String city;
/**
* @OneToOne
* mappedBy:放弃外键维护。mappedBy 只有在双向关联的时候设置。值为对方类引用本类的属性名
*/
@OneToOne(mappedBy = "personAddress")
private Person person;
}
清单 4. 双向一对一关系中的接受端
@Data
@Entity
@Table(name = "t_person")
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "person_id")
private Long personId;
private String username;
/**
* @JoinColumn
* name: person_address表外键字段名(数据库字段名)
* referencedColumnName: person表主键字段(数据库字段名)
*/
@OneToOne
@JoinColumn(name = "address_id",referencedColumnName = "address_id")
private PersonAddress personAddress;
}
执行代码测试:
/**
* 双向一对一查询
*/
@Test
public void testOneToOne2(){
EntityManager entityManager = JpaUtils.getEntityManager();
// 开启事务
entityManager.getTransaction().begin();
PersonAddress personAddress = new PersonAddress();
personAddress.setCountry("中国");
personAddress.setCity("广州");
entityManager.persist(personAddress);
Person person = new Person();
person.setUsername("Sam");
person.setPersonAddress(personAddress);
entityManager.persist(person);
// 提交事务
entityManager.getTransaction().commit();
// 查询拥有端(外键表端)先清理缓存
entityManager.clear();
System.out.println(entityManager.find(PersonAddress.class,personAddress.getAddressId()));
// 查询被拥有端(主键表端)
System.out.println(entityManager.find(Person.class,person.getPersonId()));
entityManager.close();
}
查看日志(建表语句就不重复打印了,因为是一摸一样的):
// ...省略插入语句和查询语句
java.lang.StackOverflowError
at java.lang.Long.toString(Long.java:396)
at java.lang.Long.toString(Long.java:1032)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at OneToOne.Person.toString(Person.java:8)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at OneToOne.PersonAddress.toString(PersonAddress.java:8)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at OneToOne.Person.toString(Person.java:8)
at java.lang.String.valueOf(String.java:2994)
...
现在在查询步骤中会出现死循环
2、一对多关联映射
2.1、单向一对多
- 单向1-N:需要在控制关系的一方实体中使用注解 @OneToMany、@JoinColumn 标注类型为对方的集合属性。(有两种方式:一种是只加@OneToMany、另一种是 @OneToMany+@JoinColumn)
清单 5. 单向一对多关系的拥有端(一方、主键表方、主表)
@Data
@Entity
@Table(name = "t_people")
public class People {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "people_id")
private Long peopleId;
private String name;
/**
* @OneToMany
* cascade = CascadeType.ALL
* 级联保存、更新、删除、刷新
* fetch = FetchType.LAZY
* 延迟加载
* @JoinColumn
* name 指定外键列,这里注意指定的是people_id,
* 实际上是为了外键表定义的字段。该字段在PeoplePhone类必须定义
*/
@OneToMany
@JoinColumn(name="people_id")
private List<PeoplePhone> peoplePhones = new ArrayList<>();
}
清单 6. 单向一对多关系的接收端(多方、外键表方、从表)
@Data
@Entity
@Table(name = "t_people_phone")
public class PeoplePhone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "phone_id")
private Long phoneId;
private String type;
private String phone;
}
测试代码:
/**
* 单向一对多查询
*/
@Test
public void testOneToMany(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
PeoplePhone peoplePhoneA = new PeoplePhone();
peoplePhoneA.setType("date_time");
peoplePhoneA.setPhone("13011113333");
PeoplePhone peoplePhoneB = new PeoplePhone();
peoplePhoneB.setType("mobile");
peoplePhoneB.setPhone("0208514851");
entityManager.persist(peoplePhoneA);
entityManager.persist(peoplePhoneB);
People people = new People();
people.setName("Sam");
people.getPeoplePhones().add(peoplePhoneA);
people.getPeoplePhones().add(peoplePhoneB);
entityManager.persist(people);
entityManager.getTransaction().commit();// 提交更新事务
// 查询拥有端(主键表端,注意:单向一对多是配置在 一方/拥有端)
System.out.println(entityManager.find(People.class,people.getPeopleId()));
entityManager.close();
}
查看日志1:(建表语句)
Hibernate:
create table t_people (
people_id bigint not null auto_increment,
name varchar(255),
primary key (people_id)
) engine=InnoDB
Hibernate:
create table t_people_phone (
phone_id bigint not null auto_increment,
phone varchar(255),
type varchar(255),
people_id bigint,
primary key (phone_id)
) engine=InnoDB
Hibernate:
alter table t_people_phone
add constraint FKnied6axrmqsyl5olnjywa7set
foreign key (people_id)
references t_people (people_id)
查看日志2:
Hibernate:
insert
into
t_people_phone
(phone, type)
values
(?, ?)
Hibernate:
insert
into
t_people_phone
(phone, type)
values
(?, ?)
Hibernate:
insert
into
t_people
(name)
values
(?)
Hibernate:
update
t_people_phone
set
people_id=?
where
phone_id=?
Hibernate:
update
t_people_phone
set
people_id=?
where
phone_id=?
People(peopleId=1, name=Sam, peoplePhones=[PeoplePhone(phoneId=1, type=date_time, phone=13011113333), PeoplePhone(phoneId=2, type=mobile, phone=0208514851)])
2.2、单向多对一
- 单向N-1:需要在控制关系的一方实体中使用注解@ManyToOne、@JoinColumn标注类型为对方的属性。
清单 7. 单向多对一关系的拥有端(多方、外键表方、从表)
@Data
@Entity
@Table(name = "t_people_phone")
public class PeoplePhone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "phone_id")
private Long phoneId;
private String type;
private String phone;
/**
* @JoinColumn
* name 指定外键列
* referencedColumnName: people 表主键字段(数据库字段名)
*/
@ManyToOne
@JoinColumn(name="people_id", referencedColumnName = "people_id")
private People people;
}
清单 8. 单向多对一关系的被拥有端(一方、主键表方、主表)
@Data
@Entity
@Table(name = "t_people")
public class People {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "people_id")
private Long peopleId;
private String name;
}
测试代码:
/**
* 单向多对一查询
*/
@Test
public void testManyToOne(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();// 开启事务
// 先保存主键(被维护端)数据
People people = new People();
people.setName("Sam");
entityManager.persist(people);
// 然后保存外键(维护端)数据
PeoplePhone peoplePhoneA = new PeoplePhone();
peoplePhoneA.setType("date_time");
peoplePhoneA.setPhone("13011113333");
peoplePhoneA.setPeople(people);
PeoplePhone peoplePhoneB = new PeoplePhone();
peoplePhoneB.setType("mobile");
peoplePhoneB.setPhone("0208514851");
peoplePhoneB.setPeople(people);
entityManager.persist(peoplePhoneA);
entityManager.persist(peoplePhoneB);
entityManager.getTransaction().commit();// 提交更新事务
// 查询拥有端(外键表端,注意:单向多对一是配置在多方外键拥有端)
entityManager.clear();
System.out.println(entityManager.find(PeoplePhone.class,peoplePhoneA.getPhoneId()));
System.out.println(entityManager.find(PeoplePhone.class,peoplePhoneB.getPhoneId()));
entityManager.close();
}
查看日志:
Hibernate:
insert
into
t_people
(name)
values
(?)
Hibernate:
insert
into
t_people_phone
(people_id, phone, type)
values
(?, ?, ?)
Hibernate:
insert
into
t_people_phone
(people_id, phone, type)
values
(?, ?, ?)
// ...省略了查询语句
PeoplePhone(phoneId=1, type=date_time, phone=13011113333, people=People(peopleId=1, name=Sam))
// ...省略了查询语句
PeoplePhone(phoneId=2, type=mobile, phone=0208514851, people=People(peopleId=1, name=Sam))
2.3、双向一对多
双向1-N(N-1):1的一端需要使用注解@OneToMany标注类型为对方的集合属性,同时指定mappedBy属性表示1的一端不控制关系,N的一端则需要使用注解@ManyToOne、@JoinColumn标注类型为对方的属性。
清单 9. 双向一对多关系的接受端(一方、主键表方、主表)
@Data
@Entity
@Table(name = "t_people")
public class People {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "people_id")
private Long peopleId;
private String name;
/**
*
*/
@OneToMany(
mappedBy = "people",
targetEntity = PeoplePhone.class,
cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
private List<PeoplePhone> peoplePhones = new ArrayList<>();
}
清单 10. 双向一对多关系的发出端(多方、外键表方、从表)
@Data
@Entity
@Table(name = "t_people_phone")
public class PeoplePhone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "phone_id")
private Long phoneId;
private String type;
private String phone;
/**
* @JoinColumn
* name 指定外键列
* referencedColumnName: people 表主键字段(数据库字段名)
*/
@ManyToOne // cascade | fetch | targetEntity 都是可选
@JoinColumn(name="people_id", referencedColumnName = "people_id")
private People people;
}
3、多对多关联映射
3.1、单向多对多
- 单向N-N:需要在控制关系的一方实体中使用注解@ManyToMany、@JoinTable标注类型为对方的属性,这里应该是一个集合属性
清单 11. 单向多对多关系的发出端(拥有端)
@Data
@Entity
@Table(name = "t_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
/**
* @JoinTable:
* name:中间表名称
* joinColumns 中间表对应本类的信息
* @JoinColumn;
* name:本类的外键字段(中间表的数据库字段)
* referencedColumnName:本类与外键(表)对应的主键(本类的主键字段)
* inverseJoinColumns 中间表对应对方类的信息
* @JoinColumn:
* name:对方类的外键(中间表的数据字段)
* referencedColumnName:对方类与外键(表)对应的主键(对方类的主键字段)
*/
@ManyToMany
@JoinTable(name="t_role_permission", // 中间表明
joinColumns=@JoinColumn(
name="role_id", // 本类的外键
referencedColumnName = "role_id"), // 本类与外键(表)对应的主键
inverseJoinColumns=@JoinColumn(
name="permission_id", // 对方类的外键
referencedColumnName = "permission_id")) // 对方类与外键(表)对应的主键
private Set<Permission> permissions = new HashSet<>();
}
清单 11. 单向多对多关系的接收端(被拥有端)什么都不用配置
@Data
@Entity
@Table(name = "t_permission")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "permission_id")
private Long permissionId;
@Column(name = "permission_name")
private String permissionName;
}
测试代码:
/**
* 单向多对多
*/
@Test
public void testManyToMany(){
EntityManager entityManager = JpaUtils.getEntityManager();
// 开启事务
entityManager.getTransaction().begin();
// 增加权限实际
Permission permissionA = new Permission();
permissionA.setPermissionName("增加");
Permission permissionB = new Permission();
permissionB.setPermissionName("查询");
entityManager.persist(permissionA);
entityManager.persist(permissionB);
entityManager.persist(permissionB);
// 增加角色数据
Role role = new Role();
role.setRoleName("网络管理员");
role.getPermissions().add(permissionA);
role.getPermissions().add(permissionB);
entityManager.persist(role);
// 提交更新事务
entityManager.getTransaction().commit();
// 查询角色信息
entityManager.clear();
System.out.println(entityManager.find(Role.class,role.getRoleId()));
entityManager.close();
}
查看日志1:(建表语句)
Hibernate:
create table t_permission (
permission_id bigint not null auto_increment,
permission_name varchar(255),
primary key (permission_id)
) engine=InnoDB
Hibernate:
create table t_role (
role_id bigint not null auto_increment,
role_name varchar(255),
primary key (role_id)
) engine=InnoDB
Hibernate:
create table t_role_permission (
role_id bigint not null,
permission_id bigint not null,
primary key (role_id, permission_id)
) engine=InnoDB
Hibernate:
alter table t_role_permission
add constraint FKjobmrl6dorhlfite4u34hciik
foreign key (permission_id)
references t_permission (permission_id)
Hibernate:
alter table t_role_permission
add constraint FK90j038mnbnthgkc17mqnoilu9
foreign key (role_id)
references t_role (role_id)
查看日志2:(数据插入与查询)
// ..数据插入代码省略
Role(roleId=1, roleName=网络管理员, permissions=[Permission(permissionId=2, permissionName=查询), Permission(permissionId=1, permissionName=增加)])
3.2、双向多对多
发出端(拥有端)代码不变
@Data
@Entity
@Table(name = "t_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
/**
* @JoinTable:
* name:中间表名称
* joinColumns 中间表对应本类的信息
* @JoinColumn;
* name:本类的外键字段(中间表的数据库字段)
* referencedColumnName:本类与外键(表)对应的主键(本类的主键字段)
* inverseJoinColumns 中间表对应对方类的信息
* @JoinColumn:
* name:对方类的外键(中间表的数据字段)
* referencedColumnName:对方类与外键(表)对应的主键(对方类的主键字段)
*/
@ManyToMany
@JoinTable(name="t_role_permission", // 中间表明
joinColumns=@JoinColumn(
name="role_id", // 本类的外键
referencedColumnName = "role_id"), // 本类与外键(表)对应的主键
inverseJoinColumns=@JoinColumn(
name="permission_id", // 对方类的外键
referencedColumnName = "permission_id")) // 对方类与外键(表)对应的主键
private Set<Permission> permissions = new HashSet<>();
}
接收端(被拥有端)代码增加了@ManyToMany注解和mappedBy属性
@Data
@Entity
@Table(name = "t_permission")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "permission_id")
private Long permissionId;
@Column(name = "permission_name")
private String permissionName;
/**
* @ManyToMany
* mappedBy:对方类应用该类的属性名
* cascade | fetch | targetEntity 为可选属性
*/
@ManyToMany(mappedBy = "permissions")
private Set<Role> role = new HashSet<>();
}
测试代码:
/**
* 单向/双向 多对多查询代码
*/
@Test
public void testManyToManyFind(){
EntityManager entityManager = JpaUtils.getEntityManager();
System.out.println(entityManager.find(Role.class,1L));
System.err.println("--------------华丽的分割线-------------------");
System.out.println(entityManager.find(Permission.class,1L));
entityManager.close();
}
可以发现单向查询依旧正常,而双向查询依旧会有死循环问题
SpringData JPA 快速入门
1、SpringData JPA 简介
1、Spring Data JPA 认识
参考官网:https://spring.io/projects/spring-data-jpa
SpringData:其实SpringData就是Spring提供了一个操作数据的框架。而SpringData JPA只是SpringData框架下的一个基于JPA标准操作数据的模块,简化操作持久层的代码。只需要编写接口就可以。
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,可使开发者用极简的代码即可实现对数据库的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用 Spring Data JPA 可以极大提高开发效率!
Spring Data JPA 让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现,在实际的工作工程中,推荐使用Spring Data JPA + ORM(如:hibernate)完成操作,这样在切换不同的ORM框架时提供了极大的方便,同时也使数据库层操作更加简单,方便解耦
2、Spring Data JPA 特性
Features:
- Sophisticated support to build repositories based on Spring and JPA
- Support for Querydsl predicates and thus type-safe JPA queries
- Transparent auditing of domain class
- Pagination support, dynamic query execution, ability to integrate custom data access code
- Validation of @Query annotated queries at bootstrap time
- Support for XML based entity mapping
- JavaConfig based repository configuration by introducing @EnableJpaRepositories.
SpringData Jpa
极大简化了数据库访问层代码。 使用了SpringData Jpa
,我们的dao层中只需要写接口,就自动具有了增删改查、分页查询等方法。
3、hibernate 与 JPA 和 Spring Data JPA 的关系
Java代码-----》Spring Data JPA-----》JPA 规范-----》hibernate-----》封装JDBC操作-----》数据库(mysql)
- JPA是一套规范,内部是有接口和抽象类组成的。
- hibernate是一套成熟的ORM框架,而且Hibernate实现了JPA规范,所以也可以称 hibernate 为 JPA 的一种实现方式,我们使用 JPA 的 API 编程,意味着站在更高的角度上看待问题(面向接口编程)
- Spring Data JPA 是 Spring 提供的一套对JPA操作更加高级的封装,是在JPA规范下的专门用来进行数据持久化的解决方案
2、SpringBoot 集成 JPA
1、pom.xml导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<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.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>
</dependencies>
2、application.properties
配置数据源信息
# 数据库配置信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springdata_jpa?useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 控制台打印SQL
spring.jpa.show-sql=true
# 格式化SQL
spring.jpa.properties.hibernate.format_sql=true
# 自动建表模式:create-drop,create,update,validate,none
spring.jpa.hibernate.ddl-auto=update
# 建表使用MyISAM引擎,默认是InnoDB
# spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
3、新建User实体类
@Data
@Entity
@Table(name = "sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
}
4、编写Dao接口
// 接口内可以什么也不写
public interface UserRepository extends JpaRepository<User, Long> {
}
5、测试
@SpringBootTest
public class JpaApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
@Transactional// 在测试类对于事务提交方式默认的是回滚。
@Rollback(false)//取消自动回滚
public void save() {
User user = new User();
user.setUsername("admin");
user.setPassword("password");
userRepository.save(user);
}
@Test
public void find(){
System.out.println(userRepository.findAll());
System.out.println(userRepository.findById(1L));
}
}
查看日志:
Hibernate:
insert
into
sys_user
(username, password)
values
(?, ?)
3、SpringBoot+JPA+H2
1、使用IDEA创建SpringBoot项目。
2、导入依赖 pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 可以不配置版本号,默认就是最新版本 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<!--<artifactId>1.4.200</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>
</dependencies>
2、application.properties
配置
#################### H2 数据库配置 ####################
# 文件数据库
# spring.datasource.url = jdbc:h2:file:./dbh2/dbc2m;AUTO_SERVER=TRUE
# 内存数据库,platform #表明使用的数据库平台是 h2
spring.datasource.url = jdbc:h2:mem:test_jpa
spring.datasource.driverClassName =org.h2.Driver
spring.datasource.username = sa
spring.datasource.password = sa
spring.datasource.platform = h2
#################### H2 Web Console设置 ####################
# enabled:程序开启时就会启动 H2 Web Console, 默认就是启动的
# path:配置访问地址为:/h2,访问 H2 Web Console,默认:/h2-console
# settings.web-allow-others:开启H2 Web Console远程访问,默认为false不开启只能在本机访问
spring.h2.console.enabled = true
spring.h2.console.path = /h2
spring.h2.console.settings.web-allow-others = true
#################### JPA 配置 ####################
# 控制台打印SQL、格式化SQL、设置ddl模式(如果数据库是内存模式,ddl-auto配置没有什么作用)、设置方言
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.format_sql = true
spring.jpa.hibernate.ddl-auto = update
spring.jpa.database-platform = org.hibernate.dialect.H2Dialect
3、其他启动和测试一样的
SpringData JPA 接口查询
1、SpringData 接口继承结构
- Repository:最顶层的接口,是一个空的接口,目的是为了统一所有Repository的类型,且能让组件扫描的时候自动识别。
- CrudRepository :是Repository的子接口,提供CRUD的功能
- PagingAndSortingRepository:是CrudRepository的子接口,添加分页和排序的功能
- JpaRepository:是PagingAndSortingRepository的子接口,增加了一些实用的功能,比如:批量操作等。
- JpaSpecificationExecutor:用来做负责查询的接口
- Specification:是SpringData JPA提供的一个查询规范,要做复杂的查询,只需围绕这个规范来设置查询条件
2、Repository 接口
Repository 接口自带了如下两种查询方式
- 提供了方法名称命名查询方式
- 提供了@Query注解查询与更新
2.1、方法名称命名规则查询
SpringData里面有一种很不错的操作,那就是在接口中定义方法而不用实现也不用注解就可以实现其查询操作,但是写方法名查询是有一套规范的,这里我在官方文档里面整理出来了分享给大家【注意:方法名命名规则不能对其增加(insert)和修改(update)】
规则: findBy(关键字) + 属性名称(属性名称的首字母大写) + 查询条件(首字母大写)
1、常用规则速查
关键字 | 方法命名 | sql where字句 |
---|---|---|
And | findByNameAndPwd | where name= ? and pwd =? |
Or | findByNameOrSex | where name= ? or sex=? |
Is、Equals | findById、findByIdEquals | where id = ? |
Between | findByIdBetween | where id between ? and ? |
LessThan | findByIdLessThan | where id < ? |
LessThanEquals | findByIdLessThanEquals | where id <= ? |
GreaterThan | findByIdGreaterThan | where id > ? |
GreaterThanEquals | findByIdGreaterThanEquals | where id > = ? |
After | findByIdAfter | where id > ? |
Before | findByIdBefore | where id < ? |
IsNull | findByNameIsNull | where name is null |
isNotNull,NotNull | findByNameNotNull | where name is not null |
Like | findByNameLike | where name like ? |
NotLike | findByNameNotLike | where name not like ? |
StartingWith | findByNameStartingWith | where name like '?%' |
EndingWith | findByNameEndingWith | where name like '%?' |
Containing | findByNameContaining | where name like '%?%' |
OrderBy | findByIdOrderByXDesc | where id=? order by x desc |
Not | findByNameNot | where name <> ? |
In | findByIdIn(Collection<?> c) | where id in (?) |
NotIn | findByNameNot | where name <> ? |
True | findByAaaTue | where aaa = true |
False | findByAaaFalse | where aaa = false |
IgnoreCase | findByNameIgnoreCase | where UPPER(name)=UPPER(?) |
top | findTop10 | top 10 / where ROWNUM <=10 |
2、SpringData
方法名查询的具体规则
1、规则描述
按照Spring data 定义的规则,查询方法以find|read|get开头(比如 find、findBy、read、readBy、get、getBy),涉及条件查询时,条件的属性用条件关键字连接,要注意的是:条件属性首字母需大写。框架在进行方法名解析时,会先把方法名多余的前缀截取掉,然后对剩下部分进行解析。
如果方法的最后一个参数是 Sort 或者 Pageable 类型,也会提取相关的信息,以便按规则进行排序或者分页查询。
2、举例说明
比如 findByUserAddressZip()。框架在解析该方法时,首先剔除 findBy,然后对剩下的属性进行解析,详细规则如下(此处假设该方法针对的域对象为 AccountInfo 类型):
- 先判断 userAddressZip (根据POJO规范,首字母变为小写)是否为 AccountInfo 的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,继续第二步;
- 从右往左截取第一个大写字母开头的字符串(此处为 Zip),然后检查剩下的字符串是否为 AccountInfo 的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后假设 user 为 AccountInfo 的一个属性;
- 接着处理剩下部分(AddressZip )先判断 user 所对应的类型是否有 addressZip 属性,如果有,则表示该方法最终是根据"AccountInfo.user.addressZip" 的取值进行查询;否则继续按照步骤 2 的规则从右往左截取,最终表示根据"AccountInfo.user.address.zip" 的值进行查询。
可能会存在一种特殊情况,比如 AccountInfo 包含一个 user 的属性,也有一个 userAddress 属性,此时会存在混淆。读者可以明确在属性之间加上 "" 以显式表达意图,比如 "findByUser_AddressZip()" 或者 "findByUserAddress_Zip()"。**(强烈建议:无论是否存在混淆,都要在不同类层级之间加上"" ,增加代码可读性)**
3、方法名查询其他各种情况(重点必看)
1、当查询条件为null时
举例说明如下:
- 实体定义:对于一个客户实体User,包含有username和password,均是`1234567890-类型。
- 查询方法定义:
List<User> findByUsernameAndPassword(String username,String password);
- 使用时:
dao.findByUsernameAndPassword(null, "123456");
- 后台生成sql片断:
where (user0_.username is null) and user0_.password=?
- 结论:当查询时传值是null时,数据库中只有该字段是null的记录才符合条件,并不是说忽略这个条件。也就是说,这种查询方式,只适合于明确查询条件必须传的业务,对于动态查询,这种简单查询是不能满足要求的。
2、排序功能
/**
* 假如你需要所有的数据而不需要按照条件查询,就要按照这样来写:
* findBy/getBy/queryBy.. + OrderBy + 排序字段 + 排序方式
* 按照 username 默认正序排序
* ..order by username
*/
List<User> findByOrderByUsername();
/**
* 查询全部数据:先按age字段倒序再按照id字段正序排序
* ..order by age desc, id asc
*/
List<User> findByOrderByAgeDescIdAsc();
/**
* 查询名称结果集按正序排序:
* ...where username = ? order by age asc
*/
List<User> findByUsernameOrderByAgeAsc(String username);
/**
* 查询名称结果集按倒序排序:
* ...where username = ? order by id desc
*/
List<User> findByUsernameOrderByIdDesc(String username);
3、限定查询结果集大小
/**
* 说明:对表的全部数据根据age进行Asc(升序)排序后再选择第一条数据返回
* SQL:...order by age asc limit 1
* 注意:findFirst3ByOrderByAgeAsc()/findTop3ByOrderByAgeAsc()则每次返回3条 limit 3
*/
User findFirstByOrderByAgeAsc();
User findTopByOrderByAgeAsc();
/**
* 说明:首先进行数据查询并通过Sort进行排序后 再筛选数据列表中最前面的2行数据返回 limit 2
* SQL:where username = ? order by field asc/desc
*/
List<User> findFirst2ByUsername(String username, Sort sort);
List<User> findTop2ByUsername(String username,Sort sort);
/**
* 说明:首先进行数据查询,查询全部指定的username后进行分页,分页完后进行数据控制
* 控制说明:
* 关于带分页的控制是,假设分页过后查询的数据id为3,4,5 查询出这三条数据后进行数据控制,
* 本案例是控制为2条,那返回的id数据就是3,4两条记录,id为5的就舍弃,
* 那么如果数据控制是5条(Top5),那么就会打印3,4,5另外再去添加6,7补充数据长度
*/
Page<User> queryFirst2ByUsername(String username, Pageable pageable);
List<User> queryTop2ByUsername(String username, Pageable pageable);
总结:这里的这里的First和Top意思相同都是最前面,query也和find一样;
4、计数
/**
* 计数 返回总数
* countBy()等同于countAllBy()
* SQL:
* select count(id) from sys_user where username = ?
* select count(id) from sys_user
*/
Long countByUsername(String username);
Long countAllBy();
5、删除
/**
* 说明:必须添加事务和回滚,这样根据条件找到几条就删几条
* 对应的SQL语句:
* Hibernate: select * from sys_user where username = ?
* Hibernate: delete from sys_user where id=?
* Hibernate: delete from sys_user where id=?
* ....
* 关于removeBy和deleteBy方法区别:
* 他们是一样的 .选择哪一个取决于您的个人偏好.
*/
void deleteByUsername(String username);
void removeByUsername(String username);
2.2、基于@Query查询与更新
通过 JPQL 语句查询
JPQL:通过 Hibernate 的 HQL 演变过来的。他和 HQL 语法及其相似。
1、创建Repository接口:
/**
* Repository接口:使用@Query注解查询JPQL
* JPQL查询 和 SQL查询,区别在于nativeQuery属性。
* nativeQuery:默认的是false,为JPQL查询。true为开启SQL查询。
* 更新删除操作需要加上 @Modifying 注解(注意:不支持新增)
*/
public interface UserRepositoryJPQL extends Repository<User, Long> {
/**
* ? 展示位置参数绑定:?1
* 必须使用?1形式,如果有多个参数以此类推,如: ?1,?2..
* JPQL如果只输入?查询会报错,SQL查询不会
*/
@Query("from User where username = ?1")
List<User> queryUserByUsernameUseJPQL(String username);
/**
* : 展示名字参数绑定::username
* 注意:使用 :username 绑定,必须在参数前加@Param("username")
*/
@Query("from User where username like :username")
List<User> queryUserByLikeUsernameUseJPQL(@Param("username")String username);
@Query("from User where username = :username and age > :age")
List<User> queryUserByUsernameAndAgeUserJPQL(@Param("username")String username, @Param("age")Integer age);
@Query("update User set username = ?1 where id = ?2")
@Modifying // 需要执行一个更新操作
Integer updateUsernameById(String username,Long id);
}
2、测试类:
@SpringBootTest
public class UserRepositoryJPQLTests {
@Autowired
private UserRepositoryJPQL userRepositoryJPQL;
/**
* 测试@Query查询 JPQL
*/
@Test
public void test1(){
List<User> listA = userRepositoryJPQL.queryUserByUsernameUseJPQL("王五");
listA.forEach(System.out::println);
}
@Test
public void test2(){
List<User> list = userRepositoryJPQL.queryUserByLikeUsernameUseJPQL("%王%");
list.forEach(System.out::println);
}
@Test
public void test3(){
List<User> list = userRepositoryJPQL.queryUserByUsernameAndAgeUserJPQL("隔壁小王", 15);
list.forEach(System.out::println);
}
/**
* 更新删除操作记得加上事务操作,不然会报错:
* Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query
*/
@Test
@Transactional //@Transactional与@Test 一起使用时 事务是自动回滚的。
@Rollback(false) //取消自动回滚
public void test4(){
Integer integer = userRepositoryJPQL.updateUsernameById("隔壁小王", 5L);
System.out.println("更新数据行数:" + integer);
}
}
3、查看日志(更新操作的)
Hibernate:
update
sys_user
set
username=?
where
id=?
更新数据行数:1
通过 SQL 语句查询
1、创建Repository接口:
/**
* Repository接口:使用@Query注解查询SQL
* JPQL查询 和 SQL查询,区别在于nativeQuery属性。
* nativeQuery:默认的是false.表示不开启sql查询。是否对value中的语句做转义
* 更新删除操作需要加上 @Modifying 注解(注意:不支持新增)
*/
public interface UserRepositorySQL extends Repository<User, Long> {
/**
* ? 展示位置参数绑定:? 或者 ?1
*/
@Query(value = "select * from sys_user where username = ?1",nativeQuery=true)
List<User> queryUserByUsernameUseSQL(String username);
/**
* 如果有多个参数以此类推,如: ?1,?2..或者 ? ?....
* 如果只用?的话,方法中的参数必须按顺序填写,如果 ?1 则不用
* JPQL查询如果只输入?查询会报错,SQL查询不会
*/
@Query(value = "select * from sys_user where username = ?1 and age >= ?2",nativeQuery=true)
List<User> queryUserByUsernameAndAgeUseSQL(String name, Integer age);
/**
* : 展示名字参数绑定::username
* 注意:使用 :username 绑定,必须在参数前加@Param("username")
*/
@Query(value = "select * from sys_user where username like :username",nativeQuery=true)
List<User> queryUserByLikeUsernameUseSQL(@Param("username") String username);
@Query(value = "update sys_user set username = ? where id = ?",nativeQuery=true)
@Modifying // 需要执行一个更新操作
Integer updateUsernameById(String username,Long id);
}
2、测试代码
@SpringBootTest
public class UserRepositorySQLTests {
@Autowired
private UserRepositorySQL userRepositorySQL;
/**
* 测试@Query查询 SQL
*/
@Test
public void test1(){
List<User> list = this.userRepositorySQL.queryUserByUsernameUseSQL("隔壁小王");
list.forEach(System.out::println);
}
@Test
public void test2(){
List<User> list = this.userRepositorySQL.queryUserByUsernameAndAgeUseSQL("隔壁小王", 15);
list.forEach(System.out::println);
}
@Test
public void test3(){
List<User> list = this.userRepositorySQL.queryUserByLikeUsernameUseSQL("%王%");
list.forEach(System.out::println);
}
/**
* 更新删除操作记得加上事务操作,不然会报错:
* Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query
*/
@Test
@Transactional //@Transactional与@Test 一起使用时 事务是自动回滚的。
@Rollback(false) //取消自动回滚
public void test4(){
Integer integer = userRepositorySQL.updateUsernameById("隔壁小王", 5L);
System.out.println("更新数据行数:" + integer);
}
}
3、查看日志
Hibernate:
update
sys_user
set
username = ?
where
id = ?
更新数据行数:1
从日志上看JPQL与SQL查询看不出什么区别,不过使用SpringData时推荐还是使用JPQL方式。
总结:
1、可以通过自定义的 JPQL 完成 UPDATE 和 DELETE 操作。 注意: JPQL不支持使用 INSERT
2、在 @Query 注解中编写 JPQL 语句, 如果是UPDATE或DELETE 操作,必须使用 @Modifying修饰.
3、UPDATE 或 DELETE 操作需要使用事务,此时需要定义 Service 层,在 Service 层的方法上添加事务操作;
4、默认情况下, SpringData的每个方法上有事务, 但都是一个只读事务。 他们不能完成修改操作。
3、CrudRepository 接口
CrudRepository接口,主要是完成一些增删改查的操作。注意:CrudRepository接口继承了Repository接口
1、创建Repository层接口
/**
* CrudRepository 接口
* 主要是完成一些增删改查的操作。注意:CrudRepository接口继承了Repository接口
*/
public interface UserCrudRepository extends CrudRepository<User,Long> {
}
2、测试代码
package com.example.jpa;
import com.example.jpa.entity.User;
import com.example.jpa.repository.UserCrudRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import javax.transaction.Transactional;
import java.util.Arrays;
import java.util.Optional;
@SpringBootTest
public class UserCrudRepositoryTests {
@Autowired
private UserCrudRepository userCrudRepository;
/**
* 数据保存:
* 1.添加单条数据
* 2.批量添加数据
*/
@Test
public void testSave(){
// 保存单个数据
User user = new User();
user.setUsername("隔壁小王");
user.setPassword("password");
user.setAge(18);
User save = this.userCrudRepository.save(user);
// 保存多条数据
User userA = new User();
userA.setUsername("赵小丽");
userA.setPassword("666666");
userA.setAge(21);
User userB = new User();
userB.setUsername("王小虎");
userB.setPassword("123456");
userB.setAge(25);
Iterable<User> saveAll = this.userCrudRepository.saveAll(Arrays.asList(userA, userB));
}
/**
* 数据查询:
* 1.根据ID查询单条数据
* 2.查询全部数据
* 3.根据多个id查询多条数据
* 4.根据ID判断数据是否存在
* 5.查询该表(实体类)有多少数据
*/
@Test
public void testFind(){
// 1.根据ID查询单条数据
Optional<User> user = this.userCrudRepository.findById(1L);
System.out.println(user.get());
System.err.println("--------------华丽的分割线-------------------");
// 2.查询全部数据
Iterable<User> userIterableA = this.userCrudRepository.findAll();
userIterableA.forEach(System.out::println);
System.err.println("--------------华丽的分割线-------------------");
// 3.根据多个id查询多条数据
Iterable<User> userIterableB = this.userCrudRepository.findAllById(Arrays.asList(1L, 2L));
userIterableB.forEach(System.out::println);
System.err.println("--------------华丽的分割线-------------------");
// 4.根据ID判断数据是否存在
System.out.println(this.userCrudRepository.existsById(1L));
System.err.println("--------------华丽的分割线-------------------");
// 5.查询该表(实体类)有多少数据
System.out.println(this.userCrudRepository.count());
}
/**
* 删除数据
* 1.根据id删除
* 2.根据实体类删除单个数据
* 3.根据实体类数组删除多调数据
* 4.删除全部数据
*/
@Test
public void testDelete(){
// 1.根据id删除
this.userCrudRepository.deleteById(1L);
// 2.根据实体类删除单个数据
User user = this.userCrudRepository.findById(2L).get();
this.userCrudRepository.delete(user);
// 3.根据实体类数组删除多调数据
this.userCrudRepository.deleteAll(this.userCrudRepository.findAllById(Arrays.asList(1L, 2L)));
// 4.删除全部数据
this.userCrudRepository.deleteAll();
}
/**
* 更新数据 方式一
*/
@Test
public void testUpdate1(){
User user = this.userCrudRepository.findById(1L).get();
user.setUsername("王小红");
this.userCrudRepository.save(user);
}
/**
* 更新数据 方式二
*/
@Test
@Transactional
@Rollback(false)
public void testUpdate2(){
// 持久化状态的
User user = this.userCrudRepository.findById(1L).get();
user.setUsername("王小小");
}
}
4、PagingAndSortingRepository 接口
该接口提供了分页与排序的操作,注意:该接口继承了CrudRepository接口
1、创建Repository层接口
/**
* PagingAndSortingRepository 接口:
* 该接口提供了分页与排序的操作,注意:该接口继承了CrudRepository接口
*/
public interface UserPagingAndSortingRepository extends PagingAndSortingRepository<User,Integer> {
}
2、测试代码
package com.example.jpa;
import com.example.jpa.entity.User;
import com.example.jpa.repository.UserPagingAndSortingRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@SpringBootTest
public class UserPagingAndSortingTests {
@Autowired
private UserPagingAndSortingRepository userPagingAndSortingRepository;
/**
* 分页操作
*/
@Test
public void test1(){
/**
* Pageable:封装了分页的参数,当前页,每页显示的条数。注意:当前页是从0开始
* PageRequest(page,size):page表示当前页,size表示每页显示多少条
*/
Pageable pageable = PageRequest.of(0, 2);
Page<User> page = this.userPagingAndSortingRepository.findAll(pageable);
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("当前第几页: " + (page.getNumber() + 1));
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页面的 List: " + page.getContent());
System.out.println("当前页面的记录数: " + page.getNumberOfElements());
page.getContent().forEach(System.out::println);
}
/**
* 排序操作:
* 1.对单列做排序处理
* 2.多列的排序处理
*/
@Test
public void test2(){
/**
* Sort:该对象封装了排序规则以及指定的排序字段(对象的属性来表示)
* direction:排序规则
* properties:指定做排序的属性
*/
// 1.对单列做排序处理,对id做降序排序
Sort sortOne = Sort.by(Sort.Direction.DESC,"id");
Iterable iterable = this.userPagingAndSortingRepository.findAll(sortOne);
iterable.forEach(System.out::println);
System.err.println("--------------华丽的分割线-------------------");
/**
* 可以用如下两种方式得到Order
* 1.new Sort.Order()
* 2.Sort.Order.desc()/asc()/by()
*/
// 2.多列的排序处理,先对age列做降序处理,然后对id字段做升序处理
Sort.Order age = Sort.Order.desc("age");
Sort.Order id = new Sort.Order(Sort.Direction.ASC,"id");
Sort sortMany = Sort.by(age,id);
this.userPagingAndSortingRepository.findAll(sortMany).forEach(System.out::println);
}
/**
* 分页 + 排序 处理
*
*/
@Test
public void test3(){
// 排序操作对象
Sort.Order age = Sort.Order.desc("age");
Sort.Order id = Sort.Order.asc("id");
Sort sort = Sort.by(age,id);
// 排序操作还是没变化,只需要在PageRequest.of内多加一个Sort参数
Pageable pageable = PageRequest.of(0, 2 ,sort);
Page<User> page = this.userPagingAndSortingRepository.findAll(pageable);
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("当前第几页: " + (page.getNumber() + 1));
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页面的记录数: " + page.getNumberOfElements());
System.out.println("当前页面的 List: " + page.getContent());
System.err.println("--------------华丽的分割线-------------------");
page.getContent().forEach(System.out::println);
}
}
查看日志test3():
Hibernate:
select
user0_.id as id1_0_,
user0_.age as age2_0_,
user0_.password as password3_0_,
user0_.username as username4_0_
from
sys_user user0_
order by
user0_.age desc,
user0_.id asc limit ?
Hibernate:
select
count(user0_.id) as col_0_0_
from
sys_user user0_
总记录数: 3
当前第几页: 1
总页数: 2
当前页面的记录数: 2
当前页面的 List: [User(id=6, username=王小虎, password=123456, age=25), User(id=5, username=赵小丽, password=666666, age=21)]
--------------华丽的分割线-------------------
User(id=6, username=王小虎, password=123456, age=25)
User(id=5, username=赵小丽, password=666666, age=21)
5、JpaRepository 接口
该接口继承了PagingAndSortingRepository。是我们开发时使用的最多的接口。其特点是可以帮助我们将其他接口的方法的返回值做适配处理。可以使得我们在开发时更方便的使用这些方法。
1、创建Repository层接口
/**
* JpaRepository 接口
* 该接口继承了PagingAndSortingRepository。是我们开发时使用的最多的接口。
* 接口其特点是可以帮助我们将其他接口的方法的返回值做适配处理。可以使得我们在开发时更方便的使用这些方法。
*/
public interface UserJpaRepository extends JpaRepository<User,Long> {
}
2、测试代码
package com.example.jpa;
import com.example.jpa.entity.User;
import com.example.jpa.repository.UserJpaRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Sort;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@SpringBootTest
public class UserJpaRepositoryTests {
@Autowired
private UserJpaRepository userJpaRepository;
/**
* 这几个方法跟之前的接口内的方法虽然一样,但是最返回值做了自适应
* 返回值都由 Iterable 变成了 List
*/
@Test
public void test1(){
List<User> usersA = this.userJpaRepository.findAll();
usersA.forEach(System.out::println);
System.err.println("--------------华丽的分割线-------------------");
List<User> usersB = this.userJpaRepository.findAll(Sort.by("age"));
usersB.forEach(System.out::println);
System.err.println("--------------华丽的分割线-------------------");
List<User> usersC = this.userJpaRepository.findAllById(Arrays.asList(1L, 2L));
usersC.forEach(System.out::println);
System.err.println("--------------华丽的分割线-------------------");
User user = new User();
user.setUsername("隔壁小王");
user.setPassword("password");
user.setAge(18);
List<User> users = this.userJpaRepository.saveAll(Arrays.asList(user));
System.out.println(users);
}
/**
* JpaRepository 接口内还新增了一方法
* flush():强制写入数据库
* saveAndFlush:保存并且立马写入数据库(不需要等事务走完就写入数据库)
*/
@Test
public void test2(){
User user = new User();
user.setUsername("隔壁小王");
user.setPassword("password");
user.setAge(18);
// 立马写入数据库
User saveAndFlush = this.userJpaRepository.saveAndFlush(user);
System.out.println(saveAndFlush);
}
/**
* 批量删除方法:deleteInBatch(Iterable<T> var1) 和 deleteAllInBatch()
* 之前CrudRepository 接口内也有删除方法:delete、deleteById、deleteAll
* 对比:deleteAll() 和 deleteAllInBatch() 区别:
* deleteAll()是删除全部,先findALL查找出来,再一条一条删除,最后提交事务。
* deleteAllInBatch()是删除全部,一条sql。显然:deleteAllInBatch()方法效率更高
*
* 分别执行批量删除方法,查看日志对比.可以得出结论:
* 在进行批量删除操作时尽量使用JpaRepository自带的批量删除方法deleteInBatch()及deleteAllInBatch()
*
* 可参考:https://jingyan.baidu.com/article/597a06431601fb312b52431c.html
*/
@Test
public void test3(){
// 不推荐使用
this.userJpaRepository.deleteAll();
}
@Test
public void test4(){
// 推荐使用
this.userJpaRepository.deleteAllInBatch();
}
/**
* getOne()与findById()方法:延迟加载/立马加载
* 区别:
* getOne()属于JpaRepository,底层调用getReference()方法,懒加载的模式,当调用对象时才会发送SQL。
* findById()属于,底层调用了find()方法,当调用该方法的时就直接发送SQL
* getOne()返回一个实体的引用,无结果会抛出异常
* findById()返回一个Optional对象
*/
@Test
public void test5(){
// getOne()返回一个实体的引用,无结果会抛出异常
User user = this.userJpaRepository.getOne(1L);
System.out.println(user);
// findById()返回一个Optional对象,如果Optional为空get()会抛异常
Optional<User> optional = this.userJpaRepository.findById(1L);
System.out.println(optional.orElse(null));
}
}
查看日志:注意:getOne()可能会报错。
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.jpa.entity.User#4] - no Session
原因:没有开启懒加载
解决方案:
1、在出问题的实体类上加@Proxy(lazy = false)
@Proxy(lazy = false)
public class User {
}
2、application.properties增加配置
# 开启延迟加载
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
6、JpaSpecificationExecutor 接口
该接口主要是提供了多条件查询(动态查询),并且可以在查询中添加排序与分页。
注意:
JpaSpecificationExecutor
不能单独使用,需要配合着JPA
中的其他接口一起使用
JpaSpecificationExecutor
接口中的方法
/**
* JpaSpecificationExecutor:用来做动态查询的接口
* Specification:是SpringDataJPA提供的一个查询规范,要做复杂的查询,只需围绕这个规范来设置查询条件。
* JpaSpecificationExecutor接口下一共就5个接口方法
*/
public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> spec);// 查询单个
List<T> findAll(Specification<T> spec);// 查询全部
Page<T> findAll(Specification<T> spec, Pageable pageable);// 查询全部【分页】
List<T> findAll(Specification<T> spec, Sort sort);// 查询全部【排序】
long count(Specification<T> spec);// 统计总数
}
Specification
是我们传入进去的查询参数,实际上它是一个接口,并且只有一个方法:
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
1、创建Repository层接口
/**
* JpaSpecificationExecutor 接口
* 该接口主要是提供了多条件查询的支持,并且可以在查询中添加排序与分页。
* 注意:JpaSpecificationExecutor不能单独使用,需要配合着jpa中的其他接口一起使用
*/
public interface UserJpaSpecificationExecutor extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}
2、测试类:
条件查询
package com.example.jpa;
import com.example.jpa.entity.User;
import com.example.jpa.repository.UserJpaSpecificationExecutor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@SpringBootTest
public class JpaApplicationTests4 {
@Autowired
private UserJpaSpecificationExecutor userJpaSpecificationExecutor;
/**
* 条件查询——单条件
* Predicate 过滤条件
* jpql:from User where username = '隔壁小王'
* sql:select * from sys_user where username = '隔壁小王'
*/
@Test
public void test1(){
Specification<User> spec = new Specification<User>() {
/**
* @param root 根对象。封装了查询条件的对象
* @param criteriaQuery 定义了一个基本的查询。一般使用较少
* @param criteriaBuilder 创建一个查询条件
* @return Predicate:定义了查询条件
*/
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path<Object> username = root.get("username");
Predicate predicate = criteriaBuilder.equal(username, "隔壁小王");
return predicate;
}
};
List<User> userList = userJpaSpecificationExecutor.findAll(spec);
userList.forEach(System.out::println);
}
/**
* 多条件查询一
* jpql:from User where username = '隔壁小王' and password = 'password' and age < 30
* sql:select * from sys_user where username = '隔壁小王' and password = 'password' and age < 30
*/
@Test
public void test2(){
Specification<User> spec = (root, criteriaQuery, criteriaBuilder) -> {
Predicate username = criteriaBuilder.equal(root.get("username"), "隔壁小王");
Predicate password = criteriaBuilder.equal(root.get("password"), "password");
Predicate age = criteriaBuilder.lt(root.get("age"), 30);
List<Predicate> list = Arrays.asList(username, password, age);
return criteriaBuilder.and(list.toArray(new Predicate[]{}));
};
List<User> userList = userJpaSpecificationExecutor.findAll(spec);
userList.forEach(System.out::println);
}
/**
* 多条件查询二
* jpql:from User where username like '%小王' and password = 'password' and age < 30
* sql:select * from sys_user where username like '%小王' and password = 'password' and age < 30
*/
@Test
public void test3(){
Specification<User> spec = (root, criteriaQuery, criteriaBuilder) -> {
Predicate username = criteriaBuilder.like(root.get("username"), "%小王");
Predicate password = criteriaBuilder.equal(root.get("password"), "password");
Predicate age = criteriaBuilder.lt(root.get("age"), 30);
Predicate predicate = criteriaBuilder.and(username, password);
predicate = criteriaBuilder.and(predicate, age);
return predicate;
};
List<User> userList = userJpaSpecificationExecutor.findAll(spec);
userList.forEach(System.out::println);
}
}
分页查询
/**
* 分页操作
*/
@Test
public void testPageable(){
Specification<User> spec = (root, criteriaQuery, criteriaBuilder) -> {
Predicate username = criteriaBuilder.equal(root.get("username"), "隔壁小王");
Predicate password = criteriaBuilder.equal(root.get("password"), "password");
return criteriaBuilder.and(username, password);
};
//分页
Pageable pageable = PageRequest.of(0, 2);
Page<User> page = this.userJpaSpecificationExecutor.findAll(spec, pageable);
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("当前第几页: " + (page.getNumber() + 1));
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页面的 List: " + page.getContent());
System.out.println("当前页面的记录数: " + page.getNumberOfElements());
page.getContent().forEach(System.out::println);
}
排序查询
/**
* 排序操作
*/
@Test
public void testSort(){
Specification<User> spec = (root, criteriaQuery, criteriaBuilder) -> {
Predicate username = criteriaBuilder.equal(root.get("username"), "隔壁小王");
Predicate password = criteriaBuilder.equal(root.get("password"), "password");
return criteriaBuilder.and(username, password);
};
// 排序
Sort sort = Sort.by(Sort.Direction.DESC,"id");
List<User> users = this.userJpaSpecificationExecutor.findAll(spec, sort);
users.forEach(System.out::println);
}
分页+排序查询
/**
* 分页+排序 操作
*/
@Test
public void testPageableAndSort(){
Specification<User> spec = (root, criteriaQuery, criteriaBuilder) -> {
Predicate username = criteriaBuilder.equal(root.get("username"), "隔壁小王");
Predicate password = criteriaBuilder.equal(root.get("password"), "password");
return criteriaBuilder.and(username, password);
};
// 排序
Sort sort = Sort.by(Sort.Direction.DESC,"id");
// 分页
Pageable pageable = PageRequest.of(0, 2, sort);
Page<User> page = this.userJpaSpecificationExecutor.findAll(spec, pageable);
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("当前第几页: " + (page.getNumber() + 1));
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页面的 List: " + page.getContent());
System.out.println("当前页面的记录数: " + page.getNumberOfElements());
page.getContent().forEach(System.out::println);
}
多表查询
使用单向多对一模式,实体类:
/**
* 多方外键表
* 单向多对一模式
*/
@Data
@Entity
@Table(name = "sys_address")
public class UserAddress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "address_id")
private Long addressId;
private String country;
private String city;
@ManyToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
}
/**
* 一方主键表
*/
@Data
@Entity
@Table(name = "sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private Integer age;
}
测试代码:
查看日志:
Hibernate:
select
many0_.many_id as many_id1_3_,
many0_.many_name as many_nam2_3_,
many0_.one_id as one_id3_3_
from
t_many many0_
left outer join
t_one one1_
on many0_.one_id=one1_.one_id
where
one1_.one_type=?
and many0_.many_name=?
Hibernate:
select
one0_.one_id as one_id1_4_0_,
one0_.one_type as one_type2_4_0_
from
t_one one0_
where
one0_.one_id=?
Many(manyId=1, manyName=初一, one=One(oneId=1, oneType=初中))
注意(重点):
可以发现Specification重写方法里面的root
,criteriaQuery
和builder
都已经被Jpa赋值好了,我们只需要关注Predicate的构建,也就是说,在调用JpaSpecificationExecutor接口内的findXXX
方法只能完成where
条件的构建,而不能实现select
后面属性的选择和groupBy
的构建。
投影和分组查询
不过可以直接使用Criteria
方式来完成
1、创建接口实现代码类(为了简便没有写接口了,直接写了类)
package com.example.jpa.repository;
import com.example.jpa.entity.User;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.List;
@Repository
public class UserCriteriaRepository {
@PersistenceContext
private EntityManager entityManager;
/**
* 投影查询 — criteriaBuilder.createTupleQuery()
* sql:select username, age from sys_user
* 注意:
* 使用:返回元组(Tuple)的查询
*/
public List<Tuple> testFindAllMultiSelect(){
// 1.CriteriaBuilder 安全查询创建工厂
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// 2.CriteriaQuery 安全查询主语句
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
// 3.Root 定义查询的From子句中能出现的类型
Root<User> from = criteriaQuery.from(User.class);
// 需要给字段取别名,否则无法通过tuple.get(field)获取数据
criteriaQuery.multiselect(from.get("username").alias("username"), from.get("age").alias("age"));
TypedQuery<Tuple> query = entityManager.createQuery(criteriaQuery);
List<Tuple> list = query.getResultList();
return list;
}
/**
* 分组查询
* sql:select username,count(username),max(age),min(age),sum(age),avg(age) from sys_user group by username
*/
public List<Tuple> testFindGroupBy() {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
Root<User> from = criteriaQuery.from(User.class);
// .alias("name") 取别名
criteriaQuery.multiselect(
from.get("username").alias("username"),
criteriaBuilder.count(from.get("username")).alias("count"),
criteriaBuilder.max(from.get("age")).alias("max"),
criteriaBuilder.min(from.get("age")).alias("min"),
criteriaBuilder.sum(from.get("age")).alias("sum"),
criteriaBuilder.avg(from.get("age")).alias("avg"));
criteriaQuery.groupBy(from.get("username"));
// criteriaQuery.having(criteriaBuilder.disjunction());
List<Tuple> tupleList = entityManager.createQuery(criteriaQuery).getResultList();
return tupleList;
}
}
2、测试代码:
@SpringBootTest
public class UserCriteriaRepositoryTests {
@Autowired
private UserCriteriaRepository userCriteriaRepository;
@Test
public void testFindAllMultiSelect(){
List<Tuple> tuples = userCriteriaRepository.testFindAllMultiSelect();
// name = list.get(0).get(0); age = list.get(0).get(1)
tuples.forEach(x-> System.out.println(x.get("username")+"、"+x.get("age")));
}
@Test
public void testFindGroupBy(){
List<Tuple> tuples = userCriteriaRepository.testFindGroupBy();
tuples.forEach(x-> System.out.println(x.get("username") + "、" +
x.get("count") + "、" +
x.get("max") + "、" +
x.get("min") + "、" +
x.get("sum") + "、" +
x.get("avg"))
);
}
}
3、查看投影查询日志
Hibernate:
select
user0_.username as col_0_0_,
user0_.age as col_1_0_
from
sys_user user0_
赵小丽、21
隔壁小王、18
王小虎、25
隔壁小王、18
隔壁小王、18
4、查看分组查询日志
Hibernate:
select
user0_.username as col_0_0_,
count(user0_.username) as col_1_0_,
max(user0_.age) as col_2_0_,
min(user0_.age) as col_3_0_,
sum(user0_.age) as col_4_0_,
avg(user0_.age) as col_5_0_
from
sys_user user0_
group by
user0_.username
王小虎、1、25、25、25、25.0
赵小丽、1、21、21、21、21.0
隔壁小王、3、18、18、54、18.0
所以如果想投影查询和分组查询的话还得使用
Criteria
方式来查询。使用CriteriaBuilder、CriteriaQuery和Root来完成如果不需要构建Selection、groupBy时,也可以只构建
Predicate
,然后使用JPA的findAll()
方法即可。
7、自定义Repository接口
有时候系统提供的接口中的方法并不足以满足我们的需求,这时我们就可以通过自定义Repository来扩展。
1、定义接口
/**
* 自定义Repository接口
*/
public interface UserCustomRepository{
User findUserById(Long id);
}
2、创建接口实现类
public class UserCustomRepositoryImpl implements UserCustomRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public User findUserById(Long id) {
return entityManager.find(User.class,id);
}
}
3、使用接口
/**
* 用户自定义Repository接口讲解
*/
public interface UserDao extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User>, UserCustomRepository{
}
4、测试代码
@SpringBootTest
public class UserCustomRepositoryTests {
@Autowired
private UserDao userDao;
@Test
public void testFind(){
System.out.println(userDao.findUserById(6L));
}
}
5、查看日志
Hibernate:
select
user0_.id as id1_0_0_,
user0_.age as age2_0_0_,
user0_.password as password3_0_0_,
user0_.username as username4_0_0_
from
sys_user user0_
where
user0_.id=?
User(id=6, username=王小虎, password=123456, age=25)
SpringData JPA 数据表映射
1、映射注解说明
1.配置多表联系注解介绍
@OneToOne 一对一映射
targetEntityClass: 指定另一方类的字节码
cascade: 级联操作
CascadeType.MERGE 级联更新
CascadeType.PERSIST 级联保存
CascadeType.REFRESH 级联刷新
CascadeType.REMOVE 级联删除
CascadeType.ALL 级联上述4种操作
fetch: 抓取策略
FetchType.LAZY: 延迟加载(默认)
FetchType.EAGER: 迫切查询(多表连接查询)
mappedBy: 放弃外键维护
orphanRemoval: 是否使用孤儿删除
@OneToMany(一对多)
targetEntityClass: 指定多方类的字节码
cascade: 指定要使用的级联操作
fetch: 指定是否采用延迟加载
mappedBy: 指定从表实体类中引用主表对象的名称
orphanRemoval: 是否使用孤儿删除
@ManyToOne(多对一)
targetEntityClass: 指定一的一方实体类字节码
cascade: 指定要使用的级联操作
fetch: 指定是否采用延迟加载
optional: 关联是否可选。如果设置为false,则必须始终存在非空关系
@ManyToMany(多对多)
targetEntityClass: 指定另一方类的字节码
cascade: 配置级联操作
fetch: 配置延迟加载和立即加载
mappedBy: 放弃外键维护
2:配置外键关系的注解
@JoinColumn:用于定义主键字段和外键字段的对应关系。
属性:
name: 指定外键字段的名称
referencedColumnName: 指定引用主表的主键字段名称
unique: 是否唯一。默认值不唯一
nullable: 是否允许为空。默认值允许
insertable: 是否允许插入。默认值允许
updatable: 是否允许更新。默认值允许
columnDefinition: 列的定义信息
table
foreignKey
@JoinTable(针对中间表的设置)
name: 配置中间表的名称
joinColumns: 中间表的外键字段关联当前实体类所对应表的主键字段
@JoinColumn:
name: 本类的外键
referencedColumnName: 本类与外键(表)对应的主键
inverseJoinColumn: 中间表的外键字段关联对方表的主键字段
@JoinColumn:
name: 对方类的外键
referencedColumnName: 对方类与外键(表)对应的主键
3:首先确定多表之间的关系:
一对一和一对多:
一的一方称为主表,而多的一方称为从表,外键就要建立在从表上,它们的取值的来源主要来自主键
多对多:
这个时候需要建立一个中间表,中间表中最少由2个字段组成,这2个字段作为外键指向2张表的主键又组成了联合主键
2、一对一表设计
2.1、准备一对一
1、表结构和需求
需求:学生表与学生信息表的一对一的关联关系
学生表(主键表):一方
学生信息表(外键表):一方
学生表结构
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| student_id | bigint(20) | NO | PRI | NULL | auto_increment |
| student_name | varchar(255) | YES | | NULL | |
| student_pwd | varchar(255) | YES | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
学生信息表结构
+----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+----------------+
| information_id | bigint(20) | NO | PRI | NULL | auto_increment |
| student_age | int(11) | YES | | NULL | |
| student_number | varchar(255) | YES | | NULL | |
| student_id | bigint(20) | YES | MUL | NULL | |
+----------------+--------------+------+-----+---------+----------------+
2、创建2个实体类对应的Dao接口
/**
* 学生表repository接口
*/
public interface StudentRepository extends JpaRepository<Student,Long> {
}
/**
* 学生信息表repository接口
*/
public interface StudentInformationRepository extends JpaRepository<StudentInformation,Long> {
}
2.2、单向一对一
1、创建:学生信息实体类(拥有方,外键表)
@Data
@Entity
@Table(name = "tb_student_information")
public class StudentInformation implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "information_id")
private Long informationId;
@Column(name = "student_number")
private String studentNumber;
@Column(name = "student_age")
private Integer studentAge;
// 单向的话配置可以只配置拥有方(外键方)
@OneToOne
@JoinColumn(name = "student_id")
private Student student;
}
2、创建:学生实体类(被拥有方,主键表)
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
}
3、测试代码:
@SpringBootTest
public class AssociationTableTests {
@Autowired
StudentRepository studentRepository;
@Autowired
StudentInformationRepository informationRepository;
/**
* 单向一对一
*/
@Test
public void testOneToOneSave(){
// 首先给学生表(主键表)增加一条数据
Student student = new Student();
student.setStudentName("小明");
student.setStudentPwd("123456");
studentRepository.saveAndFlush(student);
// 然后再给学生信息表(外键表)增加一条数据
StudentInformation information = new StudentInformation();
information.setStudentNumber("001");
information.setStudentAge(13);
// 设置这个字段相当于给外键设置值,如果没有设置那么保存后外键字段将为null
information.setStudent(student);
informationRepository.saveAndFlush(information);
// 查询信息
System.out.println(teacherRepository.findById(student.getStudentId()));
System.out.println(informationRepository.findById(information.getInformationId()));
}
}
查看日志:
Hibernate:
insert
into
tb_student
(student_name, student_pwd)
values
(?, ?)
Hibernate:
insert
into
tb_student_information
(student_id, student_age, student_number)
values
(?, ?, ?)
Hibernate:
select
teacher0_.teacher_id as teacher_1_5_0_,
teacher0_.teacher_name as teacher_2_5_0_,
teacher0_.teacher_pwd as teacher_3_5_0_
from
tb_teacher teacher0_
where
teacher0_.teacher_id=?
Optional.empty
Hibernate:
select
studentinf0_.information_id as informat1_3_0_,
studentinf0_.student_id as student_4_3_0_,
studentinf0_.student_age as student_2_3_0_,
studentinf0_.student_number as student_3_3_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_information studentinf0_
left outer join
tb_student student1_
on studentinf0_.student_id=student1_.student_id
where
studentinf0_.information_id=?
Optional[StudentInformation(informationId=8, studentNumber=001, studentAge=13, student=Student(studentId=8, studentName=小明, studentPwd=123456))]
2.3、双向一对一
1、学生信息实体类(拥有方,外键表)不变
2、学生实体类(主键表)需要增加属性与注解
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
// 上面的属性与单向一对一是一样的,就增加了如下信息
/**
* mappedBy:放弃维护外键,值为另一方引用本类的属性名
*/
@OneToOne(mappedBy = "student")
private StudentInformation studentInformation;
}
3、测试代码与单向一对一也是一样的
4、查看日志:
// 插入语句与查询语句都省略了...
java.lang.StackOverflowError
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.example.jpa.entity.Student.toString(Student.java:13)
at java.lang.String.valueOf(String.java:2994)
可以发现会报如上栈溢出错误。原因:双向查询后使用(System.out)输出时,toString()
方式时各自对象相互引用导致死循环。
解决方案:
- 去除@Data注解,替换成@Getter和@Setter注解
- 也可以重写toString()方法,把引用类型的输出删掉即可
- 查询后可以不要输出打印该对象,这样就不会死循环输出对象中的对象了(治标不治本, 不推荐)
在实体类上增加@JsonIgnoreProperties(value = "对方类引用本类的属性名")
(暂时我试了时无效的)
3、一对多表设计
3.1、准备一对多
1、表结构和需求
需求:学生表与学生称即表的一对多的关联关系
学生表(主键表):一方
学生成绩表(外键表):多方
学生表结构
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| student_id | bigint(20) | NO | PRI | NULL | auto_increment |
| student_name | varchar(255) | YES | | NULL | |
| student_pwd | varchar(255) | YES | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
学生成绩表结构
+----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+----------------+
| result_id | bigint(20) | NO | PRI | NULL | auto_increment |
| result_score | float | YES | | NULL | |
| result_subject | varchar(255) | YES | | NULL | |
| student_id | bigint(20) | YES | MUL | NULL | |
+----------------+--------------+------+-----+---------+----------------+
2、创建2个实体类对应的Dao接口
/**
* 学生表repository接口
*/
public interface StudentRepository extends JpaRepository<Student,Long> {
}
/**
* 学生成绩表repository接口
*/
public interface StudentResultRepository extends JpaRepository<StudentResult,Long> {
}
3.2、单向一对多
单向一对多的话,那么只能是:一方(主键表)为拥有方,多方(外键表)为被拥有端。(只配置一方即可)
1、创建:学生实体类(拥有方,主键表)只配置一方(主键方)即可
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
/**
* 单向一对多:只需要配置一方(主键表)即可
* 一般配置@OneToMany + @JoinColumn
* 也可以只配置@OneToMany,@JoinColumn会按照默认规则生成外键
* @OneToMany:
* targetEntity:指定对方类的字节码(可选)
* fetch:指定是懒加载还是立即加载(@OneToMany中默认是懒加载)(最好必填)
* @JoinColumn:
* name:外键字段名称(必填,不然会默认生成外键字段名)
* referencedColumnName:指定引用主表的主键字段名称(注意是数据库字段不是实体类属性名)
* 注意!注意!注意!
* 一对多时一方默认时开启懒加载的,需要关闭,非则查询会报错
*/
@OneToMany(targetEntity = StudentResult.class,fetch = FetchType.EAGER)
@JoinColumn(name="student_id",referencedColumnName = "student_id")
private List<StudentResult> studentResults = new ArrayList<>();
}
2、创建:学生成绩实体类(被拥有方,外键表),多方(外键方)无需配置
@Data
@Entity
@Table(name = "tb_student_result")
public class StudentResult {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "result_id")
private Long resultId;
@Column(name = "result_subject")
private String resultSubject;
@Column(name = "result_score")
private float resultScore;
}
3、测试代码:
/**
* 单向 一对多
* 数据插入的步骤为:
* 先插入外键表数据,此时外键表中的外键为null,
* 然后在插入主键表数据,最后更新外键表中的外键
* 可以查看日志输入的顺序
*/
@Test
public void testOneToManySave(){
// 首先给学生成绩表插入两条数据
StudentResult studentResultA = new StudentResult();
studentResultA.setResultSubject("语文");
studentResultA.setResultScore(89.5f);
resultRepository.saveAndFlush(studentResultA);
StudentResult studentResultB = new StudentResult();
studentResultB.setResultSubject("数学");
studentResultB.setResultScore(92f);
resultRepository.saveAndFlush(studentResultB);
// 然后给学生表(主键表)增加一条数据
Student student = new Student();
student.setStudentName("小明");
student.setStudentPwd("123456");
student.setStudentResults(Arrays.asList(studentResultA, studentResultB));
studentRepository.saveAndFlush(student);
// 查询学生(主)表数据
System.out.println(studentRepository.findById(student.getStudentId()));
// 查询成绩(从)表数据
System.out.println(resultRepository.findById(studentResultA.getResultId()));
System.out.println(resultRepository.findById(studentResultB.getResultId()));
}
4、查看日志
// 插入数据语句和查询语句省略了...
Optional[Student(studentId=3, studentName=小明, studentPwd=123456, studentResults=[StudentResult(resultId=5, resultSubject=语文, resultScore=89.5), StudentResult(resultId=6, resultSubject=数学, resultScore=92.0)])]
// 查询语句省略了...
Optional[StudentResult(resultId=5, resultSubject=语文, resultScore=89.5)]
// 查询语句省略了...
Optional[StudentResult(resultId=6, resultSubject=数学, resultScore=92.0)]
可能会出现如下报错信息:
org.hibernate.LazyInitializationException: failed to lazily initialize XXXXX could not initialize proxy - no Session
解决:
这个问题是由于实体中一对多或者多对多关联关系的加载方式配置不当引起的。
一对多或者多对多关联关系的加载策略使用了懒加载,结果在加载子实体时就会报 org.hibernate.LazyInitializationException: failed to lazily initialize XXXXX could not initialize proxy - no Session错误,只需要将懒加载改为急加载即可
// 修改学生实体类(主表),从一方获取多方的时候默认时懒加载.改成立即加载
@OneToMany(fetch = FetchType.EAGER)
配置文件方式:
# 开启延迟加载
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
3.3、单向多对一
单向多对一的话:多方(外键表)为拥有方,一方(主键表)为被拥有端。(只配置多方即可)
1、创建:学生成绩实体类(拥有方,外键表)(只配置多方(外键方)即可)
@Data
@Entity
@Table(name = "tb_student_result")
public class StudentResult {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "result_id")
private Long resultId;
@Column(name = "result_subject")
private String resultSubject;
@Column(name = "result_score")
private float resultScore;
/**
* 单向多对一:只配置多方(外键方)即可。
* @ManyToOne
* name:指定对方类的字节码(可选)
* @JoinColumn
* name:外键字段名称(必填,不然会默认生成外键字段名)
* referencedColumnName:指定引用主表的主键字段名称(注意是数据库字段不是实体类属性名)
*/
@ManyToOne(targetEntity = Student.class)
@JoinColumn(name = "student_id", referencedColumnName = "student_id")
private Student student;
}
2、创建:学生实体类(被拥有方,主键表)(一方(主键方)无需配置)
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
}
3、测试代码
/**
* 单向 多对一
* 数据插入的步骤为:先插入插入主键表数据,然后在外键表数据
*/
@Test
public void testManyToOneSave(){
// 首先给学生表(主键表)增加一条数据
Student student = new Student();
student.setStudentName("小明");
student.setStudentPwd("123456");
studentRepository.saveAndFlush(student);
/**
* 然后给学生成绩表插入两条数据
* 与单向一对多不同的是:setStudent(student),这个步骤就是给外键字段设置值
*/
StudentResult studentResultA = new StudentResult();
studentResultA.setResultSubject("语文");
studentResultA.setResultScore(89.5f);
studentResultA.setStudent(student);
resultRepository.saveAndFlush(studentResultA);
StudentResult studentResultB = new StudentResult();
studentResultB.setResultSubject("数学");
studentResultB.setResultScore(92f);
studentResultB.setStudent(student);
resultRepository.saveAndFlush(studentResultB);
// 查询学生(主)表数据
System.out.println(studentRepository.findById(student.getStudentId()));
// 查询成绩(从)表数据
System.out.println(resultRepository.findById(studentResultA.getResultId()));
System.out.println(resultRepository.findById(studentResultB.getResultId()));
}
4、查看日志
// 插入语句与查询语句省略...
Optional[Student(studentId=6, studentName=小明, studentPwd=123456)]
// 查询语句省略...
Optional[StudentResult(resultId=11, resultSubject=语文, resultScore=89.5, student=Student(studentId=6, studentName=小明, studentPwd=123456))]
// 查询语句省略...
Optional[StudentResult(resultId=12, resultSubject=数学, resultScore=92.0, student=Student(studentId=6, studentName=小明, studentPwd=123456))]
3.4、双向一对多
双向一对多的话:多方(外键表)为拥有方,一方(主键表)为被拥有端,
1、创建:学生成绩实体类(拥有方,多方、外键表)(多方 / 外键方 配置:@ManyToOne + @JoinColumn)
实际跟单向多对一代码一样,多了一个重写toString方法,为了避免输出死循环
@Data
@Entity
@Table(name = "tb_student_result")
public class StudentResult {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "result_id")
private Long resultId;
@Column(name = "result_subject")
private String resultSubject;
@Column(name = "result_score")
private float resultScore;
/**
* 单向多对一:只配置多方(外键方)即可。
* @ManyToOne
* name:指定对方类的字节码(可选)
* @JoinColumn
* name:外键字段名称(必填,不然会默认生成外键字段名)
* referencedColumnName:指定引用主表的主键字段名称(注意是数据库字段不是实体类属性名)
*/
@ManyToOne(targetEntity = Student.class)
@JoinColumn(name = "student_id", referencedColumnName = "student_id")
private Student student;
// 为了避免输出死循环重写了toString
@Override
public String toString() {
return "StudentResult{" +
"resultId=" + resultId +
", resultSubject='" + resultSubject + '\'' +
", resultScore=" + resultScore +
'}';
}
2、创建:学生实体类(被拥有方,一方、主键表)(一方 / 主键方 配置:@OneToMany)
实际上跟单向多对一相比:加了一个属性加上了@OneToMany,并且重写toString方法避免死循环
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
/**
* 双向一对多:(实际就比单向多对一多了一个@OneToMany)
* 必须在一方配置:@OneToMany:mappedBy + fetch 属性
* @OneToMany
* mappedBy:放弃维护外键,值为另一方(拥有方 / 外键方 / 多方)引用本类的属性名
* fetch:指定是懒加载还是立即加载(@OneToMany中默认是懒加载)(最好必填)
*/
@OneToMany(mappedBy = "student",fetch = FetchType.EAGER)
private List<StudentResult> studentResults = new ArrayList<>();
// 为了避免输出死循环重写了toString
@Override
public String toString() {
return "Student{" +
"studentId=" + studentId +
", studentName='" + studentName + '\'' +
", studentPwd='" + studentPwd + '\'' +
'}';
}
3、测试代码
/**
* 双向 一对多
* 数据插入的步骤为:先插入插入主键表数据,然后在外键表数据
*/
@Test
public void testOneToManyTwoWaySave2(){
// 首先给学生表(主键表)增加一条数据
Student student = new Student();
student.setStudentName("小明");
student.setStudentPwd("123456");
studentRepository.saveAndFlush(student);
/**
* 然后给学生成绩表插入两条数据
* 与单向一对多不同的是:setStudent(student),这个步骤就是给外键字段设置值
*/
StudentResult studentResultA = new StudentResult();
studentResultA.setResultSubject("语文");
studentResultA.setResultScore(89.5f);
resultRepository.saveAndFlush(studentResultA);
StudentResult studentResultB = new StudentResult();
studentResultB.setResultSubject("数学");
studentResultB.setResultScore(92f);
resultRepository.saveAndFlush(studentResultB);
// 查询学生(主)表数据
System.out.println(studentRepository.findById(student.getStudentId()));
// 查询成绩(从)表数据
System.out.println(resultRepository.findById(studentResultA.getResultId()));
System.out.println(resultRepository.findById(studentResultB.getResultId()));
}
4、查看日志
// 插入语句与查询语句省略...
Optional[Student{studentId=12, studentName='小明', studentPwd='123456'}]
// 查询语句省略...
Optional[StudentResult{resultId=23, resultSubject='语文', resultScore=89.5}]
// 查询语句省略...
Optional[StudentResult{resultId=24, resultSubject='数学', resultScore=92.0}]
注意1:
双向一对多中,一方/被拥有方/主键表 一般我们是放弃外键维护的,所以想使用单向一对多的方式插入数据,那么外键是会为空的(即:先插入多方/外键表数据,然后插入一方/主键表数据,最后在一方/主键表 内给引用的多方属性设置值。如果是在单向一对多的情况下,最后会有一个update语句修改外键null的值,当时在双向一对多中并不会,最后插入完数据,外键表会为空)
注意2:
双向一对多中,一方/被拥有方/主键表 中我们去掉放弃外键维护配置,然后再次使用单向一对多的方式插入数据,会发现外键字段依旧为null,当时可以看到日志中显示:JPA给我多创建了一个中间表来维护两个表的主外键。还是把两张表关联起来了。
// 去除放弃外键维护配置,然后再运行一对多中测试代码,我们再查看日志
@OneToMany(/*mappedBy = "student",*/fetch = FetchType.EAGER)
private List<StudentResult> studentResults = new ArrayList<>();
Hibernate:
create table tb_student_student_results (
student_student_id bigint not null,
student_results_result_id bigint not null
) engine=MyISAM
Hibernate:
alter table tb_student_student_results
drop index UK_3ifcd0d9c34kmalbnsk94a3h2
Hibernate:
alter table tb_student_student_results
add constraint UK_3ifcd0d9c34kmalbnsk94a3h2 unique (student_results_result_id)
Hibernate:
alter table tb_student_result
add constraint FK5101rgeygkbcenssvt342o3kg
foreign key (student_id)
references tb_student (student_id)
Hibernate:
alter table tb_student_student_results
add constraint FKknyiphtyp1nnff3jm5c5ahe3a
foreign key (student_results_result_id)
references tb_student_result (result_id)
Hibernate:
alter table tb_student_student_results
add constraint FKfpfap07ekhngj6ya5e9sx9ngb
foreign key (student_student_id)
references tb_student (student_id)
4、多对多表设计
4.1、准备多对多
1、表结构和需求
需求:学生表与学生称即表的一对多的关联关系
学生表(主键表):多方
老师表(主键表):多方
学生老师表(外键中间表)
学生表结构
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| student_id | bigint(20) | NO | PRI | NULL | auto_increment |
| student_name | varchar(255) | YES | | NULL | |
| student_pwd | varchar(255) | YES | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
老师表结构
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| teacher_id | bigint(20) | NO | PRI | NULL | auto_increment |
| teacher_name | varchar(255) | YES | | NULL | |
| teacher_pwd | varchar(255) | YES | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
学生老师表结构
+------------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------+------+-----+---------+-------+
| student_id | bigint(20) | NO | MUL | NULL | |
| teacher_id | bigint(20) | NO | MUL | NULL | |
+------------+------------+------+-----+---------+-------+
2、创建2个实体类对应的Dao接口
/**
* 学生表repository接口
*/
public interface StudentRepository extends JpaRepository<Student,Long> {
}
/**
* 老师表repository接口
*/
public interface TeacherRepository extends JpaRepository<Teacher,Long> {
}
4.2、单向多对多
单向多对多:两张表都为多方,谁都可以为拥有方和被拥有方。这里定义:学生表为拥有方,老师表为被拥有方
1、创建:学生实体类(拥有方)
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
/**
* @JoinTable:
* name:中间表名称
* joinColumns 中间表对应本类的信息
* @JoinColumn;
* name:本类的外键字段(中间表的数据库字段)
* referencedColumnName:本类与外键(表)对应的主键(本类的主键字段)
* inverseJoinColumns 中间表对应对方类的信息
* @JoinColumn:
* name:对方类的外键(中间表的数据字段)
* referencedColumnName:对方类与外键(表)对应的主键(对方类的主键字段)
*
* 注意!注意!注意:
* 不管是一对多或者多对多:
* @OneToMany、@ManyToMany 中的fetch属性默认都是懒加载.
* 可以理解为查询本实体类中的多方(List/Set)属性时,默认是懒加载
*/
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="tb_student_teacher",
joinColumns=@JoinColumn(
name="student_id",
referencedColumnName = "student_id"),
inverseJoinColumns=@JoinColumn(
name="teacher_id",
referencedColumnName = "teacher_id"))
private List<Teacher> teachers = new ArrayList<>();
}
2、创建:老师实体类(被拥有方)(无需配置)
@Data
@Entity
@Table(name = "tb_teacher")
public class Teacher {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "teacher_id")
private Long teacherId;
@Column(name = "teacher_name")
private String teacherName;
@Column(name = "teacher_pwd")
private String teacherPwd;
}
3、测试代码
/**
* 单向 多对多
* 先插入被拥有端数据,然后插入拥有端数据
*/
@Test
public void testManyToMany(){
// 首先给老师表(被拥有方)增加数据
Teacher teacherA = new Teacher();
teacherA.setTeacherName("语文老师");
teacherA.setTeacherPwd("666666");
teacherRepository.saveAndFlush(teacherA);
Teacher teacherB = new Teacher();
teacherB.setTeacherName("数学老师");
teacherB.setTeacherPwd("888888");
teacherRepository.saveAndFlush(teacherB);
Teacher teacherC = new Teacher();
teacherC.setTeacherName("英语老师");
teacherC.setTeacherPwd("888888");
teacherRepository.saveAndFlush(teacherC);
// 然后给学生表数据(拥有方)增加数据
Student studentA = new Student();
studentA.setStudentName("小明");
studentA.setStudentPwd("123456");
// 这步骤设置值就是给中间表插入数据
studentA.setTeachers(Arrays.asList(teacherA,teacherB));
studentRepository.saveAndFlush(studentA);
Student studentB = new Student();
studentB.setStudentName("小明");
studentB.setStudentPwd("123456");
// 这步骤设置值就是给中间表插入数据
studentB.setTeachers(Arrays.asList(teacherB,teacherC));
studentRepository.saveAndFlush(studentB);
// 查询学生表(拥有方)数据
System.out.println(studentRepository.findById(studentA.getStudentId()));
System.out.println(studentRepository.findById(studentB.getStudentId()));
}
4、查看日志
// 建立中间表日志
Hibernate:
alter table tb_student_teacher
add constraint FKf3vdcta2bcm9seh723ge2tel
foreign key (teacher_id)
references tb_teacher (teacher_id)
Hibernate:
alter table tb_student_teacher
add constraint FKkhrh8vhu4pnumefhvuwdiby8y
foreign key (student_id)
references tb_student (student_id)
// 省略了插入数据语句...,查询语句如下:
Hibernate:
select
student0_.student_id as student_1_2_0_,
student0_.student_name as student_2_2_0_,
student0_.student_pwd as student_3_2_0_,
teachers1_.student_id as student_1_5_1_,
teacher2_.teacher_id as teacher_2_5_1_,
teacher2_.teacher_id as teacher_1_6_2_,
teacher2_.teacher_name as teacher_2_6_2_,
teacher2_.teacher_pwd as teacher_3_6_2_
from
tb_student student0_
left outer join
tb_student_teacher teachers1_
on student0_.student_id=teachers1_.student_id
left outer join
tb_teacher teacher2_
on teachers1_.teacher_id=teacher2_.teacher_id
where
student0_.student_id=?
Optional[Student(studentId=1, studentName=小明, studentPwd=123456, teachers=[Teacher(teacherId=1, teacherName=语文老师, teacherPwd=666666), Teacher(teacherId=2, teacherName=数学老师, teacherPwd=888888)])]
//查询语句省略...可以参考上面查询语句,一样的
Optional[Student(studentId=2, studentName=小明, studentPwd=123456, teachers=[Teacher(teacherId=2, teacherName=数学老师, teacherPwd=888888), Teacher(teacherId=3, teacherName=英语老师, teacherPwd=888888)])]
4.3、双向多对多
双向多对多:两张表都为多方,谁都可以为拥有方和被拥有方。这里定义:学生表为拥有方,老师表为被拥有方
1、创建:学生实体类(拥有方)(参考单向多对多代码,为了避免死循环重写了ToString方法)
@Data
@Entity
@Table(name = "tb_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "student_id")
private Long studentId;
@Column(name = "student_name")
private String studentName;
@Column(name = "student_pwd")
private String studentPwd;
/**
* @JoinTable:
* name:中间表名称
* joinColumns 中间表对应本类的信息
* @JoinColumn;
* name:本类的外键字段(中间表的数据库字段)
* referencedColumnName:本类与外键(表)对应的主键(本类的主键字段)
* inverseJoinColumns 中间表对应对方类的信息
* @JoinColumn:
* name:对方类的外键(中间表的数据字段)
* referencedColumnName:对方类与外键(表)对应的主键(对方类的主键字段)
*
* 注意!注意!注意:
* 不管是一对多或者多对多:
* @OneToMany、@ManyToMany 中的fetch属性默认都是懒加载.
* 可以理解为查询本实体类中的多方(List/Set)属性时,默认是懒加载
*/
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="tb_student_teacher",
joinColumns=@JoinColumn(
name="student_id",
referencedColumnName = "student_id"),
inverseJoinColumns=@JoinColumn(
name="teacher_id",
referencedColumnName = "teacher_id"))
private List<Teacher> teachers = new ArrayList<>();
@Override
public String toString() {
return "Student{" +
"studentId=" + studentId +
", studentName='" + studentName + '\'' +
", studentPwd='" + studentPwd + '\'' +
'}';
}
}
2、创建:老师实体类(被拥有方)(增加@ManyToMany和重写ToString方法)
@Data
@Entity
@Table(name = "tb_teacher")
public class Teacher {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "teacher_id")
private Long teacherId;
@Column(name = "teacher_name")
private String teacherName;
@Column(name = "teacher_pwd")
private String teacherPwd;
/**
* 双向多对多:
* 需要配置 @ManyToMany:mappedBy + fetch 属性
* @ManyToMany
* mappedBy:放弃维护外键,值为另一方(拥有方)引用本类的属性名
* fetch:指定是懒加载还是立即加载(@OneToMany中默认是懒加载)(最好必填)
*/
@ManyToMany(mappedBy = "teachers",fetch = FetchType.EAGER)
private List<Student> students = new ArrayList<>();
}
3、测试代码
/**
* 单向/双向 多对多
* 先插入被拥有端数据,然后插入拥有端数据
*/
@Test
public void testManyToMany(){
// 首先给老师表(被拥有方)增加数据
Teacher teacherA = new Teacher();
teacherA.setTeacherName("语文老师");
teacherA.setTeacherPwd("666666");
teacherRepository.saveAndFlush(teacherA);
Teacher teacherB = new Teacher();
teacherB.setTeacherName("数学老师");
teacherB.setTeacherPwd("888888");
teacherRepository.saveAndFlush(teacherB);
Teacher teacherC = new Teacher();
teacherC.setTeacherName("英语老师");
teacherC.setTeacherPwd("888888");
teacherRepository.saveAndFlush(teacherC);
// 然后给学生表数据(拥有方)增加数据
Student studentA = new Student();
studentA.setStudentName("小明");
studentA.setStudentPwd("123456");
// 这步骤设置值就是给中间表插入数据
studentA.setTeachers(Arrays.asList(teacherA,teacherB));
studentRepository.saveAndFlush(studentA);
Student studentB = new Student();
studentB.setStudentName("小明");
studentB.setStudentPwd("123456");
// 这步骤设置值就是给中间表插入数据
studentB.setTeachers(Arrays.asList(teacherB,teacherC));
studentRepository.saveAndFlush(studentB);
// 查询学生表(拥有方)数据
Student student1 = studentRepository.findById(studentA.getStudentId()).get();
System.out.println(student1.getTeachers().size());
Student student2 = studentRepository.findById(studentB.getStudentId()).get();
System.out.println(student2.getTeachers().size());
// 查询老师表(被拥有方)数据
Teacher teacher = teacherRepository.findById(teacherB.getTeacherId()).get();
System.out.println(teacher);
System.out.println(teacher.getStudents().size());
}
4、查看日志
Hibernate:
select
student0_.student_id as student_1_2_0_,
student0_.student_name as student_2_2_0_,
student0_.student_pwd as student_3_2_0_,
teachers1_.student_id as student_1_5_1_,
teacher2_.teacher_id as teacher_2_5_1_,
teacher2_.teacher_id as teacher_1_6_2_,
teacher2_.teacher_name as teacher_2_6_2_,
teacher2_.teacher_pwd as teacher_3_6_2_
from
tb_student student0_
left outer join
tb_student_teacher teachers1_
on student0_.student_id=teachers1_.student_id
left outer join
tb_teacher teacher2_
on teachers1_.teacher_id=teacher2_.teacher_id
where
student0_.student_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Hibernate:
select
teachers0_.student_id as student_1_5_0_,
teachers0_.teacher_id as teacher_2_5_0_,
teacher1_.teacher_id as teacher_1_6_1_,
teacher1_.teacher_name as teacher_2_6_1_,
teacher1_.teacher_pwd as teacher_3_6_1_
from
tb_student_teacher teachers0_
inner join
tb_teacher teacher1_
on teachers0_.teacher_id=teacher1_.teacher_id
where
teachers0_.student_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Student{studentId=7, studentName='小明', studentPwd='123456'}
2
Hibernate:
select
student0_.student_id as student_1_2_0_,
student0_.student_name as student_2_2_0_,
student0_.student_pwd as student_3_2_0_,
teachers1_.student_id as student_1_5_1_,
teacher2_.teacher_id as teacher_2_5_1_,
teacher2_.teacher_id as teacher_1_6_2_,
teacher2_.teacher_name as teacher_2_6_2_,
teacher2_.teacher_pwd as teacher_3_6_2_
from
tb_student student0_
left outer join
tb_student_teacher teachers1_
on student0_.student_id=teachers1_.student_id
left outer join
tb_teacher teacher2_
on teachers1_.teacher_id=teacher2_.teacher_id
where
student0_.student_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Hibernate:
select
teachers0_.student_id as student_1_5_0_,
teachers0_.teacher_id as teacher_2_5_0_,
teacher1_.teacher_id as teacher_1_6_1_,
teacher1_.teacher_name as teacher_2_6_1_,
teacher1_.teacher_pwd as teacher_3_6_1_
from
tb_student_teacher teachers0_
inner join
tb_teacher teacher1_
on teachers0_.teacher_id=teacher1_.teacher_id
where
teachers0_.student_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Student{studentId=8, studentName='小明', studentPwd='123456'}
2
Hibernate:
select
teacher0_.teacher_id as teacher_1_6_0_,
teacher0_.teacher_name as teacher_2_6_0_,
teacher0_.teacher_pwd as teacher_3_6_0_,
students1_.teacher_id as teacher_2_5_1_,
student2_.student_id as student_1_5_1_,
student2_.student_id as student_1_2_2_,
student2_.student_name as student_2_2_2_,
student2_.student_pwd as student_3_2_2_
from
tb_teacher teacher0_
left outer join
tb_student_teacher students1_
on teacher0_.teacher_id=students1_.teacher_id
left outer join
tb_student student2_
on students1_.student_id=student2_.student_id
where
teacher0_.teacher_id=?
Hibernate:
select
teachers0_.student_id as student_1_5_0_,
teachers0_.teacher_id as teacher_2_5_0_,
teacher1_.teacher_id as teacher_1_6_1_,
teacher1_.teacher_name as teacher_2_6_1_,
teacher1_.teacher_pwd as teacher_3_6_1_
from
tb_student_teacher teachers0_
inner join
tb_teacher teacher1_
on teachers0_.teacher_id=teacher1_.teacher_id
where
teachers0_.student_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Hibernate:
select
teachers0_.student_id as student_1_5_0_,
teachers0_.teacher_id as teacher_2_5_0_,
teacher1_.teacher_id as teacher_1_6_1_,
teacher1_.teacher_name as teacher_2_6_1_,
teacher1_.teacher_pwd as teacher_3_6_1_
from
tb_student_teacher teachers0_
inner join
tb_teacher teacher1_
on teachers0_.teacher_id=teacher1_.teacher_id
where
teachers0_.student_id=?
Hibernate:
select
students0_.teacher_id as teacher_2_5_0_,
students0_.student_id as student_1_5_0_,
student1_.student_id as student_1_2_1_,
student1_.student_name as student_2_2_1_,
student1_.student_pwd as student_3_2_1_
from
tb_student_teacher students0_
inner join
tb_student student1_
on students0_.student_id=student1_.student_id
where
students0_.teacher_id=?
Teacher{teacherId=11, teacherName='数学老师', teacherPwd='888888'}
2
5、多表操作异常
关于实体对象查询异常
WARNING: Please consider reporting this to the maintainers of org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Hibernate: sql语句
java.lang.StackOverflowError
at java.base/java.lang.AbstractStringBuilder.inflate(AbstractStringBuilder.java:202)
at java.base/java.lang.AbstractStringBuilder.putStringAt(AbstractStringBuilder.java:1639)
at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:513)
.......
关以这种错误大多都是编写的toString语句发生了错误,大家在写toString的时候一定要分清主表和从表,在编写主表的toString的时候一定要去除外键字段,而在编写从表的时候一定要加上外键字段,因为平时我们都是通过从表查询数据(因为从表有指向主表的外键),这样可以把主表的数据通过外键查询出来,但是主表上如果也有从表的字段的话就会一只循环,数据没完没了,所有抛异常也是对的,
总结一句话,从表有主表的字段,主表有从表的字段,打印从表顺带打印主表,但是主表里面还有从表字段,然后继续打印从表字段.......
解决方案:
- 去除@Data注解,替换成@Getter和@Setter注解
- 也可以重写toString()方法,把引用类型的输出删掉即可
- 查询后可以不要输出打印该对象,这样就不会死循环输出对象中的对象了(治标不治本, 不推荐)
在实体类上增加@JsonIgnoreProperties(value = "对方类引用本类的属性名")
(暂时我试了时无效的)
关于延迟加载事务问题
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: cn.xw.domain.Teacher.students, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149)
at org.hibernate.collection.internal.PersistentSet.toString(PersistentSet.java:327)
at java.base/java.lang.String.valueOf(String.java:2801)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
关于在使用延迟加载的时候,在当前的方法上必须设置@Transactional,因为在使用延迟加载底层已经使用了事务的相关方法
关于懒加载no session异常
org.hibernate.LazyInitializationException: failed to lazily initialize XXXXX could not initialize proxy - no Session
仔细看英文,懒加载存在,session 都关了,获取不大数据。
解决办法:lazy=“false”
若是用注解生成的。fetch = FetchType.EAGER
# 开启延迟加载,这个配置的意思就是在没有事务的情况下允许懒加载。
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
当然了,这样的话,你的缓存就不起作用了。
另外解决办法,获取数据时,使用用left join fetch或inner join fetch语法。
关于数据库方言问题(dialect)
6月 03, 2020 4:57:00 下午 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
省去部分...
Hibernate: create table family (...) type=MyISAM
//上面一局为我们创建的是一张表并设置MyISAM引擎 错误就在这 无法运行了
6月 03, 2020 4:57:01 下午 org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl handleException
WARN: GenerationTarget encountered exception accepting command : Error executing DDL "create table family (...) type=MyISAM" via JDBC Statement
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "create table family (...) type=MyISAM" via JDBC Statement
//从上面错误可以看出 程序运行的时候默认的数据库方言设置了 org.hibernate.dialect.MySQLDialect 而这个默认是MyISAM引擎
问题所在:因为我导入的hibernate坐标是5.4.10.Final,在导入这类高版本的坐标往往要为数据库方言设置MySQL5InnoDBDialect的配置,在我前面也测试了,关于坐标版本问题,发现5.0.x.Final左右的版本不用设置数据库方言,默认即可。
# 设置数据库方言,建表使用MyISAM,默认是InnoDB
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
具体的版本在创建数据库表的时候会抛各种异常,这里我整理了一下数据库方言,方便大家参考
数据库方言
DB2 org.hibernate.dialect.DB2Dialect
DB2 AS/400 org.hibernate.dialect.DB2400Dialect
DB2 OS390 org.hibernate.dialect.DB2390Dialect
PostgreSQL org.hibernate.dialect.PostgreSQLDialect
MySQL org.hibernate.dialect.MySQLDialect
MySQL with InnoDB org.hibernate.dialect.MySQLInnoDBDialect
MySQL with MyISAM org.hibernate.dialect.MySQLMyISAMDialect
Oracle (any version) org.hibernate.dialect.OracleDialect
Oracle 9i/10g org.hibernate.dialect.Oracle9Dialect
Sybase org.hibernate.dialect.SybaseDialect
Sybase Anywhere org.hibernate.dialect.SybaseAnywhereDialect
Microsoft SQL Server org.hibernate.dialect.SQLServerDialect
SAP DB org.hibernate.dialect.SAPDBDialect
Informix org.hibernate.dialect.InformixDialect
HypersonicSQL org.hibernate.dialect.HSQLDialect
Ingres org.hibernate.dialect.IngresDialect
Progress org.hibernate.dialect.ProgressDialect
Mckoi SQL org.hibernate.dialect.MckoiDialect
Interbase org.hibernate.dialect.InterbaseDialect
Pointbase org.hibernate.dialect.PointbaseDialect
FrontBase org.hibernate.dialect.FrontbaseDialect
Firebird org.hibernate.dialect.FirebirdDialect
6、多表映射总结
JPA自动建表不生成外键
SpringBoot项目搭配的JPA使用时候,有一对多的关系注解,那么自动会生成外键。外键在有些时候,会导致代码不能走通,我们不想要怎么做。有两种解决方案.
一对多
情况一:单向一对多/多对一,在@JoinColumn内加上foreignKey属性配置即可
@OneToMany
@JoinColumn(name="cid",foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT))
@ManyToOne
@JoinColumn(name="cid",foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT))
情况二:双向一对多,除了在@JoinColumn加foreignKey配置,还需要再另一方(一方)加配置
1.解决方案一:
// 一方加@org.hibernate.annotations.ForeignKey,注意:这个注解被废弃了,所以更新jar包的时候需要注意下
@org.hibernate.annotations.ForeignKey(name = “none”)
@OneToMany
// 多方配置不变
@ManyToOne
@JoinColumn(name="cid",foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT))
2.解决方案二:
// 一方加@Transient,意思是忽略该字段,我认为加上这个字段后在加@OneToMany也无意义了.
@Transient
@OneToMany
// 多方配置不变
@ManyToOne
@JoinColumn(name="cid",foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT))
多对多也是类似的处理方式
单向多对多:只要在@JoinTable内的@JoinColumn内加上foreignKey属性配置即可,记得连个表外键都要加
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="tb_student_teacher",
joinColumns=@JoinColumn(
name="student_id",
referencedColumnName = "student_id",
foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT)),
inverseJoinColumns=@JoinColumn(
name="teacher_id",
referencedColumnName = "teacher_id",
foreignKey = @ForeignKey(name = "none",value = ConstraintMode.NO_CONSTRAINT)))
双向多对多:参照双向一对多/多对一,在另一方加注解即可。