diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 5824fa11bd8c..462889390898 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -2506,16 +2506,9 @@ There are several ways to call native or user-defined SQL functions. - A native or user-defined function may be called using JPQL's `function` syntax, for example, ``function('sinh', phi)``. (This is the easiest way, but not the best way.) - A user-written `FunctionContributor` may register user-defined functions. -- A custom `Dialect` may register additional native functions by overriding `initializeFunctionRegistry()`. +- A custom `Dialect` may register additional native functions by overriding `initializeFunctionRegistry()`. <> -[TIP] -==== -Registering a function isn't hard, but is beyond the scope of this chapter. - -(It's even possible to use the APIs Hibernate provides to make your own _portable_ functions!) -==== - -Fortunately, every built-in `Dialect` already registers many native functions for the database it supports. +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. [TIP] ==== @@ -4160,3 +4153,130 @@ Hibernate does emulate the `search` and `cycle` clauses though if necessary, so Note that most modern database versions support recursive CTEs already. ==== + +[[hql-user-defined-functions-implementation]] +=== User-defined functions implementation + +Hibernate Dialects can register additional functions known to be available for that particular database product. +These functions are also available in HQL (and JPQL, though only when using Hibernate as the JPA provider, obviously). +However, they would only be available when using that database Dialect. +Applications that aim for database portability should avoid using functions in this category. + +Application developers can also supply their own set of functions. +This would usually represent either user-defined SQL functions or aliases for snippets of SQL. + +Such function can be declared by using the `register()` method of `org.hibernate.query.sqm.function.SqmFunctionRegistry`. + +For example, we have the following SQL function: + +[[hql-user-defined-function-example]] +.Custom aggregate function +==== +[source, SQL, indent=0] +---- +include::{extrasdir}/hql-user-defined-function-example.sql[] +---- +==== + +Also, we have the `Employee` entity. + +[[hql-user-defined-function-domain-model]] +.Domain model +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/Employee.java[tags=hql-examples-domain-model-example] +---- +==== + +Let’s persist the following entities in our database: + +[[hql-user-defined-function-inital-data]] +.Initial data +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/CustomDialectFunctionTest.java[tags=hql-user-defined-dialect-function-inital-data] +---- +==== + +The first step for implementing a custom function is to create a custom dialect `ExtendedPGDialect`, which inherits from `PostgreSQLDialect`. + +[[hql-user-defined-dialect-function-cutom-dialect]] +.Custom dialect +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/ExtendedPGDialect.java[tags=hql-user-defined-dialect-function-custom-dialect] +---- +==== + +Secondly, we will set the `ExtendedPGDialect` to Hibernate config. + +[[hql-user-defined-dialect-function-cutom-dialect-property]] +.Custom dialect property +==== +[source, xml, indent=0] +---- + + + path.to.the.ExtendedPGDialect + +---- +==== + +For implementing custom function we should inherit the new class `CountItemsGreaterValSqmFunction` from `AbstractSqmSelfRenderingFunctionDescriptor` class. + +[NOTE] +==== +Constructor of `org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor` contains the following fields: + +* `String name` - name of the function _in the database_ +* `FunctionKind` - type of the function: `NORMAL`, `AGGREGATE`, `ORDERED_SET_AGGREGATE` or `WINDOW` +* `ArgumentsValidator` - validator of the arguments provided to an JPQL/HQL function +* `FunctionReturnTypeResolver` - resolver of the function return type +* `FunctionArgumentTypeResolver` - resolver of the function argument types +==== + +[[hql-user-defined-dialect-function-sqm-renderer]] +.Custom function renderer +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/CountItemsGreaterValSqmFunction.java[tags=hql-user-defined-dialect-function-sqm-renderer] +---- +==== + +Next step we should define the renderer: + +[[hql-user-defined-dialect-function-sqm-renderer-definition]] +.Custom function renderer definition +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/CountItemsGreaterValSqmFunction.java[tags=hql-user-defined-dialect-function-sqm-renderer-definition] +---- +==== + +Then we'll extend the `initializeFunctionRegistry()` method of the `ExtendedPGDialect` with new the logic: +adding `CountItemsGreaterValSqmFunction` to the default function registry of `FunctionContributions`. + +[[hql-user-defined-dialect-function-registry-extending]] +.Custom dialect +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/ExtendedPGDialect.java[tags=hql-user-defined-dialect-function-registry-extending] +---- +==== + +Once the `countItemsGreaterVal` function has been registered, we are able to use it in our JPQL/HQL queries. + +[[hql-user-defined-dialect-function-test]] +.Test of the custom function +==== +[source, JAVA, indent=0] +---- +include::{example-dir-hql}/customFunctions/CustomDialectFunctionTest.java[tags=hql-user-defined-dialect-function-test] +---- +==== diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/hql-user-defined-function-example.sql b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/hql-user-defined-function-example.sql new file mode 100644 index 000000000000..02bfb4170e0a --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/hql-user-defined-function-example.sql @@ -0,0 +1,20 @@ +CREATE OR REPLACE FUNCTION greater_than(count BIGINT, value NUMERIC, gr_val NUMERIC) + RETURNS BIGINT AS +$$ +BEGIN + RETURN CASE WHEN value > gr_val THEN (count + 1)::BIGINT ELSE count::BIGINT END; +END; +$$ LANGUAGE "plpgsql"; + +CREATE OR REPLACE FUNCTION agg_final(c bigint) RETURNS BIGINT AS +$$ +BEGIN + return c; +END; +$$ LANGUAGE "plpgsql"; + +CREATE OR REPLACE AGGREGATE count_items_greater_val(NUMERIC, NUMERIC) ( + SFUNC = greater_than, + STYPE = BIGINT, + FINALFUNC = agg_final, + INITCOND = 0); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/CountItemsGreaterValSqmFunction.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/CountItemsGreaterValSqmFunction.java new file mode 100644 index 000000000000..47922016a880 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/CountItemsGreaterValSqmFunction.java @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql.customFunctions; + +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.function.CastFunction; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.*; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.BasicType; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.NUMERIC; + +//tag::hql-user-defined-dialect-function-sqm-renderer[] +public class CountItemsGreaterValSqmFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + private final CastFunction castFunction; + private final BasicType bigDecimalType; + + public CountItemsGreaterValSqmFunction(String name, Dialect dialect, TypeConfiguration typeConfiguration) { + super( + name, + FunctionKind.AGGREGATE, + /* Function consumes 2 numeric typed args: + - the aggregation argument + - the bottom edge for the count predicate*/ + new ArgumentTypesValidator(StandardArgumentsValidators.exactly(2), + FunctionParameterType.NUMERIC, + FunctionParameterType.NUMERIC + ), + // Function returns one value - the number of items + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry() + .resolve(StandardBasicTypes.BIG_INTEGER) + ), + StandardFunctionArgumentTypeResolvers.invariant( + typeConfiguration, NUMERIC, NUMERIC + ) + ); + // Extracting cast function for setting input arguments to correct the type + castFunction = new CastFunction( + dialect, + dialect.getPreferredSqlTypeCodeForBoolean() + ); + bigDecimalType = typeConfiguration.getBasicTypeRegistry() + .resolve(StandardBasicTypes.BIG_DECIMAL); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker) { + render(sqlAppender, sqlAstArguments, null, walker); + } + + //tag::hql-user-defined-dialect-function-sqm-renderer-definition[] + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator translator) { + // Renderer definition + //end::hql-user-defined-dialect-function-sqm-renderer[] + + // Appending name of SQL function to result query + sqlAppender.appendSql(getName()); + sqlAppender.appendSql('('); + + // Extracting 2 arguments + final Expression first_arg = (Expression) sqlAstArguments.get(0); + final Expression second_arg = (Expression) sqlAstArguments.get(1); + + // If JPQL contains "filter" expression, but database doesn't support it + // then append: function_name(case when (filter_expr) then (argument) else null end) + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + if (caseWrapper) { + translator.getCurrentClauseStack().push(Clause.WHERE); + sqlAppender.appendSql("case when "); + + filter.accept(translator); + translator.getCurrentClauseStack().pop(); + + sqlAppender.appendSql(" then "); + renderArgument(sqlAppender, translator, first_arg); + sqlAppender.appendSql(" else null end)"); + } else { + renderArgument(sqlAppender, translator, first_arg); + sqlAppender.appendSql(", "); + renderArgument(sqlAppender, translator, second_arg); + sqlAppender.appendSql(')'); + if (filter != null) { + translator.getCurrentClauseStack().push(Clause.WHERE); + sqlAppender.appendSql(" filter (where "); + + filter.accept(translator); + sqlAppender.appendSql(')'); + translator.getCurrentClauseStack().pop(); + } + } + //tag::hql-user-defined-dialect-function-sqm-renderer[] + } + + //end::hql-user-defined-dialect-function-sqm-renderer[] + private void renderArgument( + SqlAppender sqlAppender, + SqlAstTranslator translator, + Expression arg) { + // Extracting the type of argument + final JdbcMapping sourceMapping = arg.getExpressionType().getJdbcMappings().get(0); + if (sourceMapping.getJdbcType().isNumber()) { + castFunction.render(sqlAppender, + Arrays.asList(arg, new CastTarget(bigDecimalType)), + translator + ); + } else { + arg.accept(translator); + } + } + //tag::hql-user-defined-dialect-function-sqm-renderer[] + //end::hql-user-defined-dialect-function-sqm-renderer-definition[] +} +//end::hql-user-defined-dialect-function-sqm-renderer[] diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/CustomDialectFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/CustomDialectFunctionTest.java new file mode 100644 index 000000000000..4deb1e6d1b50 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/CustomDialectFunctionTest.java @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql.customFunctions; + +import jakarta.persistence.EntityManager; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.junit.Test; + + +import java.sql.Statement; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; + +@RequiresDialect(PostgreSQLDialect.class) +public class CustomDialectFunctionTest extends BaseCoreFunctionalTestCase { + + @Override + protected void configure(Configuration configuration) { + super.configure(configuration); + configuration.addAnnotatedClass(Employee.class); + + configuration.setProperty(AvailableSettings.DIALECT, "org.hibernate.orm.test.hql.customFunctions.ExtendedPGDialect"); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ + Employee.class + }; + } + + @Test + @RequiresDialect(PostgreSQLDialect.class) + public void test_custom_sqm_functions() { + doInJPA(this::sessionFactory, session -> { + try (EntityManager entityManager = session.getEntityManagerFactory().createEntityManager()) { + var tx = entityManager.getTransaction(); + tx.begin(); + + entityManager.unwrap(Session.class).doWork(connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "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'; " + + "create or replace function agg_final(c bigint) returns bigint as $$ begin return c; end; $$ language 'plpgsql'; " + + "create or replace aggregate count_items_greater_val(numeric, numeric) (sfunc = greater_than, stype = bigint, finalfunc = agg_final, initcond = 0);" + ); + } + }); + + //tag::hql-user-defined-dialect-function-inital-data[] + entityManager.persist(new Employee(1L, 200L, "Jonn", "Robson")); + entityManager.persist(new Employee(2L, 350L, "Bert", "Marshall")); + entityManager.persist(new Employee(3L, 360L, "Joey", "Barton")); + entityManager.persist(new Employee(4L, 400L, "Bert", "Marshall")); + //end::hql-user-defined-dialect-function-inital-data[] + + tx.commit(); + //tag::hql-user-defined-dialect-function-test[] + var res = entityManager + .createQuery("select count_items_greater_val(salary, 220) from Employee") + .getSingleResult(); + assertEquals(3L, res); + //end::hql-user-defined-dialect-function-test[] + } + }); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/Employee.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/Employee.java new file mode 100644 index 000000000000..cbccdac197ed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/Employee.java @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql.customFunctions; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + + +//tag::hql-examples-domain-model-example[] +@Entity +public class Employee { + @Id + private Long id; + private Long salary; + private String name; + private String surname; + + public Employee(Long id, Long salary, String name, String surname) { + this.id = id; + this.salary = salary; + this.name = name; + this.surname = surname; + } + + //Getters and setters are omitted for brevity + +//end::hql-examples-domain-model-example[] + + public Employee() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getSalary() { + return salary; + } + + public void setSalary(Long salary) { + this.salary = salary; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + //tag::hql-examples-domain-model-example[] +} +//end::hql-examples-domain-model-example[] diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/ExtendedPGDialect.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/ExtendedPGDialect.java new file mode 100644 index 000000000000..a8c4e1afd17e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/customFunctions/ExtendedPGDialect.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql.customFunctions; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.PostgreSQLDriverKind; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; + +//tag::hql-user-defined-dialect-function-custom-dialect[] +public class ExtendedPGDialect extends PostgreSQLDialect { + + // Default constructors + + //end::hql-user-defined-dialect-function-custom-dialect[] + public ExtendedPGDialect() { + super(); + } + + public ExtendedPGDialect(DialectResolutionInfo info) { + super(info); + } + + public ExtendedPGDialect(DatabaseVersion version) { + super(version); + } + + public ExtendedPGDialect(DatabaseVersion version, PostgreSQLDriverKind driverKind) { + super(version, driverKind); + } + + //tag::hql-user-defined-dialect-function-custom-dialect[] + //tag::hql-user-defined-dialect-function-registry-extending[] + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry(functionContributions); + //end::hql-user-defined-dialect-function-custom-dialect[] + // Custom aggregate function + functionContributions.getFunctionRegistry().register( + "countItemsGreaterVal", // Name that can be used in JPQL queries + new CountItemsGreaterValSqmFunction( + "count_items_greater_val", // Name of the function in the database + this, + functionContributions.getTypeConfiguration()) + ); + //tag::hql-user-defined-dialect-function-custom-dialect[] + } + //end::hql-user-defined-dialect-function-registry-extending[] +} +//end::hql-user-defined-dialect-function-custom-dialect[]