写在前面:从2018年底开始学习SpringBoot,也用SpringBoot写过一些项目。现在对学习Springboot的一些知识总结记录一下。如果你也在学习SpringBoot,可以关注我,一起学习,一起进步。相关文章:初识Spring-Data-Jpa
相关文章:
【Springboot系列】Springboot入门到项目实战
目录
底层分析
1、Specification接口
2、JpsSpecificationExecutor接口
编写代码
1、创建持久化类
2、定义数据访问层接口
3、定义业务层类
4、定义分页的页面数据对象
5、定义控制器类
测试应用
1、测试应用
2、源码下载
JPA允许基于Criteria对象进行按条件查询,而SpringDataJpa提供了一个Specification接口,Specification接口封装了JPA的Criteria查询条件,从而可以通过此接口更加方便地使用Criteria查询,Specification接口的源代码如下。
import static org.springframework.data.jpa.domain.SpecificationComposition.*;
import java.io.Serializable;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.lang.Nullable;
public interface Specification extends Serializable {
long serialVersionUID = 1L;
static Specification not(@Nullable Specification spec) {
return spec == null //
? (root, query, builder) -> null//
: (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder));
}
@Nullable
static Specification where(@Nullable Specification spec) {
return spec == null ? (root, query, builder) -> null : spec;
}
@Nullable
default Specification and(@Nullable Specification other) {
return composed(this, other, (builder, left, rhs) -> builder.and(left, rhs));
}
@Nullable
default Specification or(@Nullable Specification other) {
return composed(this, other, (builder, left, rhs) -> builder.or(left, rhs));
}
@Nullable
Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder criteriaBuilder);
}
Specification接口提供了一个toPredicate方法用来构造查询条件。值得注意的是:如果自己定义的数据访问接口希望使用Specification接口的规范,则必须实现JpaSpecificationExecutor接口,JpaSpecificationExecutor接口不属于Repository体系,它实现了一组JPACriteria查询相关的方法,查询JpsSpecificationExecutor接口的源代码如下。
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.lang.Nullable;
public interface JpaSpecificationExecutor {
//根据Specification接口封装的查询条件,查询出单一实例
Optional findOne(@Nullable Specification spec);
//根据Specification接口封装的查询条件,查询出满足条件的所有对象数据,返回的是List集合
List findAll(@Nullable Specification spec);
/**
* 根据Specification接口封装的查询条件以及Pageable封装的分页及排序规则查询出满足条件
* 的对象数据,返回的是Page对象,Page对象中封装了查询出的数据信息以及分页信息。
*/
Page findAll(@Nullable Specification spec, Pageable pageable);
//根据Specification接口封装的查询条件以及Sort封装的排序规则查询出满足条件的所有对象数据,返回的是List集合
List findAll(@Nullable Specification spec, Sort sort);
//根据Specification接口封装的查询条件,查询出满足此条件的数据总数
long count(@Nullable Specification spec);
}
从JpaSpecificationExecutor的方法中可以看出,一般通过创建Specification的匿名内部类对象来封装Criteria条件查询信息然后交由JpaSpecification提供的方法进行相关数据的查询操作。
所以在实际项目开发中,如果希望使用Specification查询,数据访问层的定义通常如下代码所示。
public interface StuRepository1 extends JpaRepository, JpaSpecificationExecutor {
}
上一篇讲了Spring-Data-Jpa入门,这一篇来看一下Spring-Data-Jpa条件查询(基于上一篇Spring-Data-Jpa入门的项目中写的,配置文件没有任何改动,详情可以参考Spring-Data-Jpa入门)。
在entity包中创建两个持久化类学生类Stu.java和班级类Clazz.java,代码如下
Stu.java类(Stu和Clazz是多对一关系,表关联注解代码中有注释)
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.ManyToOne;
//用于标记持久化类,SpringBoot项目加载后会自动根据持久化类建表
@Entity
//设置表名为tb_stu
@Table(name="tb_stu")
public class Stu {
private Integer id; //主键
private String name; //姓名
private String address; //地址
private Integer age; //年龄
private char sex; //性别
//@JsonIgnore注解是类注解,作用是json序列化时将java bean中的一些属性忽略掉
@JsonIgnore
private Clazz clazz;
/**
* 使用@id指定主键。使用代码@GeneratedValue
* 指定主键的生存策略,mysql默认为自动增长
*/
@Id
@GeneratedValue
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
//关联表对应关系,学生与班级的多对一的关系
@ManyToOne
@CreatedBy
public Clazz getClazz() {
return clazz;
}
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
public Stu() {
}
public Stu(String name, String address, Integer age, char sex, Clazz clazz) {
this.name = name;
this.address = address;
this.age = age;
this.sex = sex;
this.clazz = clazz;
}
}
Class.java类(Clazz和Stu是一对多关系,表关联注解代码中有注释)
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
//用于标记持久化类,SpringBoot项目加载后会自动根据持久化类建表
@Entity
//设置表名为tb_clazz
@Table(name="tb_clazz")
public class Clazz {
private Integer id; //主键
private String name; //班级名称
//@JsonIgnore注解是类注解,作用是json序列化时将java bean中的一些属性忽略掉
@JsonIgnore
private List stuList = new ArrayList<>();
/**
* 使用@id指定主键。使用代码@GeneratedValue(strategy = GenerationType.IDENTITY)
* 指定主键的生存策略,mysql默认为自动增长
*/
@Id
@GeneratedValue
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//班级与学生是一对多的关联,mappedBy对应Stu表中clazz字段
@OneToMany(cascade=CascadeType.ALL,mappedBy="clazz")
public List getStuList() {
return stuList;
}
public void setStuList(List stuList) {
this.stuList = stuList;
}
public Clazz(String name) {
this.name = name;
}
public Clazz() {
}
}
从上述代码可以看出,班级与学生是一对多的关系,一个班级可以有多名学生,此处做的是双向关联,即在班级对象中关联了学生对象,在学生对象中也关联了班级对象。
之后在repository包下新建一个接口,命名为StuRepository1,该接口继承JpaRepository接口,以持久化对象Stu作为JpaRepository的第一个类型参数,表示当前所操作的持久化对象类型,Integer作为JpaRepository的第二个类型参数,用于指定ID类型,同时创建一个接口名称为ClazzRepository1,继承JpaRepository的接口,用于访问班级信息的数据。完整代码如下。
ClazzRepository1.java接口
import com.mcy.springdatajpa.entity.Clazz;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ClazzRepository1 extends JpaRepository, JpaSpecificationExecutor {
}
以上数据访问层接口用于对班级表进行相关的CRUD操作,同时由于实现了JpaSpecificationExecutor接口,ClazzRepository接口也将拥有JpaSpecificationExecutor接口提供的功能。
StuRepository1.java接口
import com.mcy.springdatajpa.entity.Stu;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface StuRepository1 extends JpaRepository, JpaSpecificationExecutor {
}
在service包下新建一个SchoolService1.java类,其中主要的几个查询方法有。
根据性别查询学生信息方法。
@SuppressWarnings("serial")
public List
动态查询学生信息:可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息,如果没有传入参数,默认查询所有的学生信息。
@SuppressWarnings("serial")
public List> getStusByDynamic(Stu stu){
List stus = stuRepository1.findAll(new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
//本集合用于封装查询条件
List predicates = new ArrayList<>();
if(stu != null){
//是否传入用于查询的姓名
if(!StringUtils.isEmpty(stu.getName())){
predicates.add(cb.like(root.get("name"), "%"+stu.getName()+"%"));
}
//判断是否传入查询的地址
if(!StringUtils.isEmpty(stu.getAddress())){
predicates.add(cb.like(root.get("address"), "%"+stu.getAddress()+"%"));
}
//判断是否传入查询的性别
if(stu.getSex() != '\0'){
predicates.add(cb.equal(root.get("sex"), stu.getSex()));
}
//判断是否传入用于查询的班级信息
if(stu.getClazz() != null && !StringUtils.isEmpty(stu.getClazz().getName())){
root.join("clazz", JoinType.INNER);
Path clazzName = root.get("clazz").get("name");
predicates.add(cb.equal(clazzName, stu.getClazz().getName()));
}
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
});
List> results = new ArrayList<>();
//遍历查询出的学生对象,提取姓名,年龄,性别信息
for(Stu s : stus){
Map stuMap = new HashMap<>();
stuMap.put("name", stu.getName());
stuMap.put("age", stu.getAge());
stuMap.put("sex", stu.getSex());
stuMap.put("address", stu.getAddress());
stuMap.put("clazzName", stu.getClazz().getName());
results.add(stuMap);
}
return results;
}
分页查询某个班级的学生信息 @param clazzName 代表班级名称,@param pageIndex 代表当前查询第几页 ,@param pageSize 代表每页查询的最大数据量。
@SuppressWarnings("serial")
public Page getStusByPage(String clazzName, int pageIndex, int pageSize){
//指定排序参数对象:根据id,进行降序查询
Sort sort = Sort.by(Sort.Direction.DESC, "id");
//Specification动态查询
Specification spec = buildSpec(clazzName, pageIndex, pageSize);
//分页查询学生信息,返回分页实体对象数据
//pages对象中包含了查询出来的数据信息以及与分页相关的信息
Page pages = stuRepository1.findAll(spec, PageRequest.of(pageIndex-1, pageSize, sort));
return pages;
}
private Specification buildSpec(String clazzName, int pageIndex, int pageSize) {
Specification spec = new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
root.join("clazz", JoinType.INNER);
Path cn = root.get("clazz").get("name");
Predicate p1 = cb.equal(cn, clazzName);
return p1;
}
};
return spec;
}
SchoolService1.java中全部代码。
import com.mcy.springdatajpa.entity.Clazz;
import com.mcy.springdatajpa.entity.Stu;
import com.mcy.springdatajpa.repository.ClazzRepository1;
import com.mcy.springdatajpa.repository.StuRepository1;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class SchoolService1 {
//注入数据访问层接口对象
@Resource
private StuRepository1 stuRepository1;
@Resource
private ClazzRepository1 clazzRepository1;
@Transactional
public void saveClazzAll(List clazzes){
clazzRepository1.saveAll(clazzes);
}
@Transactional
public void saveStuAll(List stu){
stuRepository1.saveAll(stu);
}
/**
* 根据性别查询学生信息
* @param sex
* @return
*/
@SuppressWarnings("serial")
public List> getStusBySex(char sex){
List stus = stuRepository1.findAll(new Specification(){
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
//root.get("sex")表示获取sex这个字段名称,equal表示执行equal查询
//相当于select s from Stu s where s.sex = ?1
Predicate p1 = cb.equal(root.get("sex"), sex);
return p1;
}
});
List> results = new ArrayList<>();
//遍历查询出的学生对象,提取姓名,年龄,性别信息
for(Stu s: stus){
Map stu = new HashMap<>();
stu.put("name", s.getName());
stu.put("age", s.getAge());
stu.put("sex", s.getSex());
results.add(stu);
}
return results;
}
/**
* 动态查询学生信息:可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息
* 如果没有传入参数,默认查询所有的学生信息
* @param stu
* @return
*/
@SuppressWarnings("serial")
public List> getStusByDynamic(Stu stu){
List stus = stuRepository1.findAll(new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
//本集合用于封装查询条件
List predicates = new ArrayList<>();
if(stu != null){
//是否传入用于查询的姓名
if(!StringUtils.isEmpty(stu.getName())){
predicates.add(cb.like(root.get("name"), "%"+stu.getName()+"%"));
}
//判断是否传入查询的地址
if(!StringUtils.isEmpty(stu.getAddress())){
predicates.add(cb.like(root.get("address"), "%"+stu.getAddress()+"%"));
}
//判断是否传入查询的性别
if(stu.getSex() != '\0'){
predicates.add(cb.equal(root.get("sex"), stu.getSex()));
}
//判断是否传入用于查询的班级信息
if(stu.getClazz() != null && !StringUtils.isEmpty(stu.getClazz().getName())){
root.join("clazz", JoinType.INNER);
Path clazzName = root.get("clazz").get("name");
predicates.add(cb.equal(clazzName, stu.getClazz().getName()));
}
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
});
List> results = new ArrayList<>();
//遍历查询出的学生对象,提取姓名,年龄,性别信息
for(Stu s : stus){
Map stuMap = new HashMap<>();
stuMap.put("name", s.getName());
stuMap.put("age", s.getAge());
stuMap.put("sex", s.getSex());
stuMap.put("address", s.getAddress());
stuMap.put("clazzName", s.getClazz().getName());
results.add(stuMap);
}
return results;
}
/***
* 分页查询某个班级的学生信息
* @param clazzName 代表班级名称
* @param pageIndex 代表当前查询第几页
* @param pageSize 代表每页查询的最大数据量
* @return
*/
@SuppressWarnings("serial")
public Page getStusByPage(String clazzName, int pageIndex, int pageSize){
//指定排序参数对象:根据id,进行降序查询
Sort sort = Sort.by(Sort.Direction.DESC, "id");
//Specification动态查询
Specification spec = buildSpec(clazzName, pageIndex, pageSize);
//分页查询学生信息,返回分页实体对象数据
//pages对象中包含了查询出来的数据信息以及与分页相关的信息
Page pages = stuRepository1.findAll(spec, PageRequest.of(pageIndex-1, pageSize, sort));
return pages;
}
private Specification buildSpec(String clazzName, int pageIndex, int pageSize) {
Specification spec = new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
root.join("clazz", JoinType.INNER);
Path cn = root.get("clazz").get("name");
Predicate p1 = cb.equal(cn, clazzName);
return p1;
}
};
return spec;
}
}
在业务层中需要注入数据访问层对象,在上述代码中我们是通过@Resource注解将StuRepository接口以及ClazzRepository接口对应的实现类对象注入的,同时在业务层方法中定义三个方法,分别实现了对班级信息的条件查询,动态SQL语句以及分页查询。
在项目下新建一个包,命名为custom,在custom下新建一个java类,命名为PageData.java,此类用于封装分页查询出的数据信息,主要包含了当前页码(pageIndex)、满足查询条件下用于分页的数据总量(totalCount)、当前条件下总共可以分的总页数(pageSize)、当前页码展示的数据量(pageNum)以及查询出的数据信息(stuDatas)。详细代码如下。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 定义一个对象用于封装一页的数据
*/
public class PageData {
//定义一个变量用于存放当前页码
private int pageIndex;
//定义一个变量用于保存满足查询条件下用于分页的数据总量
private long totalCount;
//定义一个变量用于保存当前条件下可以分页的总页数
private int pageSize;
//定义一个变量用于保存当前页码查询出的数据总量
private int pageNum;
//定义一个变量用于保存当前查询出来的学生信息
private List> stuDates = new ArrayList<>();
public int getPageIndex() {
return pageIndex;
}
public void setPageIndex(int pageIndex) {
this.pageIndex = pageIndex;
}
public long getTotalCount() {
return totalCount;
}
public void setTotalCount(long totalCount) {
this.totalCount = totalCount;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public List> getStuDates() {
return stuDates;
}
public void setStuDates(List> stuDates) {
this.stuDates = stuDates;
}
}
在controller包下新建一个StuController1类,代码如下(访问方法)。
import com.mcy.springdatajpa.custom.PageData;
import com.mcy.springdatajpa.entity.Clazz;
import com.mcy.springdatajpa.entity.Stu;
import com.mcy.springdatajpa.service.SchoolService1;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/stu1")
public class StuController1 {
//注入SchoolService1
@Resource
private SchoolService1 schoolService1;
//保存,初始化数据
@RequestMapping("/save")
public String save(){
Clazz clazz1 = new Clazz("软件工程1班");
Clazz clazz2 = new Clazz("软件工程2班");
//保存班级对象数据
List clazzs = new ArrayList<>();
clazzs.add(clazz1);
clazzs.add(clazz2);
schoolService1.saveClazzAll(clazzs);
Stu stu1 = new Stu("张三", "湖北", 20, '男', clazz1 );
Stu stu2 = new Stu("李四", "湖北", 18, '女', clazz1 );
Stu stu3 = new Stu("诸葛亮", "湖北", 19, '女', clazz1 );
Stu stu4 = new Stu("刘备", "湖北", 21, '男', clazz2 );
Stu stu5 = new Stu("张飞", "湖北", 32, '女', clazz2 );
Stu stu6 = new Stu("关于", "湖北", 17, '男', clazz2 );
List stus = new ArrayList<>();
stus.add(stu1);
stus.add(stu2);
stus.add(stu3);
stus.add(stu4);
stus.add(stu5);
stus.add(stu6);
schoolService1.saveStuAll(stus);
return "保存学生对象成功";
}
@RequestMapping("/getStusBySex")
public List> getStusBySex(char sex){
return schoolService1.getStusBySex(sex);
}
//动态查询学生信息
//可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息
@RequestMapping("/getStusByDynamic")
List> getStusByDynamic(Stu stu){
return schoolService1.getStusByDynamic(stu);
}
//分页查询摸个班级的学生信息
@RequestMapping("/getStusBypage")
PageData getStusByPage(String clazzName, int pageIndex, int pageSize){
//分页查询某个班级的学生信息
Page page = schoolService1.getStusByPage(clazzName, pageIndex, pageSize);
//对查询出来的结果数据进行分析
List stus = page.getContent();
List> stuDatas = new ArrayList<>();
for(Stu s: stus){
Map stuMap = new HashMap<>();
stuMap.put("name", s.getName());
stuMap.put("id", s.getId());
stuMap.put("age", s.getAge());
stuMap.put("sex", s.getSex());
stuMap.put("address", s.getAddress());
stuMap.put("clazzName", clazzName);
stuDatas.add(stuMap);
}
//将分页查询出的结果数据进行分析
//然后把数据存入PageData对象中响应给浏览器展示
PageData data = new PageData();
data.setStuDates(stuDatas);
data.setPageIndex(page.getNumber()+1);
data.setPageSize(page.getTotalPages());
data.setTotalCount(page.getTotalElements());
data.setPageNum(page.getSize());
return data;
}
}
在控制器类中定义了四个方法,其中save方法用于保存数据,也作为初始化的测试数据使用。getStusBySex(char sex)方法用于根据性别查询学生信息。getStusByDynamic(Stu stu)方法用于根据Stu对象中的姓名(模糊匹配),地址查询(模糊匹配),性别,班级信息动态查询满足条件的学生信息,如果没有任何查询条件则查询出系统所有的学生信息。getStusByPage(String clazzName, int pageIndex, int pageSize)方法用于分页查询某个班级的学生信息,返回的是一个分页信息Page
启动MySQL数据库,在数据库中创建名为jpa的数据库。springboot项目启动后,JPA会在数据库中自动创建持久化类对应的tb_stu和tb_clazz表。
测试添加学生和班级信息,在浏览器中输入http://localhost:8080/stu1/save请求会提交到StuController1类的save方法进行处理,执行完成返回“保存学生对象成功”,查询数据库中的数据,如图:
tb_stu表。
tb_clazz表。
测试根据性别查询学生的信息,在浏览器中输入http://localhost:8080/stu1/getStusBySex?sex=女,请求会提交到StuController1类的getStusBySex方法进行处理,执行完成以后,查询出的学生信息如图。
测试动态查询学生信息,可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息,在浏览器中输入http://localhost:8080/stu1/getStusByDynamic?clazz.name=软件工程1班&sex=女,请求会提交到StuController1类的getStusByDynamic方法进行处理,此地址是通过学生的班级和性别来查询对应的学生信息的,执行完成以后,查询出的学生信息如图。
测试分页查询某个班级下的学生信息,在浏览器中输入http://localhost:8080/stu1/getStusBypage?clazzName=软件工程1班&pageIndex=1&pageSize=2,请求会提交给StuController1类的getStusBypage方法进行处理,地址是降序查询“软件工程1班”的第1页数据,每页最多展示2条数据,执行完成后,查询出的学生信息如图。
可以看出pageIndex表示这是第1页数据,totalCount表示“软件工程班当前总共有3条学生记录”,pageSize表示一页最多展示2条数据,pageNum表示当前总共有2页数据,stuDates即查询出的当前页数据。
在浏览器中输入http://localhost:8080/stu1/getStusBypage?clazzName=软件工程1班&pageIndex=2&pageSize=2,来查询第2页数据,执行完成后结果如图。
从图中可以看到,pageIndex表示这是第2页数据,totalCount表示“软件工程班当前总共有3条学生记录”,pageSize表示一页最多展示2条数据,pageNum表示当前总共有2页数据,stuDates即查询出的当前页数据。大家可以自行更换条件进行测试。
案例代码下载链接:https://github.com/machaoyin/spring-data-jpa