grails-data-mapping icon indicating copy to clipboard operation
grails-data-mapping copied to clipboard

GORM 7 Regression for Criteria projection

Open sdelamo opened this issue 4 years ago • 6 comments

Given these domain classes:

class Role {
    String authority
}
class User {
    String username
}
class UserRole implements Serializable {
    private static final long serialVersionUID = 1

    User user
    Role role

    static mapping = {
        id composite: ['user', 'role']
        version false
    }
}

and this query:

@ReadOnly
List<Long> findAllRoleIdsByUser(Long userId) {
    def c = UserRole.createCriteria()
    c.list {
        projections {
            role {
                property('id')
            }
        }
        user {
            eq('id', userId)
        }
    } as List<Long>
}

With Grails 3.3.11 and GORM 6.1.12.RELEASE it works.

For Grails 4.0.2 and GORM 7.0.2, it fails with:

Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [role] on this ManagedType [example.grails.UserRole]

It seems to be related with the composite key of UserRole Removing that key gets rids of the error.

Environment Information

  • Operating System: MacOS
  • Grails Version: 4.0.2
  • JDK Version: 1.8
  • Container Version (If Applicable): TODO

Example Application

https://github.com/grails-core-issues-forks/grails-core-issue-11505

Steps to reproduce with the example app

Grails 3

cd grailsthree;
./gradlew bootRun
curl localhost:8080/home
{"ids":[1,2]}

Grails 4

cd grailsfour;
./gradlew bootRun
curl localhost:8080/home
Unable to locate Attribute  with the the given name [role] on this ManagedType [example.grails.UserRole]. Stacktrace follows:
...

sdelamo avatar Mar 17 '20 13:03 sdelamo

hi, I'm facing the same issue. it seems a problem with Groovy version. I have upgraded a Grails 3 application to latest Groovy 2.5.12 and @Where clause with an association query stopped working. It seems to me that the breaking point is around 2.5.7 which is still working. Here are the decompiled closure classes:

working one:

    protected List $tt__findAllEnabled(TransactionStatus transactionStatus) {
        DetachedCriteria $query = new DetachedCriteria(InstagramSearch.class);

        final class __tt__findAllEnabled_closure8 extends Closure implements GeneratedClosure {
            public __tt__findAllEnabled_closure8(Object _outerInstance, Object _thisObject) {
                super(_outerInstance, _thisObject);
            }

            public Object doCall(Object it) {
                final class _closure9 extends Closure implements GeneratedClosure {
                    public _closure9(Object _outerInstance, Object _thisObject) {
                        super(_outerInstance, _thisObject);
                    }

                    public Object doCall(Object it) {
                        final class _closure10 extends Closure implements GeneratedClosure {
                            public _closure10(Object _outerInstance, Object _thisObject) {
                                super(_outerInstance, _thisObject);
                            }

                            public Object doCall(Object it) {
                                final class _closure12 extends Closure implements GeneratedClosure {
                                    public _closure12(Object _outerInstance, Object _thisObject) {
                                        super(_outerInstance, _thisObject);
                                    }

                                    public Object doCall(Object it) {
                                        return ((AbstractDetachedCriteria)((_closure12)this).getDelegate()).eq("enabled", true);
                                    }

                                    public Object call(Object args) {
                                        return this.doCall(args);
                                    }

                                    public Object call() {
                                        return this.doCall((Object)null);
                                    }

                                    @Generated
                                    public Object doCall() {
                                        return this.doCall((Object)null);
                                    }
                                }

                                ((AbstractDetachedCriteria)((_closure10)this).getDelegate()).and(new _closure12(this, this.getThisObject()));

                                final class _closure13 extends Closure implements GeneratedClosure {
                                    public _closure13(Object _outerInstance, Object _thisObject) {
                                        super(_outerInstance, _thisObject);
                                    }

                                    public Object doCall(Object it) {
                                        return ((AbstractDetachedCriteria)((Closure)((_closure13)this).getDelegate()).getDelegate()).eq("syncEnabled", true);
                                    }

                                    public Object call(Object args) {
                                        return this.doCall(args);
                                    }

                                    public Object call() {
                                        return this.doCall((Object)null);
                                    }

                                    @Generated
                                    public Object doCall() {
                                        return this.doCall((Object)null);
                                    }
                                }

                                return ScriptBytecodeAdapter.invokeMethodN(_closure10.class, ((_closure10)this).getDelegate(), (String)"account", new Object[]{new _closure13(this, this.getThisObject())});
                            }

                            public Object call(Object args) {
                                return this.doCall(args);
                            }

                            public Object call() {
                                return this.doCall((Object)null);
                            }

                            @Generated
                            public Object doCall() {
                                return this.doCall((Object)null);
                            }
                        }

                        ((AbstractDetachedCriteria)((_closure9)this).getDelegate()).and(new _closure10(this, this.getThisObject()));

                        final class _closure11 extends Closure implements GeneratedClosure {
                            public _closure11(Object _outerInstance, Object _thisObject) {
                                super(_outerInstance, _thisObject);
                            }

                            public Object doCall(Object it) {
                                return ((AbstractDetachedCriteria)((Closure)((_closure11)this).getDelegate()).getDelegate()).eq("tokenValid", true);
                            }

                            public Object call(Object args) {
                                return this.doCall(args);
                            }

                            public Object call() {
                                return this.doCall((Object)null);
                            }

                            @Generated
                            public Object doCall() {
                                return this.doCall((Object)null);
                            }
                        }

                        return ScriptBytecodeAdapter.invokeMethodN(_closure9.class, ((_closure9)this).getDelegate(), (String)"account", new Object[]{new _closure11(this, this.getThisObject())});
                    }

                    public Object call(Object args) {
                        return this.doCall(args);
                    }

                    public Object call() {
                        return this.doCall((Object)null);
                    }

                    @Generated
                    public Object doCall() {
                        return this.doCall((Object)null);
                    }
                }

                return ((DetachedCriteria)((__tt__findAllEnabled_closure8)this).getDelegate()).and(new _closure9(this, this.getThisObject()));
            }

            public Object call(Object args) {
                return this.doCall(args);
            }

            public Object call() {
                return this.doCall((Object)null);
            }

            @Generated
            public Object doCall() {
                return this.doCall((Object)null);
            }
        }

        DetachedCriteria var3 = $query.build(new __tt__findAllEnabled_closure8(this, this));
        return (List)var3.list();
    }

