springDataJPA(三)


一. Specifications查询

有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在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对应关系
equlefiled = value
likefiled like value
gt(greaterThan )filed > value
lt(lessThan )filed < value
ge(greaterThanOrEqualTo )filed >= value
le( lessThanOrEqualTo)filed <= value
notEqulefiled != value
notLikefiled not like value
and( ):连接多个查询条件与关系
or( ):连接多个查询条件或关系

1.1 使用Specifications完成条件查询

单条件查询:

//依赖注入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);
    }

1.2基于Specifications的分页查询

@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();

1.3 使用Specifications完成动态查询

需求: 存在一个博客系统,可根据标题,分类,是否被推荐,是否是草稿动态查询博客并分页显示

可创建一个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);
}

二. JPA多表操作

2.1 映射的注解说明

@OneToMany:

​ 作用:建立一对多的关系映射

​ 属性:

​ targetEntityClass:指定多的多方的类的字节码

mappedBy:指定从表实体类中引用主表对象的名称。

​ cascade:指定要使用的级联操作

fetch:指定是否采用延迟加载

​ orphanRemoval:是否使用孤儿删除

@ManyToOne

​ 作用:建立多对一的关系

​ 属性:

​ targetEntityClass:指定一的一方实体类字节码

​ cascade:指定要使用的级联操作

fetch:指定是否采用延迟加载

​ optional:关联是否可选。如果设置为false,则必须始终存在非空关系。

@JoinColumn

​ 作用:用于定义主键字段和外键字段的对应关系。

​ 属性:

​ name:指定外键字段的名称

​ referencedColumnName:指定引用主表的主键字段名称

​ unique:是否唯一。默认值不唯一

​ nullable:是否允许为空。默认值允许。

​ insertable:是否允许插入。默认值允许。

​ updatable:是否允许更新。默认值允许。

​ columnDefinition:列的定义信息

2.2 一对多

2.2.1 配置一对多映射关系

在一对多关系中,一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对多的关系,需要使用数据库的外键约束。

什么是外键?

指的是从表中有一列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方法
}

2.2.2 级联操作

级联操作:指操作一个对象同时操作它的关联对象

使用方法:只需要在操作主体的注解上配置cascade

/**
 * cascade:配置级联操作
 * 		CascadeType.MERGE	级联更新
 * 		CascadeType.PERSIST	级联保存
 * 		CascadeType.REFRESH 级联刷新
 * 		CascadeType.REMOVE	级联删除
 * 		CascadeType.ALL		包含所有
 */
@OneToMany(mappedBy="customer",cascade=CascadeType.ALL)

在实际开发中,级联删除请慎用!(在一对多的情况下)

2.3 多对多

例如:一个博客可以有很多个标签,一个标签也可以有很多个博客使用,所以博客和标签的关系为多对多

多对多的表关系建立靠的是中间表,其中博客表和中间表的关系是一对多,标签表和中间表的关系也是一对多。

其中在中间表内存在博客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<>();

三. Spring Data JPA中的多表查询

3.1 对象导航查询

对象导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。

例如:我们通过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);
    }
}

3.2 使用Specification查询

/**
 * 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);
    }
}
SpringDataJPA
  • 作者:管理员(联系作者)
  • 发表时间:2019-11-21 22:49
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论