11

In PostgreSQL, string_agg(column, separator) allows to aggregate some Strings. I try to use it with JPA but it is not a JPA standard function.

Note : This is not the equivalent of CriteriaBuilder#concat().

So, I tried to tell JPA that this function exists, like this :

public class StringAgg extends ParameterizedFunctionExpression<String> implements Serializable {

  public static final String NAME = "string_agg";

  @Override
  public boolean isAggregation() {
    return true;
  }

  @Override
  protected boolean isStandardJpaFunction() {
    return false;
  }

  public StringAgg(CriteriaBuilderImpl criteriaBuilder, Expression<String> expression, String separator) {
    super(criteriaBuilder, String.class, NAME, expression, new LiteralExpression(criteriaBuilder, separator));
  }
}

Then :

Expression<String> exprStr = ...
CriteriaBuilder cb = ...
cb.construct(MyClass.class, 
             myClass.get(MyClass_.name),
             myClass.get(MyClass_.surname),
             new StringAgg(cb, exprStr, "/"));

Problem, I get a NullPointerException !

java.lang.NullPointerException: null
at org.hibernate.internal.util.ReflectHelper.getConstructor(ReflectHelper.java:355) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.ast.tree.ConstructorNode.resolveConstructor(ConstructorNode.java:179) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.ast.tree.ConstructorNode.prepare(ConstructorNode.java:152) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.ast.HqlSqlWalker.processConstructor(HqlSqlWalker.java:1028) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectExpr(HqlSqlBaseWalker.java:2279) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectExprList(HqlSqlBaseWalker.java:2145) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectClause(HqlSqlBaseWalker.java:1451) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:571) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:299) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:247) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:261) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:189) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:141) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:119) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:87) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:190) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.internal.AbstractSessionImpl.getHQLQueryPlan(AbstractSessionImpl.java:288) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]
at org.hibernate.internal.AbstractSessionImpl.createQuery(AbstractSessionImpl.java:223) ~[hibernate-core-4.3.0.Beta3.jar:4.3.0.Beta3]

The debugger shows that the last Selection of cb.construct() (new StringAgg(cb, exprStr, "/")) is ignored. As a consequence, the searched constructor is MyClass(String,String) instead of MyClass(String, String, String).

Is there something wrong in the implementation of StringAgg? Did someone already tried to use string_agg in JPA?

Solution (thanks to vzamanillo)

Extend the dialect :

public class PGDialect extends PostgreSQLDialect{

  public PGDialect() {
    super();
    registerFunction("string_agg", new SQLFunctionTemplate( StandardBasicTypes.STRING, "string_agg(?1, ?2)"));
  }
}

Use it in persistence.xml

<properties>
  <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
  <property name="hibernate.dialect" value="path.to.PGDialect"/>

Then use CriteriaBuilder#function() :

Expression<String> exprStr = ...
CriteriaBuilder cb = ...
cb.construct(MyClass.class, 
             myClass.get(MyClass_.name),
             myClass.get(MyClass_.surname),
             cb.function( "string_agg", myColPath, cb.literal("delimiter" )));

To ease it, I created a helper method :

public static Expression<String> strAgg(CriteriaBuilder cb, Expression<String> expression, String delimiter) {
  return cb.function( "string_agg", String.class, expression, cb.literal(delimiter));
}

So the code becomes :

Expression<String> exprStr = ...
CriteriaBuilder cb = ...
cb.construct(MyClass.class, 
             myClass.get(MyClass_.name),
             myClass.get(MyClass_.surname),
             strAgg(cb, myColPath, "delimiter"));
2
  • FUNCTION is a JPA (2.1) standard function. Perhaps use that Dec 7, 2013 at 8:07
  • 1
    Why don't you use the normal constructor of a class, the equivalent of SELECT new com.me.Entity(path1, path2)... in JPQL? (PS: +1 as I learned something new)
    – V G
    Dec 9, 2013 at 20:35

3 Answers 3

13
+150

Maybe this helps you,

you can invoke database functions in a JPA Criteria Query.

The CriteriaBuilder Interface has a "function" method.

