ORM
概述
ORM(Object-Relational Mapping) 表示对象关系映射。在面向对象的软件开发中,通过ORM,就可以把对象映射到关系型数据库中。只要有一套程序能够做到建立对象与数据库的关联,操作对象就可以直接操作数据库数据,就可以说这套程序实现了ORM对象关系映射
简单的说:ORM就是建立实体类和数据库表之间的关系,从而达到操作实体类就相当于操作数据库表的目的。
为什么使用
当实现一个应用程序时(不使用O/R Mapping),我们可能会写特别多数据访问层的代码,从数据库保存数据、修改数据、删除数据,而这些代码都是重复的。而使用ORM则会大大减少重复性代码。对象关系映射(Object Relational Mapping,简称ORM),主要实现程序对象到关系数据库数据的映射。
常见ORM框架
常见的orm框架:Mybatis(ibatis)、Hibernate、Jpa
hibernate与JPA和SpringDataJpa
hibernate概述
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。
JPA概述
JPA的全称是Java Persistence API, 即Java 持久化API,是SUN公司推出的一套基于ORM的规范,内部是由一系列的接口和抽象类构成。
JPA通过JDK 5.0注解描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
JPA的优势
1. 标准化
JPA 是 JCP 组织发布的 Java EE 标准之一,因此任何声称符合 JPA 标准的框架都遵循同样的架构,提供相同的访问API,这保证了基于JPA开发的企业应用能够经过少量的修改就能够在不同的JPA框架下运行。
2. 容器级特性的支持
JPA框架中支持大数据集、事务、并发等容器级事务,这使得 JPA 超越了简单持久化框架的局限,在企业应用发挥更大的作用。
3. 简单方便
JPA的主要目标之一就是提供更加简单的编程模型:在JPA框架下创建实体和创建Java 类一样简单,没有任何的约束和限制,只需要使用 javax.persistence.Entity进行注释,JPA的框架和接口也都非常简单,没有太多特别的规则和设计模式的要求,开发者可以很容易的掌握。JPA基于非侵入式原则设计,因此可以很容易的和其它框架或者容器集成
4. 查询能力
JPA的查询语言是面向对象而非面向数据库的,它以面向对象的自然语法构造查询语句,可以看成是Hibernate HQL的等价物。JPA定义了独特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一种扩展,它是针对实体的一种查询语言,操作对象是实体,而不是关系数据库的表,而且能够支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能够提供的高级查询特性,甚至还能够支持子查询。
5. 高级特性
JPA 中能够支持面向对象的高级特性,如类之间的继承、多态和类之间的复杂关系,这样的支持能够让开发者最大限度的使用面向对象的模型设计企业应用,而不需要自行处理这些特性在关系数据库的持久化。
JPA与hibernate的关系
JPA规范本质上就是一种ORM规范,注意不是ORM框架——因为JPA并未提供ORM实现,它只是制订了一些规范,提供了一些编程的API接口,但具体实现则由服务厂商来提供实现。
JPA和Hibernate的关系就像JDBC和JDBC驱动的关系,JPA是规范,Hibernate除了作为ORM框架之外,它也是一种JPA实现。JPA怎么取代Hibernate呢?JDBC规范可以驱动底层数据库吗?答案是否定的,也就是说,如果使用JPA规范进行数据库操作,底层需要hibernate作为其实现类完成数据持久化工作。
SpringDataJPA概述
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,可使开发者用极简的代码即可实现对数据库的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用 Spring Data JPA 可以极大提高开发效率!
Spring Data JPA 让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现,在实际的工作工程中,推荐使用Spring Data JPA + ORM(如:hibernate)完成操作,这样在切换不同的ORM框架时提供了极大的方便,同时也使数据库层操作更加简单,方便解耦
SpringDataJPA特性
SpringData Jpa 极大简化了数据库访问层代码。 如何简化的呢? 使用了SpringDataJpa,我们的dao层中只需要写接口,就自动具有了增删改查、分页查询等方法。
JPA是一套规范,内部是有接口和抽象类组成的。hibernate是一套成熟的ORM框架,而且Hibernate实现了JPA规范,所以也可以称hibernate为JPA的一种实现方式,我们使用JPA的API编程,意味着站在更高的角度上看待问题(面向接口编程)
springDataJpa,jpa规范和hibernate之间的关系说明
JPA是一套规范,内部是有接口和抽象类组成的。hibernate是一套成熟的ORM框架,而且Hibernate实现了JPA规范,所以也可以称hibernate为JPA的一种实现方式,我们使用JPA的API编程,意味着站在更高的角度上看待问题(面向接口编程)
Spring Data JPA是Spring提供的一套对JPA操作更加高级的封装,是在JPA规范下的专门用来进行数据持久化的解决方案。
小结
Hibernate是JPA的一种实现,是一个框架
Spring Data JPA是一种JPA的抽象层,底层依赖Hibernate
JPA入门案列
需求介绍
我们实现的功能是保存一个客户到数据库的客户表中。
创建数据库
/*创建客户表*/
CREATE TABLE cst_customer (
cust_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客户编号(主键)',
cust_name varchar(32) NOT NULL COMMENT '客户名称(公司名称)',
cust_source varchar(32) DEFAULT NULL COMMENT '客户信息来源',
cust_industry varchar(32) DEFAULT NULL COMMENT '客户所属行业',
cust_level varchar(32) DEFAULT NULL COMMENT '客户级别',
cust_address varchar(128) DEFAULT NULL COMMENT '客户联系地址',
cust_phone varchar(64) DEFAULT NULL COMMENT '客户联系电话',
PRIMARY KEY (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
</dependencies>
</project>
配置文件
spring:
datasource:
username: jiang
password: jiang
url: jdbc:mysql://localhost:3306/learn?characterEncoding=UTF-8&serverTimezone=UTC
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x';
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,slf4j
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
useGlobalDataSourceStat: true
jpa:
hibernate:
# 更新或者创建数据表结构,create:程序运行时创建数据库表(如果有表,先删除表再创建); update:程序运行时创建表(如果有表,不会建表); none:不会创建表
ddl-auto: update
show-sql: true #控制台显示SQ
logging:
level:
top.codekiller.test.testspringdataone: debug
实体类
@Entity
作用:指定当前类是实体类。
@Table
作用:指定实体类和表之间的对应关系。
属性:name:指定数据库表的名称
@Id
作用:指定当前字段是主键。
@GeneratedValue
作用:指定主键的生成方式。。
属性:strategy :指定主键生成策略。通过GenerationType枚举获取值
@Column
作用:指定实体类属性和数据库表之间的对应关系
属性:
name:指定数据库表的列名称。
unique:是否唯一
nullable:是否可以为空
inserttable:是否可以插入
updateable:是否可以更新
columnDefinition: 定义建表时创建此列的DDL
secondaryTable: 从表名。如果此列不建在主表上(默认建在主表),该属性定义该列所在从表的名字搭建开
发环境[重点]
package top.codekiller.test.testspringdataone.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
/**
* @author codekiller
* @date 2020/6/27 20:49
* @Description 客户实体类
*/
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name="cst_customer")
public class Customer {
/**
* 客户编号(主键)
*/
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY) //自增主键,默认是Auto
@Column(name="cust_id") //如果可以直接映射,这个注解不需要写
private Long custId;
/**
* 客户名称(公司名称)
*/
@Column(name="cust_name")
private String custName;
/**
* 客户信息来源
*/
@Column(name="cust_source")
private String custSource;
/**
* 客户所属行业
*/
@Column(name="cust_industry")
private String custIndustry;
/**
* 客户级别
*/
@Column(name="cust_level")
private String custLevel;
/**
* 客户联系地址
*/
@Column(name="cust_address")
private String custAddress;
/**
* 客户联系电话
*/
@Column(name="cust_phone")
private String custPhone;
}
Repository类
如果不适用spring整合,那么不需要创建Repository,这一步可跳过
@Repository
public interface CustomerRepository extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
}
- JpaRepository:基本CRUD操作
- JpaSpecificationExecutor:复杂CRUD操作,比如分页
操作
新增数据
不适用spring整合(不需要创建Repository)
/**
* 创建实体管理类工厂,借助Persistence的静态方法获取
* 其中传递的参数为持久化单元名称,需要jpa配置文件中指定
*/
EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
//创建实体管理类
EntityManager em = factory.createEntityManager();
//获取事务对象
EntityTransaction tx = em.getTransaction();
//开启事务
tx.begin();
Customer c = new Customer();
c.setCustName("传智播客");
//保存操作
em.persist(c);
//提交事务
tx.commit();
//释放资源
em.close();
factory.close();
spring整合操作
@Autowired
private CustomerRepository customerRepository;
/**
* @Description 保存数据
* @date 2020/6/27 22:52
* @return void
*/
@Test
@Transactional(rollbackFor = Exception.class)
public void testSave(){
Customer customer=new Customer();
customer.setCustId(null);
customer.setCustName("飞飞飞");
customer.setCustIndustry("娱乐");
//保存
this.customerRepository.save(customer);
}
查询数据
@Test
public void testQuery(){
Optional<Customer> optional = this.customerRepository.findById(1L);
System.out.println(optional.get());
}
基本更新
@Test
public void testUpdate(){
Customer customer=new Customer();
customer.setCustId(2L);
customer.setCustName("飞飞飞走了");
this.customerRepository.save(customer);
}
基本删除
@Test
public void testDelete(){
this.customerRepository.deleteById(3L);
}
JPA中的主键生成策略
通过annotation(注解)来映射hibernate实体的,基于annotation的hibernate主键标识为@Id, 其生成规则由@GeneratedValue设定的.这里的@id和@GeneratedValue都是JPA的标准用法。
JPA提供的四种标准用法为TABLE
,SEQUENCE
,IDENTITY
,AUTO
。后两种了解即可
IDENTITY
IDENTITY:主键由数据库自动生成(主要是自动增长型),一般用在允许自增长的数据库(mysql)中
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long custId;
SEQUENCE
SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。一般用在oracle中,因为oracle中没有自增
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,generator="payablemoney_seq")
@SequenceGenerator(name="payablemoney_seq", sequenceName="seq_payment")
private Long custId;
@SequenceGenerator源码中的定义
//@SequenceGenerator源码中的定义
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface SequenceGenerator {
//表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中
String name();
//属性表示生成策略用到的数据库序列名称。
String sequenceName() default "";
//表示主键初识值,默认为0
int initialValue() default 0;
//表示每次主键值增加的大小,例如设置1,则表示每次插入新记录后自动加1,默认为50
int allocationSize() default 50;
}
AUTO:主键由程序控制
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long custId;
TABLE:使用一个特定的数据库表格来保存主键
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
@TableGenerator(name = "pk_gen",
table="tb_generator",
pkColumnName="gen_name",
valueColumnName="gen_value",
pkColumnValue="PAYABLEMOENY_PK",
allocationSize=1
)
private Long custId;
@TableGenerator的定义
//@TableGenerator的定义:
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface TableGenerator {
//表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中
String name();
//表示表生成策略所持久化的表名,例如,这里表使用的是数据库中的“tb_generator”。
String table() default "";
//catalog和schema具体指定表所在的目录名或是数据库名
String catalog() default "";
String schema() default "";
//属性的值表示在持久化表中,该主键生成策略所对应键值的名称。例如在“tb_generator”中将“gen_name”作为主键的键值
String pkColumnName() default "";
//属性的值表示在持久化表中,该主键当前所生成的值,它的值将会随着每次创建累加。例如,在“tb_generator”中将“gen_value”作为主键的值
String valueColumnName() default "";
//属性的值表示在持久化表中,该生成策略所对应的主键。例如在“tb_generator”表中,将“gen_name”的值为“CUSTOMER_PK”。
String pkColumnValue() default "";
//表示主键初识值,默认为0。
int initialValue() default 0;
//表示每次主键值增加的大小,例如设置成1,则表示每次创建新记录后自动加1,默认为50。
int allocationSize() default 50;
UniqueConstraint[] uniqueConstraints() default {};
}
//这里应用表tb_generator,定义为 :
CREATE TABLE tb_generator (
id NUMBER NOT NULL,
gen_name VARCHAR2(255) NOT NULL,
gen_value NUMBER NOT NULL,
PRIMARY KEY(id)
)
JPA的API介绍
/**
* 创建实体管理类工厂,借助Persistence的静态方法获取
* 其中传递的参数为持久化单元名称,需要jpa配置文件中指定
*/
EntityManagerFactory factory = Persistence.createEntityManagerFactory("myJpa");
//创建实体管理类
EntityManager em = factory.createEntityManager();
//获取事务对象
EntityTransaction tx = em.getTransaction();
//开启事务
tx.begin();
Customer c = new Customer();
c.setCustName("传智播客");
//保存操作
em.persist(c);
//提交事务
tx.commit();
//释放资源
em.close();
factory.close();
Persistence
Persistence对象主要作用是用于获取EntityManagerFactory对象的 。通过调用该类的createEntityManagerFactory静态方法,根据配置文件中持久化单元名称创建EntityManagerFactory。
//1. 创建 EntitymanagerFactory
@Test
String unitName = "myJpa";
EntityManagerFactory factory= Persistence.createEntityManagerFactory(unitName);
EntityManagerFactory
EntityManagerFactory 接口主要用来创建 EntityManager 实例
//创建实体管理类
EntityManager em = factory.createEntityManager();
由于EntityManagerFactory 是一个线程安全的对象(即多个线程访问同一个EntityManagerFactory 对象不会有线程安全问题),并且EntityManagerFactory 的创建极其浪费资源,所以在使用JPA编程时,我们可以对EntityManagerFactory 的创建进行优化,只需要做到一个工程只存在一个EntityManagerFactory 即可
优化方法
创建一个工具类,每次使用时来这个工具类中获取即可。这样就可以保证了一个工程中只存在一个EntityManagerFactory
package cn.itcast.dao;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public final class JPAUtil {
// JPA的实体管理器工厂:相当于Hibernate的SessionFactory
private static EntityManagerFactory em;
// 使用静态代码块赋值
static {
// 注意:该方法参数必须和persistence.xml中persistence-unit标签name属性取值一致
em = Persistence.createEntityManagerFactory("myPersistUnit");
}
/**
* 使用管理器工厂生产一个管理器对象
*
* @return
*/
public static EntityManager getEntityManager() {
return em.createEntityManager();
}
}
EntityManager
在 JPA 规范中, EntityManager是完成持久化操作的核心对象。实体类作为普通 java对象,只有在调用 EntityManager将其持久化后才会变成持久化对象。EntityManager对象在一组实体类与底层数据源之间进行 O/R 映射的管理。它可以用来管理和更新 Entity Bean, 根椐主键查找 Entity Bean, 还可以通过JPQL语句查询实体。
我们可以通过调用EntityManager的方法完成获取事务,以及持久化数据库的操作
方法说明
- getTransaction : 获取事务对象
- persist : 保存操作
- merge : 更新操作
- remove : 删除操作
- find/getReference : 根据id查询
EntityTransaction
在 JPA 规范中, EntityTransaction是完成事务操作的核心对象,对于EntityTransaction在我们的java代码中承接的功能比较简单
方法说明
- begin:开启事务
- commit:提交事务
- rollback:回滚事务
getOne和findOne
getOne
getOne是延迟加载。(返回的是一个动态代理对象,什么时候用,什么时候查询)
getOne是JpaRepository中的方法
getOne返回的是一个引用,即代理对象
当getOne查询不到结果时会抛出异常
@Test
@Transactional(rollbackFor = Exception.class)
public void testGetOneAndSetOne(){
Customer customer = this.customerRepository.getOne(1L);
System.out.println(customer);
}
getOne需要添加事务管理
看一下源码就可以发现,真正调用的是jpa中的em.getReference(getDomainClass(), id);
findOne
findOne是立即加载
findOne是CrudRepository中的方法,
findOne返回的是一个实体对象
当findOne查询不到结果时会返回null
@Test
public void testGetOneAndSetOne(){
Specification<Customer> specification=(root,query,cb)->{
Path<Object> custId = root.get("custId");
Predicate predicate = cb.equal(custId, 1L);
return predicate;
};
Optional<Customer> customer = this.customerRepository.findOne(specification);
System.out.println(customer.get());
}
比较
现在两者都不进行输出(即两者产生的数据都不进行调用),查看一下执行的sql语句情况
getOne:不执行任何sql语句
findOne:执行sql语句
总结
getOne是延迟加载,而findOne是懒加载
getOne是JpaRepository中的方法,而findOne是CrudRepository中的方法
getOne返回的是一个引用,即代理对象,而findOne返回的是一个实体对象
当getOne查询不到结果时会抛出异常,当findOne查询不到结果时会返回null
JPQL
基本概述
jpql的查询方式
jpql : jpa query language (jpq查询语言)
特点:语法或关键字和sql语句类似。 查询的是类和类中的属性
需要将JPQL语句配置到接口方法上
1.特有的查询:需要在dao接口上配置方法
2.在新添加的方法上,使用注解的形式配置jpql查询语句
3.注解 : @Query
查询
单参数查询
// @Query(value="select * from cst_customer where cust_name=?1",nativeQuery = true)
// @Query(value="from Customer where cust_name= ?1")
// @Query(value="select c from Customer c where c.custName=?1")
// @Query(value="from Customer c where c.custName=:#{#custName}")
@Query(value="from Customer c where c.custName=:custName")
List<Customer> findAllCustomerByName(@Param("custName") String custName);
这几种方式是等价的
- @Query(value=”select * from cst_customer where cust_name=?1”,nativeQuery = true)
- @Query(value=”from Customer where cust_name= ?1”)
- @Query(value=”select c from Customer c where c.custName=?1”)
- @Query(value=”from Customer c where c.custName=:#{ #custName}”)
- @Query(value=”from Customer c where c.custName=:custName”)
多参数查询
// @Query(value="from Customer c where c.custId=?2 and c.custName=?1")
// @Query(value="from Customer c where c.custId=:#{#custId} and c.custName=:#{#custName}")
@Query(value="from Customer c where c.custId=:custId and c.custName=:custName")
List<Customer> findCustomersByNameAndIndus(@Param("custName") String name,@Param("custId") Long id);
这几种方式是等价的(还有一种原生sql的方式)
- @Query(value=”from Customer c where c.custId=?2 and c.custName=?1”)
- @Query(value=”from Customer c where c.custId=:#{ #custId} and c.custName=:#{ #custName}”)
- @Query(value=”from Customer c where c.custId=:custId and c.custName=:custName”)
参数为对象查询
@Query(value="from Customer c where c.custId=:#{#customer.custId}")
Customer findCustomerByInfo(@Param("customer") Customer customer);
更新和删除
注意
1.使用 delete 或者 update 操作时
@query注解下方需要增加 @Modifying注解
2.在测试dao层(update、delete)方法时,必须开启事务@Transactional注解
3.在测试dao层(update、delete)方法时,默认不开启事务提交,
需要配置@Rollback(value = false)
update
@Query(value="update Customer c set c.custName=:custName where c.custId=:custId")
@Modifying
Integer updateCustomer(@Param("custId")Long custId,@Param("custName") String custName);
测试
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(value=false) //设置是否自动回滚
public void testJpqlUpdate() {
Integer flag = this.customerRepository.updateCustomer(2L, "黑马飞起来");
System.out.println(flag==1?"更新成功":"更新失败");
}
delete
@Query(value="delete Customer c where c.custId=:custId")
@Modifying
Integer deleteCustomer(@Param("custId")Long custId);
测试
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(value=false) //设置是否自动回滚
public void testJpqlUpdate() {
Integer flag = this.customerRepository.deleteCustomer(3L);
System.out.println(flag==1?"删除成功":"删除失败");
}
specification的使用
Specification是一个函数式接口,需要实现toPredicate(Root
root
:查询的根对象(查询的任何属性都可以从根对象中获取)CriteriaQuery
:顶层查询对象,自定义查询方式(了解:一般不用)CriteriaBuilder
:查询的构造器,封装了很多的查询条件
单参数查询
@Test
public void testSpecificationOneParam(){
Specification<Customer> specification=(root,query,cb)->{
//获取比较属性
Path<Object> custName = root.get("custName");
//构建查询条件
Predicate predicate = cb.equal(custName, "飞飞飞");
return predicate;
};
List<Customer> customers = this.customerRepository.findAll(specification);
customers.forEach((customer -> System.out.println(customer)));
}
多参数查询
@Test
public void testSpecificationMoreParam(){
Specification<Customer> specification=(root, query, criteriaBuilder) -> {
//获取比较属性
Path<Object> custName = root.get("custName");
Path<Object> custIndustry = root.get("custIndustry");
//构造条件查询
Predicate preName = criteriaBuilder.equal(custName, "飞飞飞");
Predicate preIndy = criteriaBuilder.equal(custIndustry, "娱乐");
//与相连
Predicate predicate = criteriaBuilder.and(preName, preIndy);
return predicate;
};
List<Customer> customers = this.customerRepository.findAll(specification);
customers.forEach((customer)-> System.out.println(customer));
}
模糊查询
@Test
public void testSpecificationLike(){
Specification<Customer> specification=(root, query, criteriaBuilder) -> {
//获取比较属性
Path<Object> custName = root.get("custName");
//构建查询条件
Predicate predicate = criteriaBuilder.like(custName.as(String.class), "%飞__");
return predicate;
};
List<Customer> customers = this.customerRepository.findAll(specification);
customers.forEach(customer -> System.out.println(customer));
}
分页查询
@Test
public void testSpecificationPage(){
//每页数目
int row=5;
//当前页
int page=0;
//添加查询条件
Specification<Customer> specification=(root, query, criteriaBuilder) -> {
//获取比较属性
Path<Object> custName = root.get("custName");
//构建查询条件
Predicate predicate = criteriaBuilder.like(custName.as(String.class), "%飞__");
return predicate;
};
//排序
// Sort sort=Sort.by(Sort.Direction.ASC,"custId");
//分页条件
Pageable pageable=PageRequest.of(page,row,Sort.Direction.ASC,"custId");
//分页查询
Page<Customer> cpage = this.customerRepository.findAll(pageable);
//获取数据集合
List<Customer> customers = cpage.getContent();
//获取总页数
int totalPages = cpage.getTotalPages();
//获取总条数
long totalElements = cpage.getTotalElements();
System.out.println("数据总页数:"+totalPages);
System.out.println("数据总条数:"+totalElements);
customers.forEach(customer -> System.out.println(customer));
}
输出
Example的使用
简单查询
@Test
public void testExample(){
Customer customer=new Customer();
customer.setCustName("飞飞飞");
Example<Customer> example=Example.of(customer);
List<Customer> customers = this.customerRepository.findAll(example);
customers.forEach(value -> System.out.println(value));
}
使用ExampleMatcher
/**
* 测试ExampleMatcher
*/
@Test
public void testExampleMatcher(){
Customer customer=new Customer();
customer.setCustName("记忆");
customer.setCustIndustry("溜溜");
customer.setCustPhone("5566");
customer.setCustAddress("china");
customer.setCustSource("aaa");
ExampleMatcher exampleMatcher=ExampleMatcher.matching()
.withMatcher("custName",ExampleMatcher.GenericPropertyMatchers.startsWith()) //模糊查询匹配开头
.withMatcher("custIndustry",ExampleMatcher.GenericPropertyMatchers.contains()) //全部模糊查询
.withIgnoreCase("custAddress") //忽略大小写
.withMatcher("custPhone",ExampleMatcher.GenericPropertyMatchers.exact()) //精准匹配
.withIgnorePaths("custSource"); //忽略字段,即不管custSource是什么值都不加入查询条件
Example<Customer> example=Example.of(customer,exampleMatcher);
List<Customer> customers = this.customerRepository.findAll(example);
customers.forEach(value-> System.out.println(value));
}
输出
lambda的方式
ExampleMatcher exampleMatcher2=ExampleMatcher.matching()
.withMatcher("custName",match->match.startsWith())
.withMatcher("custIndustry",match->match.contains())
.withMatcher("custAddress",match->match.ignoreCase())
.withMatcher("custPhone",match->match.exact())
.withIgnorePaths("custSource");
该种方式和上面的ExampleMatcher效果是相同的
StringMatcher 参数
Matching | ç成的语句 | 说明 |
---|---|---|
DEFAULT (case-sensitive) | firstname = ?0 | 默认(大小写敏感) |
DEFAULT (case-insensitive) | LOWER(firstname) = LOWER(?0) | 默认(忽略大小写) |
EXACT (case-sensitive) | firstname = ?0 | 精确匹配(大小写敏感) |
EXACT (case-insensitive) | LOWER(firstname) = LOWER(?0) | 精确匹配(忽略大小写) |
STARTING (case-sensitive) | firstname like ?0 + ‘%’ | 前缀匹配(大小写敏感) |
STARTING (case-insensitive) | LOWER(firstname) like LOWER(?0) + ‘%’ | 前缀匹配(忽略大小写) |
ENDING (case-sensitive) | firstname like ‘%’ + ?0 | 后缀匹配(大小写敏感) |
ENDING (case-insensitive) | LOWER(firstname) like ‘%’ + LOWER(?0) | 后缀匹配(忽略大小写) |
CONTAINING (case-sensitive) | firstname like ‘%’ + ?0 + ‘%’ | 模糊查询(大小写敏感) |
CONTAINING (case-insensitive) | LOWER(firstname) like ‘%’ + LOWER(?0) + ‘%’ | 模糊查询(忽略大小写) |
多表设计
表之间的划分
数据库中多表之间存在着三种关系,如图所示
从图可以看出,系统设计的三种实体关系分别为:多对多
、一对多
和一对一
关系。注意:一对多关系可以看为两种: 即一对多
,多对一
。所以说四种更精确。
分析步骤
在实际开发中,我们数据库的表难免会有相互的关联关系,在操作表的时候就有可能会涉及到多张表的操作。而在这种实现了ORM思想的框架中(如JPA),可以让我们通过操作实体类就实现对数据库表的操作。所以今天我们的学习重点是:掌握配置实体之间的关联关系。
第一步:首先确定两张表之间的关系。
如果关系确定错了,后面做的所有操作就都不可能正确。
第二步:在数据库中实现两张表的关系
第三步:在实体类中描述出两个实体的关系
第四步:配置出实体类和数据库表的关系映射(重点)
一对多
例子
我们采用的示例为客户和联系人。
客户:指的是一家公司,我们记为A。
联系人:指的是A公司中的员工。
在不考虑兼职的情况下,公司和员工的关系即为一对多。
表关系建立
在一对多关系中,我们习惯把一的一方称之为主表
,把多的一方称之为从表
。在数据库中建立一对多的关系,需要使用数据库的外键约束。
创建数据库表
/*创建客户表*/
CREATE TABLE cst_customer (
cust_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客户编号(主键)',
cust_name varchar(32) NOT NULL COMMENT '客户名称(公司名称)',
cust_source varchar(32) DEFAULT NULL COMMENT '客户信息来源',
cust_industry varchar(32) DEFAULT NULL COMMENT '客户所属行业',
cust_level varchar(32) DEFAULT NULL COMMENT '客户级别',
cust_address varchar(128) DEFAULT NULL COMMENT '客户联系地址',
cust_phone varchar(64) DEFAULT NULL COMMENT '客户联系电话',
PRIMARY KEY (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=94 DEFAULT CHARSET=utf8;
/*创建联系人表*/
CREATE TABLE cst_linkman (
lkm_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '联系人编号(主键)',
lkm_name varchar(16) DEFAULT NULL COMMENT '联系人姓名',
lkm_gender char(1) DEFAULT NULL COMMENT '联系人性别',
lkm_phone varchar(16) DEFAULT NULL COMMENT '联系人办公电话',
lkm_mobile varchar(16) DEFAULT NULL COMMENT '联系人手机',
lkm_email varchar(64) DEFAULT NULL COMMENT '联系人邮箱',
lkm_position varchar(16) DEFAULT NULL COMMENT '联系人职位',
lkm_memo varchar(512) DEFAULT NULL COMMENT '联系人备注',
lkm_cust_id bigint(32) NOT NULL COMMENT '客户id(外键)',
PRIMARY KEY (`lkm_id`),
KEY `FK_cst_linkman_lkm_cust_id` (`lkm_cust_id`),
CONSTRAINT `FK_cst_linkman_lkm_cust_id` FOREIGN KEY (`lkm_cust_id`) REFERENCES `cst_customer` (`cust_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
创建实体类映射
Customer
/**
* @author codekiller
* @date 2020/6/29 23:25
* @Description 用户实体类
*/
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="cst_customer")
public class Customer implements Serializable {
/**
* 客户编号(主键)
*/
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY) //自增主键,默认是Auto
@Column(name="cust_id") //如果可以直接映射,这个注解不需要写
private Long custId;
/**
* 客户名称(公司名称)
*/
@Column(name="cust_name")
private String custName;
/**
* 客户信息来源
*/
@Column(name="cust_source")
private String custSource;
/**
* 客户所属行业
*/
@Column(name="cust_industry")
private String custIndustry;
/**
* 客户级别
*/
@Column(name="cust_level")
private String custLevel;
/**
* 客户联系地址
*/
@Column(name="cust_address")
private String custAddress;
/**
* 客户联系电话
*/
@Column(name="cust_phone")
private String custPhone;
/**
* 联系人集合
*
* 配置多表一对多关系
* 声明关系
* 在客户实体类上(一的一方)添加了外键配置,所以对于客户而言,也具备了维护外键的作用
*/
@OneToMany(mappedBy = "customer",cascade=CascadeType.ALL,fetch=FetchType.LAZY)
//级联保存、更新、删除、刷新;延迟加载。当删除用户,会级联删除该用户的所有文章
//拥有mappedBy注解的实体类为关系被维护端
//mappedBy="customer"中的customer是LinkMan中的customer属性
private Set<LinkMan> linkMans=new HashSet<>();
@Override
public String toString() {
return "Customer{" +
"custId=" + custId +
", custName='" + custName + '\'' +
", custSource='" + custSource + '\'' +
", custIndustry='" + custIndustry + '\'' +
", custLevel='" + custLevel + '\'' +
", custAddress='" + custAddress + '\'' +
", custPhone='" + custPhone + '\'' +
", linkMans=" + linkMans +
'}';
}
}
LinkMan
/**
* @author codekiller
* @date 2020/6/29 23:29
* @Description 联系人实体类
*/
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="cst_linkman")
public class LinkMan implements Serializable {
/**
* 联系人id
*/
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="lkm_id")
private Long lkmId;
/**
* 联系人姓名
*/
@Column(name="lkm_name")
private String lkmName;
/**
* 联系人性别
*/
@Column(name="lkm_gender")
private String lkmGender;
/**
* 联系人办公电话
*/
@Column(name="lkm_phone")
private String lkmPhone;
/**
* 联系人手机
*/
@Column(name="lkm_mobile")
private String lkmMobile;
/**
* 联系人邮箱
*/
@Column(name="lkm_email")
private String lkmEmail;
/**
* 联系人职位
*/
@Column(name="lkm_position")
private String lkmPosition;
/**
* 联系人备注
*/
@Column(name="lkm_memo")
private String lkmMemo;
/**
*客户
*
* 配置多表多对一关系
* 1.声明关系
* 2.配置外键(中间表)
*
*/
@ManyToOne(targetEntity = Customer.class,cascade=CascadeType.ALL)
@JoinColumn(name="lkm_cust_id")
private Customer customer;
@Override
public String toString() {
return "LinkMan{" +
"lkmId=" + lkmId +
", lkmName='" + lkmName + '\'' +
", lkmGender='" + lkmGender + '\'' +
", lkmPhone='" + lkmPhone + '\'' +
", lkmMobile='" + lkmMobile + '\'' +
", lkmEmail='" + lkmEmail + '\'' +
", lkmPosition='" + lkmPosition + '\'' +
", lkmMemo='" + lkmMemo + '\'' +
'}';
}
}
创建数据库操作类
@Repository
public interface CustomerRepository extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
}
@Repository
public interface LinkManRepository extends JpaRepository<LinkMan,Long>, JpaSpecificationExecutor<LinkMan> {
}
注解说明
@OneToMany:
作用:建立一对多的关系映射
属性:
targetEntityClass:指定多的多方的类的字节码
mappedBy:指定从表实体类中引用主表对象的名称。
cascade:指定要使用的级联操作
fetch:指定是否采用延迟加载
orphanRemoval:是否使用孤儿删除
@ManyToOne
作用:建立多对一的关系
属性:
targetEntityClass:指定一的一方实体类字节码
cascade:指定要使用的级联操作
fetch:指定是否采用延迟加载
optional:关联是否可选。如果设置为false,则必须始终存在非空关系。
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息。
操作
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testSave(){
//创建一个客户
Customer customer=new Customer();
customer.setCustName("百度");
//创建一个联系人
LinkMan linkMan=new LinkMan();
linkMan.setLkmName("小李");
//保存到客户集合中
//customer.getLinkMans().add(linkMan);
//保存客户到联系人
linkMan.setCustomer(customer);
//先插入客户信息再插入联系人信息
this.customerRepository.save(customer);
this.linkManRepository.save(linkMan);
}
删除
删除从表数据:可以随时任意删除
删除主表数据
有从表数据
1). 在默认情况下,它会把外键字段置为null,然后删除主表数据。如果在数据库的表 结构上,外键字段有非空约束,默认情况就会报错了。
2). 如果配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null, 没有关系)因为在删除时,它根本不会去更新从表的外键字段了。
3). 如果还想删除,使用级联删除引用
没有从表数据引用:随便删
级联操作
级联操作:指操作一个对象同时操作它的关联对象
使用方法:只需要在操作主体的注解上配置cascade
cascade:配置级联操作
- CascadeType.MERGE 级联更新
- CascadeType.PERSIST 级联保存:
- CascadeType.REFRESH 级联刷新:
- CascadeType.REMOVE 级联删除:
- CascadeType.ALL 包含所有
@OneToMany(mappedBy = "customer",cascade=CascadeType.ALL,fetch=FetchType.LAZY)
private Set<LinkMan> linkMans=new HashSet<>();
级联删除
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testRemove(){
//获取数据
Customer customer = this.customerRepository.getOne(28L);
//从主表中删除数据
this.customerRepository.delete(customer);
}
出现的一个错误
:point_right: 解决办法:SpringDataJpa在一对多、多对多关系的级联操作时出现StackOverflowError(是真滴坑)
多对多
例子
我们采用的示例为用户和角色
用户:指的是咱们班的每一个同学。
角色:指的是咱们班同学的身份信息。
比如A同学,它是我的学生,其中有个身份就是学生,还是家里的孩子,那么他还有个身份是子女。
同时B同学,它也具有学生和子女的身份。
那么任何一个同学都可能具有多个身份。同时学生这个身份可以被多个同学所具有。
所以我们说,用户和角色之间的关系是多对多。
表关系建立
多对多的表关系建立靠的是中间表
,其中用户表和中间表的关系是一对多,角色表和中间表的关系也是一对多,如下图所示:
创建实体类
Role
/**
* @author codekiller
* @date 2020/7/4 17:03
* @Description 角色实体类
*/
@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="sys_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="role_id")
private Long roleId;
@Column(name="role_name")
private String roleName;
/**
* 配置多对多关系
* 被动的一方放弃维护权
*/
// @ManyToMany(targetEntity = User.class,cascade = CascadeType.ALL)
// @JoinTable(name="sys_user_role",
// //当前对象在中间表的外键
// joinColumns = {@JoinColumn(name="sys_role_id",referencedColumnName = "role_id")},
// //对方对象在中间表的外键
// inverseJoinColumns = {@JoinColumn(name="sys_user_id",referencedColumnName = "user_id")})
@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL)
private Set<User> users=new HashSet<>();
@Override
public String toString() {
return "Role{" +
"roleId=" + roleId +
", roleName='" + roleName + '\'' +
'}';
}
}
User
/**
* @author codekiller
* @date 2020/7/4 17:05
* @Description 用户实体类
*/
@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name="sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="user_id")
private Long userId;
@Column(name="user_name")
private String userName;
@Column(name="age")
private Integer age;
/**
* 配置多对多关系
*/
@ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL)
@JoinTable(name="sys_user_role",
//当前对象在中间表的外键
joinColumns = {@JoinColumn(name="sys_user_id",referencedColumnName = "user_id")},
//对方对象在中间表的外键
inverseJoinColumns = {@JoinColumn(name="sys_role_id",referencedColumnName = "role_id")})
private Set<Role> roles=new HashSet<>();
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", age=" + age +
", roles=" + roles +
'}';
}
}
创建数据库操作类
@Repository
public interface RoleRepository extends JpaRepository<Role,Long>, JpaSpecificationExecutor<Role> {
}
@Repository
public interface UserRepository extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}
注解说明
@ManyToMany
作用:用于映射多对多关系
属性:
cascade:配置级联操作。
fetch:配置是否采用延迟加载。
targetEntity:配置目标的实体类。映射多对多的时候不用写。
@JoinTable
作用:针对中间表的配置
属性:
name:配置中间表的名称
joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
inverseJoinColumn:中间表的外键字段关联对方表的主键字段
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息。
操作
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testSave(){
User user=new User();
user.setUserName("小李");
Role role=new Role();
role.setRoleName("java程序员");
//配置用户到角色关系,可以对中间表中的数据进行维护 1-1
user.getRoles().add(role);
//配置角色到用户关系,可以对中间表中的数据进行维护 1-1
// role.getUsers().add(user);
this.userRepository.save(user);
this.roleRepository.save(role);
}
注意
如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,中间表的2个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一方放弃对中间表的维护权即可,推荐在被动的一方放弃
。
将
@ManyToMany(targetEntity = User.class,cascade = CascadeType.ALL)
@JoinTable(name="sys_user_role",
//当前对象在中间表的外键
joinColumns = {@JoinColumn(name="sys_role_id",referencedColumnName = "role_id")},
//对方对象在中间表的外键
inverseJoinColumns = {@JoinColumn(name="sys_user_id",referencedColumnName = "user_id")})
private Set<User> users=new HashSet<>();
改为:
@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL)
private Set<User> users=new HashSet<>();
级联删除
@Test
@Transactional(rollbackFor = Exception.class)
@Rollback(false)
public void testDelete(){
this.userRepository.deleteById(1L);
}
多表查询
对象导航查询
对象图导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。例如:我们通过ID查询方式查出一个客户,可以调用Customer类中的getLinkMans()方法来获取该客户的所有联系人。对象导航查询的使用要求是:两个对象之间必须存在关联关系。
延迟加载的方式
/**
* 测试对象导航查询,使用延迟加载方式
*/
@Test
@Transactional(rollbackFor = Exception.class)
public void testObjectQuery(){
//查询id为1的客户
Customer customer=this.customerRepository.getOne(31L);
//对象导航查询,此客户的所有联系人
Set<LinkMan> linkMans = customer.getLinkMans();
linkMans.forEach(value-> System.out.println(value));
}
输出
LinkMan{lkmId=4, lkmName='小李', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
立即加载的方式
/**
* 测试对象导航查询,使用立即加载方式
*/
@Test
@Transactional(rollbackFor = Exception.class)
public void testObjectQueryByFindOne(){
//查询id为1的客户
Optional<Customer> oCustomer = this.customerRepository.findOne((root, query, criteriaBuilder) -> {
//获取比较属性
Path<Object> custId = root.get("custId");
//构建查询条件
Predicate predicate = criteriaBuilder.equal(custId, 31L);
return predicate;
});
Customer customer = oCustomer.get();
//对象导航查询,此客户的所有联系人
Set<LinkMan> linkMans = customer.getLinkMans();
linkMans.forEach(value-> System.out.println(value));
}
配置关联对象的加载方式,默认是LAZY
输出
LinkMan{lkmId=4, lkmName='小李', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
FetchType有两个实例对象LAZY
和EAGER
从多的一方查询
/**
* 测试对象导航查询,使用延迟加载方式,从多的一方查询
*/
@Test
@Transactional(rollbackFor = Exception.class)
public void testObjectQueryByGetOne2(){
//查询id为1的联系人
LinkMan linkman = this.linkManRepository.getOne(4L);
//对象导航查询,此联系人的客户
Customer customer = linkman.getCustomer();
System.out.println(customer);
}
输出
Customer{custId=31, custName='百度', custSource='null', custIndustry='null', custLevel='null', custAddress='null', custPhone='null', linkMans=[LinkMan{lkmId=4, lkmName='小李', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}]}