PostgreSQL function string_agg in JPA

Viewed 9743

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"));
3 Answers

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);    

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 "
Related