<T> Expression<T> function(String name,
                         Class<T> type,
                         Expression<?>... args)

Create an expression for the execution of a database function.

Parameters:
    name - function name
    type - expected result type
    args - function arguments
Returns:
    expression

Then you can try creating a CriteriaBuilder helper class to get a plain criteria Expression that you can use as usual in our criteria query

public abstract class CriteriaBuilderHelper {

    private static final String PG_STRING_AGG  = "string_agg";

    /**
    * @param cb the CriteriaBuilder to use
    * @param toJoin the string to join
    * @param delimiter the string to use
    * @return Expression<String>
    */
    public static Expression functionStringAgg(CriteriaBuilder cb, String toJoin, String delimiter) {
        return cb.function(PG_STRING_AGG, 
            String.class,
            cb.literal(toJoin),
            cb.literal(delimiter))
        );
    }
}

or you can use a custom dialect to register a new function

public class PGDialect extends PostgreSQLDialect{

    public PGDialect() {
        super();
        registerFunction("string_agg", new SQLFunctionTemplate( StandardBasicTypes.STRING, "string_agg(?1, ?2)"));
    }
}

and use it in your CriteriaBuilder as a normal function

Expression<String> functionStringAgg = cb.function( "string_agg", String.class, 
                                cb.parameter(String.class, "toJoin" ), 
                                cb.parameter(String.class, "delimiter"));

after all don't forget to set the parameter values to the your CriteriaQuery

setParameter( "toJoin", toJoin);
setParameter( "delimiter", delimiter);
4
  • You rock :) The first solution did not work : I ended up with the same problem. My code did almost the same. But registering the function via a new Dialect worked like a charm. Dec 11, 2013 at 14:06
  • @Arnaud Denoyelle can you please help on stackoverflow.com/questions/55218784/… issue
    – bharathi
    Mar 19, 2019 at 7:45
  • @vzamanillo can you please help on stackoverflow.com/questions/55218784/… issue
    – bharathi
    Mar 19, 2019 at 9:19
  • I got this error ` java.lang.IllegalStateException: No data type for node: org.hibernate.hql.internal.ast.tree.MethodNode \-[METHOD_CALL] MethodNode: 'function (string_agg)' +-[METHOD_NAME] IdentNode: 'string_agg' {originalText=string_agg} \-[EXPR_LIST] SqlNode: 'exprList' +-[NAMED_PARAM] ParameterNode: '?' {name=empId, expectedType=null} \-[NAMED_PARAM] ParameterNode: '?' {name=delimiter, expectedType=null} ` Oct 6, 2020 at 12:00
2

I did it using the same structure, but in an easy way:

Modified the persistence file:

    <property name="jpaProperties">
        <props>
                <prop key="hibernate.dialect">es.gmrcanarias.saga.utiles.PGDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
...
        </props>
    </property>

Then registerede the function:

import org.hibernate.dialect.PostgreSQL82Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.type.StandardBasicTypes;

public class PGDialect extends PostgreSQL82Dialect {

   public PGDialect() {
       super();
      registerFunction("string_agg", new  
       SQLFunctionTemplate(StandardBasicTypes.STRING, "string_agg(?1, ?2)"));
   }
}

Then added the query:

    Join<Razon, Incidenc> subquery;
    ....
    Expression<String> functionStringAgg = criteriaBuilder.function("string_agg", 
        String.class,
        subquery.get(CODIGO), 
        criteriaBuilder.literal(", "));
...
    subqueryList.select(functionStringAgg);    
1

One detail for PGDialect: You can add the 3rd argument for using ORDER BY

public class PGDialect extends PostgreSQL9Dialect {


     public PGDialect() {
         super();
         this.registerFunction("string_agg", new SQLFunctionTemplate( StandardBasicTypes.STRING, "string_agg(?1, ?2)") );
         this.registerFunction("string_agg", new SQLFunctionTemplate( StandardBasicTypes.STRING, "string_agg(?1, ?2 ORDER BY ?3 )") );
     }
 }

HQL Query usage

 "SELECT string_agg(f.name, '; ', f.name) FROM Foo as f "

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.