引言

动态查询接口不仅为用户提供了多样的查询参数,还会根据分页和排序参数对查询结果进行处理。

本文将在重构动态查询代码的基础上,进一步探讨对分页和排序代码的重构,设计出一套简洁高效的数据访问接口。

分页和排序

假设一条查询请求如下:

/user/?ageGt=3&pageNumber=3&pageSize=10&sort=age,desc;name,asc

其中,分页参数被定义pageNumber(页码)和pageSize(每页大小),排序参数sort为字符串,在Controller中我们一般这样定义对应的查询方法:

@GetMapping("/user/")
public List<UserEntity> query(String name, String nameLike, Integer ageGt, Integer pageNumber, Integer pageSize, String sort) {
    // ...
}

分页查询对象

根据先前的重构过程,我们在使用引入参数对象(Introduce Parameter Object)手法进行重构时,将分页和排序参数也封装到查询对象UserQuery中。

由于分页和排序是通用需求,我们进一步通过提取父类(Extract Superclass)的重构手法,将分页和排序字段提取到父类PageQuery,由所有查询对象继承:

public class PageQuery {
    private Integer pageNumber;
    private Integer pageSize;
    private String sort;
}

public class UserQuery extends PageQuery {
    private String name;     // name = ?
    private String nameLike; // name LIKE ?
    private Integer ageGt;   // age > ?
}

这样做还能避免对UserQuery的字段进行遍历时访问到分页和排序字段。

响应对象

除了返回查询数据列表外,分页查询还需返回总数据条数total,以便前端计算总页数。常用的计算公式为⌈total/size⌉。因此,我们可以为分页查询结果定义如下响应对象:

public record PageList<T>(List<T> list, long total) {
}

分页子句构造

根据页号pageNumber和每页大小pageSize,我们可以先计算出偏移量offset,再构造分页子句LIMIT {pageSize} OFFSET {offset}。假设页号从1开始,offset的计算公式为:(pageNumber - 1) * pageSize

后续为了支持不同的数据库方言,我们还可以定义一个接口Dialect,根据数据库类型执行相应的分页实现:

public interface Dialect {
    String buildPageSql(String sql, int limit, long offset);
}

排序子句构造

对于排序参数sort=age,desc;name,首先我们将逗号,转为空格,再将分号;转为逗号,,并加上ORDER BY关键字,最终得到排序子句ORDER BY age desc, name

由于排序参数是字符串类型,为防止出现SQL注入问题,我们可以使用Validation提供的注解对sort字段进行校验。

数据访问接口

现在,我们通过查询对象构建了数据库访问语句中需要由用户提供参数的查询子句、排序子句和分页子句,再结合从实体对象中获取的表名和列名,便能为单张数据表构建全部增删查改语句。

而表名和列名这些信息并不会改变,所以我们可以将对实体对象的解析代码移至构造函数,将表名和列名保存起来避免重复处理。这样,在查询方法中只需要处理查询对象。以下是优化后的查询方法:

public class JdbcDataAccess<E> {
    private JdbcTemplate jdbcTemplate;
    private RowMapper<E> rowMapper;
    private String table;
    private String columns;

    public JdbcDataAccess(JdbcTemplate jdbcTemplate, Class<E> clazz) {
        this.jdbcTemplate = jdbcTemplate;
        this.rowMapper = new BeanPropertyRowMapper<>(clazz);
        this.table = clazz.getAnnotation(Table.class).name();
        this.columns = QueryBuilder.buildColumns(clazz);
    }

    public List<E> query(PageQuery query) {
        String sql = "SELECT " + columns + " FROM " + table;
        ArrayList<Object> argList = new ArrayList<>();
        sql += QueryBuilder.buildWhere(query, argList); // WHERE ...
        sql += QueryBuilder.buildOrderBy(query); // ORDER BY ...
        sql += QueryBuilder.buildPaging(query); // LIMIT ... OFFSET ...
        return jdbcTemplate.query(sql, rowMapper, argList.toArray());
    }

    public long count(PageQuery query) {
        String sql = "SELECT count(*) FROM " + table;
        ArrayList<Object> argList = new ArrayList<>();
        sql += QueryBuilder.buildWhere(query, argList);
        return jdbcTemplate.queryForObject(sql, argList.toArray(), long.class);
    }

    public PageList<E> page(PageQuery query) {
        return new PageList<>(query(query), count(query));
    }
}

接着,我们参考SpringDataJPA的CrudRepository接口,围绕实体对象和查询对象为单表交互设计了一套新的数据访问接口DataAccess<E, I, Q>,其中,E表示实体对象的类型,Q表示查询对象的类型。

public interface DataAccess<E extends Persistable<I>, I extends Serializable, Q extends PageQuery> {
    List<E> query(Q query);
    long count(Q query);
    PageList<E> page(Q query);
    <V> List<V> queryColumns(Q q, Class<V> clazz, String... columns);
    int delete(Q query);
    int patch(E entity, Q query);

    E get(I id);
    int delete(I id);
    void create(E e);
    int create(List<E> entities);
    int update(E e); // update all fields
    int patch(E e);  // update non-null fields
}

此接口提供了一套标准化的数据操作方法,进而帮助Controller层实现一套通用的RESTful接口:

public class AbstractController<E extends Persistable<I>, I extends Serializable, Q extends PageQuery> {
    @Resource
    private DataAccess<E, I, Q> dataAccess;

    @GetMapping("/")
    public PageList<E> page(Q query) {
        return dataAccess.page(query);
    }

    @GetMapping("/{id}")
    public E get(@PathVariable I id) {
        return dataAccess.get(id);
    }

    // POST/PUT/PATCH/DELETE methods
}

总结

我们通过合理运用重构方法得到了一个查询对象,可以用于构建数据库访问语句中需要由用户提供参数的查询子句、排序子句和分页子句,这一部分可以视为查询语句中的动态部分。而从实体对象中获取的表名和列名可以视为查询语句中的静态部分。

于是,SQL语句就被分为动静两部分由查询对象和实体对象分别映射得到。在此基础之上,我们为面向对象开发中单表的数据库交互设计了一套简单且完整的访问接口。

显然,我们也可以根据查询对象构建复杂查询语句里的动态部分,那么复杂查询语句里的静态部分如何构造呢?我们将在下一篇文章中继续探讨复杂查询语句的面向对象的构造方法。

© 2024 Yuan Zhen. All rights reserved.