前言

GoooQo是一个可以仅根据对象自动构建SQL语句并执行的OQM框架。

OQM是一项仅通过对象来构建数据库查询语句的技术,专注于研究面向对象编程语言和数据库查询语言之间的映射关系。

本文就标题中的问题来探讨一下SQL语句中逻辑运算符和对象结构之间的关系。

查询子句的层级结构

对于SQL语言中的WHERE子句,OQM技术提出一个新的观点:查询子句并不是扁平的,而是有层级的。

其中,使用AND连接的查询条件位于一个层级,使用OR连接的查询条件位于一个层级,每一层级的查询条件使用同一种逻辑运算符进行连接。

查询子句之所以有层级划分,是因为逻辑运算符的优先级不同,AND的优先级高于OR。

一般情况下,多个查询条件是用AND连接的,我们默认AND作为第一层级,那么标题中的查询语句里,OR连接的两个条件就在第二层级,OR语句的第二个条件里用AND连接的两个条件就在第三层级。

使用JSON表示这样的层级结构如下:

{
  "userOr": {
    "id": 5,
    "userAnd": {
      "name": "John",
      "age": 30
    }
  }
}

在这条数据中,我们通过键名称的后缀Or和And来确定子查询条件的逻辑运算符。

这条数据转换为WHERE子句的构建过程如下:

  • 第一层级只有一个键userOr,可以省掉AND连接;
  • userOr下的多个查询条件使用OR进行连接;
  • 第一个键值对"id": 5对应的查询条件为id = ?
  • 第二个键userAnd的后缀为And,使用AND连接子查询条件,得到name = ? ADN age = ?
  • 再把这个条件加上括号和查询条件id = ?用OR连起来。

这样,我们就得到了标题中的查询子句:id = ? OR (name = ? AND age = ?),以及对应的三个参数:[5, name, 30]。

由于AND的优先级高于OR,这里的括号也可以省略。

对比图

通过这张对比图,我们可以更清晰的看清楚查询子句和JSON数据库之间的对应关系。

数据构造和映射关系

然后我们尝试使用Go语言来构造这条JSON数据。

首先,我们定义对应的结构体UserQuery,可以使用如下嵌套的方式来定义:

type UserQuery struct {
    Id      *int       `json:"id,omitempty"`
    Name    *string    `json:"name,omitempty"`
    Age     *int       `json:"age,omitempty"`
    UserOr  *UserQuery `json:"userOr,omitempty"`
    UserAnd *UserQuery `json:"userAnd,omitempty"`
}

然后,根据JSON数据中的赋值构造对应的变量:

func P[T any](t T) *T { return &t }

func main() {
    userQuery := UserQuery{UserOr: &UserQuery{Id: P(1), UserAnd: &UserQuery{Name: P("John"), Age: P(30)}}}
    bytes, _ := json.Marshal(query)
    print(string(bytes))
}

运行后输出如下,没有赋值的字段都被忽略了:

{"userOr":{"id":1,"userAnd":{"name":"John","age":30}}}

这与上面的JSON数据完全一致。也就是说,我们可以根据这里的userQuery变量构造出同样的查询子句。

GoooQo接口调用

GoooQo就是利用对象结构和查询子句的层级之间的联系来构造包含AND和OR的查询子句的。

首先为user表定义实体对象和查询对象:

type UserEntity struct {
    goooqo.IntId
    Name     *string `json:"name,omitempty"`
    Age      *int    `json:"age,omitempty"`\
}

func (u UserEntity) GetTableName() string { return "user" }

type UserQuery struct {
    goooqo.PageQuery
    Id      *int       `json:"id,omitempty"`
    Name    *string    `json:"name,omitempty"`
    Age     *int       `json:"age,omitempty"`
    UserOr  *UserQuery `json:"userOr,omitempty"`
    UserAnd *UserQuery `json:"userAnd,omitempty"`
}

再根据数据库连接为user表创建数据访问接口:

db := rdb.Connect("local.properties")
defer rdb.Disconnect(db)
tm := rdb.NewTransactionManager(db)

userDataAccess := rdb.NewTxDataAccess[UserEntity](tm)

最后,根据需要执行的SQL语句,使用创建的查询对象调用userDataAccess的Query方法即可:

userQuery := UserQuery{UserOr: &UserQuery{Id: P(1), UserAnd: &UserQuery{Name: P("John"), Age: P(30)}}}
userEntities, err := userDataAccess.Query(context.Background(), userQuery)

对应的日志信息如下,与标题中的SQL语句基本一致:

time="2024-09-02T17:11:27+08:00" level=info msg=Executing SQL="SELECT id, name, age FROM user WHERE (id = ? OR name = ? AND age = ?)" args="[5 John 30]"

这样,当我们需要执行不同的SQL语句的时候,只需要根据查询条件给对应的字段赋值即可。对于前后端交互的项目,使用这类查询对象接收前端传入的参数,可以极大地减少后端的编码量。

改进

这里还有个小问题,当Or后缀的字段的类型是个数组呢,例如UserOr *[]UserQuery

GoooQo里的处理如下:

  • 数组中每个结构体内部字段映射得到的查询条件采用AND连接;
  • 多个结构体对应的每组查询条件再用OR连接。

新的结构体定义如下:

type UserQuery struct {
    goooqo.PageQuery
    Id      *int         `json:"id,omitempty"`
    Name    *string      `json:"name,omitempty"`
    Age     *int         `json:"age,omitempty"`
    UserOr  *[]UserQuery `json:"userOr,omitempty"`
    UserAnd *UserQuery   `json:"userAnd,omitempty"`
}

然后我们使用新的结构体构造同样的查询语句如下:

userQuery := UserQuery{UserOr: &[]UserQuery{ {Id: P(1)}, {Name: P("John"), Age: P(30)}}}
//json: {"userOr":[{"id":1},{"name":"John","age":30}]}

此时,键userOr变为了一个包含两个元素的数组,元素之间的运算符为OR,元素内部的运算符为AND。

于是,查询语句的每个查询条件和对象字段的对应关系表示如下:
对比图

另外,我们还可以发现,当数组userOr中仅包含一个元素时,它实际上与字段UserAnd映射得到的条件相同,于是,我们可以进一步优化掉UserAnd字段。

结语

本文介绍了查询子句的层级关系以及与对象结构之间的联系,并详细介绍了OQM技术中如何利用对象结构来构造包含AND和OR运算符的查询子句。

传统的ORM技术专注于对象模型和关系模型的映射,并没有指出对象语言和查询语言的这种联系,因而ORM框架对于含OR的查询子句的构造没有统一的实现方式。

而且进一步研究我们会发现,这样的查询条件既然能用JSON数据表示出来,那就意味着只要是支持JSON的编程语言都可以构造出这样的查询条件。下一步,我们可以尝试在不同的编程语言中实现本文所述方案。


© 2024 Yuan Zhen. All rights reserved.