开篇说明
最近做的一个项目中,表和表之间的关联属性非常多,因此就考虑使用JPA。以前学过一点Hibernate,而Spring Data JPA和Hibernate非常相似,在Spring Boot的项目中引入也很方便,因此为了在项目中能简单驾驭JPA,我花了3天收集整理了这篇学习笔记。
网上的关于JPA文章很多,但很少有很全面的。JPA的内容点非常多,我查找学习时也没见过一篇很完整的,作为一个小白,我在从无到有的自学过程中对遇到的每一个不懂的知识点,都查阅参考了大量文章,将一些比较复杂、模糊
的概念有条理的梳理、整合
,最后进行了总结
。因此有了这篇我自认为比较详细的学习笔记。
要感谢网上那些博主的无私奉献~此篇文章将网上的各个知识点中我觉得介绍的比较详细的文章内容进行了整合,具体可以参考扩展阅读
章节,参考过的文章中比较好的我都列出来了,不过有些杂乱。
本文中的示例代码大都是我自己测试过的,并且做了补充。文章中的一些原理分析、接口说明,有些是参考别人的,有些是自己分析的,但由于本学生的水平有限,可能有错误或不足之处,欢迎留言指出!
对于JPA的动态查询,这个是重中之重,如果有基础的建议直接看动态查询
相关的示例,我将我目前遇到的所有情况的示例都写出来了,如果你们有什么别的特殊业务需求无法解决的,欢迎留言讨论!
最后,我还放了一些可能会遇到的问题,并提供了解决方案。
Spring Data JPA
一、三大框架介绍
Hibernate、Mybatis、Spring Data JPA
1.Hibernate VS Mybatis
1、简介
Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/R Mapping实现了POJO 和数据库表之间的映射,以及SQL 的自动生成和执行。程序员往往只需定义好了POJO 到数据库表的映射关系,即可通过Hibernate 提供的方法完成持久层操作。程序员甚至不需要对SQL 的熟练掌握, Hibernate/OJB 会根据制定的存储逻辑,自动生成对应的SQL 并调用JDBC 接口加以执行。
iBATIS 的着力点,则在于POJO 与SQL之间的映射关系。然后通过映射配置文件,将SQL所需的参数,以及返回的结果字段映射到指定POJO。 相对Hibernate“O/R”而言,iBATIS 是一种“Sql Mapping”的ORM实现。
2、开发对比
Hibernate的真正掌握要比Mybatis来得难些。Mybatis框架相对简单很容易上手,但也相对简陋些。个人觉得要用好Mybatis还是首先要先理解好Hibernate。针对高级查询,Mybatis需要手动编写SQL语句,以及ResultMap。而Hibernate有良好的映射机制,开发者无需关心SQL的生成与结果映射,可以更专注于业务流程。
3、系统调优对比
-
Hibernate调优方案:
- 制定合理的缓存策略;
- 尽量使用延迟加载特性;
- 采用合理的Session管理机制;
- 使用批量抓取,设定合理的批处理参数(batch_size);
- 进行合理的O/R映射设计
-
Mybatis调优方案:
- MyBatis在Session方面和Hibernate的Session生命周期是一致的,同样需要合理的Session管理机制。MyBatis同样具有二级缓存机制。 MyBatis可以进行详细的SQL优化设计。
-
SQL优化方面
- Hibernate的查询会将表中的所有字段查询出来,这一点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。而Mybatis的SQL是手动编写的,所以可以按需求指定查询的字段。
- Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了。MyBatis的SQL是自己手动写的所以调整方便。但Hibernate具有自己的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。
4.缓存机制对比
-
Hibernate缓存
Hibernate一级缓存是Session缓存,利用好一级缓存就需要对Session的生命周期进行管理好。建议在一个Action操作中使用一个Session。一级缓存需要对Session进行严格管理。Hibernate二级缓存是SessionFactory级的缓存。 SessionFactory的缓存分为内置缓存和外置缓存。内置缓存中存放的是SessionFactory对象的一些集合属性包含的数据(映射元素据及预定SQL语句等),对于应用程序来说,它是只读的。外置缓存中存放的是数据库数据的副本,其作用和一级缓存类似.二级缓存除了以内存作为存储介质外,还可以选用硬盘等外部存储设备。二级缓存称为进程级缓存或SessionFactory级缓存,它可以被所有session共享,它的生命周期伴随着SessionFactory的生命周期存在和消亡。
-
Mybatis缓存
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。MyBatis 3 中的缓存实现的很多改进都已经实现了,使得它更加强大而且易于配置。
默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现而且处理循环 依赖也是必须的。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:
<cache/>
字面上看就是这样。这个简单语句的效果如下:- 映射语句文件中的所有 select 语句将会被缓存。
- 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
- 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
- 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
所有的这些属性都可以通过缓存元素的属性来修改。
5.总结
-
Mybatis:小巧、方便、高效、简单、直接、半自动
-
Hibernate:强大、方便、高效、复杂、绕弯子、全自动
2.Hibernate 和 JPA的关系
1.JPA
全称Java Persistence API,通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
-
JPA的出现有两个原因:
- 其一,简化现有Java EE和Java SE应用的对象持久化的开发工作
- 其二,Sun希望整合对ORM技术,实现持久化领域的统一
-
JPA提供的技术:
- ORM映射元数据:JPA支持XML和JDK 5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中
- JPA 的API:用来操作实体对象,执行CRUD操作,框架在后台替我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来
- 查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合
3.JPA & Hibernate 关系
JPA是需要Provider来实现其功能的,Hibernate就是JPA Provider中很强的一个。从功能上来说,JPA现在就是Hibernate功能的一个子集。可以简单的理解为JPA是标准接口,Hibernate是实现。Hibernate主要是通过三个组件来实现的,及hibernate-annotation、hibernate-entitymanager 和hibernate-core。
- hibernate-annotation是Hibernate支持annotation方式配置的基础,它包括了标准的JPA annotation以及 Hibernate自身特殊功能的annotation。
- hibernate-core是Hibernate的核心实现,提供了Hibernate所有的核心功能。
- hibernate-entitymanager实现了标准的JPA,可以把它看成hibernate-core和JPA之间的适配器,它并不直接提供ORM的功能,而是对hibernate-core进行封装,使得Hibernate符合JPA的规范。
总的来说,JPA是规范,Hibernate是框架,JPA是持久化规范,而Hibernate实现了JPA。
4.JPA 概要
1.概述
JPA维护一个Persistence Context(持久化上下文),在持久化上下文中维护实体的生命周期。主要包含三个方面的内容:
- ORM元数据。JPA支持annotion或xml两种形式描述对象-关系映射。
- 实体操作API。实现对实体对象的CRUD操作。
- 查询语言。约定了面向对象的查询语言JPQL(Java Persistence Query Language。
JPA的主要API都定义在javax.persistence包中。如果你熟悉Hibernate,可以很容易做出对应:
org.hibernate | javax.persistence | 说明 |
---|---|---|
cfg.Configuration | Persistence | 读取配置信息 |
SessionFactory | EntityManagerFactory | 用于创建会话/实体管理器的工厂类 |
Session | EntityManager | 提供实体操作API,管理事务,创建查询 |
Transaction | EntityTransaction | 管理事务 |
Query | Query | 执行查询 |
二、扩展阅读
实体类相关
1.1.注解相关
@GeneratedValue 四种标准用法为TABLE,SEQUENCE,IDENTITY,AUTO.
hibernate中的@GeneratedValue与@GenericGenerator
JPA @Id 和 @GeneratedValue 注解详解(重要)
1.2.实体关系映射
1.3.级联概念(重要)
1.4.了解即可
查询相关
1.1.简单入门
1.2.完整流程
1.3.查询进阶
spring boot 配置及使用 spring data jpa
spring data jpa 实现多条件复杂查询及多表联查
spring-data-jpa 介绍 复杂查询,包括多表关联,分页,排序等
1.4.扩展阅读
三、简单入门
-
创建Spring Boot项目,在pom.xml引入jpa坐标
<!--Spring Data Jpa,会自动导入依赖的包如hibernate的实现类--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--Mysql,指定了5.XX版本,JPA依赖此包--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>
-
配置application.properties
#数据库连接设置 spring.datasource.url=jdbc:mysql://xxxx spring.datasource.username=root spring.datasource.password=xxxx spring.datasource.driver-class-name=com.mysql.jdbc.Driver #JPA自动建表 spring.jpa.database=mysql ## update:若数据接表不存在,则自动创建表,若存在,不做表创建操作 spring.jpa.hibernate.ddl-auto=update ## 是否注册OpenEntityManagerInViewInterceptor。将JPA EntityManager绑定到用于整个请求处理的线程。 ## true可以解决web测试下懒加载的session关闭问题 spring.jpa.open-in-view=true ## 非web环境下的懒加载解决方案 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true ## 是否显示sql语句 spring.jpa.show-sql=true ## 其中spring.jpa.hibernate.naming.physical-strategy是为了修改生成的表和属性的命名策略 ## 默认是自动转成小写和下划线形式,versionCode就变成了version_code,其实这种命名策略是比较好的。 ## 但是有时候我们可能更加希望属性名称和数据库名称统一,所以增加这个配置后生成的表和属性就和Java类一致 spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
-
创建实体类并配置(这举一个一对多的实体类例子,详细的理解往下看)
FairyAdmin
package cn.dmdream.entity; import lombok.Data; import lombok.ToString; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Data//自动生成get/set/tostring @ToString(exclude = "catList")//防止内存溢出 @Entity//交给jpa管理实体,并设置映射到数据库的表名 @Table(name = "tab_admin") public class FairyAdmin { @Id //主键 @Column(name = "adminId",unique = true,nullable = false) //name属性不写默认和字段一样 @GeneratedValue(strategy = GenerationType.IDENTITY)//主键由数据库自动生成(主要是自动增长型) private Integer adminId; @Column(length = 100,unique = true)//设置长度和唯一性 private String adminUsername; @Column(length = 100) private String adminPassword; @Column(length = 100) private String adminNickname; @Column(length = 255) private String adminNicpic; //一的一方 @OneToMany(mappedBy = "admin",cascade = CascadeType.ALL,fetch = FetchType.LAZY) private List<FairyCat> catList = new ArrayList<FairyCat>(); }
FairyCat
package cn.dmdream.entity; import lombok.Data; import javax.persistence.*; @Data //@ToString(exclude = {"admin"}) @Entity @Table(name = "tab_cat") public class FairyCat { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer catId; @Column(nullable = false) private String catName; //多的一方 @ManyToOne(cascade = {CascadeType.PERSIST,CascadeType.MERGE,CascadeType.REFRESH},fetch = FetchType.EAGER) private FairyAdmin admin; }
-
创建Dao并继承JpaRepository接口,所有基础的增删改查方法都提供了
FairyAdminDao
package cn.dmdream.dao; import cn.dmdream.entity.FairyAdmin; import cn.dmdream.entity.FairyCat; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface FairyAdminDao extends JpaRepository<FairyAdmin,Integer> {//两个参数,一个是对应的实体类,一个是该实体类主键的类型 //自定义方法,使用Jpa标准化查询语言 public FairyAdmin findByAdminUsername(String username); //使用原生的sql进行查询 @Query(value = "select * from tab_admin a,tab_cat c where a.adminId=c.admin_adminId and a.adminUsername like " + "%?1%",nativeQuery = true) public List<FairyAdmin> myFindAllByUsernameLike(String username); //使用JPQL查询,这里会有深坑 @Query(value = "select a,c from FairyAdmin a,FairyCat c where a.adminId=c.admin.adminId and a.adminUsername like " + "%?1%") public List<FairyAdmin> myFindAllByUsernameLike_JPQL(String username); }
FairyCatDao
package cn.dmdream.dao; import cn.dmdream.entity.FairyCat; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface FairyCatDao extends JpaRepository<FairyCat,Integer> { public FairyCat findByCatName(String name); }
-
编写测试类TestFairyAdminDao
package cn.dmdream.dao; import cn.dmdream.entity.FairyAdmin; import cn.dmdream.entity.FairyCat; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.test.context.junit4.SpringRunner; import javax.persistence.criteria.*; import java.util.ArrayList; import java.util.List; @RunWith(SpringRunner.class)//指定测试运行类 @SpringBootTest(properties = {"classpath:application.properties"})//使用springboot测试 public class TestFairyAdminDao { @Autowired private FairyAdminDao fairyAdminDao; @Autowired private FairyCatDao fairyCatDao; @Test//新增 public void saveTest() { FairyAdmin admin = new FairyAdmin(); admin.setAdminUsername("yoko"); FairyCat cat = new FairyCat(); cat.setCatName("yoko_Cat"); cat.setAdmin(admin);//双向关联保存 admin.getCatList().add(cat); fairyAdminDao.save(admin); } //按用户名查询 @Test public void findByUsernameTest() { FairyAdmin admin = fairyAdminDao.findByAdminUsername("yoko"); System.out.println(admin); System.out.println(admin.getCatList());//关联查询 } //修改 @Test public void updateTest() { FairyAdmin admin = fairyAdminDao.findByAdminUsername("yoko"); admin.setAdminNickname("yokoNicname"); admin.setAdminPassword("yokoPassword"); admin.getCatList().get(0).setCatName("yoko_cat_edit1"); fairyAdminDao.save(admin);//这里会自动根据主键判断是否是更新 } //删除 @Test public void deleteTest() { FairyAdmin admin = fairyAdminDao.findByAdminUsername("yoko"); fairyAdminDao.delete(admin);//级联删除,猫也会被删除 } //批量保存 @Test public void saveListTest() { List<FairyAdmin> list = new ArrayList<>(); for (int i = 1; i <= 10; i++) { FairyAdmin admin = new FairyAdmin(); admin.setAdminUsername("yoko_" + i); FairyCat cat = new FairyCat(); cat.setCatName("yoko_cat_" + i); cat.setAdmin(admin);//双向关联保存 admin.getCatList().add(cat); list.add(admin); } fairyAdminDao.saveAll(list); } //查询所有 @Test public void findAllTest() { List<FairyAdmin> list = fairyAdminDao.findAll(); list.forEach(System.out::println); } //原生查询语句测试 @Test public void myFindAllByUsernameLikeTest() { List<FairyAdmin> list = fairyAdminDao.myFindAllByUsernameLike("yoko_1"); list.forEach(System.out::println); } //JPQL查询语句测试 @Test public void myFindAllByUsernameLike_JPQLTest() { List<FairyAdmin> list = fairyAdminDao.myFindAllByUsernameLike_JPQL("yoko_1"); list.forEach(System.out::println); } //排序和分页 @Test public void sortAndPageSearchTest() { //排序的几种方式 //方式一:多字段排序查询,无法自定义顺序,输出语句...order by adminId desc, adminUsername desc //Sort sort = new Sort(Sort.Direction.DESC,"adminId","adminUsername"); //方式二:多字段排序查询,可自定义顺序,x输出语句:...order by adminUsername asc, adminId desc Sort sort = new Sort(Sort.Direction.DESC,"adminId");//第一个参数,排序类型:ASC/DESC,第二个参数:按照排序的字段,可以设置多个 Sort sort1 = new Sort(Sort.Direction.ASC,"adminUsername"); final Sort mergeSort = sort1.and(sort); //方式三:多字段排序查询,先创建order对象,再构造sort List<Sort.Order> orders = new ArrayList<Sort.Order>(); Sort.Order order1 = new Sort.Order(Sort.Direction.DESC,"adminId"); Sort.Order order2 = new Sort.Order(Sort.Direction.ASC,"adminUsername"); //可以直接单对象创建 Sort orderSort = Sort.by(order2);//...order by adminUsername asc //可以使用order列表创建 orders.add(order1); orders.add(order2); Sort orderListSort = Sort.by(orders);//...order by adminId desc, adminUsername desc //需要注意的是:page从0开始,不是从1开始! PageRequest pageRequest = PageRequest.of(0,5, orderListSort); Page<FairyAdmin> content = fairyAdminDao.findAll(pageRequest); if (content.hasContent()) { System.out.println("总记录数:"+content.getTotalElements()); System.out.println("总页数:"+content.getTotalPages()); System.out.println("当前页:"+(content.getPageable().getPageNumber()+1)); System.out.println("当前页面大小:"+content.getPageable().getPageSize()); List<FairyAdmin> list = content.getContent(); list.forEach(System.out::println); System.out.println(content); } System.out.println("=========================================="); } //测试Example查询,简单测试 @Test public void exampleQueryTest() { FairyAdmin admin = new FairyAdmin(); admin.setAdminUsername("yoko_5");//精确匹配 Example<FairyAdmin> example = Example.of(admin);//默认会忽略空值的字段 List<FairyAdmin> list = fairyAdminDao.findAll(example); list.forEach(System.out::println); } //测试Example查询,多条件 @Test public void exampleQueryTest2() { FairyAdmin admin = new FairyAdmin(); admin.setAdminUsername("YOKO"); admin.setAdminPassword("123"); //ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase("adminUsername").withMatcher("adminUsername", ExampleMatcher.GenericPropertyMatchers.startsWith()).withIgnorePaths("adminPassword"); //忽略adminUsername,模糊查询开头匹配,忽略adminPassword(无论是否由值都忽略) //使用lambda版本 ExampleMatcher matcher1 = ExampleMatcher.matching().withIgnoreCase().withMatcher("adminUsername", matcher -> matcher.startsWith()) .withIgnorePaths("adminPassword"); Example<FairyAdmin> example = Example.of(admin,matcher1); List<FairyAdmin> list = fairyAdminDao.findAll(example); list.forEach(System.out::println); } //测试Criteria查询 @Test public void criteriaQueryTest() { //1.新建排序 Sort sort = new Sort(Sort.Direction.DESC,"adminUsername"); //2.新建分页 PageRequest pageRequest = PageRequest.of(0, 5, sort); //3.实现接口方法specification,添加条件 Specification<FairyAdmin> specification = new Specification<FairyAdmin>() { @Override public Predicate toPredicate(Root<FairyAdmin> root, CriteriaQuery<?> query, CriteriaBuilder cb) { //3.1.混合条件查询 Path<String> path1 = root.get("adminUsername");//一定要类型匹配 //Path<String> path2 = root.get("adminPassword");//可以像下面直接写到参数里 Predicate predicate = cb.and(cb.like(path1, "%yoko%"),cb.like(root.get("adminPassword"), "%456%")); //3.2.多表查询 //Join<FairyAdmin, FairyCat> join = root.join("catList", JoinType.INNER); Join<FairyAdmin, FairyCat> join = root.join(root.getModel().getList("catList",FairyCat.class), JoinType .INNER); Path<String> catName = join.get("catName"); Predicate predicate2 = cb.and(cb.like(path1, "%yoko%"),cb.like(root.get("adminPassword"), "%456%"),cb.like(catName,"%cat%")); //输3.2.1出语句...inner join tab_cat c on adminId=c.admin_adminId where // (adminUsername like ?) and (adminPassword like ?) and (c.catName like ?) order by adminUsername // desc limit ?,? //return predicate2; //3.3.使用CriteriaQuery直接设置条件,不再需要返回值 query.where(predicate2);//这里可以设置任意条查询条件 return null;//这种方式使用JPA的API设置了查询条件,所以不需要再返回查询条件Predicate给Spring Data Jpa,故最后return null } }; //4.执行查询 List<FairyAdmin> list = fairyAdminDao.findAll(specification, pageRequest).getContent(); //list.forEach(System.out::println); for (FairyAdmin admin: list) { System.out.println(admin); System.out.println(admin.getCatList());//设置了级联关系,默认自动查询了 } } }
PS:如果都能理解的,看到这里就可以不用看了( ̄3 ̄)a
四、实体关系映射(重要)
1.作用
简化编程操作。把冗余的操作交给底层框架来处理。
例如,如果我要给一位新入学的学生添加一位新的老师。而这个老师又是新来的,在学生数据库与教师数据库中均不存在对应的数据。那么我需要先在教师数据库中保存新来的老师的数据,同时在学生数据库中保存新学生的数据,然后再给两者建立关联。
而如果我们使用了实体关系映射,我们只需要将该新教师实体交给该学生实体,然后保存该学生实体即可完成。
2.多对多
举例:学生和课程的关系。一位学生,会修多门课程;而一门课程,也会被多位学生修习。此时,双方的关系即为多对多关系。拥有多对多关系的两个实体将会有一个中间表来记录两者之间的关联关系。
Student
package cn.dmdream.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(generator = "idGenerator")//用了下面Hibernate提供的生成器来做主键
@GenericGenerator(name = "idGenerator", strategy = "uuid")//Hibernate的通用主键生成器,生成策略时uuid
private String id;
private String sName;
@ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY)
private Set<Course> courses = new HashSet<>();//这里直接new了空对象方便直接设置
}
Course
package cn.dmdream.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Course {
@Id
@GeneratedValue(generator = "idGenerator")
@GenericGenerator(name = "idGenerator", strategy = "uuid")
private String id;
private String cName;
@ManyToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="courses")//cascade(级联关系)fetch(加载策略)mappedBy(声明关系的维护方)mappedBy出现在哪一方意味着哪一方不用维护外键关系了
private Set<Student> students= new HashSet<>();//这里直接new了空对象方便直接设置
}
/*
mappedBy声明于关系的被维护方,声明的值为关系的维护方的关系对象属性名。
在实例中,mappedBy被声明于Course类中,其值为Student类中的Set对象"courses"。即,Student为关系维护方,Course为被维护方。
*/
- PS:关于mappedBy可以参考 有关mappedBy的思考
- PS:fetch管读取,cascade管增删改,关于fetch解释如下
- FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载。
- FetchType.EAGER:急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载。
- @OneToMany默认的是LAZY,@ManyToOne默认是EAGER
测试代码ManyToManyTest
PS:如果发现测试时建表失败,是Mysql数据库版本的问题,要不更换到5.7版本要不将实体的主键增长策略从uuid换成Int自增类型。
package cn.dmdream.dao;
import cn.dmdream.entity.Course;
import cn.dmdream.entity.Student;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ManyToManyTest {
@Test
public void contextLoads() {
}
@Autowired
private StudentDao studentDao;
@Autowired
private CourseDao courseDao;
/**
* 仅将被维护方对象添加进维护方对象Set中
* 保存维护方对象
*/
@Test
public void 多对多插入1() {//运行后发现数据库创建了3表,其中有一张时中间表
Student s = new Student();
s.setSName("二狗");
Course c = new Course();
c.setCName("语文");
s.getCourses().add(c);
studentDao.save(s);//由于操作对象是维护方,成功地在student、course以及中间表student_courses中分别添加了数据
}
/**
* 仅将维护方对象添加进被维护方对象Set中
* 保存被维护方对象
*/
@Test
public void 多对多插入2() {
Student s = new Student();
s.setSName("三汪");
Course c = new Course();
c.setCName("英语");
c.getStudents().add(s);
courseDao.save(c);//操作对象在这里换成了被维护方。不负众望,出问题了。保存的时候,student表和course表倒是都成功地插入了数据,但是中间表中,并未产生对两者数据的关联。
}
/**
* 将双方对象均添加进双方Set中
* 保存被维护方对象
*/
@Test
public void 多对多插入3() {
Student s = new Student();
s.setSName("一晌");
Course c = new Course();
c.setCName("数学");
s.getCourses().add(c);
c.getStudents().add(s);
courseDao.save(c);//将双方对象均添加进双方Set中,操作对象是被维护方,操作结果与第一组相同
}
/**
* 删除维护方对象
*/
@Test
public void 多对多删除1(){
Student s = studentDao.findBySName("二狗");
System.out.println(s);
System.out.println(s.getCourses());//报错解决方式:实体采用立即加载fetch=FetchType.EAGER
studentDao.delete(s);//操作对象是维护方,成功删除三张表数据
}
/**
* 删除被维护方对象
*/
@Test
public void 多对多删除2(){
//Course c = courseDao.findByCName("英语");//操作对象是被维护方,在删除的时候只删除了course中的数据
Course c = courseDao.findByCName("数学");//操作对象是被维护方,但双方对象均添加进双方Set中,成功删除三张表数据
courseDao.delete(c);
}
}
/*
由此可知,实际操作中,只要中间表建立了关联,即使是注解定义的被维护方也是可以对双方关系进行维护的。
*/
2.一对多和多对一
- 一对多关系即数据库中的一行数据关联另一个数据库中的多行关系。多对一与之相反。
- 一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键)。
- 在一的一方使用@OneToMany,fetch默认的是LAZY
- 在多的一方使用@ManyToOne,fetch默认是EAGER
举例一:学生和班级的关系,学生是多的一方,班级是一的一方。如果不在多的一方(学生)中使用mapperBy来增加外键字段,那么将会生成中间表来维护,不建议这么做。
Student
public class Student {
//其余略
@ManyToOne(cascade=CascadeType.ALL,fetch=FetchType.LAZY)//学生(对于班级来说是多的一方)是维护方,需要维护外键关系,因此使用@JoinColumn添加外键
@JoinColumn(name = "stu_class",referencedColumnName = "id")//name:对应的是数据库里的要映射的外键名 referencedColumnName:指向的是班级类(一的一方)的主键
private ClassEntity classEntity;
}
ClassEntity(班级)
public class ClassEntity {
@Id
@GeneratedValue(generator = "idGenerator")
@GenericGenerator(name = "idGenerator", strategy = "uuid")
private String id;
@OneToMany(cascade=CascadeType.PERSIST,fetch=FetchType.LAZY,mappedBy="classEntity")//这里级联仅做了保存关系,班级(一的一方)是被维护方
private Set<Student> students= new HashSet<>();
}
**举例二(带测试代码):**管理员和猫,一个管理员可以有多只猫,管理员能对猫设置所有级联操作,但是猫不能对管理员设置删除级联。
FairyAdmin(一的一方,被维护方)
package cn.dmdream.entity;
import lombok.Data;
import lombok.ToString;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Data
@ToString(exclude = "catList")
@Entity//交给jpa管理实体,并设置映射到数据库的表名
@Table(name = "tab_admin")
public class FairyAdmin {
@Id //主键
@Column(name = "adminId",unique = true,nullable = false) //name属性不写默认和字段一样
@GeneratedValue(strategy = GenerationType.IDENTITY)//主键由数据库自动生成(主要是自动增长型)
private Integer adminId;
@Column(length = 100,unique = true)
private String adminUsername;
@Column(length = 100)
private String adminPassword;
@Column(length = 100)
private String adminNickname;
@Column(length = 255)
private String adminNicpic;
//一的一方
@OneToMany(mappedBy = "admin",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private List<FairyCat> catList = new ArrayList<FairyCat>();
}
FairyCat(多的一方,关系的维护方)
package cn.dmdream.entity;
import lombok.Data;
import javax.persistence.*;
@Data
//@ToString(exclude = {"admin"})
@Entity
@Table(name = "tab_cat")
public class FairyCat {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer catId;
@Column(nullable = false)
private String catName;
//多的一方
@ManyToOne(cascade = {CascadeType.PERSIST,CascadeType.MERGE,CascadeType.REFRESH},fetch = FetchType.EAGER)//设置了级联增/改/刷新
private FairyAdmin admin;
}
测试代码OneToManyAndReverseTest
package cn.dmdream.dao;
import cn.dmdream.entity.FairyAdmin;
import cn.dmdream.entity.FairyCat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class OneToManyAndReverseTest {
@Autowired
FairyAdminDao fairyAdminDao;
@Autowired
FairyCatDao fairyCatDao;
/**
* 将维护方对象添加到被维护方集合
* 保存被维护方对象
*/
@Test
public void 一对多插入1() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("admin");
admin.setAdminPassword("123");
admin.setAdminNickname("yoko");
FairyCat cat = new FairyCat();
cat.setCatName("Tom");
FairyCat cat1 = new FairyCat();
cat1.setCatName("Miki");
admin.getCatList().add(cat);
admin.getCatList().add(cat1);
fairyAdminDao.save(admin);//保存结果:双方都插入了对象,但是维护方(cat)没有外键
}
/**
* 将被维护方对象设置到维护方对象中
* 仅保存维护方对象
*/
@Test
public void 多对一插入2() {
FairyAdmin admin = new FairyAdmin();//被维护方
admin.setAdminUsername("admin2");
admin.setAdminPassword("123");
admin.setAdminNickname("yoko");
FairyCat cat = new FairyCat();//维护方
cat.setCatName("Kuku");
cat.setAdmin(admin);
fairyCatDao.save(cat);//保存结果:双方都插入了对象,并且维护方有外键
}
/**
* 将双方对象互相保存
* 仅保存被维护方(admin)对象
*/
@Test
public void 一对多插入3() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("admin3");
admin.setAdminPassword("123");
admin.setAdminNickname("yoko");
FairyCat cat = new FairyCat();
cat.setCatName("Tom2");
cat.setAdmin(admin);
FairyCat cat1 = new FairyCat();
cat1.setCatName("Miki2");
cat1.setAdmin(admin);
admin.getCatList().add(cat);
admin.getCatList().add(cat1);
fairyAdminDao.save(admin);//保存结果:设置了双向关联,即便插入对象是被维护方,2张表都有数据并且维护方(cat)中有外键
}
/**
* 删除维护方对象,这里维护方(cat)未设置级联删除
*/
@Test
public void 多对一删除1() {
FairyCat cat = fairyCatDao.findByCatName("Kuku");
System.out.println(cat);
cat.getAdmin().getCatList().remove(cat);//由于二级缓存机制,要清空级联对象中的自己对象,要不无法删除,不会有删除语句
//可以看到在进行上一步设置后,成功有了删除语句delete from tab_cat where catId=?
fairyCatDao.delete(cat);//删除结果:由于未设置级联删除,即便删除的是维护方对象,也只删除了自己,不能删除Admin
}
/**
* 删除被维护方对象,这里被维护方(一的一方,admin)设置了全部级联
*/
@Test
public void 一对多删除2() {
//FairyAdmin admin = fairyAdminDao.findByAdminUsername("admin");
FairyAdmin admin3 = fairyAdminDao.findByAdminUsername("admin3");
System.out.println(admin3);
fairyAdminDao.delete(admin3);//虽然删除的是被维护端对象,但是设置了级联删除,能顺利删除两张表的数据,这里没有缓存机制作祟
}
}
3.一对一
- 一对一关系即两个数据库中的数据一一对应,这个比较简单
**举例:**员工和身份证
Emp(员工)
//双向 一对一
@Entity
public class Emp{
@Id
@GeneratedValue
privte Integer eId;
@Column(length = 40)
private String empName;
@OneToOne(cascade = CascadeType.ALL)
private Identity identity;
//get,set方法省略
}
Identity(身份证)
@Entity
public class Identity{
@Id
@GeneratedValue
privte Integer iId;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "identity")
private Emp emp;
//...
}
分析
以上例子,双向一对一,Emp 为关系拥有方,Identity 为关系被拥有方。
执行spring-data-jpa新增操作时,如果通过Identity的数据访问层进行新增操作(IdentityRepository.save())
,Emp表和Identity表都有数据,但是不会设置这两条数据的关系,Emp表中的外键为null。
反之,以关系拥有方Emp的数据访问层进行新增操作(EmpRepository.save()),Emp表和Identity表都有数据,并且
设置了两条数据的关系,即Emp表中的外键也得到正确新增
4.级联关系(重要)
4.1.级联类型
-
CascadeType.PERSIST
Cascade persist operation,级联保存操作。
持久保存拥有对方实体时,也会持久保存该实体的所有相关数据。
-
CascadeType.REMOVE
Cascade remove operation,级联删除操作。
删除当前实体时,与它有映射关系的实体也会跟着被删除。 -
CascadeType.MERGE
Cascade merge operation,级联更新(合并)操作。
当Student中的数据改变,会相应地更新Course中的数据。 -
CascadeType.DETACH
Cascade detach operation,级联脱管/游离操作。
如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。 -
CascadeType.REFRESH
Cascade refresh operation,级联刷新操作。
假设场景 有一个订单,订单里面关联了许多商品,这个订单可以被很多人操作,那么这个时候A对此订单和关联的商品进行了修改,与此同时,B也进行了相同的操作,但是B先一步比A保存了数据,那么当A保存数据的时候,就需要先刷新订单信息及关联的商品信息后,再将订单及商品保存。 -
CascadeType.ALL
Cascade all operations,清晰明确,拥有以上所有级联操作权限。
4.2.级联保存Demo
public class Student {
@ManyToMany(cascade=CascadeType.PERSIST,fetch=FetchType.LAZY)//配置多对多关系
private Set<Course> courses = new HashSet<>();
//其他代码略。
}
解释:
上面的代码中给了Student对Course进行级联保存(cascade=CascadeType.PERSIST)的权限。此时,若Student实体持有的Course实体在数据库中不存在时,保存该Student时,系统将自动在Course实体对应的数据库中保存这条Course数据。而如果没有这个权限,则无法保存该Course数据。
进一步:如何设置级联保存?
简单理解:想要级联保存谁,就在这个想要保存对象的字段属性上进行级联的设置cascade=CascadeType.类型
-
实体中需要有别的实体对象,如A类中有一个B的Set集合(需要new出来?看清空)
class A{ private Set<B> bSet = new HashSet<B>();//B的Set集合,此对象需要new创建出来?看情况 }
-
需要在这个
bSet
集合属性上使用关系映射的注解,并配置级联关系@ManyToMany(cascade=CascadeType.PERSIST,fetch=FetchType.LAZY) private Set<B> bSet = new HashSet<B>();
-
然后直接保存对象A,当A中有bSet对象时,就会级联保存了
5.封装抽象实体(了解)
5.1.通用抽象基类
BaseEntity(用于继承,因此是抽象类)
package cn.dmdream.entity.base;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@MappedSuperclass//表明这是父类,可以将属性映射到子类中使用JPA生成表
public abstract class BaseEntity implements Serializable {
@Getter @Setter
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id",columnDefinition = "bigint(30) comment '无意义自增主键'")
protected Long id;//无意义自增主键
@Getter @Setter
@Column(name="createTime",columnDefinition="DATETIME comment '创建时间'")
protected Date createTime = new Date(); //创建时间
@Getter @Setter
@Column(name="destroyTime",columnDefinition="DATETIME comment '销毁时间'")
protected Date destroyTime; //销毁时间
@Getter @Setter
@Version @Column(name="version",nullable=false,columnDefinition="int(20) default 1 comment '版本号'")
protected Integer version = 1;//乐观锁,使用@Version注解标明
@Getter @Setter
@Column(length=1,name="isValid",nullable=false,columnDefinition="int(1) default 1 comment '是否启用,1:启用 0:不启用'")
protected Integer isValid = 1; //是否启用
@Getter @Setter
@Transient//不被映射到数据库
protected String createTimeStart; //创建时间的开始点
@Getter @Setter
@Transient//不被映射到数据库
protected String createTimeEnd; //创建时间的结束点
}
5.2.用户抽象基类
BaseUserEntity
package cn.dmdream.entity.base;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
/**
* User和Admin的父类信息
*/
@MappedSuperclass
public abstract class BaseUserEntity extends BaseEntity{
@Getter @Setter
@Column(length = 32,name = "username",nullable = false,unique = true,columnDefinition = "varchar(32) unique " +
"comment '用户名'")
protected String username;//用户名不为空,唯一,长度32
@Getter @Setter
@Column(length = 11,name = "phone",nullable = false,unique = true,columnDefinition = "varchar(11) unique " +
"comment '手机号'")
protected String phone;//手机号不为空,唯一,长度11
@Getter @Setter
@Column(length = 64,name = "email",columnDefinition = "varchar(64) comment '邮箱'")
protected String email;//邮箱
@Getter @Setter
@Column(length = 32,name = "realname",columnDefinition = "varchar(32) comment '真实姓名'")
protected String realname;//真实姓名
@Getter @Setter
@Column(length = 32,name = "nickname",columnDefinition = "varchar(32) comment '昵称'")
protected String nickname;//昵称
@Getter @Setter
@Column(name = "nickpic",columnDefinition = "varchar(255) comment '头像地址'")
protected String nickpic;//头像地址
}
五、JPA的注解和属性
PS:常用的注解都已加粗标明
-
@MappedSuperclass(标注当前实体类是基类,该基类不会被映射)
- @MappedSuperclass注解使用在父类上面,是用来标识父类的
- 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
- 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口
-
@Entity(标注实体类,也可以设置表名)
被Entity标注的实体类将会被JPA管理控制,在程序运行时,JPA会识别并映射到指定的数据库表 唯一参数name:指定实体类名称,默认为当前实体类的非限定名称。 若给了name属性值即@Entity(name="XXX"),则jpa在仓储层(数据层)进行自定义查询时,所查的表名应是XXX。 如:select s from XXX s
-
@Table(用于指定表名,但通常可以用@Entity(name=“table_name”)替代)
当你想生成的数据库表名与实体类名称不同时,使用 @Table(name="数据库表名"),与@Entity标注并列使用,置于实体 类声明语句之前 @Entity @Table(name="t_student") public class student{ ... }
@Table中的参数(不常用)
- catalog: 用于设置表所映射到的数据库的目录
- schema: 用于设置表所映射到的数据库的模式
- uniqueConstraints: 设置约束条件
-
@Id(用于标明主键)
@Id 用于实体类的一个属性或者属性对应的getter方法的标注,被标注的的属性将映射为数据库主键
-
@GeneratedValue(用于指定主键自增策略)
与@Id一同使用,用于标注主键的生成策略,通过 strategy 属性指定。默认是JPA自动选择合适的策略 在 javax.persistence.GenerationType 中定义了以下几种可供选择的策略: - IDENTITY:采用数据库ID自增长的方式产生主键,Oracle 不支持这种方式。 - AUTO: JPA 自动选择合适的策略,是默认选项。 - SEQUENCE:通过序列产生主键,通过@SequenceGenerator标注指定序列名,MySQL 不支持这种方式。 - TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略更易于做数据库移植。
-
@Basic(用于标注字段是一个数据库映射字段,默认有)
@Basic表示一个简单的属性到数据库表的字段的映射,对于没有任何标注的 getXxxx() 方法,默认即为@Basic(fetch=FetechType.EAGER) @Basic参数: 1. fetch 表示该属性的加载读取策略 1.1 EAGER 主动抓取 (默认为EAGER) 1.2 LAZY 延迟加载,只有用到该属性时才会去加载 2. optional (默认为 true) 表示该属性是否允许为null
-
@Transient(和上面@Basic相对,建表时忽略有该注解的属性)
JPA会忽略该属性,不会映射到数据库中,即程序运行后数据库中将不会有该字段
-
@Column(设置实体的属性声明)
通常置于实体的属性声明之前,可与 @Id 标注一起使用 @Column参数: 1. name: 指定映射到数据库中的字段名 2. unique: 是否唯一,默认为false 3. nullable: 是否允许为null,默认为true 5. insertable: 是否允许插入,默认为true 6. updatetable: 是否允许更新,默认为true 7. columnDefinition: 指定该属性映射到数据库中的实际类型,通常是自动判断。
-
@Temporal(设置时间精度)
Java中没有定义 Date 类型的精度,而数据库中,表示时间类型的数据有 DATE,TIME,TIMESTAMP三种精度 - @Temporal(TemporalType.DATE) 表示映射到数据库中的时间类型为 DATE,只有日期 - @Temporal(TemporalType.TIME) 表示映射到数据库中的时间类型为 TIME,只有时间 - @Temporal(TemporalType.TIMESTAMP) 表示映射到数据库中的时间类型为 TIMESTAMP,日期和时间都有
-
@Embedded 和 @Embeddable(了解即可,深入理解)
用于一个实体类要在多个不同的实体类中进行使用,而本身又不需要独立生成一个数据库表
-
@JoinColumn(用于设置外键字段)
定义表关联的外键字段名 常用参数有: 1. name: 指定映射到数据库中的外键的字段名 2. unique: 是否唯一,默认为false 3. nullable: 是否允许为null,默认为true 4. insertable: 是否允许插入,默认为true 5. updatetable: 是否允许更新,默认为true 6. columnDefinition: 指定该属性映射到数据库中的实际类型,通常是自动判断。 7. referencedColumnName:设置引用的类的属性名
PS:@JoinColumn常与@ManyToOne(optional=false)一起用,也就是用在多的一方,用于添加外键字段
常用属性名 说明 name = “你要添加的数据库字段名” 设置对应数据表的列名(用作外键) referencedColumnName = “引用的类的属性名” 设置引用的类的属性名 -
@OneToOne(一对一关系)
1.targetEntity: 指定关联实体类型,默认为被注解的属性或方法所属的类 2.cascade: 级联操作策略 CascadeType.ALL 级联所有操作 CascadeType.PERSIST 级联新增 CascadeType.MERGE 级联归并更新 CascadeType.REMOVE 级联删除 CascadeType.REFRESH 级联刷新 CascadeType.DETACH 级联分离 3.fetch: fetch 表示该属性的加载读取策略 (默认值为 EAGER) EAGER 主动抓取 LAZY 延迟加载,只有用到该属性时才会去加载 4.optional: 默认为true,关联字段是否为空 如果为false,则常与@JoinColumn一起使用 5.mappedBy: 指定关联关系,该参数只用于关联关系的被拥有方 只用于双向关联@OneToOne,@OneToMany,@ManyToMany。而@ManyToOne中没有 @OneToOne(mappedBy = “xxx”) 表示xxx所对应的类为关系被拥有方,而关联的另一方为关系拥有方 * 关系拥有方:对应拥有外键的数据库表 * 关系被拥有方:对应主键被子表引用为外键的数据库表 6.orphanRemoval:默认值为false 判断是否自动删除与关系拥有方不存在联系的关系被拥有方(关系被拥有方的一个主键在关系拥有方中未被引用,当jpa执行更新操作时,是否删除数据库中此主键所对应的一条记录,若为true则删除)
-
@ManyToOne、@OneToMany(多对一和一对多)
多对一(也可叫一对多,只是前后表颠倒一下而已),只有双向多对一时才用得到@OneToMany。多对一中多的一方必定是对应数据库中拥有外键的表,即是关系拥有方,
@ManyToOne
只用在多对一中代表多的一类中,因为mappedBy
只用于关系被拥有方,所以@ManyToOne
参数中不包含mappedBy
。//@ManyToOne参数: 1.targetEntity: 指定关联实体类型,默认为被注解的属性或方法所属的类 2.cascade: 级联操作策略 CascadeType.ALL 级联所有操作 CascadeType.PERSIST 级联新增 CascadeType.MERGE 级联归并更新 CascadeType.REMOVE 级联删除 CascadeType.REFRESH 级联刷新 CascadeType.DETACH 级联分离 3.fetch: fetch 表示该属性的加载读取策略(@ManyToOne 的默认值是 EAGER,@OneToMany 的默认值是 LAZY) EAGER 主动抓取 LAZY 延迟加载,只有用到该属性时才会去加载 4.optional: 默认为true,关联字段是否为空 如果为false,则常与@JoinColumn一起使用 //@OneToMany 参数除上述以外还有: 1.mappedBy: 指定关联关系,该参数只用于关联关系被拥有方 * 一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键) 只用于双向关联@OneToOne,@OneToMany,@ManyToMany。而@ManyToOne中没有 @OneToMany(mappedBy = “xxx”) 表示xxx所对应的类为关系被拥有方,而关联的另一方为关系拥有方 * 关系拥有方:对应拥有外键的数据库表 * 关系被拥有方:对应主键被子表引用为外键的数据库表 2.orphanRemoval:默认值为false 判断是否自动删除与关系拥有方不存在联系的关系被拥有方(关系被拥有方的一个主键在关系拥有方中未被引用,当jpa执行更新操作时,是否删除数据库中此主键所对应的一条记录,若为true则删除)
-
@ManyToMany(多对多)
1.targetEntity: 指定关联实体类型,默认为被注解的属性或方法所属的类 2.cascade: 级联操作策略 CascadeType.ALL 级联所有操作 CascadeType.PERSIST 级联新增 CascadeType.MERGE 级联归并更新 CascadeType.REMOVE 级联删除 CascadeType.REFRESH 级联刷新 CascadeType.DETACH 级联分离 3.fetch: fetch 表示该属性的加载读取策略 (默认值为 LAZY) EAGER 主动抓取 LAZY 延迟加载,只有用到该属性时才会去加载 4.mappedBy: 指定关联关系,该参数只用于关联关系被拥有方 只用于双向关联@OneToOne,@OneToMany,@ManyToMany。而@ManyToOne中没有。 @ManyToMany(mappedBy = “xxx”) 表示xxx所对应的类为关系被拥有方,而关联的另一方为关系拥有方: * 关系拥有方:对应拥有外键的数据库表 * 关系被拥有方:对应主键被子表引用为外键的数据库表
PS:稍微总结一下关系注解一共4个:@OneToOne、@OneToMany、@ManyToOne、@ManyToMany
常见属性名 说明 cascade=CascadeType.XXX 值:CascadeType.PERSIST/REMOVE/MERGE/DETACH/REFRESH/ALL mappedBy=“自己在对方的属性名” **哪一方使用了此属性,代表这一方放弃维护,即成为被维护方。**一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键) fetch=FetchType.XXX FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载。 FetchType.EAGER:急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载。 @OneToMany默认的是LAZY,@ManyToOne默认是EAGER optional=true/false optional 属性是定义该关联类对是否必须存在,值为false时,关联类双方都必须存在,如果关系被维护端不存在,查询的结果为null。 值为true 时, 关系被维护端可以不存在,查询的结果仍然会返回关系维护端,在关系维护端中指向关系被维护端的属性为null。 optional 属性的默认值是true。 -
@Enumerated(用于枚举,了解)
当实体类中有枚举类型的属性时,默认情况下自动生成的数据库表中对应的字段类型是枚举的索引值,是数字类型的,若希望数据库中存储的是枚举对应的String类型,在属性上加入
@Enumerated(EnumType.STRING)
注解即可。@Enumerated(EnumType.STRING) @Column(nullable = true) private RoleEnum role;
六、CRUD
1.JPA提供的接口
-
Repository
最顶层的接口,是一个空的接口,目的是为了统一所有Repository的类型,且能让组件扫描的时候自动识别。
-
CrudRepository
是Repository的子接口,提供基础的CRUD的功能
@NoRepositoryBean public interface CrudRepository<T, ID> extends Repository<T, ID> { <S extends T> S save(S entity); <S extends T> Iterable<S> saveAll(Iterable<S> entities); Optional<T> findById(ID id); boolean existsById(ID id); Iterable<T> findAll(); Iterable<T> findAllById(Iterable<ID> ids); long count(); void deleteById(ID id); void delete(T entity); void deleteAll(Iterable<? extends T> entities); void deleteAll(); }
-
PagingAndSortingRepository
是CrudRepository的子接口,添加分页和排序的功能
@NoRepositoryBean public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> { Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); }
-
JpaRepository(常用)(重要,一般直接继承此接口)
是PagingAndSortingRepository的子接口,增加了一些实用的功能,比如:批量操作等。
@NoRepositoryBean public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { List<T> findAll(); List<T> findAll(Sort sort); List<T> findAllById(Iterable<ID> ids); <S extends T> List<S> saveAll(Iterable<S> entities); void flush(); <S extends T> S saveAndFlush(S entity); void deleteInBatch(Iterable<T> entities); void deleteAllInBatch(); T getOne(ID id); @Override <S extends T> List<S> findAll(Example<S> example); @Override <S extends T> List<S> findAll(Example<S> example, Sort sort); }
-
-
-
-
JpaSpecificationExecutor(常用):用来做负责查询的接口,使用到criteria查询需要继承此接口
public interface JpaSpecificationExecutor<T> { Optional<T> findOne(@Nullable Specification<T> spec); List<T> findAll(@Nullable Specification<T> spec); Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable); List<T> findAll(@Nullable Specification<T> spec, Sort sort); long count(@Nullable Specification<T> spec); }
- JpaRepositoryImplementation(最佳版本):继承了JpaSpecificationExecutor和JpaRepository,做所有的CRUD,如果想要使用JPQL和Criteria两种查询,建议继承此接口
- SimpleJpaRepository:是上面接口的实现类,负责规范化查询语句,里面有EntityManager
- QuerydslJpaRepository:上面的子类,负责处理@Query的语句,里面有EntityManager
- SimpleJpaRepository:是上面接口的实现类,负责规范化查询语句,里面有EntityManager
- JpaRepositoryImplementation(最佳版本):继承了JpaSpecificationExecutor和JpaRepository,做所有的CRUD,如果想要使用JPQL和Criteria两种查询,建议继承此接口
2.JPA自带的方法
-
直接继承接口JpaRepository即可
public interface UserDao extends JpaRepository<对应的Entity类, 该类的主键类型>{...}
-
接口里自带的所有增删改查方法
<S extends T> S save(S entity); <S extends T> Iterable<S> saveAll(Iterable<S> entities); Optional<T> findById(ID id); boolean existsById(ID id); Iterable<T> findAll(); Iterable<T> findAllById(Iterable<ID> ids); long count(); void deleteById(ID id); void delete(T entity); void deleteAll(Iterable<? extends T> entities); void deleteAll(); Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); List<T> findAll(); List<T> findAll(Sort sort); List<T> findAllById(Iterable<ID> ids); <S extends T> List<S> saveAll(Iterable<S> entities); void flush(); <S extends T> S saveAndFlush(S entity); void deleteInBatch(Iterable<T> entities); void deleteAllInBatch(); T getOne(ID id); <S extends T> List<S> findAll(Example<S> example); <S extends T> List<S> findAll(Example<S> example, Sort sort);
3.标准查询(重要)
-
继承接口
JpaRepository<对应的Entity类, 该类的主键类型>
public interface UserRepository extends JpaRepository<User, Long>{...}
-
基础的增删上面继承接口后已经全部实现了,需要自定义的部分只有查询
-
查询方法需要按照语法规范书写,规范下面表格已详细给出,这里举一个例子
public interface UserRepository extends JpaRepository<User, Long>{ //按照用户名模糊查询,username是User类的一个字段 findByUsernameLike(String username); }
标准化查询对应的JPQL语法
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1(parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1(parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1(parameter bound wrapped in%) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
4.JPQL查询
4.1.使用
-
继承接口
JpaRepository<对应的Entity类, 该类的主键类型>
-
编写方法和对象查询语言,注意不是传统的Sql语句
-
在方法名上使用注解@Query,两种写法
- @Query(“原生的SQL语句”)
- @Query(value=“原生的SQL语句”)
-
占位符
-
方式一:使用
?1
、?2
… 占位符从一开始@Query("SELECT p FROM Person p WHERE name LIKE %?1% AND sex=?2") Person findByNameLikeAndSexIs(String name,Integer sex);//注意要按顺序
-
方式二:Sql语句使用
:param
,并且在方法形参使用注解@Param("param")
注意注解中的值要与Sql中的:param
一致@Query("SELECT p FROM Person p WHERE name LIKE %:name% AND sex=:sex") Person findByNameLikeAndSexIs(@Param("sex")Integer sexInt,@Param("name")String nameStr);//可以不按顺序
-
4.2.示例
public interface UserRepository extends JpaRepository<User, Long> {
//第一种传参方式;另外,在JPQL语法中,User只的不是表而是User实体类
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
//第二种传参方式
@Query("select u from User u where u.firstname like %:firstname")
List<User> findByFirstnameEndsWith(@Param("firstname")String firstname);
//使用了原生Sql语句查询
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
//原生语句的分页条件查询,Page需要两个对象,因此由两条语句:一个负责结果,一个负责总数
@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",nativeQuery = true)
Page<User> findByLastname(String lastname, Pageable pageable);
//更新语句,一定要用@Modifying声明,在service层一定要使用事务@Transactional,要不会报错
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
}
5.原生SQL查询
-
在@Query注解中添加属性
nativeQuery=true
即可,然后书写原生的Sql即可@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true) User findByEmailAddress(String emailAddress);
-
其余需要注意的参考上面的 JPQL查询
6.自定义增删改
- 在进行增删改的时候,一定要使用注解
@Modifying
进行标注,作用:提示 JPA 该操作是修改操作
-
如果报错,添加事务:在方法上添加注解
@Transactional
//更新语句,一定要用@Modifying声明,在service层一定要使用事务@Transactional,要不会报错 @Modifying @Query("update User u set u.firstname = ?1 where u.lastname = ?2") int setFixedFirstnameFor(String firstname, String lastname);
-
有可能会有缓存存在而无法更新的情况: 阅读参考
//1.当进行 find 操作时,JPA 在 EntityManager 中缓存了 find 生成的对象,当再次 find 时会直接返回该对象。于是可能会出现下面这种情况 用 @Query 定义一个修改状态的方法 public interface EntityRepository extends JpaRepository<Entity, Integer> { @Modifying @Query("update Entity set status = 'IGNORED' where id = ?1") int updateStatus(int id); } //2.先读取一个对象,再修改对象状态,再次读取对象 Optional<Entity> entityBefore = repository.findById(1); repository.updateStatus(1); Optional<Entity> entityAfter = repository.findById(1); //3.结果会发现 entityBefore 和 entityAfter 中的 Entity 对象 id 是相同的,中间对状态的修改并没有体现出来!当然,其原因也很明确,@Query 跟 find 和 save 系列方法是两套不同的体系,@Query 引起的数据库变更 EntityManager 并不能发现,更进一步说,使用其它工具或者其它框架修改数据库中的数据,也不能及时反应到 JPA 的 find 系列方法上来。 //4.当然,只要有缓存机制就一定不可避免存在此类问题,这仅是个取舍问题而不要认为是 BUG。如果要解决 find 得到的值不是数据库中最新值的问题可以有几种方式,避免使用 @Query 是一种方式,在需要时显式清理 EntityManager 的缓存也是一种方式。Spring Data JPA 提供了另外一种方式则是 @Modifying(clearAutomatically = true),@Modifying 的 clearAutomatically 属性为 true 时,执行完 modifying query 之后就会清理缓存,从而在下次 find 时就可以读取到数据库中的最新值。 //5.自动清理之后还会带来一个新的问题,clear 操作清理的缓存中,还包括提交后未 flush 的数据,例如调用 save 而不是 saveAndFlush 就有可能不会立即将修改内容更新到数据库中,在 save 之后 flush 之前调用 @Modifying(clearAutomatically = true) 修饰的方法就有可能导致修改丢失。如果再要解决这个问题,还可以再加上另外一个属性 @Modifying(clearAutomatically = true, flushAutomatically = true),@Modifying 的 flushAutomatically 属性为 true 时,执行 modifying query 之前会先调用 flush 操作,从而避免数据丢失问题。 //6.在实际运行中,clear 和 flush 操作都可能需要消耗一定的时间,要根据系统实际情况可以选择使用其中的一个或两个属性,以保证系统的正确性。
7.动态查询
7.1.接口关系
-
AbstractQuery接口:可以获取root,构建查询条件
public interface AbstractQuery<T> extends CommonAbstractCriteria { //最关键的是from方法,可以得到语句的条件部分根对象 <X> Root<X> from(Class<X> entityClass); <X> Root<X> from(EntityType<X> entity); AbstractQuery<T> where(Expression<Boolean> restriction); AbstractQuery<T> where(Predicate... restrictions); AbstractQuery<T> groupBy(Expression<?>... grouping); AbstractQuery<T> groupBy(List<Expression<?>> grouping); AbstractQuery<T> having(Expression<Boolean> restriction); AbstractQuery<T> having(Predicate... restrictions); AbstractQuery<T> distinct(boolean distinct); Set<Root<?>> getRoots(); Selection<T> getSelection(); List<Expression<?>> getGroupList(); Predicate getGroupRestriction(); boolean isDistinct(); Class<T> getResultType(); }
-
CriteriaQuery接口:继承了AbstractQuery接口,用于构建查询条件
public interface CriteriaQuery<T> extends AbstractQuery<T> { CriteriaQuery<T> select(Selection<? extends T> selection); CriteriaQuery<T> multiselect(Selection<?>... selections); CriteriaQuery<T> multiselect(List<Selection<?>> selectionList); CriteriaQuery<T> where(Expression<Boolean> restriction);//应用查询条件实例的方法 CriteriaQuery<T> where(Predicate... restrictions);//应用查询条件实例的方法 CriteriaQuery<T> groupBy(Expression<?>... grouping); CriteriaQuery<T> groupBy(List<Expression<?>> grouping); CriteriaQuery<T> having(Expression<Boolean> restriction); CriteriaQuery<T> having(Predicate... restrictions); CriteriaQuery<T> orderBy(Order... o); CriteriaQuery<T> orderBy(List<Order> o); CriteriaQuery<T> distinct(boolean distinct); List<Order> getOrderList(); Set<ParameterExpression<?>> getParameters(); }
-
-
CriteriaBuilder接口,能创建CriteriaQuery对象,还能创建查询条件(Expression表达式),定义了几乎所有的sql逻辑判断相关的表达式创建方法,功能非常全,因此一般都是用此接口中的方法。实现类是由hibernate提供
//这里列一下Expression的继承树,仅列出常见的 * Expression (定义了:isNull、isNotNull、in、as。。。) * Predicate(定义了:not,还有枚举对象{AND,OR}。。。) * Subquery(子查询接口,里面有select、where、groupBy、having。。。) * Path(JPA定义的,定义了一些获取方法,有和实体相关的,有和表达式相关的) * From(JPA定义的,定义了许多join相关的方法) * Join(连接查询,定义了join的条件on。。。) * Root(就一个获取实体的方法)
-
Specification接口:处理复杂查询,如多条件分页查询 Specification接口使用和意义
里面定义了基本的连接语句动态添加的方法,
Specifications
是该接口的实现类注意方法toPredicate(断言方法),此方法负责处理criteria语句的条件,需要实现此方法
@SuppressWarnings("deprecation") public interface Specification<T> extends Serializable { long serialVersionUID = 1L; static <T> Specification<T> not(Specification<T> spec) { return Specifications.negated(spec); } static <T> Specification<T> where(Specification<T> spec) { return Specifications.where(spec); } default Specification<T> and(Specification<T> other) { return Specifications.composed(this, other, AND); } default Specification<T> or(Specification<T> other) { return Specifications.composed(this, other, OR); } //上面几个方法中,Specifications是该接口的实现类,可能适用于语句的连接,方法都过期了,我没用过 @Nullable Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); //需要实现改方法,默认绑定了3个参数,root和criteriaBuilder都十分有用 /*网上看到有人这么总结(其实就是上面的参考链接~) Root:查询哪个表 CriteriaQuery:查询哪些字段,排序是什么 CriteriaBuilder:字段之间是什么关系,如何生成一个查询条件,每一个查询条件都是什么方式 Predicate(Expression):单独每一条查询条件的详细描述*/ }
-
JpaSpecificationExecutor接口:是复杂查询的主要入口,传入Specification对象
这个接口里的方法全部是围绕着上面的接口
Specification
写的想使用Specification对象构造复杂查询语句,Dao层需要先继承该接口,然后实现Specification的toPredicate方法。
import org.springframework.data.jpa.domain.Specification; public interface JpaSpecificationExecutor<T> { Optional<T> findOne(@Nullable Specification<T> spec); List<T> findAll(@Nullable Specification<T> spec); Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);//分页,推荐使用 List<T> findAll(@Nullable Specification<T> spec, Sort sort); long count(@Nullable Specification<T> spec); }
-
JpaRepositoryImplementation接口(最佳partner):查看过JPA提供的接口我们可以发现,继承JpaRepositoryImplementation 将是我们最好的选择!既能使用JpaRepository的自带方法,又能使用specification自己构造查询,最全解决方案~接口定义如下:
@NoRepositoryBean public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {...}
-
7.2.Criteria 查询原理
是一种类型安全和更面向对象的查询
-
基本对象的构建
- 通过EntityManager的getCriteriaBuilder或EntityManagerFactory的getCriteriaBuilder方法可以得到CriteriaBuilder对象(该对象由hibernate提供)
- 通过调用CriteriaBuilder的createQuery或createTupleQuery方法可以获得CriteriaQuery的实例
- 通过调用CriteriaQuery的from方法(方法定义在AbstractQuery,但是由CriteriaQueryImpl实现)可以获得Root实例
-
过滤条件
- 过滤条件会被应用到SQL语句的FROM子句中。在criteria 查询中,查询条件通过Predicate或Expression实例应用到CriteriaQuery对象上。
- 这些条件使用 CriteriaQuery .where 方法应用到CriteriaQuery 对象上
- CriteriaBuilder也作为Predicate实例的工厂,通过调用CriteriaBuilder 的条件方法( equal,notEqual, gt, ge,lt, le,between,like等)创建Predicate对象。
- 复合的Predicate 语句可以使用CriteriaBuilder的and, or andnot 方法构建
7.3.Criteria查询
Spring Data Jpa:分页、Specification、Criteria PS:这个写的也不错,最后才发现。。
使用条件
-
Dao层需要继承JpaSpecificationExecutor接口,但前面我也介绍了,推荐继承JpaRepositoryImplementation接口 ,这里我也继承它!
@Repository public interface FairyAdminDao extends JpaRepositoryImplementation<FairyAdmin,Integer> {//两个参数,一个是对应的实体类,一个是该实体类主键的类型}
7.4.综合查询示例
//测试Criteria查询
@Test
public void criteriaQueryTest() {
//1.新建排序
Sort sort = new Sort(Sort.Direction.DESC,"adminUsername");
//2.新建分页
PageRequest pageRequest = PageRequest.of(0, 5, sort);
//3.实现接口方法specification,添加条件
Specification<FairyAdmin> specification = new Specification<FairyAdmin>() {
@Override
public Predicate toPredicate(Root<FairyAdmin> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//3.1.混合条件查询
Path<String> path1 = root.get("adminUsername");//注意String,一定要类型匹配
//Path<String> path2 = root.get("adminPassword");//可以像下面直接写到参数里
Predicate predicate = cb.and(cb.like(path1, "%yoko%"),cb.like(root.get("adminPassword").as(String.class),"%456%"));
//3.2.多表查询
//Join<FairyAdmin, FairyCat> join = root.join("catList", JoinType.INNER);
Join<FairyAdmin, FairyCat> join = root.join(root.getModel().getList("catList",FairyCat.class), JoinType.INNER);
Path<String> catName = join.get("catName");
Predicate predicate2 = cb.and(cb.like(path1, "%yoko%"),cb.like(root.get("adminPassword").as(String.class),"%456%"),cb.like(catName,"%cat1%"));
//输3.2.1出语句...inner join tab_cat c on adminId=c.admin_adminId where (adminUsername like ?) and (adminPassword like ?) and (c.catName like ?) order by adminUsername desc limit ?,?
//return predicate2;
//3.3.使用CriteriaQuery直接设置条件,不再需要返回值
query.where(predicate2);//这里可以设置任意条查询条件
return null;//这种方式使用JPA的API设置了查询条件,所以不需要再返回查询条件Predicate给Spring Data Jpa,故最后return null
}
};
//4.执行查询
List<FairyAdmin> list = fairyAdminDao.findAll(specification, pageRequest).getContent();
//list.forEach(System.out::println);
for (FairyAdmin admin:
list) {
System.out.println(admin);
System.out.println(admin.getCatList());//设置了级联关系,默认自动查询了
}
}
7.5.封装条件列表示例
示例一
public List<Student> queryStudent(final StudentParam studentParam) {
Specification<StudentEntity> specification = new Specification<StudentEntity>() {
public Predicate toPredicate(Root<StudentEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();//表达式列表
if (studentParam.getName() != null && !"".equals(studentParam.getName())) {
predicates.add(cb.equal(root.<String>get("name"), studentParam.getName()));
}
if (studentParam.getStartDate() != null && !"".equals(studentParam.getStartDate())) {
predicates.add(cb.greaterThan(root.<String>get("birthday"), studentParam.getStartDate()));
}
if (studentParam.getEndDate() != null && !"".equals(studentParam.getEndDate())) {
predicates.add(cb.lessThan(root.<String>get("birthday"), studentParam.getEndDate()));
}
//定义or的条件数组
List<Predicate> orPredicates = new ArrayList<Predicate>();
if (studentParam.getSchool() != null && !"".equals(studentParam.getSchool())) {
orPredicates.add(cb.equal(root.<String>get("school"), studentParam.getSchool()));
}
if (studentParam.getAddress() != null && !"".equals(studentParam.getAddress())) {
orPredicates.add(cb.equal(root.<String>get("address"), studentParam.getAddress()));
}
//生成or的查询表达式
Predicate orPredicate = cb.or(orPredicates.toArray(new Predicate[orPredicates.size()]));
//添加到表达式列表,这里生成的or会被()包起来
predicates.add(orPredicate);
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
List<StudentEntity> studentEntities = studentRepo.findAll(specification);
List<Student> students = new ArrayList<Student>();
for (StudentEntity studentEntity : studentEntities) {
students.add(studentCopier.copy(studentEntity));
}
return students;
}
/**
SQL:
Hibernate:
select
studentent0_.id as id1_0_,
studentent0_.address as address2_0_,
studentent0_.birthday as birthday3_0_,
studentent0_.name as name4_0_,
studentent0_.school as school5_0_,
studentent0_.tel as tel6_0_
from
t_student studentent0_
where
studentent0_.name=?
and studentent0_.birthday>?
and studentent0_.birthday<?
and (
studentent0_.school=?
or studentent0_.address=?
)
*/
网上大佬的详细示例代码二(建议使用这种方式抽取方法)
/**
* 动态生成where语句
* @param searchArticle
* @return
*/
private Specification<Article> getWhereClause(final SearchArticle searchArticle){
return new Specification<Article>() {
@Override
public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicate = new ArrayList<>();
if(searchArticle.getPostTimeStart()!=null){
predicate.add(cb.greaterThanOrEqualTo(root.get("postTime").as(Date.class), searchArticle.getPostTimeStart()));
}
if(searchArticle.getPostTimeEnd()!=null){
predicate.add(cb.lessThanOrEqualTo(root.get("postTime").as(Date.class), searchArticle.getPostTimeEnd()));
}
if(searchArticle.getRecTimeStart()!=null){
predicate.add(cb.greaterThanOrEqualTo(root.get("recommendTime").as(Date.class), searchArticle.getRecTimeStart()));
}
if (searchArticle.getRecTimeEnd()!=null){
predicate.add(cb.lessThanOrEqualTo(root.get("recommendTime").as(Date.class), searchArticle.getRecTimeEnd()));
}
if (StringUtils.isNotBlank(searchArticle.getNickname())){
//两张表关联查询
Join<Article,User> userJoin = root.join(root.getModel().getSingularAttribute("user",User.class),JoinType.LEFT);
predicate.add(cb.like(userJoin.get("nickname").as(String.class), "%" + searchArticle.getNickname() + "%"));
}
Predicate[] pre = new Predicate[predicate.size()];
return query.where(predicate.toArray(pre)).getRestriction();
}
};
}
7.6.双表连接动态查询
Service
@Override
public Page<AgentEntity> findAllByPage(AgentEntity agent,Sort sort, Integer page, Integer pageSize) {
//1.创建排序,交给Controller创建了
//2.创建分页对象,page是从0开始
PageRequest pageRequest = PageRequest.of(page - 1, pageSize, sort);
//3.查询<1.条件对象:下面的方法getWhereClause已经封装好 2.page对象:里面有分页信息和排序信息> 返回pageBean
Page<AgentEntity> pageModel = agentDao.findAll(getWhereClause(agent), pageRequest);
return pageModel;
}
/**
* 动态创建条件
*/
private Specification<AgentEntity> getWhereClause(final AgentEntity searchAgent) {
return new Specification<AgentEntity>() {
@Override
public Predicate toPredicate(Root<AgentEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//1.创建条件列表
List<Predicate> predicates = new ArrayList<Predicate>();
//2.遍历对象依次增加条件
//-----普通字段开始-----
//2.1.其它字段略······
//2.2.按手机号phone
if (!EmptyUtils.isEmpty(searchAgent.getPhone())) {
predicates.add(cb.equal(root.get("phone").as(String.class), searchAgent.getPhone()));
}
//2.3.按用户名username模糊查询
if (!EmptyUtils.isEmpty(searchAgent.getUsername())) {
predicates.add(cb.like(root.get("username").as(String.class),"%"+searchAgent.getUsername()+"%"));
}
//-----普通字段结束-----
//-----一对一的实体属性开始-----
//2.5.按级别grade
if (!EmptyUtils.isEmpty(searchAgent.getGrade())) {
Join<AgentEntity,DictEntity> entityJoin = root.join("grade",JoinType.LEFT);
//方式一:做好左外连接后,根据连接实体中的id(实际上是连接的实体DictEntity的id)获取表达式
predicates.add(cb.equal(entityJoin.get("id").as(Long.class), searchAgent.getGrade().getId()));
}
//2.6.按能力值abilityTag
if (!EmptyUtils.isEmpty(searchAgent.getAbilityTag())) {
Join<AgentEntity,DictEntity> entityJoin = root.join("abilityTag",JoinType.LEFT);
//方式二:做好左外连接后,直接根据连接实体的类获取表达式
predicates.add(cb.equal(root.get("abilityTag").as(DictEntity.class), searchAgent.getAbilityTag()
.getId()));
}
//2.7.其它条件略······
//-----一对一的实体属性结束-----
//3.将条件转化返回
Predicate[] predicateArray = new Predicate[predicates.size()];
return query.where(predicates.toArray(predicateArray)).getRestriction();
}
};
}
Controller
@RequestMapping("user/personalAgent")
public ModelAndView toPersonalAgent(Model model) {
AgentEntity test = new AgentEntity();
test.setUsername("yoko");
//创建排序对象,使用字段“score”降序
Sort sort = Sort.by(Sort.Direction.DESC, "score");
Page<AgentEntity> pageModel = agentService.findAllByPage(test, sort, 1, 5);//查询
List<AgentEntity> list = pageModel.getContent();//获取实体列表
list.forEach(System.out::println);
model.addAttribute("list", list);//添加到域
return new ModelAndView("user/personal-center-agent");//转发
}
7.7.三表连接动态查询
多:级联关系中的多方;一:级联关系中的一方
双向:双向关联;单向:单向关联
业务场景:
-
实体HouseEntity(多,双向)中有实体CommunityEntity(一,双向)
- 实体CommunityEntity(一,单向)中有实体AddressEntity(一)
- 实体AddressEntity中的字段areaName表示精确到区/县的地址(如杭州市的江干区/西湖区之类的)
- **需求:**要根据areaName查询实体HouseEntity,因此要使用到三张表的关联
- 实体CommunityEntity(一,单向)中有实体AddressEntity(一)
-
Dao层继承接口
@Repository public interface HouseDao extends JpaRepositoryImplementation<HouseEntity, Long> {}
-
Service编写动态查询方法
/** * 动态创建条件 */ private Specification<HouseEntity> getWhereClause(final HouseEntity house) { return new Specification<HouseEntity>() { @Override public Predicate toPredicate(Root<HouseEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) { List<Predicate> predicates = new ArrayList<>(); //地址head查询,三表联查 if (!EmptyUtils.isEmpty(house.getCommunityEntity())&&!EmptyUtils.isEmpty(house.getCommunityEntity().getAddressHead()) && !EmptyUtils.isEmpty(house.getCommunityEntity().getAddressHead().getAreaName())) {//三个判空操作 Join<HouseEntity, CommunityEntity> entityJoin = root.join("communityEntity", JoinType.LEFT); Join<Object, AddressEntity> JoinThird = entityJoin.join("addressHead", JoinType.LEFT); predicates.add(cb.like(JoinThird.get("areaName").as(String.class), "%"+house.getCommunityEntity().getAddressHead().getAreaName()+"%")); } //售价范围,两表联查 if (!EmptyUtils.isEmpty(house.getPriceType()) && !EmptyUtils.isEmpty(house.getPriceType().getId())) { Join<HouseEntity, DictEntity> join = root.join("priceType", JoinType.LEFT); predicates.add(cb.equal(join.get("id").as(Long.class), house.getPriceType().getId())); } //其它查询条件略······ Predicate[] array = new Predicate[predicates.size()]; return query.where(predicates.toArray(array)).getRestriction(); } }; } /** * 查询方法 * @param houseEntity * @param sort * @param page * @param pageSize * @return */ @Override public Page<HouseEntity> findByHouseByPage(HouseEntity houseEntity, Sort sort, Integer page, Integer pageSize) { try { PageRequest pageRequest = PageRequest.of(page - 1, pageSize, sort); Specification<HouseEntity> specification = getWhereClause(houseEntity); Page<HouseEntity> pageModel = houseDao.findAll(specification, pageRequest); return pageModel; } catch (Exception e) { e.printStackTrace(); return null; } }
Controller
@Autowired private HouseService houseService; @RequestMapping("toSecondList") public ModelAndView toSecondListPage(HouseEntity houseSearch, @RequestParam(value = "sortField", defaultValue = "createTime") String sortField, @RequestParam(defaultValue = "DESC") String order) { //仅显示关键代码,其余略······ //2.2.设置排序,三个循环防止判断攻击 Sort sort = null; if (order.equalsIgnoreCase("DESC")) { sort = Sort.by(Sort.Direction.DESC, sortField); } else if (order.equalsIgnoreCase("ASC")) { sort = Sort.by(Sort.Direction.ASC, sortField); } else { sort = Sort.by(Sort.Direction.DESC, sortField); } //3.执行动态查询 Page<HouseEntity> pageModel = houseService.findByHouseByPage(houseSearch, sort, 1, 10); List<HouseEntity> list = pageModel.getContent(); for (HouseEntity h : list) { System.out.println(h); System.out.println(h.getCommunityEntity().getAddressHead().getAreaName()); } //4.返回视图模型 ModelAndView modelAndView = new ModelAndView("user/SecondHousePage"); //4.1.分页对象存入域 modelAndView.addObject("pageModel", pageModel); return modelAndView; }
7.8.Example查询
和上面介绍的有所不同,不是一个体系,了解
Example api的组成
- Probe: 含有对应字段的实例对象。
- ExampleMatcher:ExampleMatcher携带有关如何匹配特定字段的详细信息,相当于匹配条件。
- Example:由Probe和ExampleMatcher组成,用于查询。
缺点
- 属性不支持嵌套或者分组约束,比如:
firstname = ?0 or (firstname = ?1 and lastname = ?2)
- 灵活匹配只支持字符串类型,其他类型只支持精确匹配
- 对于非字符串属性的只能精确匹配,比如想查询在某个时间段内注册的用户信息,就不能通过Example来查询
优点
- 通过在使用springdata jpa时可以通过Example来快速的实现动态查询,同时配合Pageable可以实现快速的分页查询功能。
示例代码
//测试Example查询,简单测试
@Test
public void exampleQueryTest() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("yoko_5");//精确匹配
Example<FairyAdmin> example = Example.of(admin);//默认会忽略空值的字段
List<FairyAdmin> list = fairyAdminDao.findAll(example);
list.forEach(System.out::println);
}
//测试Example查询,多条件
@Test
public void exampleQueryTest2() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("YOKO");
admin.setAdminPassword("123");
//ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase("adminUsername").withMatcher("adminUsername", ExampleMatcher.GenericPropertyMatchers.startsWith()).withIgnorePaths("adminPassword");
//推荐使用lambda写
ExampleMatcher matcher1 = ExampleMatcher.matching().withIgnoreCase().withMatcher("adminUsername", matcher -> matcher.startsWith()).withIgnorePaths("adminPassword");//忽略adminUsername,模糊查询开头匹配,忽略adminPassword(无论是否由值都忽略)
Example<FairyAdmin> example = Example.of(admin,matcher1);
List<FairyAdmin> list = fairyAdminDao.findAll(example);
list.forEach(System.out::println);
}
还有两个网上大佬写的例子
//创建查询条件数据对象
Customer customer = new Customer();
customer.setName("zhang");
customer.setAddress("河南省");
customer.setRemark("BB");
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withStringMatcher(StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
.withIgnoreCase(true) //改变默认大小写忽略方式:忽略大小写
.withMatcher("address", GenericPropertyMatchers.startsWith()) //地址采用“开始匹配”的方式查询
.withIgnorePaths("focus"); //忽略属性:是否关注。因为是基本类型,需要忽略掉
//创建实例
Example<Customer> ex = Example.of(customer, matcher);
//查询
List<Customer> ls = dao.findAll(ex);
//创建查询条件数据对象
Customer customer = new Customer();
//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
.withIncludeNullValues() //改变“Null值处理方式”:包括
.withIgnorePaths("id","name","sex","age","focus","addTime","remark","customerType"); //忽略其他属性
//创建实例
Example<Customer> ex = Example.of(customer, matcher);
//查询
List<Customer> ls = dao.findAll(ex);
8.排序和分页
8.1.排序
-
方式一:直接构造Sort对象排序
//方式一:多字段排序查询,无法自定义顺序,输出语句...order by adminId desc, adminUsername desc Sort sortMulti = new Sort(Sort.Direction.DESC,"adminId","adminUsername");//可多字段 Sort sort = new Sort(Sort.Direction.DESC,"adminId");//单字段
-
方式二:构造多个sort对象,并合并
//方式二:多字段排序查询,可自定义顺序,x输出语句:...order by adminUsername asc, adminId desc Sort sort = new Sort(Sort.Direction.DESC,"adminId");//第一个参数,排序类型:ASC/DESC,第二个参数:按照排序的字段,可以设置多个 Sort sort1 = new Sort(Sort.Direction.ASC,"adminUsername"); final Sort mergeSort = sort1.and(sort);
-
方式三:先构造多个Order对象,再构造Sort对象
//方式三:多字段排序查询,先创建order对象,再构造sort List<Sort.Order> orders = new ArrayList<Sort.Order>(); Sort.Order order1 = new Sort.Order(Sort.Direction.DESC,"adminId"); Sort.Order order2 = new Sort.Order(Sort.Direction.ASC,"adminUsername"); //可以直接单对象创建 Sort orderSort = Sort.by(order2);//...order by adminUsername asc //可以使用orders列表创建 orders.add(order1); orders.add(order2); Sort orderListSort = Sort.by(orders);//...order by adminId desc, adminUsername desc
8.2.分页
Dao层需要继承接口
JpaRepository
(是PagingAndSortingRepository的子接口,主要方法再该接口内)
-
构建PageRequest对象
PS:注意page是从0开始的!
//需要注意的是:page从0开始,不是从1开始! PageRequest pageRequest = PageRequest.of(0,5, orderListSort);//最后一个参数是排序对象 Page<FairyAdmin> content = fairyAdminDao.findAll(pageRequest); if (content.hasContent()) { System.out.println("总记录数:"+content.getTotalElements()); System.out.println("总页数:"+content.getTotalPages()); System.out.println("当前页:"+(content.getPageable().getPageNumber()+1)); System.out.println("当前页面大小:"+content.getPageable().getPageSize()); List<FairyAdmin> list = content.getContent(); list.forEach(System.out::println); System.out.println(content); }
8.3.用关联表字段排序
-
直接
实体属性名.该实体的属性
即可CommunityEntity
public class CommunityEntity{ private AddressEntity addressHead; }
AddressEntity
public class AddressEntity{ private Date createTime; }
Controller
Sort sort = Sort.by(Sort.Direction.DESC, "addressHead.createTime");
8.4.完整示例
//排序和分页
@Test
public void sortAndPageSearchTest() {
//排序的几种方式
//方式一:多字段排序查询,无法自定义顺序,输出语句...order by adminId desc, adminUsername desc
//Sort sort = new Sort(Sort.Direction.DESC,"adminId","adminUsername");
//方式二:多字段排序查询,可自定义顺序,x输出语句:...order by adminUsername asc, adminId desc
Sort sort = new Sort(Sort.Direction.DESC,"adminId");//第一个参数,排序类型:ASC/DESC,第二个参数:按照排序的字段,可以设置多个
Sort sort1 = new Sort(Sort.Direction.ASC,"adminUsername");
final Sort mergeSort = sort1.and(sort);
//方式三:多字段排序查询,先创建order对象,再构造sort
List<Sort.Order> orders = new ArrayList<Sort.Order>();
Sort.Order order1 = new Sort.Order(Sort.Direction.DESC,"adminId");
Sort.Order order2 = new Sort.Order(Sort.Direction.ASC,"adminUsername");
//可以直接单对象创建
Sort orderSort = Sort.by(order2);//...order by adminUsername asc
//可以使用order列表创建
orders.add(order1);
orders.add(order2);
Sort orderListSort = Sort.by(orders);//...order by adminId desc, adminUsername desc
//需要注意的是:page从0开始,不是从1开始!
PageRequest pageRequest = PageRequest.of(0,5, orderListSort);
Page<FairyAdmin> content = fairyAdminDao.findAll(pageRequest);
if (content.hasContent()) {
System.out.println("总记录数:"+content.getTotalElements());
System.out.println("总页数:"+content.getTotalPages());
System.out.println("当前页:"+(content.getPageable().getPageNumber()+1));
System.out.println("当前页面大小:"+content.getPageable().getPageSize());
List<FairyAdmin> list = content.getContent();
list.forEach(System.out::println);
System.out.println(content);
}
System.out.println("==========================================");
}
9.关联查询
-
只要在entity中为属性设置好了一对一、一对多、多对一、多对多之类的关系,就会自动关联查询了
-
要注意的点,在@ManyToMany这一类的注解有个属性
fetch=FetchType.LAZY/EAGER
-
使用EAGER会立即加载
-
使用Lazy会延迟加载(访问的时候才会发送sql语句加载)
但是,有些情况下,session如果被关闭,将无法调用session进行懒加载,从而报错
-
七、总结
套用的一个大佬的话
- Hibernate/JPA的DAO层开发比较简单,对于刚接触ORM的人来说,能够简化开发工程,提高开发速度。
- Hibernate/JPA对对象的维护和缓存做的很好,对增删改查的对象的维护要方便。
- Hibernate/JPA数据库移植性比较好。
- Hibernate/JPA功能强大,如果对其熟悉,对其进行一定的封装,那么项目的整个持久层代码会比较简单。
- 感谢阅读~
八、问题与解决
1.JPA不自动创建表
-
方式一:将spring boot的运行入口类放在顶级包目录下,entity、dao、service等包结构在其子目录下,包结构如下所示:
- cn.dmdream
- 入口类FairyhouseApplication.java
- entity
- dao
- service
- 。。。其它包
- cn.dmdream
-
方式二:在入口类使用注解
@EnableJpaRepositories(basePackages = {"cn.dmdream.dao"}) @EntityScan(basePackages = {"cn.dmdream.entity"})//注意此注解
2.多对一删除的问题
/**
* 删除维护方对象,这里维护方(cat)未设置级联删除
*/
@Test
public void 多对一删除1() {
FairyCat cat = fairyCatDao.findByCatName("Kuku");
System.out.println(cat);
cat.getAdmin().getCatList().remove(cat);//由于二级缓存机制,要清空级联对象中的自己对象,要不无法删除,不会有删除语句
//可以看到在进行上一步设置后,成功有了删除语句delete from tab_cat where catId=?
fairyCatDao.delete(cat);//删除结果:由于未设置级联删除,即便删除的是维护方对象,也只删除了自己,不能删除Admin
}
3.JPQL语句查询问题
旧版(BUG版)实体类
@Data//自动生成get/set/tostring
@Entity(name = "tab_admin")//交给jpa管理实体,并设置映射到数据库的表名(坑)
public class FairyAdmin {
//字段省略...
}
Dao
@Repository
public interface FairyAdminDao extends JpaRepository<FairyAdmin,Integer> {
//使用JPQL查询
//@Query(value = "select a from FairyAdmin a where a.adminUsername like %?1%")
//public List<FairyAdmin> myFindAllByUsernameLike_JPQL(String username);
@Query("select a from FairyAdmin a")
List<FairyAdmin> myFind();
}
看起来一切OK?
结果:测试运行时一直提示错误
java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException
org.hibernate.hql.internal.ast.QuerySyntaxException: XXX is not mapped
反复的检查JPQL语法,没问题啊,网上查找类似的问题,也就是一直说需要写上实体类的类名,我也写上了啊,(我甚至还考虑到是不是LomBok的锅。。徒劳。。)但为什么还是出错呢?
终极原因
在JPQL语法中,select a from 类名 a
这语句中的类名
其实不只是参照你的实体类名,他会优先参照的是你在实体类上使用的注解@Entity(name = "tab_admin")
中的自定义name字段,因此,修改JPQL语句
@Query("select a from tab_admin a")
List<FairyAdmin> myFind();
这样子终于能查询出来了!好坑啊有木有!!!花了2小时找这BUG真的难受。。。网上怎么没人和我一样碰到呢,QAQ。。
还有个建议,在实体类上不要使用@Entity(name = "tab_admin")
来标注名称,需要指定映射的数据库名,最好使用@Table(name="XXX")
。
4.懒加载问题
-
方式一:Spring Boot的话,配置文件添加
## 是否注册OpenEntityManagerInViewInterceptor。将JPA EntityManager绑定到用于整个请求处理的线程 ## true可以解决web测试下懒加载的session关闭问题。 spring.jpa.open-in-view=false ## 非web环境下的懒加载解决方案 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
-
方式二:在查询中使用 fetch 的方式
@Query("from User u join fetch u.authorityList") public User findOne(Long id);
5.JPA建表失败
-
在实体类使用注解@columnDefinition的时候,注意一定要按 类型开头进行定义,下面就是正确的
@Column(name="createTime",columnDefinition="DATETIME comment '创建时间'") protected Date createTime; //创建时间
-
如果定义成下面这样,就会报错误,分析sql语句可以知道,使用该注解就不会帮你默认添加类型了!
@Column(name="createTime",columnDefinition="comment '创建时间'")//是不会自己判断添加类型了 protected Date createTime; //创建时间
6.JPA双向关联时Json序列化失败
-
在双向关系的某一方属性上使用注解
@JsonIgnore
7.JPA普通保存和立即保存
- save不会立刻提交到数据库,flush则立刻提交生效,save可能只是修改在内存中的