Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DetachedCriteria cause QueryException when using property of alias in projection of detachedCriteria #1616

Open
scaiandre opened this issue Feb 25, 2022 · 5 comments

Comments

@scaiandre
Copy link

scaiandre commented Feb 25, 2022

Steps to Reproduce

grails create-app detachedCriteria
cd detachedCriteria
./grailsw create-domain-class Author
./grailsw create-domain-class Book

class Book {

    String bookName
    Author author

    static constraints = {
    }
}

class Author {

    String authorName
    static constraints = {
    }
}

Try to use the domain classes in a controller or service as follows while using a grails.gorm.DetachedCriteria for a sub-select in the query. The query below is of course inefficient, but demonstrates the issue.

        final authors = Author.createCriteria().listDistinct {
            HibernateCriteriaBuilder hibernateCriteriaBuilder = delegate as HibernateCriteriaBuilder
            hibernateCriteriaBuilder.projections {
                hibernateCriteriaBuilder.property "id"
            }
            hibernateCriteriaBuilder.inList "id", new grails.gorm.DetachedCriteria(Book, "alias_books").build({
                grails.gorm.DetachedCriteria hibernateCriteriaBuilder1 = delegate as grails.gorm.DetachedCriteria
                hibernateCriteriaBuilder1.createAlias("author", "alias_author")
                hibernateCriteriaBuilder1.projections {
                    property 'alias_author.id'
                }
            })
        }

Expected Behaviour

The above query should execute just fine.

Actual Behaviour

Stacktrace:

Caused by: org.hibernate.QueryException: could not resolve property: alias_author.id of: detachedcriteria.Book
	at org.hibernate.persister.entity.AbstractPropertyMapping.propertyException(AbstractPropertyMapping.java:77)
	at org.hibernate.persister.entity.AbstractPropertyMapping.toType(AbstractPropertyMapping.java:71)
	at org.hibernate.persister.entity.AbstractEntityPersister.toType(AbstractEntityPersister.java:2053)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getType(CriteriaQueryTranslator.java:605)
	at org.hibernate.criterion.PropertyProjection.getTypes(PropertyProjection.java:42)
	at org.hibernate.criterion.ProjectionList.getTypes(ProjectionList.java:87)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getProjectedTypes(CriteriaQueryTranslator.java:410)
	at org.hibernate.criterion.SubqueryExpression.createAndSetInnerQuery(SubqueryExpression.java:135)
	at org.hibernate.criterion.SubqueryExpression.toSqlString(SubqueryExpression.java:61)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getWhereCondition(CriteriaQueryTranslator.java:428)
	at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:95)
	at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:75)
	at org.hibernate.loader.criteria.CriteriaLoader.<init>(CriteriaLoader.java:80)
	at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1903)
	at org.hibernate.internal.CriteriaImpl.list(CriteriaImpl.java:370)
	

Environment Information

  • Operating System: Ubuntu 20.04 LTS
  • Grails Version (if using Grails): Grails 5.1.2
  • JDK Version: Java 11.0.13

Demo

https://github.com/scaiandre/grails-gorm-issue-detachedCriteria

./gradlew bootRun
curl http://localhost:8080/detachedCriteria

Related issue Unknown entity: null

When instead using the alias property in a filter clause, like "eq", the exception is different.

        final authors = Author.createCriteria().listDistinct {
            HibernateCriteriaBuilder hibernateCriteriaBuilder = delegate as HibernateCriteriaBuilder
            hibernateCriteriaBuilder.projections {
                hibernateCriteriaBuilder.property "id"
            }
            hibernateCriteriaBuilder.inList "id", new grails.gorm.DetachedCriteria(Book, "alias_books").build({
                grails.gorm.DetachedCriteria hibernateCriteriaBuilder1 = delegate as grails.gorm.DetachedCriteria
                hibernateCriteriaBuilder1.createAlias("author", "alias_author")
                hibernateCriteriaBuilder1.projections {
                    property 'author.id'
                }
                hibernateCriteriaBuilder1.eq "alias_author.id", 1L
            })
        }

Stacktrace