not working one

    protected List $tt__findAllEnabled(TransactionStatus transactionStatus) {
        DetachedCriteria $query = new DetachedCriteria(InstagramSearch.class);

        final class __tt__findAllEnabled_closure8 extends Closure implements GeneratedClosure {
            public __tt__findAllEnabled_closure8(Object _outerInstance, Object _thisObject) {
                super(_outerInstance, _thisObject);
            }

            public Object doCall(Object it) {
                final class _closure9 extends Closure implements GeneratedClosure {
                    public _closure9(Object _outerInstance, Object _thisObject) {
                        super(_outerInstance, _thisObject);
                    }

                    public Object doCall(Object it) {
                        final class _closure10 extends Closure implements GeneratedClosure {
                            public _closure10(Object _outerInstance, Object _thisObject) {
                                super(_outerInstance, _thisObject);
                            }

                            public Object doCall(Object it) {
                                final class _closure12 extends Closure implements GeneratedClosure {
                                    public _closure12(Object _outerInstance, Object _thisObject) {
                                        super(_outerInstance, _thisObject);
                                    }

                                    public Object doCall(Object it) {
                                        return ((DetachedCriteria)((Closure)((Closure)((Closure)((_closure12)this).getOwner()).getOwner()).getOwner()).getDelegate()).eq("enabled", true);
                                    }

                                    public Object call(Object args) {
                                        return this.doCall(args);
                                    }

                                    public Object call() {
                                        return this.doCall((Object)null);
                                    }

                                    @Generated
                                    public Object doCall() {
                                        return this.doCall((Object)null);
                                    }
                                }

                                ((DetachedCriteria)((Closure)((Closure)((_closure10)this).getOwner()).getOwner()).getDelegate()).and(new _closure12(this, this.getThisObject()));

                                final class _closure13 extends Closure implements GeneratedClosure {
                                    public _closure13(Object _outerInstance, Object _thisObject) {
                                        super(_outerInstance, _thisObject);
                                    }

                                    public Object doCall(Object it) {
                                        return ((DetachedCriteria)((Closure)((Closure)((Closure)((_closure13)this).getOwner()).getOwner()).getOwner()).getDelegate()).eq("syncEnabled", true);
                                    }

                                    public Object call(Object args) {
                                        return this.doCall(args);
                                    }

                                    public Object call() {
                                        return this.doCall((Object)null);
                                    }

                                    @Generated
                                    public Object doCall() {
                                        return this.doCall((Object)null);
                                    }
                                }

                                return ScriptBytecodeAdapter.invokeMethodN(_closure10.class, ((_closure10)this).getDelegate(), (String)"account", new Object[]{new _closure13(this, this.getThisObject())});
                            }

                            public Object call(Object args) {
                                return this.doCall(args);
                            }

                            public Object call() {
                                return this.doCall((Object)null);
                            }

                            @Generated
                            public Object doCall() {
                                return this.doCall((Object)null);
                            }
                        }

                        ((DetachedCriteria)((Closure)((_closure9)this).getOwner()).getDelegate()).and(new _closure10(this, this.getThisObject()));

                        final class _closure11 extends Closure implements GeneratedClosure {
                            public _closure11(Object _outerInstance, Object _thisObject) {
                                super(_outerInstance, _thisObject);
                            }

                            public Object doCall(Object it) {
                                return ((DetachedCriteria)((Closure)((Closure)((_closure11)this).getOwner()).getOwner()).getDelegate()).eq("tokenValid", true);
                            }

                            public Object call(Object args) {
                                return this.doCall(args);
                            }

                            public Object call() {
                                return this.doCall((Object)null);
                            }

                            @Generated
                            public Object doCall() {
                                return this.doCall((Object)null);
                            }
                        }

                        return ScriptBytecodeAdapter.invokeMethodN(_closure9.class, ((_closure9)this).getDelegate(), (String)"account", new Object[]{new _closure11(this, this.getThisObject())});
                    }

                    public Object call(Object args) {
                        return this.doCall(args);
                    }

                    public Object call() {
                        return this.doCall((Object)null);
                    }

                    @Generated
                    public Object doCall() {
                        return this.doCall((Object)null);
                    }
                }

                return ((DetachedCriteria)((__tt__findAllEnabled_closure8)this).getDelegate()).and(new _closure9(this, this.getThisObject()));
            }

            public Object call(Object args) {
                return this.doCall(args);
            }

            public Object call() {
                return this.doCall((Object)null);
            }

            @Generated
            public Object doCall() {
                return this.doCall((Object)null);
            }
        }

        DetachedCriteria var3 = $query.build(new __tt__findAllEnabled_closure8(this, this));
        return (List)var3.list();
    }

