3

SpringData

Spring项目中,我们使用JPA进行查询,只需简单地继承SpringData提供的接口即可实现强大的数据查询功能。

之前的强检器具统计管理用的仅仅是单表查询,理解不深,这次开发的考评员综合查询涉及到了多个实体间的查询,值得学习,特此记录。

以下是ER图。

实体关系

管理部门选择本部门或其管辖的部门中的某人员作为该部门的考评人员。根据区域、部门、学科类别查询出符合条件的考评人员。

概述

复杂查询,我们就需要SpringData提供的JpaSpecificationExecutor接口,这是该接口的源代码。

public interface JpaSpecificationExecutor<T> {

    /**
     * Returns a single entity matching the given {@link Specification}.
     * 
     * @param spec
     * @return
     */
    T findOne(Specification<T> spec);

    /**
     * Returns all entities matching the given {@link Specification}.
     * 
     * @param spec
     * @return
     */
    List<T> findAll(Specification<T> spec);

    /**
     * Returns a {@link Page} of entities matching the given {@link Specification}.
     * 
     * @param spec
     * @param pageable
     * @return
     */
    Page<T> findAll(Specification<T> spec, Pageable pageable);

    /**
     * Returns all entities matching the given {@link Specification} and {@link Sort}.
     * 
     * @param spec
     * @param sort
     * @return
     */
    List<T> findAll(Specification<T> spec, Sort sort);

    /**
     * Returns the number of instances that the given {@link Specification} will return.
     * 
     * @param spec the {@link Specification} to count instances for
     * @return the number of instances
     */
    long count(Specification<T> spec);
}

该接口中声明的方法都有一个共同的参数:Specification,翻译过来就是规范的意思。

这是Specification接口源代码。

public interface Specification<T> {

    /**
     * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
     * {@link Root} and {@link CriteriaQuery}.
     * 
     * @param root
     * @param query
     * @return a {@link Predicate}, must not be {@literal null}.
     */
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}

Specification接口中有一个toPredicate方法,看不懂代码我们可以看注释。

Creates a WHERE clause for a query of the referenced entity in form of a {Predicate}

用谓词的形式为引用的实体创建一个WHERE语句。

到这里,我们应该明白了这个查询的原理:我们先生成我们需要的谓语,然后用toPredicate将谓语生成为where语句,最后再使用我们Specification类型的where语句作为参数用JpaSpecificationExecutor接口中定义的方法进行查询。

继承JpaSpecificationExecutor接口

public interface ExaminerRepository extends CrudRepository<Examiner, Long>, JpaSpecificationExecutor {
}

我们的考评员仓库原继承CrudRepository,为了实现可以复杂查询,继承JpaSpecificationExecutor接口。

实现规格接口

为了规范,我们建了一个spec的查询规格的包,将所有生成查询规格的类放入该包中。

/**
 * @author zhangxishuo on 2018/5/26
 * 考评人员查询规格
 */

public class ExaminerSpecs {

    // 日志
    private static final Logger logger = Logger.getLogger(ExaminerSpecs.class);

    public static Specification<Examiner> base(Department userDepartment, final Map<String, Object> map) {
        return new Specification<Examiner>() {

            @Override
            public Predicate toPredicate(Root<Examiner> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                return null;
            }
        };
    }
}

我们在这个类中写了一个base方法,用于根据传入的参数生成一个查询规格。

return语句中,我们new了一个接口,然后在new这个接口的同时对这个接口进行实现,实现接口中的toPredicate方法。

toPredicate中的三个参数意义。Root:查询的模型;CriteriaQuery:标准查询,用于查询的;CriteriaBuilder:标准构建,用户构建查询语句的。

大家不要去纠结是直接new接口然后在花括号中实现还是写一个类实现接口然后再去new

这里说一下我自己的理解。其实这主要是看实际需要,如果这个接口的实现类需要被好多个其他类调用,为了代码的复用,我们就会写一个类去实现接口,就像我们后台MVC架构的业务逻辑层,因为同一个方法可能会被好多个控制器进行调用,所以我们建一个Service接口,再建立一个Service接口实现类,然后其他类需要时进行Autowire

