有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在SpringDataJPA中可以通过JpaSpecificationExecutor接口查询。相比JPQL,其优势是类型安全,更加的面向对象。
/**
* 源码: JpaSpecificationExecutor中定义的方法
**/
public interface JpaSpecificationExecutor<T> {
//根据条件查询一个对象
T findOne(Specification<T> spec);
//根据条件查询集合
List<T> findAll(Specification<T> spec);
//根据条件分页查询
Page<T> findAll(Specification<T> spec, Pageable pageable);
//排序查询查询
List<T> findAll(Specification<T> spec, Sort sort);
//统计查询
long count(Specification<T> spec);
}
对于JpaSpecificationExecutor,这个接口基本是围绕着Specification接口来定义的。所以可以简单的理解为,Specification构造的就是查询条件。
Specification接口中只定义了如下一个方法:
//构造查询条件
/**
* root :Root接口,代表查询的根对象,可以通过root获取实体中的属性
* CriteriaQuery :代表一个顶层查询对象,用来自定义查询
* CriteriaBuilder :用来构建查询,此对象里有很多条件方法
**/
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
其中cb方法有:
方法名称 | Sql对应关系 |
---|---|
equle | filed = value |
like | filed like value |
gt(greaterThan ) | filed > value |
lt(lessThan ) | filed < value |
ge(greaterThanOrEqualTo ) | filed >= value |
le( lessThanOrEqualTo) | filed <= value |
notEqule | filed != value |
notLike | filed not like value |
and( ):连接多个查询条件 | 与关系 |
or( ):连接多个查询条件 | 或关系 |
单条件查询:
//依赖注入customerDao
@Autowired
private CustomerDao customerDao;
@Test
public void testSpecifications() {
//使用匿名内部类的方式,创建一个Specification的实现类,并实现toPredicate方法
Specification <Customer> spec = new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//root:从实体Customer对象中按照custName属性进行查询
//cb:构建查询,添加查询方式
//cb方法 like:模糊匹配 equal:匹配查询 and(or):连接多个查询条件
//因为root.get获取的值为Path<Object>,所以需要转换成get属性所对应的数据类型
//predicate接收查询条件并作为返回值
Predicate predicate = cb.equal(root.<String>get("custName"), "test");
return predicate;
}
};
//将spec查询条件作为参数,实现根据custName匹配查询
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
多条件查询
**
* 多条件查询
* 案例:根据客户名和客户所属行业查询
*
*/
@Test
public void testSpec1() {
/**
* root:获取属性
* 客户名
* 所属行业
* cb:构造查询
* 1.构造客户名的精准匹配查询
* 2.构造所属行业的精准匹配查询
* 3.将以上两个查询联系起来
*/
Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Object> custName = root.get("custName");//客户名
Path<Object> custIndustry = root.get("custIndustry");//所属行业
//构造查询
//1.构造客户名的精准匹配查询
Predicate p1 = cb.equal(custName, "test");//第一个参数,path(属性),第二个参数,属性的取值
//2..构造所属行业的精准匹配查询
Predicate p2 = cb.equal(custIndustry, "it");
//3.将多个查询条件组合到一起:组合(满足条件一并且满足条件二:与关系;满足条件一或满足条件二即可:或关系)
Predicate and = cb.and(p1, p2);//以与的形式拼接多个查询条件
// cb.or();//以或的形式拼接多个查询条件
return and;
}
};
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
@Test
public void testPage() {
//构造查询条件spec
Specification<Customer> spec = new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.like(root.get("custName").as(String.class), "test%");
}
};
/**
* 构造分页参数
* Pageable : 接口
* PageRequest实现了Pageable接口,调用构造方法的形式构造
* 第一个参数:页码(从0开始)
* 第二个参数:每页查询条数
* 第三个参数(可不写):排序规则
*/
//根据custId降序排列
Sort sort=new Sort(Sort.Direction.DESC,"custId");
Pageable pageable = new PageRequest(0, 5,sort);
/**
* 分页查询,封装为Spring Data Jpa 内部的page bean
* 此重载的findAll方法为分页方法需要两个参数
* 第一个参数:查询条件Specification
* 第二个参数:分页参数
*/
Page<Customer> page = customerDao.findAll(spec,pageable);
page.getTotalPages();//得到总页数
}
对于Spring Data JPA中的分页查询,是其内部自动实现的封装过程,返回的是一个Spring Data JPA提供的pageBean对象。其中的方法说明如下:
//获取总页数
int getTotalPages();
//获取总记录数
long getTotalElements();
//获取列表数据
List<T> getContent();
需求: 存在一个博客系统,可根据标题,分类,是否被推荐,是否是草稿动态查询博客并分页显示
可创建一个Vo类来存放查询条件:
public class BlogQuery {
private String title;//标题
private Long typeId;//分类id
private boolean recommend;//true为推荐
private boolean published; //true为已发布
private boolean draft; //true则为草稿
//此处已省略getter,setter方法...
}
实现功能:
因为查询条件为动态查询,所以需要在查询前通过if判断查询条件是否存在,有则进行查询
@Override
public Page<Blog> listBlog(Pageable pageable, BlogQuery blog) {
return blogRepository.findAll(new Specification<Blog>() {
/**
*第一个参数Root指定查询的对象,
*第二个参数CriteriaQuery为查询的条件容器,
*第三个参数criteriaBuilder设置具体某一条件的表达式
*/
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
//因为同时可能有多个查询条件,所以定义一个条件列表
List<Predicate> predicates=new ArrayList<>();
//predicates.add()方法,将查询条件加入一个list中;
//cb.方法第一个参数指定查询对应的实体类,第二个参数指定对应前端所传递的参数
//判断查询条件之一:根据博客标题查询
if(!"".equals(blog.getTitle()) && blog.getTitle()!=null){
predicates.add(
cb.like(root.<String>get("title"),"%"+blog.getTitle()+"%")
);
}
//判断查询条件之二:根据博客分类查询
if(blog.getTypeId()!=null){
predicates.add(
cb.equal(root.<Type>get("type").get("id"),blog.getTypeId())
);
}
//判断查询条件之三:根据博客是否被推荐查询
if(blog.isRecommend()){
predicates.add(
cb.equal(root.<Boolean>get("recommend"),blog.isRecommend())
);
}
//判断查询条件之四:根据博客是否已发布查询,过滤掉状态为草
if(blog.isPublished()){
predicates.add(
cb.equal(root.<Boolean>get("published"),blog.isPublished())
);
}
//判断查询条件之五:根据博客是否是草稿,实现后台查询草稿博客功
if(blog.isDraft()){
predicates.add(
cb.equal(root.<Boolean>get("published"),!blog.isDraft())
);
}
/*cq的where方法,相当于sql中的where,接收一个条件数组,所以要将list转为数组*/
cq.where(predicates.toArray(new Predicate[predicates.size()]));
return null;
}
},pageable);
}
@OneToMany:
作用:建立一对多的关系映射
属性:
targetEntityClass:指定多的多方的类的字节码
mappedBy:指定从表实体类中引用主表对象的名称。
cascade:指定要使用的级联操作
fetch:指定是否采用延迟加载
orphanRemoval:是否使用孤儿删除
@ManyToOne
作用:建立多对一的关系
属性:
targetEntityClass:指定一的一方实体类字节码
cascade:指定要使用的级联操作
fetch:指定是否采用延迟加载
optional:关联是否可选。如果设置为false,则必须始终存在非空关系。
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息
在一对多关系中,一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对多的关系,需要使用数据库的外键约束。
什么是外键?
指的是从表中有一列a,取值参照主表的主键,这一列a就是外键。
在实体类中建立一对多映射关系:
例如:一个分类下可以有很多博客,但是一个博客只属于一个分类,所以分类和博客关系为一对多
Type实体类:
@Entity
@Table(name = "t_type")
public class Type {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "type") //指定关系为一对多,mappedBy声明当前是被维护端,通过type连接
private List<Blog> blogs=new ArrayList<>(); //一个分类下可以有很多博客
//已省略getter,setter方法....
}
Blog实体类:
@Entity
@Table(name="t_blog")
public class Blog {
@Id
@GeneratedValue
private Long id; //博客id
private String title; //标题
private Date updateTime; //博客更新时间
//已省略部分实体类属性名...
@ManyToOne //指定对应关系为多对一
private Type type; //多个博客可从属于一个分类
//此处已省略getter,setter方法
}
级联操作:指操作一个对象同时操作它的关联对象
使用方法:只需要在操作主体的注解上配置cascade
/**
* cascade:配置级联操作
* CascadeType.MERGE 级联更新
* CascadeType.PERSIST 级联保存
* CascadeType.REFRESH 级联刷新
* CascadeType.REMOVE 级联删除
* CascadeType.ALL 包含所有
*/
@OneToMany(mappedBy="customer",cascade=CascadeType.ALL)
在实际开发中,级联删除请慎用!(在一对多的情况下)
例如:一个博客可以有很多个标签,一个标签也可以有很多个博客使用,所以博客和标签的关系为多对多
多对多的表关系建立靠的是中间表,其中博客表和中间表的关系是一对多,标签表和中间表的关系也是一对多。
其中在中间表内存在博客id和标签id,这样就可以通过中间表建立多对多关系
还需在实体类中配置多对多映射关系:
Blog实体类:
@Entity
@Table(name="t_blog")
public class Blog {
@Id
@GeneratedValue
private Long id; //博客id
private String title; //标题
private Date updateTime; //博客更新时间
//已省略部分实体类属性名...
@ManyToMany
private List<Tag> tags=new ArrayList<>(); //博客与标签的关系为多对多
//此处已省略getter,setter方法...
}
Tag实体类:
@Entity
@Table(name = "t_tag")
public class Tag {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "tags")//因为blog是主体
private List<Blog> blogs=new ArrayList<>();
//已省略getter,setter方法....
}
在多对多(保存)中,如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,中间表的2个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一方放弃对中间表的维护权即可,推荐在被动的一方放弃,对应配置如下:
@ManyToMany(mappedBy = "tags")//因为blog是主体
private List<Blog> blogs=new ArrayList<>();
对象导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。
例如:我们通过ID查询方式查出一个客户,可以调用Customer类中的getLinkMans()方法来获取该客户的所有联系人。对象导航查询的使用要求是:两个对象之间必须存在关联关系。
查询一个客户,获取该客户下的所有联系人:
@Autowired
private CustomerDao customerDao;
@Test
//由于是在java代码中测试,为了解决no session问题,将操作配置到同一个事务中
@Transactional
public void testFind() {
Customer customer = customerDao.findOne(5);//查询id为5的customer
//对象导航查询,通过查询出的customer获取该用户下的所有联系人LinkMans
Set<LinkMan> linkMans = customer.getLinkMans();
for(LinkMan linkMan : linkMans) {
System.out.println(linkMan);
}
}
/**
* Specification的多表查询
*/
@Test
public void testFinds() {
Specification<LinkMan> spec = new Specification<LinkMan>() {
public Predicate toPredicate(Root<LinkMan> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//Join代表链接查询,通过root对象获取
//创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
//JoinType.LEFT:左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连接
Join<LinkMan, Customer> join = root.join("customer",JoinType.INNER);
return cb.like(join.get("custName").as(String.class),"test");
}
};
List<LinkMan> list = linkManDao.findAll(spec);
for (LinkMan linkMan : list) {
System.out.println(linkMan);
}
}
评论