Search code examples
grailsgrails-ormgrails-domain-class

Joins in Grails Criteria


I'm trying to write a criteria query in grails, for the following domains:

class Data {
    Long createdById // this is user id
    // other fields
}
class User {
   Company company
   // other fields
}

Now data, as the name suggest stores some data. It is not directly related to User but has a field createdById which is the id of User.
(I cannot give direct reference of User in Data because these belongs to different projects, where Data is from a custom reusable plugin.)

Now I want to write criteria on Data and list all records where it was created by users of a given company. that is data.id == User.id and user.company == givenCompany


Solution

  • In order to use any GORM/Hibernate query method (criteria, where, or HQL) you need an association between the domain classes. Since you can't make an association from Data to User, you cannot use GORM to write your query with your domain classes are they are. In other words, you need to use SQL; HQL (DomainClass.executeQuery() will not work). But...

    With HQL

    If you're willing to modify User and maintain some redundant data, you can use HQL.

    class User {
       Company company
       // other fields
    
       /* Add uni-directional one-to-many */
       static hasMany = [data: Data]
    }
    
    def data = AnyDomainClass.executeQuery('SELECT d FROM User as u INNER JOIN u.data AS d WHERE u.company = :company', [company: givenCompany])
    

    The uni-directional one-to-many association creates a join table which would need to be maintained to reflect Data.createdById. The HQL join syntax hints at the fact that associations are required. If you must use a criteria query...

    With criteria query

    To use a criteria query you'll need to modify User and add an additional domain class. The reason is that a criteria query cannot project an association like HQL can. For example, this will not work:

    def data = User.withCriteria {
        eq('company', givenCompany)
    
        projections {
            property('data')
        }
    }
    

    Due to not being able to modify Data, this --which is the simplest solution-- will also not work:

    def data = Data.withCriteria {
        user {
            eq('company', givenCompany)
        }
    }
    

    So, you need a middle-man in order to project Data instances:

    class User {
       Company company
       // other fields
    
       static hasMany = [userData: UserData]
    }
    
    class UserData {
        Data data
    
        static belongsTo = [user: User]
    }
    
    def data = User.withCriteria {
        eq('company', givenCompany)
    
        projections {
            userData {
                property('data')
            }
        }
    }
    

    As you can imagine, this method also requires some data maintenance.

    Using SQL

    Lastly, you have the option of using SQL and having the query return domain class instances. It boils down to something like this:

    AnyDomainClass.withNewSession { session ->
        def data = session.createSQLQuery('SQL GOES HERE').addEntity(Data).list()
    }
    

    More info

    For more information, check out my following articles:

    1. Domain class associations and how they work on the DB level
    2. Joining domain classes with GORM queries
    3. Using SQL and returning domain class instances. Available on Jan 22nd.