2

I have two domain classes Users and Projects like following

Users{
    String firstName
    String lastName
    String emailAddress
    static hasMany = [projects:Projects]
}



class Projects {
    String projectName
    String description
    Users projectLead
    Date completionDate
    static belongsTo = Users
}

Here completionDate == null means the project has not yet been completed.

Now I want to send an email reminder to each user about their incomplete projects, How can write a query to retrieve incomplete projects per user?

I was thinking on following lines but am still not able to go ahead. In order to send an email I will need users emailid, all incomplete projects and names of them

def users = Users.list()
       for(user in users){
           user.projects.find{it.completionDate==null}
       }

Is it possible to use createCriteria in such a case?

Sap
  • 5,197
  • 8
  • 59
  • 101

2 Answers2

4

I think this should work:

def usersAndIncompleteProjects = Users.withCriteria {
    projects {
        isNull( completionDate )
    }
}

That should just return you the Users with incomplete projects, and the projects property for each User will only contain the incomplete projects. If you want the Users to have all of their projects loaded, I believe you need to use an alias


Testing...

Given the User class:

package criteriatest

class User {
    String name

    static hasMany = [ projects: Project ]
}

And the Project class:

package criteriatest

class Project {
    String name
    Date completionDate

    static belongsTo = User

    static constraints = {
        completionDate( nullable:true )
    }
}

This integration test passes (hopefully the asserts explain it)

package criteriatest

import grails.test.*

class UserTests extends GroovyTestCase {
    protected void setUp() {
        super.setUp()
        User.withSession { session ->
            def tim = new User( name:'tim' )
            def dave = new User( name:'dave' )

            [ tim, dave ]*.save()

            def project1 = new Project( name:'project 1', completionDate:null )
            def project2 = new Project( name:'project 2', completionDate:new Date() )

            tim.addToProjects project1
            tim.addToProjects project2

            [ project1, project2 ]*.save()

            session.flush()
            session.clear()
        }
    }

    protected void tearDown() {
        super.tearDown()
    }

    void testQuery() {
        def usersAndIncompleteProjects = User.withCriteria {
            projects {
                isNull 'completionDate'
            }
            order 'name', 'asc'
        }

        // We get two users back (users with no projects get returned as well)
        assert usersAndIncompleteProjects.size() == 2

        // First user (dave) has no projects
        assert usersAndIncompleteProjects[0].projects.size() == 0

        // Second user (tim) has one project (with a null completionDate)
        assert usersAndIncompleteProjects[1].projects.size() == 1

        // Check it's the right project
        assert usersAndIncompleteProjects[1].projects*.name == [ 'project 1' ]
    }
}

(this is the sql the criteria query executes in this instance):

select
    this_.id as id1_1_,
    this_.version as version1_1_,
    this_.name as name1_1_,
    projects3_.user_projects_id as user1_3_,
    projects_a1_.id as project2_3_,
    projects_a1_.id as id0_0_,
    projects_a1_.version as version0_0_,
    projects_a1_.completion_date as completion3_0_0_,
    projects_a1_.name as name0_0_ 
from
    user this_ 
left outer join
    user_project projects3_ 
        on this_.id=projects3_.user_projects_id 
left outer join
    project projects_a1_ 
        on projects3_.project_id=projects_a1_.id 
where
    (
        projects_a1_.completion_date is null
    ) 
order by
    this_.name asc
tim_yates
  • 167,322
  • 27
  • 342
  • 338
  • Just to confirm, after running this query if I were to do usersInstance.projects.each(){......} would it only iterate through projects with null completiondate? – Sap Jun 28 '11 at 08:16
  • I think that link illustrates pretty well why HQL is the way to go if you care about whether your join is inner or outer :) – ataylor Jun 28 '11 at 08:18
  • That will select all users who have at least one incomplete project, I think. – ataylor Jun 28 '11 at 08:20
  • Hey Tim, Thanks a lot the link explains a lot. – Sap Jun 28 '11 at 08:26
  • I've added a quick test to make sure it's still the same behaviour in Grails 1.3.7, and it seems to be... Updating my answer with the code... – tim_yates Jun 28 '11 at 08:46
  • +1 for tests. I forgot that's how associations in criterias work. It can cause [some surprises](http://adhockery.blogspot.com/2009/04/associations-and-criteria-queries.html) though if you ever try to load the rest of the associations in the same session. – ataylor Jun 28 '11 at 09:05
  • @ataylor Yeah...and in this situation, we **want** the suprise outcome, with only the uncompleted projects being returned ;-) – tim_yates Jun 28 '11 at 09:08
3

I'm not sure this problem requires a left join, unless you want to include Projects with a null user. Why not just select all projects with null completion dates and join against user?

In HQL, it would look something like this:

Projects.executeQuery('from Projects p join p.projectLead u where p.completionDate is null')

You can do similar in a criteria query:

Projects.withCriteria {
    isNull('completionDate')
    join('projectLead')
}
ataylor
  • 64,891
  • 24
  • 161
  • 189
  • In this case I am not iterating through projects per users! What I mean is I want to get all users and then retrieve incomplete projects for each. This way I can send one consolidated email per user. – Sap Jun 28 '11 at 08:19
  • 1
    If you call the HQL query, it'll return you a list of [project, user] pairs. You can then call `results.groupBy{ it[1] }` to transform it into a map of users to [project, user] pairs, and iterate through that. – ataylor Jun 28 '11 at 08:25
  • The query I provided gets it all in one query. The straight-forward way of iterating over all Users with list and then selecting a list of incomplete projects is probably more obvious. Unfortunately, it's a SQL anti-pattern ([n+1 selects](http://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem)) that won't scale as well. – ataylor Jun 28 '11 at 08:33