2

I've got a simple bi-directional one-to-many mapping, as follows, with a default sort order specified on the owning side. However, the sort order doesn't seem to be getting applied? I'm using Grails v2.0.1 (I've now replicated this example with v1.3.7).

package playground

class User {

    String name

    static hasMany = [ posts : Post ]

    static mapping = {
        posts sort:'position'
    }
}

and

package playground

class Post {

    int position = 1
    String message

    static belongsTo = [ user : User ]
}

this is the integration test code I'm using to exercise it ...

    def User user = new User(name:'bob')
    user.addToPosts(new Post(position:2, message:'two'))
    user.addToPosts(new Post(position:3, message:'three'))
    user.addToPosts(new Post(position:1, message:'one'))

    assertTrue user.validate()
    assertFalse user.hasErrors()
    assertNotNull user.save()

    for (post in user.posts) {
        log.debug "Post message> ${post.message}"
    }

Please put me out of my misery, it's presumably something obvious but I can't see it! Thanks.

owenrh
  • 71
  • 1
  • 6
  • It's possible that the sort is only applied when it is pulled from the database. Maybe try user.refresh() before your for loop to see if this is true? It's not a fix, but it may explain the different order. – Igor Mar 26 '12 at 17:44
  • That or get a fresh reference to it with a User.findByName('bob') to force a round trip, probably shouldn't need to but could add a flush:true to the save as well – Will Buck Mar 26 '12 at 17:46
  • 1
    Interesting. flush:true didn't work, but user.refresh() did. The weird thing is that if you call the user back out of the database (def User foundUser = User.get(user.id);) then iterate over the posts it still doesn't do it in sorted order - possibly because it's obtained from the cache (although adding flush:true doesn't work there either). This isn't the first oddity I've seen with integration tests...worrying. – ndtreviv Mar 27 '12 at 12:07
  • I assume that the default sort parameter should work outside of the integration test "sandbox". Alternatively, you could guarantee sorting by using a SortedSet. I did submit as an answer, but then voted to delete it because it doesn't really answer the question! I'd still like to know why the sort setting doesn't work, but have a hunch it's a bug... – ndtreviv Mar 27 '12 at 12:50
  • @ndtreviv yeah, ditto on the retrieving from the database weirdness with regards the user.refresh() versus User.findByName('bob'). As you note, the findByX() presumably hits the cached version, possibly, maybe. It could be that because it's all within the same transaction it's hitting the Hibernate 1st level cache? – owenrh Mar 27 '12 at 21:31
  • Tested with various caching options, and nothing helps (because I think they all relate to 2nd level cache. I think you're right in that it's all working within the same session - must be a 1st level cache issue. I eventually worked around it by using User.withNewSession{ session -> /* do stuff */} and putting the save and the read-and-list-posts in two separate withNewSession closures. This works, and is a good way in integration tests to ensure that your read and write happens in two separate sessions. HOWEVER...see next comment! – ndtreviv Mar 28 '12 at 08:28
  • I sort of expected that when you added a post the default sort options would kick in. I guess if you want to guarantee sorting you just have to use a SortedSet instead of relying on the ManyToOne relationship with default sort settings. Something learnt for me there... – ndtreviv Mar 28 '12 at 08:30
  • And finally, here's the code I eventually used to work around 1st level cache for integration tests: http://pastebin.com/u0MeAC2J – ndtreviv Mar 28 '12 at 08:36

4 Answers4

7

use this code:

package playground

    class User {

        String name
        static hasMany = [ posts : Post ]

        static mapping = {
            posts sort:'position' order:'desc'//order:'asc'
        }
    }
Jack Daniel
  • 2,397
  • 8
  • 33
  • 58
  • 1
    Thanks I have upvoted you, but order:"asc" does not work for me so I just left it out because the default is asc. – dsharew Oct 30 '14 at 08:19
1

Turns out this is a bit of odd edge-case behaviour, that's really a result of the way the test is written. Basically, everything is happening in the scope of a single Hibernate session/txn (see conversations above). So the test is fetching back the object graph it just created (with the out of order Set), rather than fetching data from the database.

If you force a separate transaction then you get the behaviour you are looking for and the 'order by' works as expected. E.g.

User.withNewSession{ session ->
   def User foundUser = User.get(user.id);

   for (post in foundUser.posts) {
      println "Post message> ${post.message}"
   }
}

Code courtesy of ndtreviv.

owenrh
  • 71
  • 1
  • 6
0

Owens answer got me out of the confusion i was in. i was trying to the the ordering defined on a relationship between users (1) and posts (many), but when i wrote the initial tests they were failing as using User.get (u.id) was in the same session - and so was just reading out of the cache and they came back in the order i'd written tem not newest first as i'd expected.

I then rewrote the test across two sessions and low and behold in the second session this time returned the posts in desc order.

So you just have to be careful. if you are in the same original session that creates the posts then you have to use User.get (u.id).posts.sort().

All these little gotchas with not understanding properly how the session cache and underlying DB work in the scope of the same session/transaction. makes your brain ache sometimes.

whilst we are noting things - this errored in integration test in 3.2.5, but i spotted a thread by Jeff and the fix that had gone. So i upgraded to grails 3.2.6 last night and this test now works

void "test query"() {
    given:"a user and where query for users, and posts "
    User u

    when: "create a post for user "
    User.withNewSession { session ->
        u = new User(username: 'will')
        u.save(flush: true, failOnError: true)
        Post p1 = new Post(comment: [food: "bought coffee and cake "])
        Post p2 = new Post(comment: [dinner: "bought wine and dinner"])
        Post p3 = new Post(comment: [view: "spectacular view of lake and sunset"])
        u.addToPosts(p1)
        u.addToPosts(p2)
        u.addToPosts(p3)
        u.save(flush: true)
        if (u.hasErrors())
            println "error saving posts on user u : ${u.errors}"


        def postList = User.get(u.id).posts
        postList.each { println "query via user.list using same session > $it.dateCreated : $it.comment" }

        Post.findAll().each { println "query via Post using same session > $it.dateCreated : $it.comment" }
    }
    //because still in same session it just returns the order from the 1st level cache - so force a
    //new session and let the DB do the sort
    def lookupPosts
    User.withNewSession{ session ->
        User uNew  = User.get(1)
        assert uNew
        lookupPosts = uNew.posts

        lookupPosts.each {println "query via user in new session > $it.dateCreated : $it.comment" }
    }



    then: " check post was added"
    !u.hasErrors ()
    lookupPosts.size() == 3
    lookupPosts[1].comment.dinner == "bought wine and dinner"

}
WILLIAM WOODMAN
  • 1,185
  • 5
  • 19
  • 36
0

If you use a list instead of a Set (the default) the framework will maintain the order for you.

List posts = new ArrayList()
static hasMany = [ posts : Post ]
Jay Prall
  • 5,295
  • 5
  • 49
  • 79
  • @j4y - using a List will maintain the insertion order. However, what I'm looking for is the ability to declare a sort order on a field in the domain object ala [grails docs](http://grails.org/doc/2.0.1/guide/GORM.html#defaultSortOrder) – owenrh Mar 27 '12 at 21:19