引言
在经过对动态查询的不断优化和重构后,我们已能够利用查询对象和实体对象为单表构建增删查改语句。本文将进一步介绍如何通过对象映射构建复杂查询语句的方案。
对于复杂查询语句的构造,我们可以复用查询对象的映射方法构造其中的查询子句。但是复杂查询语句中的多表连接、聚合计算以及分组过滤等语法无法实体对象进行映射。因此,我们需要一个设计新的对象来映射复杂查询语句,称为视图对象。
视图对象映射
复杂查询通常依赖于预定义的SQL字符串,并且需要定义相应的数据对象来映射查询结果。然而,复杂查询语句中的列通常涉及聚合计算或多个表的字段,而实体对象中的字段通常只对应单表的列。为了解决这个问题,我们提出了一种解决方案,通过转换数据对象,不仅能够映射查询结果,还能生成相应的聚合查询语句,从而实现复杂查询语句的自动映射。
基于此解决方案,我们引入了 视图对象(View Object) 来定义和构建复杂查询语句中的列,并引入了 Having 对象 来生成 HAVING 子句。查询语句的动态部分仍然使用现有的查询对象来构建。这种设计允许复用查询对象来构建复杂查询语句的动态部分,同时利用视图对象作为静态列和聚合列的载体,确保设计的简洁性和可维护性。
为了实现复杂查询的自动映射,我们将映射过程分为以下三个部分:
- 列映射(Columns Mapping):视图对象中定义的字段用于映射查询结果中所需的列,包括常规列和聚合列。
- 连接映射(Join Mapping):视图对象中的注解用于定义表之间的关系,从而自动生成必要的连接语句。
- HAVING 子句映射(HAVING Clause Mapping):使用 Having 对象 生成
HAVING子句,支持基于聚合结果的过滤条件。
这种设计不仅提高了开发人员构建和维护复杂查询语句的效率,还减少了手动拼接SQL语句的风险,显著增强了代码的可读性、可重用性和可维护性。
列映射(Columns Mapping)
我们需要解决的第一个问题是如何通过视图对象生成 SELECT 子句中的表达式和分组子句中的列。
聚合列映射(Aggregate Columns Mapping)
聚合函数的应用是聚合查询的核心。所有聚合列查询的数据都需要映射到视图类中定义的字段,因此,我们可以反过来将字段名称映射为聚合列。其中,前缀映射是一种有效的映射方案。
| 前缀 | 聚合函数名称 | 字段名 | 聚合表达式 |
|---|---|---|---|
| sum | sum | sumScore | sum(score) AS sumScore |
| max | max | maxScore | max(score) AS maxScore |
| min | min | minScore | min(score) AS minScore |
| avg | avg | avgScore | avg(score) AS avgScore |
| first | first | firstScore | first(score) AS firstScore |
| last | last | lastScore | last(score) AS lastScore |
| stdDevPop | stddev_pop | stdDevPopScore | stddev_pop(score) AS stdDevPopScore |
| stdDevSamp | stddev_samp | stdDevSampScore | stddev_samp(score) AS stdDevSampScore |
| stdDev | stddev | stdDev | stddev(score) AS stdDev |
| addToSet | addToSet | addToSetScore | addToSet(score) AS addToSetScore |
| push | push | pushScore | push(score) AS pushScore |
| count | count | countScore | count(score) AS countScore |
| count | count(*) AS count |
示例:如果我们要计算名为 score 的列的平均值,我们将使用 avg 函数。然后我们可以将字段名称定义为 avgScore,遵循聚合前缀加列名的格式,在构建查询语句时将其映射为 avg(score) AS avgScore。
表达式列映射(Expression Columns Mapping)
对于难以定义为字段名称的复杂聚合表达式,我们可以使用包含表达式的注解来映射。表达式映射到列名,字段名称映射到标签名称。
示例:我们定义一个带有表达式注解的字段:
@Column(name = "sum(l_extendedprice*(1-l_discount))")
private BigDecimal sum_disc_price;
字段 sum_disc_price 将被映射为:
sum(l_extendedprice*(1-l_discount)) AS sum_disc_price
GROUP BY 列映射(GROUP BY Columns Mapping)
对于 GROUP BY 子句,由于分组列在大多数情况下需要返回,我们可以简单地在视图类中定义的字段上添加注解,声明它们是分组列,并在映射时将其放在 GROUP BY 之后。
示例:我们在视图类中有两个带有 @GroupBy 的字段:
@GroupBy
private int l_returnflag;
@GroupBy
private int l_linestatus;
它们将被映射为:
SELECT l_returnflag, l_linestatus, ... FROM ...
GROUP BY l_returnflag, l_linestatus ...
连接映射(Join Mapping)
我们使用 TPC-H 基准测试中的第 3 个查询(Shipping Priority Query)来说明如何映射表连接。查询语句如下:
SELECT l_orderkey, SUM(l_extendedprice * (1 - l_discount)) AS revenue, o_orderdate, o_shippriority
FROM customer, orders, lineitem
WHERE o_custkey = c_custkey
AND l_orderkey = o_orderkey
AND c_mktsegment = ?
AND o_orderdate < ?
AND l_shipdate > ?
GROUP BY l_orderkey, o_orderdate, o_shippriority
ORDER BY revenue DESC, o_orderdate
这是一个典型的连接查询,包含两个需要处理的关键部分:
- 表名列表:
FROM customer, orders, lineitem - 连接条件:
WHERE o_custkey = c_custkey AND l_orderkey = o_orderkey。
多表名映射(Multiple Table Names Mapping)
定义:为了映射连接表,我们定义了注解 @ComplexView 和 @View 来配置视图类上的连接实体:
@Target(TYPE)
@Retention(RUNTIME)
public @interface ComplexView {
View[] value();
}
@Target(TYPE)
@Retention(RUNTIME)
@Repeatable(ComplexView.class)
public @interface View {
Class<?> value();
String alias() default "";
ViewType type() default ViewType.TABLE_NAME;
}
配置:对于 Shipping Priority 查询,我们定义了一个 ShippingPriorityView 类,并使用 @View 注解配置如下:
@View(CustomerEntity.class)
@View(OrdersEntity.class)
@View(LineitemEntity.class)
public class ShippingPriorityView {
//...
}
解析:从三个实体的注解配置中可以轻松解析出三个表名,从而得到 FROM customer, orders, lineitem。
连接条件映射(Joining Conditions Mapping)
定义:为了映射这些实体之间的连接条件,我们需要首先定义实体之间的关系。我们定义了注解 @ForeignKey,用于配置实体的外键字段,以确定两个实体之间的关系。
@Target(FIELD)
@Retention(RUNTIME)
public @interface ForeignKey {
Class<?> entity();
String field();
}
配置:此注解配置在外键字段上,用于指定相关实体及其主键字段。以下是一个示例:
public class OrdersEntity extends AbstractPersistable<Long> {
private String o_orderkey;
@ForeignKey(entity = CustomerEntity.class, field = "c_custkey")
private String o_custkey;
//...
}
解析:当此类实体通过 @View 注解配置时,我们会扫描其他连接实体,查找是否存在由 @ForeignKey 配置的实体,如果找到,则添加连接条件。在 ShippingPriorityView 类中,我们为 OrdersEntity 的字段 o_custkey 找到了 CustomerEntity,因此我们在 WHERE 子句后添加条件 o_custkey = c_custkey。另一个条件 l_orderkey = o_orderkey 也是如此。
示例:我们展示一个完整的示例,展示从视图和查询对象到 SELECT 语句的映射过程。
Having 对象(The Having Object)
SQL 中的 HAVING 子句用于对聚合后的分组结果进行过滤,是 SELECT 语句的可选部分,语法类似于 WHERE 子句。因此,我们设计了一个新对象,称为 Having 对象,用于映射 HAVING 子句。
Having对象映射过程可以复用查询对象的映射算法。我们同时采用前缀映射和后缀映射来映射 Having 对象中声明的字段。例如,Having 对象中定义的字段 avgScoreGe 将被映射为 HAVING avg(score) >= ?。
在 Java 中,我们让 Having 对象实现一个空的 Having 接口,以标记其构建 HAVING 子句的功能。此外,我们可以让 Having 对象继承查询对象,而查询对象继承 PageQuery,从而形成三级结构。对于 Having 对象的实例,PageQuery 中声明的字段用于构建排序和分页子句;查询对象中声明的字段用于构建 WHERE 子句;Having 对象中声明的字段用于构建 HAVING 子句。
聚合查询接口(Aggregate Query Interface)
为了支持根据视图对象进行复杂查询,我们定义了一个新的接口 AggregateClient,该接口可使开发人员通过视图对象和查询对象完成复杂查询操作:
public interface AggregateClient {
<V> List<V> query(Class<V> viewClass, Query query);
}
以下代码展示了如何使用此接口执行TPC-H的第3条查询语句:
Date date = Date.valueOf(LocalDate.of(1995, 3, 15));
ShippingPriorityQuery query = ShippingPriorityQuery.builder()
.c_mktsegment("BUILDING")
.o_orderdateLt(date).l_shipdateGt(date)
.sort("revenue,DESC;o_orderdate").build();
List<ShippingPriorityView> list = aggregateClient.query(ShippingPriorityView.class, query);
这种方法将查询语句的构建逻辑与业务逻辑分离。每次执行查询时,开发人员只需根据查询参数构建一个 ShippingPriorityQuery 对象即可完成操作。
结论
本文提出视图对象及其映射方法,再配合Page/Query/Having对象这样的层级设计映射查询条件,充分利用面向对象编程的特性,实现了通过对象自动构建复杂查询语句的功能,显著提高了复杂查询的开发效率和可维护性。
© 2024 Yuan Zhen. All rights reserved.