Caused by: org.hibernate.HibernateException: Unknown entity: null
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getPropertyMapping(CriteriaQueryTranslator.java:646)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getType(CriteriaQueryTranslator.java:604)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getTypeUsingProjection(CriteriaQueryTranslator.java:585)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getTypedValue(CriteriaQueryTranslator.java:640)
	at org.hibernate.criterion.SimpleExpression.getTypedValues(SimpleExpression.java:100)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getQueryParameters(CriteriaQueryTranslator.java:357)
	at org.hibernate.criterion.SubqueryExpression.createAndSetInnerQuery(SubqueryExpression.java:134)
	at org.hibernate.criterion.SubqueryExpression.toSqlString(SubqueryExpression.java:61)
	at org.hibernate.loader.criteria.CriteriaQueryTranslator.getWhereCondition(CriteriaQueryTranslator.java:428)
	at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:95)
	at org.hibernate.loader.criteria.CriteriaJoinWalker.<init>(CriteriaJoinWalker.java:75)
	at org.hibernate.loader.criteria.CriteriaLoader.<init>(CriteriaLoader.java:80)
	at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1903)
	at org.hibernate.internal.CriteriaImpl.list(CriteriaImpl.java:370)
		

Just using createAlias without referring to it, works

This does not raise an exception

        final authors = Author.createCriteria().listDistinct {
            HibernateCriteriaBuilder hibernateCriteriaBuilder = delegate as HibernateCriteriaBuilder
            hibernateCriteriaBuilder.projections {
                hibernateCriteriaBuilder.property "id"
            }
            hibernateCriteriaBuilder.inList "id", new grails.gorm.DetachedCriteria(Book, "alias_books").build({
                grails.gorm.DetachedCriteria hibernateCriteriaBuilder1 = delegate as grails.gorm.DetachedCriteria
                hibernateCriteriaBuilder1.createAlias("author", "alias_author")
                hibernateCriteriaBuilder1.projections {
                    property 'author.id'
                }
            })
        }
@scaiandre
Copy link
Author

scaiandre commented Feb 25, 2022

Since we need to have this kind of functionality I'm trying to work out a workaround.

I stitched together this ugly piece of code, which I do not want to put into production, actually, since it relies on internal state of DetachedCriteria and uses private methods. Furthermore I do not really understand the implications of changing the specific internal state.

I just want to leave this here for you to see what might go wrong here.

    static void propertyInHelper(grails.orm.HibernateCriteriaBuilder hibernateCriteriaBuilder, String propertyIn, grails.gorm.DetachedCriteria detachedCriteria) {
        if (detachedCriteria.associationCriteriaMap.size() > 0) {
            final assocationMap = [:]
            for (detachedAssociationCriteriaEntry in detachedCriteria.associationCriteriaMap.entrySet()) {
                assocationMap[detachedAssociationCriteriaEntry.value.associationPath] = detachedAssociationCriteriaEntry.value.alias
            }
            detachedCriteria.associationCriteriaMap.clear()
            detachedCriteria.criteria.removeAll({
                detachedCriteria.criteria.findAll {
                    it instanceof DetachedAssociationCriteria
                }
            })
            final hibernateDetachedCriteria = hibernateCriteriaBuilder.convertToHibernateCriteria(detachedCriteria)
            for (detachedAssociationCriteriaEntry in assocationMap) {
                hibernateDetachedCriteria.createAlias(detachedAssociationCriteriaEntry.key, detachedAssociationCriteriaEntry.value)
            }
            hibernateCriteriaBuilder.add Subqueries.propertyIn(propertyIn, hibernateDetachedCriteria)
        } else {
            hibernateCriteriaBuilder.inList(propertyIn, detachedCriteria)
        }
    }

Can be used like this to make the query work

        final authors = Author.createCriteria().listDistinct {
            HibernateCriteriaBuilder hibernateCriteriaBuilder = delegate as HibernateCriteriaBuilder
            hibernateCriteriaBuilder.projections {
                hibernateCriteriaBuilder.property "id"
            }
            propertyInHelper hibernateCriteriaBuilder, "id", new grails.gorm.DetachedCriteria(Book, "alias_books").build({
                grails.gorm.DetachedCriteria hibernateCriteriaBuilder1 = delegate as grails.gorm.DetachedCriteria
                hibernateCriteriaBuilder1.createAlias("author", "alias_author")
                hibernateCriteriaBuilder1.projections {
                    // No exception due to this
                    property 'alias_author.id'
                }
                // No exception due to this
                hibernateCriteriaBuilder1.eq "alias_author.id", 1L
            })
        }

