If your legacy JPA application is using occasional native queries or Hibernate @Formula
or Spring Data @Query
annotation with vendor specific native SQL embedded in it, you can use jOOQ’s parsing connection and parsing data source to translate between dialects, without having to go all in on your jOOQ adoption – though I think it’s inevitable once you see what jOOQ can do for you.
Now, let’s design a table like this:
CREATE TABLE author (
id INT NOT NULL,
first_name TEXT,
last_name TEXT NOT NULL,
CONSTRAINT pk_author PRIMARY KEY (id)
);
Now, you may want to write a native query on this table using JPA’s EntityManager.createNativeQuery()
, mapping that to entities. You could be using jOOQ’s DSL API for this, but let’s say you’re not ready for a migration to jOOQ yet, or you want to use actual SQL as provided by your DBA, not jOOQ’s DSL.
So, in MariaDB, you might be writing something like this:
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();
Where your entity is defined like this:
@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
}
The above runs just fine and produces all the authors in MariaDB, which implemented support for Oracle’s NVL()
function. But what about Oracle itself? The query fails on Oracle with:
ORA-00933: SQL command not properly ended
This is because in Oracle, you can’t use the AS
keyword to alias tables, only to alias columns. Sure, you can remove that, but what about NVL()
? You want this to work on MySQL and SQL Server, as well, but they complain:
MySQL
SQL Error [1305] [42000]: FUNCTION test.nvl does not exist
SQL Server
SQL Error [195] [S0010]: ‘nvl’ is not a recognized built-in function name.
Now, you have these options:
- Use jOOQ to generate the SQL string for you, using the DSL
- Use JPQL instead of a native query (but then heavily rewrite it, because JPQL is much less powerful than SQL)
- Try your luck writing actual vendor agnostic SQL, manually
- Or…
jOOQ’s parsing connection
You could use jOOQ’s parsing connection, which acts as a proxy to your actual connection, intercepting every SQL statement on the JDBC level in order to translate it to the target dialect.
This is as simple as wrapping your existing JDBC Connection
or DataSource
as follows:
DataSource originalDataSource = ...
DataSource parsingDataSource = DSL
.using(originalDataSource, dialect)
.parsingDataSource();
That’s it! I mean, you could pass some additional configuration Settings
after the dialect
, but that’s about as easy as it gets. The new DataSource
can now run your SQL query on all the aforementioned dialects, e.g. you might see this in your DEBUG
logs:
On 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
On 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, ifnull(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 was tricked by jOOQ! The NVL
function was translated to MySQL’s IFNULL
or SQL Server COALESCE
, and the AS
keyword was removed from the SQL Server query. These are just simple examples, your actual SQL may be much more complex. Play around with the feature set online, here.
Also, the Settings.cacheParsingConnectionLRUCacheSize
flag, which defaults to 8192, makes sure the same query doesn’t get re-translated all the time so you won’t spend too much time in jOOQ’s parser.
@Formula too, not just native queries
A quick win in Hibernate when you want to project additional values, akin to SQL’s own computed columns, which are available in many SQL dialects, is the @Formula
annotation, which can be added to any entity, like so. Assuming this additional column:
ALTER TABLE author ADD year_of_birth INT;
We might have the following amended entity:
@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
}
But unfortunately, there are still so many RDBMS that don’t actually support boolean types, and the @Formula
annotation is purely static, and doesn’t allow for vendor-specific overrides. Are we going to rewrite that SQL query manually to make sure a SQL-92, vendor agnostic native SQL fragment is present that works on all dialects?
Or will we just plug in jOOQ’s parsing connection again? Let’s try the latter with:
Author author = em.find(Author.class, 1);
The MySQL log contains:
-- 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 = ?
As you can see, jOOQ re-added the AS
keyword to alias the on MySQL for good measure, because we like to be explicit about aliasing, and because that’s the default for Settings.renderOptionalAsKeywordForTableAliases
Whereas the SQL Server log contains:
-- 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 = ?
A NULL
-safe BOOLEAN
type emulation (because if YEAR_OF_BIRTH
is NULL
(i.e. UNKNOWN
), then MILLENIAL
must be NULL
, too (i.e. UNKNOWN
))
Spring Data @Query annotation
Another case where native SQL can appear in JPA integrations is Spring Data JPA with its @Query
annotation, especially when used with @Query(nativeQuery = true)
. Just like Hibernate’s @Formula
, this annotation is compile-time static, with no way to override the value of the native query at runtime, short of maybe subtyping repositories per dialect.
But why go through all that hassle. It’s always the same thing. Just patch the DataSource
with jOOQ’s parsing connection or parsing data source, and you’re set.
Conclusion
Even if you’re not using jOOQ’s DSL API, you can profit from jOOQ in many ways in your existing JDBC, R2DBC, JPA, MyBatis, etc. based applications by hooking in the jOOQ parsing connection and translating your vendor specific input dialect to any number of configurable output dialects.
If jOOQ’s parser can’t handle a feature, chances are, you can work around this limitation using the ParseListener
SPI, e.g. when you want to support a hypothetical LOGICAL_XOR
predicate (which is supported natively in 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));
The above program will print:
-- 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));
So, profit from using jOOQ to migrate your application’s vendor specific SQL off one RDBMS onto another, or to support multiple RDBMS products in a single application, with or without using jOOQ’s DSL!
Side note: Query transformation
This isn’t the topic of this blog post, but once you have jOOQ parse your every SQL statement, you can also use jOOQ to transform this SQL and tamper with the expression tree, for example by implementing client side row level security. The possibilities are endless!
from Java, SQL and jOOQ. https://ift.tt/3DmAmNZ
via IFTTT
No comments:
Post a Comment