使用jOOQ用JPA的本地查询或@Formula来编写供应商无关的SQL。

如果你的传统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语句,以便将其翻译成目标方言。

这就像把你现有的JDBCConnectionDataSource 包装成如下那样简单。

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_BIRTHNULL (即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,并篡改表达式树,例如通过实现客户端的行级安全。这种可能性是无穷无尽的。

猜你喜欢

转载自juejin.im/post/7126363803964407845