Skip to content

Commit e5afffc

Browse files
Felixyrodiere
authored andcommitted
HHH-16648 Add documentation for implementing custom functions
1 parent a5dc413 commit e5afffc

File tree

6 files changed

+470
-9
lines changed

6 files changed

+470
-9
lines changed

documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2506,16 +2506,9 @@ There are several ways to call native or user-defined SQL functions.
25062506
- A native or user-defined function may be called using JPQL's `function` syntax, for example, ``function('sinh', phi)``.
25072507
(This is the easiest way, but not the best way.)
25082508
- A user-written `FunctionContributor` may register user-defined functions.
2509-
- A custom `Dialect` may register additional native functions by overriding `initializeFunctionRegistry()`.
2509+
- A custom `Dialect` may register additional native functions by overriding `initializeFunctionRegistry()`. <<hql-user-defined-functions-implementation, Example of implementation.>>
25102510
2511-
[TIP]
2512-
====
2513-
Registering a function isn't hard, but is beyond the scope of this chapter.
2514-
2515-
(It's even possible to use the APIs Hibernate provides to make your own _portable_ functions!)
2516-
====
2517-
2518-
Fortunately, every built-in `Dialect` already registers many native functions for the database it supports.
2511+
Fortunately, every built-in `Dialect` already registers many native functions for the database it supports. So there is no need to define this functions explicitly.
25192512
25202513
[TIP]
25212514
====
@@ -4160,3 +4153,130 @@ Hibernate does emulate the `search` and `cycle` clauses though if necessary, so
41604153

41614154
Note that most modern database versions support recursive CTEs already.
41624155
====
4156+
4157+
[[hql-user-defined-functions-implementation]]
4158+
=== User-defined functions implementation
4159+
4160+
Hibernate Dialects can register additional functions known to be available for that particular database product.
4161+
These functions are also available in HQL (and JPQL, though only when using Hibernate as the JPA provider, obviously).
4162+
However, they would only be available when using that database Dialect.
4163+
Applications that aim for database portability should avoid using functions in this category.
4164+
4165+
Application developers can also supply their own set of functions.
4166+
This would usually represent either user-defined SQL functions or aliases for snippets of SQL.
4167+
4168+
Such function can be declared by using the `register()` method of `org.hibernate.query.sqm.function.SqmFunctionRegistry`.
4169+
4170+
For example, we have the following SQL function:
4171+
4172+
[[hql-user-defined-function-example]]
4173+
.Custom aggregate function
4174+
====
4175+
[source, SQL, indent=0]
4176+
----
4177+
include::{extrasdir}/hql-user-defined-function-example.sql[]
4178+
----
4179+
====
4180+
4181+
Also, we have the `Employee` entity.
4182+
4183+
[[hql-user-defined-function-domain-model]]
4184+
.Domain model
4185+
====
4186+
[source, JAVA, indent=0]
4187+
----
4188+
include::{example-dir-hql}/customFunctions/Employee.java[tags=hql-examples-domain-model-example]
4189+
----
4190+
====
4191+
4192+
Let’s persist the following entities in our database:
4193+
4194+
[[hql-user-defined-function-inital-data]]
4195+
.Initial data
4196+
====
4197+
[source, JAVA, indent=0]
4198+
----
4199+
include::{example-dir-hql}/customFunctions/CustomDialectFunctionTest.java[tags=hql-user-defined-dialect-function-inital-data]
4200+
----
4201+
====
4202+
4203+
The first step for implementing a custom function is to create a custom dialect `ExtendedPGDialect`, which inherits from `PostgreSQLDialect`.
4204+
4205+
[[hql-user-defined-dialect-function-cutom-dialect]]
4206+
.Custom dialect
4207+
====
4208+
[source, JAVA, indent=0]
4209+
----
4210+
include::{example-dir-hql}/customFunctions/ExtendedPGDialect.java[tags=hql-user-defined-dialect-function-custom-dialect]
4211+
----
4212+
====
4213+
4214+
Secondly, we will set the `ExtendedPGDialect` to Hibernate config.
4215+
4216+
[[hql-user-defined-dialect-function-cutom-dialect-property]]
4217+
.Custom dialect property
4218+
====
4219+
[source, xml, indent=0]
4220+
----
4221+
<session-factory>
4222+
<!-- Other properties -->
4223+
<property name="hibernate.dialect">path.to.the.ExtendedPGDialect</property>
4224+
</session-factory>
4225+
----
4226+
====
4227+
4228+
For implementing custom function we should inherit the new class `CountItemsGreaterValSqmFunction` from `AbstractSqmSelfRenderingFunctionDescriptor` class.
4229+
4230+
[NOTE]
4231+
====
4232+
Constructor of `org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor` contains the following fields:
4233+
4234+
* `String name` - name of the function _in the database_
4235+
* `FunctionKind` - type of the function: `NORMAL`, `AGGREGATE`, `ORDERED_SET_AGGREGATE` or `WINDOW`
4236+
* `ArgumentsValidator` - validator of the arguments provided to an JPQL/HQL function
4237+
* `FunctionReturnTypeResolver` - resolver of the function return type
4238+
* `FunctionArgumentTypeResolver` - resolver of the function argument types
4239+
====
4240+
4241+
[[hql-user-defined-dialect-function-sqm-renderer]]
4242+
.Custom function renderer
4243+
====
4244+
[source, JAVA, indent=0]
4245+
----
4246+
include::{example-dir-hql}/customFunctions/CountItemsGreaterValSqmFunction.java[tags=hql-user-defined-dialect-function-sqm-renderer]
4247+
----
4248+
====
4249+
4250+
Next step we should define the renderer:
4251+
4252+
[[hql-user-defined-dialect-function-sqm-renderer-definition]]
4253+
.Custom function renderer definition
4254+
====
4255+
[source, JAVA, indent=0]
4256+
----
4257+
include::{example-dir-hql}/customFunctions/CountItemsGreaterValSqmFunction.java[tags=hql-user-defined-dialect-function-sqm-renderer-definition]
4258+
----
4259+
====
4260+
4261+
Then we'll extend the `initializeFunctionRegistry()` method of the `ExtendedPGDialect` with new the logic:
4262+
adding `CountItemsGreaterValSqmFunction` to the default function registry of `FunctionContributions`.
4263+
4264+
[[hql-user-defined-dialect-function-registry-extending]]
4265+
.Custom dialect
4266+
====
4267+
[source, JAVA, indent=0]
4268+
----
4269+
include::{example-dir-hql}/customFunctions/ExtendedPGDialect.java[tags=hql-user-defined-dialect-function-registry-extending]
4270+
----
4271+
====
4272+
4273+
Once the `countItemsGreaterVal` function has been registered, we are able to use it in our JPQL/HQL queries.
4274+
4275+
[[hql-user-defined-dialect-function-test]]
4276+
.Test of the custom function
4277+
====
4278+
[source, JAVA, indent=0]
4279+
----
4280+
include::{example-dir-hql}/customFunctions/CustomDialectFunctionTest.java[tags=hql-user-defined-dialect-function-test]
4281+
----
4282+
====
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
CREATE OR REPLACE FUNCTION greater_than(count BIGINT, value NUMERIC, gr_val NUMERIC)
2+
RETURNS BIGINT AS
3+
$$
4+
BEGIN
5+
RETURN CASE WHEN value > gr_val THEN (count + 1)::BIGINT ELSE count::BIGINT END;
6+
END;
7+
$$ LANGUAGE "plpgsql";
8+
9+
CREATE OR REPLACE FUNCTION agg_final(c bigint) RETURNS BIGINT AS
10+
$$
11+
BEGIN
12+
return c;
13+
END;
14+
$$ LANGUAGE "plpgsql";
15+
16+
CREATE OR REPLACE AGGREGATE count_items_greater_val(NUMERIC, NUMERIC) (
17+
SFUNC = greater_than,
18+
STYPE = BIGINT,
19+
FINALFUNC = agg_final,
20+
INITCOND = 0);
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package org.hibernate.orm.test.hql.customFunctions;
2+
3+
import org.hibernate.dialect.Dialect;
4+
import org.hibernate.dialect.function.CastFunction;
5+
import org.hibernate.metamodel.mapping.JdbcMapping;
6+
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
7+
import org.hibernate.query.sqm.function.FunctionKind;
8+
import org.hibernate.query.sqm.produce.function.*;
9+
import org.hibernate.sql.ast.Clause;
10+
import org.hibernate.sql.ast.SqlAstTranslator;
11+
import org.hibernate.sql.ast.spi.SqlAppender;
12+
import org.hibernate.sql.ast.tree.SqlAstNode;
13+
import org.hibernate.sql.ast.tree.expression.CastTarget;
14+
import org.hibernate.sql.ast.tree.expression.Expression;
15+
import org.hibernate.sql.ast.tree.predicate.Predicate;
16+
import org.hibernate.type.BasicType;
17+
import org.hibernate.type.StandardBasicTypes;
18+
import org.hibernate.type.spi.TypeConfiguration;
19+
20+
import java.math.BigDecimal;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
24+
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.NUMERIC;
25+
26+
//tag::hql-user-defined-dialect-function-sqm-renderer[]
27+
public class CountItemsGreaterValSqmFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
28+
private final CastFunction castFunction;
29+
private final BasicType<BigDecimal> bigDecimalType;
30+
31+
public CountItemsGreaterValSqmFunction(String name, Dialect dialect, TypeConfiguration typeConfiguration) {
32+
super(
33+
name,
34+
FunctionKind.AGGREGATE,
35+
/* Function consumes 2 numeric typed args:
36+
- the aggregation argument
37+
- the bottom edge for the count predicate*/
38+
new ArgumentTypesValidator(StandardArgumentsValidators.exactly(2),
39+
FunctionParameterType.NUMERIC,
40+
FunctionParameterType.NUMERIC
41+
),
42+
// Function returns one value - the number of items
43+
StandardFunctionReturnTypeResolvers.invariant(
44+
typeConfiguration.getBasicTypeRegistry()
45+
.resolve(StandardBasicTypes.BIG_INTEGER)
46+
),
47+
StandardFunctionArgumentTypeResolvers.invariant(
48+
typeConfiguration, NUMERIC, NUMERIC
49+
)
50+
);
51+
// Extracting cast function for setting input arguments to correct the type
52+
castFunction = new CastFunction(
53+
dialect,
54+
dialect.getPreferredSqlTypeCodeForBoolean()
55+
);
56+
bigDecimalType = typeConfiguration.getBasicTypeRegistry()
57+
.resolve(StandardBasicTypes.BIG_DECIMAL);
58+
}
59+
60+
@Override
61+
public void render(
62+
SqlAppender sqlAppender,
63+
List<? extends SqlAstNode> sqlAstArguments,
64+
SqlAstTranslator<?> walker) {
65+
render(sqlAppender, sqlAstArguments, null, walker);
66+
}
67+
68+
//tag::hql-user-defined-dialect-function-sqm-renderer-definition[]
69+
@Override
70+
public void render(
71+
SqlAppender sqlAppender,
72+
List<? extends SqlAstNode> sqlAstArguments,
73+
Predicate filter,
74+
SqlAstTranslator<?> translator) {
75+
// Renderer definition
76+
//end::hql-user-defined-dialect-function-sqm-renderer[]
77+
78+
// Appending name of SQL function to result query
79+
sqlAppender.appendSql(getName());
80+
sqlAppender.appendSql('(');
81+
82+
// Extracting 2 arguments
83+
final Expression first_arg = (Expression) sqlAstArguments.get(0);
84+
final Expression second_arg = (Expression) sqlAstArguments.get(1);
85+
86+
// If JPQL contains "filter" expression, but database doesn't support it
87+
// then append: function_name(case when (filter_expr) then (argument) else null end)
88+
final boolean caseWrapper = filter != null && !translator.supportsFilterClause();
89+
if (caseWrapper) {
90+
translator.getCurrentClauseStack().push(Clause.WHERE);
91+
sqlAppender.appendSql("case when ");
92+
93+
filter.accept(translator);
94+
translator.getCurrentClauseStack().pop();
95+
96+
sqlAppender.appendSql(" then ");
97+
renderArgument(sqlAppender, translator, first_arg);
98+
sqlAppender.appendSql(" else null end)");
99+
} else {
100+
renderArgument(sqlAppender, translator, first_arg);
101+
sqlAppender.appendSql(", ");
102+
renderArgument(sqlAppender, translator, second_arg);
103+
sqlAppender.appendSql(')');
104+
if (filter != null) {
105+
translator.getCurrentClauseStack().push(Clause.WHERE);
106+
sqlAppender.appendSql(" filter (where ");
107+
108+
filter.accept(translator);
109+
sqlAppender.appendSql(')');
110+
translator.getCurrentClauseStack().pop();
111+
}
112+
}
113+
//tag::hql-user-defined-dialect-function-sqm-renderer[]
114+
}
115+
116+
//end::hql-user-defined-dialect-function-sqm-renderer[]
117+
private void renderArgument(
118+
SqlAppender sqlAppender,
119+
SqlAstTranslator<?> translator,
120+
Expression arg) {
121+
// Extracting the type of argument
122+
final JdbcMapping sourceMapping = arg.getExpressionType().getJdbcMappings().get(0);
123+
if (sourceMapping.getJdbcType().isNumber()) {
124+
castFunction.render(sqlAppender,
125+
Arrays.asList(arg, new CastTarget(bigDecimalType)),
126+
translator
127+
);
128+
} else {
129+
arg.accept(translator);
130+
}
131+
}
132+
//tag::hql-user-defined-dialect-function-sqm-renderer[]
133+
//end::hql-user-defined-dialect-function-sqm-renderer-definition[]
134+
}
135+
//end::hql-user-defined-dialect-function-sqm-renderer[]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.hibernate.orm.test.hql.customFunctions;
2+
3+
import jakarta.persistence.EntityManager;
4+
import org.hibernate.Session;
5+
import org.hibernate.cfg.AvailableSettings;
6+
import org.hibernate.cfg.Configuration;
7+
import org.hibernate.cfg.Environment;
8+
import org.hibernate.dialect.PostgreSQLDialect;
9+
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
10+
import org.hibernate.testing.orm.junit.RequiresDialect;
11+
import org.junit.Test;
12+
13+
14+
import java.sql.Statement;
15+
16+
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
17+
import static org.junit.Assert.assertEquals;
18+
19+
@RequiresDialect(PostgreSQLDialect.class)
20+
public class CustomDialectFunctionTest extends BaseCoreFunctionalTestCase {
21+
22+
@Override
23+
protected void configure(Configuration configuration) {
24+
super.configure(configuration);
25+
configuration.addAnnotatedClass(Employee.class);
26+
27+
configuration.setProperty(AvailableSettings.DIALECT, "org.hibernate.orm.test.hql.customFunctions.ExtendedPGDialect");
28+
}
29+
30+
@Override
31+
protected Class<?>[] getAnnotatedClasses() {
32+
return new Class<?>[]{
33+
Employee.class
34+
};
35+
}
36+
37+
@Test
38+
@RequiresDialect(PostgreSQLDialect.class)
39+
public void test_custom_sqm_functions() {
40+
doInJPA(this::sessionFactory, session -> {
41+
try (EntityManager entityManager = session.getEntityManagerFactory().createEntityManager()) {
42+
var tx = entityManager.getTransaction();
43+
tx.begin();
44+
45+
entityManager.unwrap(Session.class).doWork(connection -> {
46+
try (Statement statement = connection.createStatement()) {
47+
statement.executeUpdate("""
48+
create or replace function greater_than(c bigint, val numeric, gr_val numeric) returns bigint as $$ begin return case when val > gr_val then (c + 1)::bigint else c::bigint end; end; $$ language "plpgsql";
49+
create or replace function agg_final(c bigint) returns bigint as $$ begin return c; end; $$ language "plpgsql";
50+
create or replace aggregate count_items_greater_val(numeric, numeric) (sfunc = greater_than, stype = bigint, finalfunc = agg_final, initcond = 0);
51+
"""
52+
);
53+
}
54+
});
55+
56+
//tag::hql-user-defined-dialect-function-inital-data[]
57+
entityManager.persist(new Employee(1L, 200L, "Jonn", "Robson"));
58+
entityManager.persist(new Employee(2L, 350L, "Bert", "Marshall"));
59+
entityManager.persist(new Employee(3L, 360L, "Joey", "Barton"));
60+
entityManager.persist(new Employee(4L, 400L, "Bert", "Marshall"));
61+
//end::hql-user-defined-dialect-function-inital-data[]
62+
63+
tx.commit();
64+
//tag::hql-user-defined-dialect-function-test[]
65+
var res = entityManager
66+
.createQuery("select count_items_greater_val(salary, 220) from Employee")
67+
.getSingleResult();
68+
assertEquals(3L, res);
69+
//end::hql-user-defined-dialect-function-test[]
70+
}
71+
});
72+
}
73+
74+
}

0 commit comments

Comments
 (0)