如果你的传统JPA应用是使用偶尔的本地查询或Hibernate@Formula
或Spring Data@Query
注释,其中嵌入了供应商特定的本地SQL,你可以使用jOOQ的 解析连接和解析数据源在方言之间进行翻译,而不必全部采用jOOQ--尽管我认为一旦你看到jOOQ能为你做什么,这就是不可避免的。
现在,让我们来设计一个这样的表:
CREATE TABLE author (
id INT NOT NULL,
first_name TEXT,
last_name TEXT NOT NULL,
CONSTRAINT pk_author PRIMARY KEY (id)
);
现在,你可能想在这个表上使用JPA的 [EntityManager.createNativeQuery()](https://jakarta.ee/specifications/persistence/3.0/apidocs/jakarta.persistence/jakarta/persistence/entitymanager#createNativeQuery(java.lang.String,java.lang.Class))
,将其映射到实体。你可以使用jOOQ的DSL API,但假设你还没有准备好迁移到jOOQ,或者你想使用由DBA提供的实际SQL,而不是jOOQ的DSL。
所以,在MariaDB中,你可能要写这样的东西:
List<Author> result =
em.createNativeQuery("""
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
""", Author.class)
.getResultList();
其中你的实体是这样定义的:
@Entity
@Table(name = "author")
public class Author {
@Id
public int id;
@Column(name = "first_name")
public String firstName;
@Column(name = "last_name")
public String lastName;
// Constructors, getters, setters, equals, hashCode, etc
}
上面的方法运行得很好,并且在MariaDB中产生了所有的作者,它实现了对Oracle的 [NVL()](https://mariadb.com/docs/reference/mdb/functions/NVL/)
函数的支持。但是Oracle本身呢?查询在Oracle上失败了,因为:
ORA-00933:SQL命令没有正确结束
这是因为在Oracle中,你不能使用AS
关键字来别名表,只能别名列。当然,你可以去掉这个,但是,NVL()
呢?你希望这在MySQL和SQL Server上也能工作,但它们会抱怨。
MySQL
SQL错误[1305] [42000]。FUNCTION test.nvl不存在
SQL服务器
SQL Error [195] [S0010]: 'nvl' 不是一个公认的内置函数名。
现在,你有这些选择:
- 使用jOOQ来为你生成SQL字符串,使用DSL
- 使用JPQL而不是本地查询(但要大量重写,因为JPQL比SQL的功能少得多)。
- 试试你的运气,手动编写实际的供应商无关的SQL。
- 或者...
jOOQ的解析连接
你可以使用jOOQ的解析连接,它作为你实际连接的代理,在JDBC层面拦截每个SQL语句,以便将其翻译成目标方言。
这就像包装你现有的JDBCConnection
或DataSource
一样简单:
DataSource originalDataSource = ...
DataSource parsingDataSource = DSL
.using(originalDataSource, dialect)
.parsingDataSource();
就是这样!我的意思是,你可以在dialect
之后传递一些额外的配置Settings
,但是这已经是最简单的了。新的DataSource
现在可以在上述所有方言上运行你的SQL查询,例如,你可能会在你的DEBUG
日志中看到这个。
在MySQL上
-- org.hibernate.SQL
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
-- org.jooq.impl.ParsingConnection Translating from:
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
-- org.jooq.impl.ParsingConnection Translation cache miss:
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
-- org.jooq.impl.ParsingConnection Translating to:
select a.id, ifnull(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
在SQL Server上
-- org.hibernate.SQL
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
-- org.jooq.impl.ParsingConnection Translating from:
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
-- org.jooq.impl.ParsingConnection Translation cache miss:
select a.id, nvl(a.first_name, 'N/A') as first_name, a.last_name
from t_author as a
order by a.id
-- org.jooq.impl.ParsingConnection] Translating to:
select a.id, coalesce(a.first_name, 'N/A') first_name, a.last_name
from author a
order by a.id
Hibernate被jOOQ欺骗了!NVL
的函数翻译成了MySQL的 [IFNULL](https://dev.mysql.com/doc/refman/8.0/en/flow-control-functions.html#function_ifnull)
或SQL Server [COALESCE](https://docs.microsoft.com/en-us/sql/t-sql/language-elements/coalesce-transact-sql?view=sql-server-ver15)
,并且从SQL Server查询中删除了AS
关键字。这些只是简单的例子,你的实际SQL可能要复杂得多。在网上玩玩这个功能集,在这里。
另外, [Settings.cacheParsingConnectionLRUCacheSize](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/conf/Settings.html#cacheParsingConnectionLRUCacheSize)
标志,默认为8192,可以确保同一个查询不会一直被重新翻译,所以你不会在jOOQ的解析器中花费太多时间。
@Formula也是,不仅仅是本地查询
在Hibernate中,当你想投射额外的值时,一个快速的胜利,类似于SQL自己的计算列,在许多SQL方言中都有,这就是@Formula
注释,它可以被添加到任何实体中,就像这样。假设有这个额外的列:
ALTER TABLE author ADD year_of_birth INT;
我们可能会有以下修正后的实体:
@Entity
@Table(name = "author")
public class Author {
@Id
public int id;
@Column(name = "first_name")
public String firstName;
@Column(name = "last_name")
public String lastName;
@Column(name = "year_of_birth")
public Integer yearOfBirth;
@Formula("year_of_birth between 1981 and 1996")
public Boolean millenial;
// Constructors, getters, setters, equals, hashCode, etc
}
但不幸的是,仍然有那么多的RDBMS实际上不支持布尔类型,而且@Formula
注解是纯粹的静态的,不允许特定厂商的覆盖。我们是否要手动重写那个SQL查询,以确保有一个SQL-92的、与厂商无关的本地SQL片段,在所有方言中都能使用?
或者我们只是再次插入jOOQ的解析连接?让我们用后者试试:
Author author = em.find(Author.class, 1);
MySQL的日志包含
-- org.hibernate.SQL
select
jpaauthorw0_.id as id1_4_0_,
jpaauthorw0_.first_name as first_na2_4_0_,
jpaauthorw0_.last_name as last_nam3_4_0_,
jpaauthorw0_.year_of_birth between 1981 and 1996 as formula1_0_
from author jpaauthorw0_
where jpaauthorw0_.id=?
-- org.jooq.impl.ParsingConnection Translating from: [...]
-- org.jooq.impl.ParsingConnection Translation cache miss: [...]
-- org.jooq.impl.ParsingConnection Translating to:
select
jpaauthorw0_.id as id1_4_0_,
jpaauthorw0_.first_name as first_na2_4_0_,
jpaauthorw0_.last_name as last_nam3_4_0_,
jpaauthorw0_.year_of_birth between 1981 and 1996 as formula1_0_
from author as jpaauthorw0_
where jpaauthorw0_.id = ?
正如你所看到的,jOOQ重新添加了AS
关键字来别名MySQL,因为我们喜欢明确的别名,也因为那是MySQL的默认值。Settings.renderOptionalAsKeywordForTableAliases
而SQL Server的日志包含
-- org.hibernate.SQL
select
jpaauthorw0_.id as id1_4_0_,
jpaauthorw0_.first_name as first_na2_4_0_,
jpaauthorw0_.last_name as last_nam3_4_0_,
jpaauthorw0_.year_of_birth between 1981 and 1996 as formula1_0_
from author jpaauthorw0_
where jpaauthorw0_.id=?
-- org.jooq.impl.ParsingConnection Translating from: [...]
-- org.jooq.impl.ParsingConnection Translation cache miss: [...]
-- org.jooq.impl.ParsingConnection Translating to:
select
jpaauthorw0_.id id1_4_0_,
jpaauthorw0_.first_name first_na2_4_0_,
jpaauthorw0_.last_name last_nam3_4_0_,
case
when jpaauthorw0_.year_of_birth between 1981 and 1996
then 1
when not (jpaauthorw0_.year_of_birth between 1981 and 1996)
then 0
end formula1_0_
from author jpaauthorw0_
where jpaauthorw0_.id = ?
一个NULL
-safeBOOLEAN
类型的模拟(因为如果YEAR_OF_BIRTH
是NULL
(即UNKNOWN
),那么MILLENIAL
也必须是NULL
,即UNKNOWN
)。
Spring Data @Query注解
在JPA集成中出现本地SQL的另一种情况是Spring Data JPA的 [@Query](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query)
注解,特别是当与@Query(nativeQuery = true)
。就像Hibernate的@Formula
,这个注解在编译时是静态的,没有办法在运行时覆盖本地查询的值,也许只是在每个方言中对资源库进行分类型。
但为什么要经历这些麻烦呢。总是同样的事情。只要用jOOQ的解析连接或解析数据源来修补DataSource
,就可以了。
结论
即使你不使用jOOQ的DSL API,你也可以在你现有的基于JDBC、R2DBC、JPA、MyBatis等的应用程序中以多种方式从jOOQ中获利,方法是钩住jOOQ解析连接,并将你的供应商特定的输入方言翻译成任何数量的可配置输出方言。
如果jOOQ的解析器不能处理某个功能,你有可能使用ParseListener
SPI来解决这个限制,例如,当你想支持一个假设的LOGICAL_XOR
谓词时(MySQL原生支持):
Query query = configuration
.derive(ParseListener.onParseCondition(ctx -> {
if (ctx.parseFunctionNameIf("LOGICAL_XOR")) {
ctx.parse('(');
Condition c1 = ctx.parseCondition();
ctx.parse(',');
Condition c2 = ctx.parseCondition();
ctx.parse(')');
return CustomCondition.of(c -> {
switch (c.family()) {
case MARIADB:
case MYSQL:
c.visit(condition("{0} xor {1}", c1, c2));
break;
default:
c.visit(c1.andNot(c2).or(c2.andNot(c1)));
break;
});
}
// Let the parser take over if we don't know the token
return null;
})
.dsl()
.parser()
.parseQuery(
"select * from t where logical_xor(t.a = 1, t.b = 2)"
);
System.out.println(DSL.using(SQLDialect.MYSQL).render(query));
System.out.println(DSL.using(SQLDialect.ORACLE).render(query));
上面的程序会打印出来:
-- MYSQL:
select *
from t
where (t.a = 1 xor t.b = 2);
-- ORACLE:
select *
from t
where (t.a = 1 and not (t.b = 2)) or (t.b = 2 and not (t.a = 1));
因此,无论是否使用jOOQ的DSL,都可以通过使用jOOQ将你的应用程序的供应商特定的SQL从一个RDBMS迁移到另一个RDBMS,或者在一个应用程序中支持多个RDBMS产品而获利。
题外话:查询转换
这不是这篇博文的主题,但一旦你让jOOQ解析了你的每条SQL语句,你也可以用jOOQ来转换这些SQL,并篡改表达式树,例如通过实现客户端的行级安全。这种可能性是无穷无尽的。