This does of course not qualify as a workaround!

@scaiandre
Copy link
Author

Any news on this one? I'm asking since for us this is a blocker to migrate to Grails 5/GORM 7, since we have 100+ queries using GORM DetachedCriteria like this. The workaround above is not feasible from my perspective, since it relies on modifying internal state which is clearly not understood by us.

Thank you!

@PhilippeRossTraction
Copy link

We have the same problem. Did you find any other solution?

@practical-programmer
Copy link

also the same issue after upgrading to Grails 5

@eugene-kamenev
Copy link

I think I experience the same:

class Person {
   Address address
}
class Address {
   Street street
}
...
def criteria = Person.where {
   address('_a') {
       street('_c')
   }
   projections {
      property('_c.id')
   }
}
criteria.list() // works fine
Street.where {
   id in criteria
}.list() // fails with QueryException

Ad-hoc fix for existing setup:

@CompileStatic
class CustomHibernateCriterionAdapter {

    private static final Method handleAssociationWithAlias;
    private static final Method handleAssociation;
    private static final Method applySubCriteriaToJunction;
    private static final Field associationStack;

    static {
        handleAssociation = MethodUtils.getMatchingMethod(HibernateQuery, 'handleAssociationQuery', Association, List)
        handleAssociationWithAlias = MethodUtils.getMatchingMethod(HibernateQuery, 'handleAssociationQuery', Association, List, String)
        associationStack = FieldUtils.getAllFields(AbstractHibernateQuery).find { it.name == 'associationStack' }
        applySubCriteriaToJunction = MethodUtils.getMatchingMethod(HibernateCriterionAdapter, 'applySubCriteriaToJunction', PersistentEntity, AbstractHibernateQuery, List, Junction, String)
        handleAssociationWithAlias.setAccessible(true)
        handleAssociation.setAccessible(true)
        associationStack.setAccessible(true)
        applySubCriteriaToJunction.setAccessible(true)
    }

    /**
     * Fixes https://github.com/grails/grails-data-mapping/issues/1267
     * Fixes https://github.com/grails/grails-data-mapping/issues/1616
     */
    static void addCustomAssociationQueryCriterionAdapters() {
        def map = (Map) FieldUtils.readStaticField(AbstractHibernateCriterionAdapter, 'criterionAdaptors', true)
        map.put(DetachedAssociationCriteria.class, new AbstractHibernateCriterionAdapter.CriterionAdaptor() {
            public Criterion toHibernateCriterion(AbstractHibernateQuery hibernateQuery, Query.Criterion criterion, String alias) {
                DetachedAssociationCriteria<?> existing = (DetachedAssociationCriteria<?>) criterion;
                Method queryMethod;
                Object[] args;
                if (existing.alias != null) {
                    queryMethod = handleAssociationWithAlias
                    args = new Object[] { existing.getAssociation(), existing.getCriteria(), existing.alias }
                } else {
                    queryMethod = handleAssociation
                    args = new Object[] { existing.getAssociation(), existing.getCriteria() }
                }
                alias = (String) queryMethod.invoke(hibernateQuery, args)
                def association = existing.getAssociation();
                def associationStack = (List<Association>) associationStack.get(hibernateQuery)
                associationStack.add(association);
                try {
                    def conjunction = Restrictions.conjunction();
                    applySubCriteriaToJunction.invoke(HibernateQuery.HIBERNATE_CRITERION_ADAPTER,
                            association.getAssociatedEntity(), hibernateQuery, existing.getCriteria(), conjunction, alias)
                    return conjunction;
                } finally {
                    associationStack.removeLast();
                }
            }
        });
    }
 }

And in Boostrap:

class BootStrap {

   def init = { servletContext ->
       HibernateQuery.HIBERNATE_CRITERION_ADAPTER // call to initialize adapter itself
       CustomHibernateCriterionAdapter.addCustomAssociationQueryCriterionAdapters()
   }
   def destroy = {
   }
}

Tested the above with Grails 5, JDK 17, GORM (7.3.4, 8.1 (both))

@puneetbehl Can you please check the above, we have much more very complex DetachedCriteria in use and it works for us. Sorry do not have much time to start a proper PR with a fix and test coverage. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants