Emparejar hasMany niños con buscadores dinámicos de Grails

In Grails, I'm attempting to find an instance of a domain class that has exact entries in a one-to-many relationship. Consider this example:

class Author {
    String name
    List<Book> books

    static hasMany = [books:Book]
}

class Book {
    String title

    static belongsTo = Author
}

My database then appears as such:

author                     book
-------------------------    ------------------------
| id | name             |    | id | title           |
|----|------------------|    ------------------------
| 1  | John Steinbeck   |    | 1  | Grapes of Wrath |
| 2  | Michael Crichton |    | 2  | East of Eden    |
-------------------------    | 3  | Timeline        |
                             | 4  | Jurassic Park   |
                             ------------------------

author_book
----------------------------------------
| author_books_id | book_id | book_idx |
----------------------------------------
| 1               | 1       | 0        | // John Steinbeck - Grapes of Wrath
| 1               | 2       | 1        | // John Steinbeck - East of Eden
| 2               | 3       | 0        | // Michael Crichton - Timeline
| 2               | 4       | 1        | // Michael Crichton - Jurassic Park
----------------------------------------

What I'd like to be able to do is use a dynamic finder on author. I'm searching for an exact match on the hasMany relation, to match this behavior:

Author.findByBooks([1]) => null
Author.findByBooks([1, 2]) => author(id:1)
Author.findByBooks([1, 3]) => null
Author.findByBooks([3, 4]) => author(id:2)

Attempting this results in an ugly Hibernate error:

hibernate.util.JDBCExceptionReporter No value specified for parameter 1.

Has anyone had dynamic finders work with hasMany relationships of domain classes? What is the most 'Grails-y' solution to get the desired behavior?

preguntado el 10 de marzo de 12 a las 00:03

3 Respuestas

Its not clear from your domain model if Book belongs to author. If so, you should add that fact to your domain model and query like Tom Metz said.

Let me get this right. You want to find the Author[s] that have writen the Books with title (or id) i.e. 'Book 1' and 'Book 2'. To make the check work you have to join the Book table two times in order to compare the book title from join one with 'Book 1' and the book title from join two with 'Book2'.

One could assume that the following test should work:

void setUp() {
    def author = new Author(name: "Ted Jones").save(flush: true)
    def author2 = new Author(name:  "Beth Peters").save(flush: true)
    def author3 = new Author(name:  "Foo Bar").save(flush: true)
    def book1 = new Book(title: 'Book 1').save(flush: true)
    def book2 = new Book(title: 'Book 2').save(flush: true)
    def book3 = new Book(title: 'Book 3').save(flush: true)
    def book4 = new Book(title: 'Book 4').save(flush: true)
    author.addToBooks(book1)
    author.addToBooks(book3)

    author2.addToBooks(book2)
    author2.addToBooks(book4)

    author3.addToBooks(book1)
    author3.addToBooks(book2)
}

void testAuthorCrit() {
    def result = Author.withCriteria() {
        books {
            eq("title", "Book 1")
        }
        books {
            eq("title", "Book 3")
        }
    }
    assert 1 == result.size()
    assertTrue(result.first().name == "Ted Jones")
}

But it turns out, that the result set is empty. Grails merges the statements in each books closure into one join.

Esta es la consulta resultante:

 select this_.id as id1_1_, this_.version as version1_1_, this_.name as name1_1_, books3_.author_books_id as author1_1_, books_alia1_.id as book2_, books3_.books_idx as books3_, books_alia1_.id as id0_0_, books_alia1_.version as version0_0_, books_alia1_.title as title0_0_ from author this_ inner join author_book books3_ on this_.id=books3_.author_books_id inner join book books_alia1_ on books3_.book_id=books_alia1_.id where (books_alia1_.title=?) and (books_alia1_.title=?)

ASFAIK this cannot be archieved using the grails criteria api. But you can use hql instead. The following test works:

void testAuthorHql() {
    def result = Author.executeQuery("select a from Author a join a.books bookOne join a.books bookTwo where bookOne.title=? and bookTwo.title=?", ['Book 1', 'Book 3'])
    assert 1 == result.size()
    assertTrue(result.first().name == "Ted Jones")
}

respondido 14 mar '12, 17:03

This is similar to the solution I came up with, although I used raw SQL rather than HQL. Supporting any number of Books makes forming the query slightly more complicated, and an additional check on Author.books.size() can be used to enforce an exact match on the collection of books. I was disappointed that the first approach with two books closures does not work. Thanks for the sensible reply confirming that Grails domain classes and criteria queries are both not up to the task. - timbonico

I'm not 100% sure you can actually get that to work (unless I am missing some docs somewhere). But to get what you need, you'd want to use a criteria:

class AuthorIntegrationTests {

  @Before
  void setUp() {
    def author = new Author(name: "Ted Jones").save(flush: true)
    def author2 = new Author(name:  "Beth Peters").save(flush: true)

    def book1 = new Book(title: 'Book 1').save(flush: true)
    def book2 = new Book(title: 'Book 2').save(flush: true)
    def book3 = new Book(title: 'Book 3').save(flush: true)
    def book4 = new Book(title: 'Book 4').save(flush: true)

    author.addToBooks(book1)
    author.addToBooks(book3)

    author2.addToBooks(book2)
    author2.addToBooks(book4)
  }

  @After
  void tearDown() {
  }

  @Test
  void testAuthorCrit() {
    def result = Author.withCriteria(uniqueResult: true) {
      books {
        inList("id", [1.toLong(), 3.toLong()])
      }
    }
    assertTrue(result.name == "Ted Jones")
  }
}

respondido 12 mar '12, 18:03

This doesn't quite work, since the query produced tries to join on books.id = 1 AND books.id = 2, which produces no result. I like the approach, I'm attempting to tease the criteria builder into creating multiple joins. I don't think it will enforce the exact set even at that point, but it may get me close enough that a little code could do the rest. - timbonico

The trouble with or is that it will pull up any author matching either book, rather than only authors having both books. - timbonico

Well, I'm totally confused. If you want all authors related to both books, use AND instead of OR. Good luck. - Gregg

The initial AND query produced SQL (or HQL, as the case may be) similar to: select author.id from author left join author_book on author.id = author_book.author_id left join book on author_book.book_id = book.id where (book.id = 1 AND book.id = 2). This will never return a result since book.id cannot be 1 Y 2. Does that make sense? It's not quite the desired query we had in mind. Instead, there would need to be two joins on the book table. - timbonico

I've updated my answer with a test and a new criteria that works for me. - Gregg

You need to add bidirectional one-to-many relation to your domain objects. Into you Book domain add:

static belongsTo = [ author:Author ]

You can then query:

Author a = Book.author

respondido 12 mar '12, 11:03

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.