if you make a diff then you can see do calls in later one contains 3 times cast to Closure

Snímek obrazovky 2020-06-11 v 15 05 12

I guess that the issue is linked to org.grails.datastore.gorm.query.transform.DetachedCriteriaTransformer

musketyr avatar Jun 11 '20 13:06 musketyr

@puneetbehl here are some research notes.

Research Notes

I reproduced all the issues described above by sergio, in 3.3.11 and 4.0.2. In 4.0.2, I also saw how when you take the composite key our if UserRole the query works fine.

The area to debug in 4.0.2. is AbstractHibernateCriteriaBuilder:1765. The exception is thrown in this line final Attribute<?, ?> attribute = entityType.getAttribute(name);

Inside AbstractManagedType:114 this line: PersistentAttributeDescriptor<? super J, ?> attribute = declaredAttributes.get( name ); yields null, and then this method checkNotNull( "Attribute ", attribute, name ); throws the exception.

However if you take out the composite key in UserRole the query works correctly. The difference is that in line :114 now the declaredAttributes property now has role, id, and user in it, so it yields an attribute. And the checkNotNull( "Attribute ", attribute, name ); passes through fine.

The only thing i can make of this that the AbstractManagedType is created correctly when the composite key is NOT present and that when it IS declared, the declaredAttributes field becomes blank so we get the exception. Of course i dont know "why".

Comparing to 3.3.11

  • I tried to compare to what the code is doing in 3.3.11 grails. This turned up empty because in the code after AbstractHibernateCriteriaBuilder:1761 is not the same in 3.3.11. it has changed from grails 3.3.11 to 4.0.2. I don't see the same code in 3.3.11 where the attribute is retrieved in from the entityType.getAttribute(name);

niravassar avatar Jul 07 '20 20:07 niravassar

Related issue on the hibernate5 repo: grails/gorm-hibernate5/issues/143

Just in case there's some insight there that helps with this.

jjelliott avatar Aug 13 '20 17:08 jjelliott

I am in the process of upgrading a grails 3.11 project to 4.0.4 and am facing the same issue. When creating a criteria on the UserRole class (created by the spring security plugin), I get the "Unable to locate Attribute with the the given name [role] on this ManagedType [net.buttonwood.bcxbroker.UserRole]". My UserRole class also implements a couple of interfaces as well, so wasn't sure if that is a factor.

To add to @niravassar's research, my User Role has two additional members/attributes other than 'role' and 'user'. When debugging I can see that declaredAttributes contains the two additional attributes, but not the two that appear in the composite key definition.

Is there a known work around for this, other than to remove the composite key?

davidbairdala avatar Oct 23 '20 06:10 davidbairdala

With GORM v7 (Hibernate 5.4.23), when sessionFactory is built, org.hibernate.metamodel.internal.MetadataContext.wrapUp is called to parse all persistent classes and applyIdMetadata method is called to evaluate id properties.

In case of composite id, properties are embbeded in a org.hibernate.mapping.Component with 2 or more elements and there is a FIXME for this case.

if ( component.getPropertySpan() > 1 ) { //FIXME we are dealing with a Hibernate embedded id (ie not type) } else { //FIXME take care of declared vs non declared property identifiableType.getInFlightAccess().applyIdAttribute( (SingularPersistentAttribute) attributeFactory.buildIdAttribute( identifiableType, (Property) component.getPropertyIterator().next() ) ); }

gelleouet avatar Apr 21 '21 16:04 gelleouet

As an work around, using createAlias did work for me. Before

compartiment {
    eq('active', true)
}

After

createAlias('compartiment', 'c')
eq('c.active', true)

dsowza avatar Oct 18 '21 15:10 dsowza