0

I'm using JPA Predicate to generate Specification for later Repository use.

code segment

public Specification<Request> multipleCriteria(List<QueryCriteria> filters) {
        log.info("filters[{}]", filters);

        return (request, query, cb) -> {

            Predicate condition = null;

            for (QueryCriteria c : filters) {
                String k = c.getKey();
                String v = c.getValue();

                Predicate tmpCondition = null;

                if (k.startsWith(JSON_PATH_SIGN)) {
                    // json prop

                    Expression<Boolean> jsonExists = cb.function(
                        "JSON_EXISTS",
                        Boolean.class,
                        request.get(JSON_REQUEST),
                        new HibernateInlineExpression(cb, ServiceHelper.toJsonPathExp(k, v)));

                    if (jsonExists instanceof Predicate) {
                        log.info("jsonExists instanceof Predicate: {}", jsonExists.getClass().getCanonicalName());
                    } else {
                        log.info("jsonExists: {}", jsonExists.getClass().getCanonicalName());
                    }

                    tmpCondition = cb.isTrue(jsonExists);
...

HibernateInlineExperssion is from http://www.istvandevai.com/2018/02/using-oracle-json-functions-via-jpa-and.html

and then I found JPA generated the wrong SQL:

Hibernate: 
    select
        request0_.ID as id1_5_,
        request0_.luby as luby2_5_,
        request0_.ludt as ludt3_5_,
        request0_.CREATED_DATE as created_date4_5_,
        request0_.REQUEST_JSON as request_json5_5_,
        request0_.REQUEST_NUMBER as request_number6_5_,
        request0_.REQUEST_TYPE as request_type7_5_,
        request0_.SCHEME_ID as scheme_id8_5_,
        request0_.SCHEME_VERSION_ID as scheme_version_id9_5_,
        request0_.STATUS_CODE as status_code10_5_ 
    from
        DSME.REQUEST request0_ 
    where
        JSON_EXISTS(request0_.REQUEST_JSON,'$.client_contact_email?(@ == "test@abc.com")')=1 
        and request0_.SCHEME_ID=11 
        and request0_.REQUEST_TYPE=? 
        and request0_.STATUS_CODE=? 
    order by
        request0_.ID asc fetch first ? rows only

So exception thrown

18:05:44.550 [http-nio-8055-exec-2] [] WARN  o.h.e.jdbc.spi.SqlExceptionHelper:logExceptions 137 - SQL Error: 933, SQLState: 42000
18:05:44.550 [http-nio-8055-exec-2] [] ERROR o.h.e.jdbc.spi.SqlExceptionHelper:logExceptions 142 - ORA-00933: SQL command not properly ended

As I used isTrue to check the expression, it appended =1 at the end of the WHERE clause: JSON_EXISTS(request0_.REQUEST_JSON,'$.client_contact_email?(@ == "test@abc.com")')=1

I traced Hibernate's code (hibernate-core-5.6.11.Final) and found in org.hibernate.query.criteria.internal.CriteriaBuilderImpl:isTrue:

    @Override
    public Predicate disjunction() {
        return new CompoundPredicate( this, Predicate.BooleanOperator.OR );
    }

    @Override
    public Predicate isTrue(Expression<Boolean> expression) {
        if ( CompoundPredicate.class.isInstance( expression ) ) {
            final CompoundPredicate predicate = (CompoundPredicate) expression;
            if ( predicate.getExpressions().size() == 0 ) {
                return new BooleanStaticAssertionPredicate(
                        this,
                        predicate.getOperator() == Predicate.BooleanOperator.AND
                );
            }
            return predicate;
        }
        else if ( Predicate.class.isInstance( expression ) ) {
            return (Predicate) expression;
        }
        return new BooleanAssertionPredicate( this, expression, Boolean.TRUE );
    }

As jsonExists is ParameterizedFunctionExpression instance, so finally isTrue returns BooleanAssertionPredicate instance.

And in BooleanAssertionPredicate.java, I found

    @Override
    public String render(boolean isNegated, RenderingContext renderingContext) {
        final String operator = isNegated ? " <> " : " = ";
        final String assertionLiteral = assertedValue ? "true" : "false";

        return ( (Renderable) expression ).render( renderingContext )
                + operator
                + assertionLiteral;
    }

That is to say, if I:

  1. use cb.function() to generate Expression (which return ParameterizedFunctionExpression instance)
  2. use cb.isTrue to generate Predicate

then the generated SQL would exist where JSON_EXISTS(...)=1 error.

I have already checked profile, yes, I'm using oracle dialect:

# application.properties
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
...
hibernate.dialect=org.hibernate.dialect.Oracle12cDialect

Actually all I want is to use CriteriaBuilder/Criteria Queries to call Oracle's JSON function, just like this article mentioned: http://www.istvandevai.com/2018/02/using-oracle-json-functions-via-jpa-and.html

BTW, for JSON_VALUE, this solution works perfectly. However, when it comes to JSON_EXISTS, which returns Boolean.class, it failed...

It would be greately appreciated if anyone could give me some hints...

Many thx~

karl li
  • 1,316
  • 15
  • 19
  • Have you tried using "tmpCondition = cb.and(jsonExists, cb.conjunction());" instead of isTrue? – Chris Mar 09 '23 at 16:17
  • Thanks for Chris's response. However still no luck... 09:15:12.183 [http-nio-8055-exec-1] [] ERROR o.h.hql.internal.ast.ErrorTracker:reportError 62 - :1:118: unexpected AST node: function (JSON_EXISTS) And read from api doc: Create a conjunction (with zero conjuncts). A conjunction with zero conjuncts is true. if only use cojunction, say, tmpCondition = cb.conjunction(), or cb.and(cb.conjunction()), the SQL is `1=1` – karl li Mar 09 '23 at 22:35
  • Seems similar to https://stackoverflow.com/questions/26283195/jpa-predicate-and-expression-both-in-querycriteria-where-clause . I'd file a bug with Hibernate so they don't require such workarounds all the time if this solution works here too. – Chris Mar 10 '23 at 15:53

0 Answers0