时不时地,我看到有人感叹SQL语法在操作的词法顺序上的特殊脱节。
- 操作的词法顺序 (
SELECT .. FROM
) - 操作的逻辑顺序(
FROM .. SELECT
)
最近一次是在Youtube上对最近的jOOQ/kotlin讲座的评论回复中。让我们看看为什么jOOQ没有落入这个试图 "解决 "的陷阱,以及为什么这甚至是一个陷阱。
英文语言
SQL有一个简单的语法模型。所有的命令都以动词的形式开始,因为我们*"命令 "*数据库执行一个语句。常见的命令包括。
SELECT
INSERT
UPDATE
DELETE
MERGE
TRUNCATE
CREATE
ALTER
DROP
所有这些都是命令式的动词。想想看,在任何地方都要加一个感叹号,比如说INSERT [this record]!
操作的顺序
我们可以争辩说,自然语言对计算机编程语言来说是非常差的灵感,而计算机编程语言往往更倾向于数学化(有些比其他的更多)。关于SQL语言的很多批评意见是,它没有 "构成"(以其原始形式)。
我们可以争辩说,对于一个更有构成性的SQL语言来说,从FROM
开始会好得多,根据操作的逻辑顺序,它是SELECT
的第一个操作。比如说。
FROM book
WHERE book.title LIKE 'A%'
SELECT book.id, book.title
是的,这将是更好的,因为它更符合逻辑。首先,我们声明数据源、谓词等,最后才会声明投射。使用JavaStream
API,我们会写。
books.stream()
.filter(book -> book.title.startsWith("A"))
.map(book -> new B(book.id, book.title))
这样做的好处是。
- 语法和逻辑之间没有脱节
- 因此。语法上没有混乱,特别是为什么你不能在
WHERE
中引用SELECT
的别名,例如。 - 更好的自动完成(因为你不会先写那些还没有声明的东西)。
在某种程度上,这种排序与一些RDBMS在RETURNING
DML语句的数据时实现的一致,例如。
- Firebird
- Oracle
- PostgreSQL
INSERT INTO book (id, title)
VALUES (3, 'The Book')
RETURNING id, created_at
对于DML语句,命令("命令")仍然是INSERT
,UPDATE
,DELETE
,即一个动词,明确告诉数据库对数据做 什么。而 "预测 "则更像是一种事后的考虑。一个偶尔有用的工具,因此RETURNING
,可以放在最后。
RETURNING
似乎是一个实用的语法选择,甚至不是标准的一部分。该标准定义了 ,由Db2和H2实现,其语法是。<data change delta table>
SELECT id, created_at
FROM FINAL TABLE (
INSERT INTO book (id, title)
VALUES (3, 'The Book')
) AS book
我的意思是,为什么不呢。我对这种或那种语法没有强烈的偏好(jOOQ同时支持这两种语法,并将它们模拟成彼此)。SQL Server发明了第三种变体,其语法可能是最不直观的(我总是要查一下OUTPUT
子句的确切位置)。
INSERT INTO book (id, title)
OUTPUT id, created_at
VALUES (3, 'The Book')
Cypher查询语言
在这里可能值得一提的是,存在一种现代查询语言,它足够流行,可以考虑用于此类讨论。来自neo4j的Cypher查询语言。通过一个简单的 "技巧",它既。
- 保持了语言模型,即以命令式的动词开始语句(动词是
MATCH
,与FROM
,但它是一个动词),所以它继承了SQL的 "优势",即对非程序员也很直观。 - 颠覆了阅读语句中操作的逻辑顺序,采用
MATCH .. RETURN
的形式,使RETURN
成为所有操作中投射事物的通用形式,而不仅仅是SELECT
。 - 重用的
MATCH
,也用于写操作,包括[DELETE](https://neo4j.com/docs/cypher-manual/4.4/clauses/delete/)
或[SET](https://neo4j.com/docs/cypher-manual/4.4/clauses/set/)
(对应于SQL的UPDATE
)
虽然在不同的数据范式(相对于关系模型的网络模型)上操作,但我总是发现Cypher查询语言在语法方面普遍优于SQL,至少在高层次上是这样。如果我不得不通过创建SQL 2.0来实际 "修复 "SQL,我会从这里得到启发。
在像jOOQ这样的API中修复它是不值得的
正如之前所讨论的,SQL有一些明显的缺点,而且还有像Cypher这样的更好的语言来解决同类问题。但是SQL在这里,它已经有50年的历史了,而且它将继续存在。它不会被修复。
这是必须要接受的事情。
SQL不会被修复
它将会被修正。它融入了新的想法,包括。
- 对象,通过ORDBMS扩展(一个SQL标准,最重要的功能是
ARRAY
类型和强大的MULTISET
,jOOQ模拟了这一点)。 - 通过SQL/XML的XML
- 通过SQL/JSON的JSON
它总是以一种习惯性的、SQL风格的方式进行。如果你正在阅读SQL标准,或者你正在使用与标准非常接近的PostgreSQL,你会觉得SQL作为一种语言是相当一致的。或者说,它是一贯的怪异,这取决于你的口味。
对于jOOQ来说,主要的成功因素之一一直是在语法方面尽可能地接近SQL的真正含义。很多人在编写本地SQL时非常有效。因为Java有文本块,所以只需从你的SQL编辑器中复制粘贴一个静态SQL查询到你的Java程序中,然后用JDBC或jOOQ的普通SQL模板API来执行它,就变得更容易忍受了。
for (Record record : ctx.fetch(
"""
SELECT id, title
FROM book
WHERE title LIKE 'A%'
"""
)) {
System.out.println(record);
}
这种方法对于外面非常简单 的应用程序来说是足够的。如果你的 "应用程序 "总共运行5个不同的SQL查询,你可以只用JDBC来做(当然,一旦你开始掌握了jOOQ,你可能也会对这些应用程序使用jOOQ)。
但是,当你的应用程序有100多个查询,包括许多动态查询,而你的数据库有100多个表时,jOOQ才会真正发光,在这种情况下,类型安全和模型安全的好处真的很有帮助。然而,只有当你的SQL查询1:1转换为jOOQ API时,它才能大放异彩。在这个最重要的语句(SELECT
)中,随机地对SQL进行某种程度的修正是不会有效果的。
因为。你在哪里会停止修复SQL?即使你切换到FROM .. SELECT
,SQL仍然很奇怪。例如, GROUP BY
的语义仍然很奇怪。或者 DISTINCT
和ORDER BY
之间的关系。例如,这起初看起来会好得多(例如,将SELECT
和DISTINCT
分开,它们不应该如此紧密地位于一起)。
FROM book
WHERE book.title LIKE 'A%'
SELECT book.title
DISTINCT
ORDER BY book.title
但是奇怪的注意事项仍然不会消失,即在没有DISTINCT
的情况下,你可以ORDER BY
,但在有DISTINCT
的情况下就不能了(见我们之前关于这个问题的文章),这些表达式没有列在SELECT
。
其他DSL API中的替代语法
那么,SQL的 "修复 "在哪里停止?SQL什么时候才能被 "固定"?它永远不会被修复,因此,像jOOQ这样的API会比它应该的更难学。一些竞争性的API遵循这种模式,例如
这两个API都是基于这样的想法:SQL需要 "修复",而更 "原生"、更 "习惯 "的API感觉会好一些。一些例子。
Slick。
这是入门指南中的一个例子。
coffees.map(_.price).max
这对应于下面的SQL。
SELECT max(price)
FROM coffees
这可以说是一个更成文的例子。它看起来就像普通的Scala集合API的使用,去掉了SQL的感觉。毕竟,通常的map(x => y)
集合方法实际上对应于一个SQLSELECT
子句(一个 "投影")。
暴露了。
这里有一个来自Baeldung的例子。
StarWarsFilms
.slice(StarWarsFilms.sequelId.count(), StarWarsFilms.director)
.selectAll()
.groupBy(StarWarsFilms.director)
该API引入了新的术语,例如
slice
意思与 或 相同,虽然对SQL或kotlin集合API都是陌生的。map()
SELECT
selectAll
,对应于关系代数术语 "选择",对应于SQLWHERE
合成方便的语法,而不是 "修复 "SQL
jOOQ没有走这条路,也永远不会走。SQL就是它,而jOOQ无法 "修复 "它。SQL语法和jOOQ API之间的1:1映射意味着,即使你想使用一些复杂的东西,比如。
[GROUPING SETS](https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/grouping-functions/)
[CONNECT BY](https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/select-statement/connect-by-clause/)
WITH
(CTEs)FOR XML
或FOR JSON
- 不管是什么
即使如此,jOOQ也不会让你失望,会让你完全 按照你心中的SQL功能来写。我是说,在Slick或Exposed中支持CONNECT BY
,真的有意义吗?可能不会。他们将不得不发明他们自己的语法,以提供对SQL递归的访问。但它会是完整的吗?这是一个jOOQ不会有的问题。
一些语法不能使用的唯一原因是它还不可能实现(请发送一个功能请求)。FOR XML
的例子是一个很好的例子。SQL Server发明了这个FOR
子句,虽然它对简单的情况很方便,但对复杂的情况就不是很强大了。我更喜欢标准的SQL/XML和SQL/JSON语法,(jOOQ也 支持这种语法)。但是,尽管我不太喜欢这种语法,jOOQ 也不会评判。一个完全由jOOQ发明的第三种语法对用户有什么好处呢?正如我之前所说。
"修复 "何时才能停止?
它永远不会停止。我所提到的替代方案在他们开始添加更多的功能时,如果他们开始添加更多的功能,将会遇到非常困难的问题。虽然实现一个简单的SELECT .. FROM .. WHERE
查询生成器很容易,并使用任意的API支持该功能,声称SQL已经被 "修复",但要发展这个API,解决各种高级的SQL用例则难得多 。只要看看他们的问题跟踪器中的功能请求,如CTE。答案总是。"使用本地SQL"。
即使是 "简单 "的SQL功能,如UNION
,一旦基本的SQL语法被改变,也会变得更加复杂。在SQL中,语义已经足够棘手了(当然,这完全是SQL的错),但 "修复 "这些东西从来都不像一开始看起来那么简单。
现在,这个规则有两个例外。
合成句法
一个例外是:"合成语法"。jOOQ中最强大的合成语法是隐式连接。隐式连接不是在 "修复 "SQL,而是在用SQL本身可能具有的语法(希望最终会有)来 "增强 "SQL。就像存在着SQL方言,它 "增强 "了SQL标准,例如
- BigQuery有其
* EXCEPT (...)
语法 - PostgreSQL有它的
DISTINCT ON
语法 - 在成为标准之前,各种方言都有
LIMIT .. OFFSET
。 - 还有很多很多
jOOQ对这种合成语法非常保守。有很多好的想法,但很少有向前兼容的。这些语法中的每一个都使其他SQL转换功能更加复杂,而且每一个都有可能尚未解决的缺陷(例如,从jOOQ 3.16开始,隐式连接在DML语句中是不可能的,如UPDATE
,DELETE
,即使它们在那里也有很大的意义。见问题#7508)。
方便的语法
另一种改进是我称之为 "便利语法 "的东西。例如,无论底层的RDBMS是什么,jOOQ都允许你写。
select(someFunction()); // No FROM clause
selectFrom(someTable); // No explicit SELECT list
在这两种情况下,用户可以省略底层 SQL方言中可能是强制性的条款,而jOOQ则用合理的默认值来填充生成的SQL。
- 一个
FROM DUAL
表声明,或类似的东西 - 一个
SELECT *
投射声明,或类似的东西
结论
jOOQ应该在1:1的基础上坚持使用SQL语法的想法是我13年前做jOOQ时的一次赌博。我想把jOOQ设计成每个已经知道SQL的人在学习jOOQ时都不会有问题,因为一切都很直接。这里描述了这种API设计背后的技术。
其他人试图通过使他们的API在考虑到目标语言的情况下非常习惯化,或者通过发明一种新的语言来 "解决 "SQL。
13年后,我发现1:1的模仿方法是唯一可行的方法,因为我不断发现新的、神秘的SQL特性。
我,制作jOOQpic.twitter.com/CyEHanYZBY
- Lukas Eder (@lukaseder)2022年5月16日
创建一种语言是非常困难的(让我们把内部DSL的API看作是一种语言)。如果目标是支持几乎所有的底层SQL功能,那么几乎不可能进行正确的设计,除非设计者放下 "修复 "东西的梦想,开始拥抱 "支持 "东西的 "梦想"。所有的东西。
SQL是它的本质。而这意味着,语法是SELECT .. FROM
,而不是FROM .. SELECT
。
喜欢这个
Like Loading...