如果我们的方法不需要复用,那Android代码就是我们最好的例子(Android中大量使用了new接口然后在new时去实现的写法)。

public class MyActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.content_layout_id);

        final Button button = findViewById(R.id.button_id);
        button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                // Code here executes on main thread after user presses button
            }
        });
    }
}

这是Android官方文档中对按钮添加点击事件的示例代码,我们看到这里的View.OnClickListener()就是在new的时候再去实现的。其实这也不难理解,打开我们每天用的APP,几乎每个按钮,每个视图的功能都是不一样的,所以不需要进行代码复用。

构建谓语

谓语是什么呢?按我的理解,一个谓语就是一个条件。

logger.debug("构建谓语,人员的部门的区域的id等于map中区域的id");
Predicate districtPredicate = criteriaBuilder
        .equal(root.join("qualifier")
                    .join("department")
                    .join("district")
                    .get("id")
                    .as(Long.class),
                ((District) map.get("district"))
                    .getId());

我们使用criteriaBuilderequal方法构建了一个等于的条件(或谓语),该条件要求考评员(root)的人员的部门的区域的id转化为Long类型的结果与传入的区域的id相等。

类似的写法,我们可以构建出部门和学科的谓语。

连接谓语

谓语创建完了,我们还需要将这些谓语进行连接。

这里我们需要用到and条件,即区域符合且部门符合且学科类别符合。

为了方便构建,这里参考之前的项目代码构建了一个用and拼接谓语的方法。

private Predicate predicate = null;
private CriteriaBuilder criteriaBuilder;

// 设置and谓语.注意,这里只能设置and关系的谓语,如果谓语为OR,则需要手动设置
private void andPredicate(Predicate predicate) {
    // 如果传入的谓语不为空
    if (null != predicate) {
        if (null == this.predicate) {
            // 如果该方法之前没有谓语,则直接赋值
            this.predicate = predicate;
        } else {
            // 如果之前有谓语。则使用criteriaBuilder的与将已有谓语和新谓语用and连接
            this.predicate = this.criteriaBuilder.and(this.predicate, predicate);
        }
    }
}

这几行代码就体现了一名优秀软件工程师的素养,优秀的人在写代码之前就能遇见哪些功能是重复的,不需实现之后再将相同代码分离重构。

logger.debug("用and连接该谓语");
this.andPredicate(districtPredicate);

CriteriaQuery

如果我们只是单纯的查询的话,没有什么特殊要求的话,那我们直接就可以把我们的谓语返回。

return this.predicate;

没错,直接将谓语返回,debug的时候发现实际查询时会获取我们的谓语,并执行query.where语句。

clipboard.png

如果需要复杂功能的话,可以使用CriteriaQuery

// 把Predicate应用到CriteriaQuery中去,可以实现更丰富的查询功能,可以分组,排序等
criteriaQuery.where(this.predicate);

return criteriaQuery.getRestriction();

查询

基础工作完成了,我们终于可以使用我们的谓语进行查询啦。

/**
 * 查询符合条件的考评员信息
 * @param district   区域
 * @param department 部门
 * @param discipline 学科类别
 * @return 符合条件的考评员信息
 */
@Override
public List<Examiner> getAllExaminerInfo(District district, Department department, Discipline discipline) {
    logger.debug("新建Map并设置属性");
    Map<String, Object> map = new HashMap<>();
    map.put("department", department);
    map.put("district", district);
    map.put("discipline", discipline);

    logger.debug("调用根据Map的查询方法");
    return this.getExaminerInfoByMap(map);
}

/**
 * 根据Map查询考评员的信息
 * @param map 考评员查询Map
 * @return 考评员列表
 */
private List<Examiner> getExaminerInfoByMap(Map<String, Object> map) {
    logger.debug("获取当前登录用户");
    User currentLoginUser = userService.getCurrentLoginUser();

    logger.debug("获取查询谓语");
    Specification<Examiner> specification = ExaminerSpecs.base(currentLoginUser.getDepartment(), map);

    logger.debug("查询并返回");
    return (List<Examiner>) examinerRepository.findAll(specification);
}

怎么样,谓语拼接完之后是不是很简单呢?


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。