Hibernate Bags in Grails 2.0

When I’ve talked in the past about collection mapping in Grails (you can see a video of a SpringOne/2GX talk here) I mentioned that the current approach of using Sets or Lists is problematic and provided workarounds. I mentioned at the time that Hibernate has support for Bags which don’t enforce uniqueness or order like Sets and Lists do, so if GORM supported Bags we could just use those. So I added support for Bags to GORM for Grails 2.0 and thought that was that.

I thought it’d be interesting to demo this at my GORM talk at this year’s SpringOne/2GX but when I created a small test application it wasn’t working like I remembered. In fact it was actually worse than the problems I was working around. So I put that away with a mental note to get back to this soon, and before 2.0 final is released.

It turns out there’s good news and bad news. The good news is that it’s not completely broken. The bad news is that it’s mostly broken.


First the good news. If you have a one-to-many that doesn’t use a join table, using a Bag works mostly as expected. As an example, consider an Author/Book mapping where a book has one author, and an author can have many books:

class Author {
   String name
   Collection books
   static hasMany = [books: Book]
}
class Book {
   String title
   static belongsTo = [author: Author]
}

Using the Map syntax for the belongsTo mapping is the key to avoiding the join table and relating the tables with a foreign key from the book table to the author table. If you run grails schema-export the output will be something like

create table author (
   id bigint generated by default as identity,
   version bigint not null,
   name varchar(255) not null,
   primary key (id)
);

create table book (
   id bigint generated by default as identity,
   version bigint not null,
   author_id bigint not null,
   title varchar(255) not null,
   primary key (id)
);

alter table book add constraint FK2E3AE9CD85EDFA
foreign key (author_id) references author;

If you run this initializing code in a Grails console with SQL logging enabled (add logSql = true in DataSource.groovy)

def author = new Author(name: 'Hunter S. Thompson')
author.addToBooks(title: 'Fear and Loathing in Las Vegas')
author.save()

you’ll see output like this:

insert into author (id, version, name) values (null, ?, ?)

insert into book (id, version, author_id, title) values (null, ?, ?, ?)

update author set version=?, name=? where id=? and version=?

which is ok; it inserts the author and the book, although it bumps the version of the Author. I’ll come back to that.

If you run this updating code:

def author = Author.get(1)
author.addToBooks(title: "Hell's Angels: A Strange and Terrible Saga")
author.save()

you’ll see output like this:

select author0_.id as id0_0_, author0_.version as version0_0_,
author0_.name as name0_0_ from author author0_ where author0_.id=?

insert into book (id, version, author_id, title) values (null, ?, ?, ?)

update author set version=?, name=? where id=? and version=?

This is also basically ok – it loads the author, inserts the book, and versions the author.

If you map the belongsTo with the non-map syntax (static belongsTo = Author) you’ll get this DDL:

create table author (
   id bigint generated by default as identity,
   version bigint not null,
   name varchar(255) not null,
   primary key (id)
);

create table author_book (
   author_books_id bigint,
   book_id bigint
);

create table book (
   id bigint generated by default as identity,
   version bigint not null,
   title varchar(255) not null,
   primary key (id)
);

alter table author_book add constraint FK2A7A111D3FA913A
foreign key (book_id) references book;

alter table author_book add constraint FK2A7A111DC46A00AF
foreign key (author_books_id) references author;

and running the initializing code above will result in output that’s similar to before, with the addition of inserting into the join table:

insert into author (id, version, name) values (null, ?, ?)

insert into book (id, version, title) values (null, ?, ?)

update author set version=?, name=? where id=? and version=?

insert into author_book (author_books_id, book_id) values (?, ?)

but running the updating code results in this:

select author0_.id as id4_0_, author0_.version as version4_0_,
author0_.name as name4_0_ from author author0_ where author0_.id=?

select books0_.author_books_id as author1_4_0_, books0_.book_id as
book2_0_ from author_book books0_ where books0_.author_books_id=?

select book0_.id as id3_0_, book0_.version as version3_0_,
book0_.title as title3_0_ from book book0_ where book0_.id=?

insert into book (id, version, title) values (null, ?, ?)

update author set version=?, name=? where id=? and version=?

delete from author_book where author_books_id=?

insert into author_book (author_books_id, book_id) values (?, ?)

insert into author_book (author_books_id, book_id) values (?, ?)

This is not good. It reads the author, then all of the books for that author (the part we’re trying to avoid), inserts the book, and then deletes every row from the join table for this author, and re-inserts rows for each element in the Bag. Ouch.


If you convert the relationship to a many-to-many with Bags on both sides:

class Author {
   String name
   Collection books
   static hasMany = [books: Book]
}
class Book {
   String title
   Collection authors
   static hasMany = [authors: Author]
   static belongsTo = Author
}

and run this initializing code:

def author = new Author(name: 'Hunter S. Thompson')
author.addToBooks(title: 'Fear and Loathing in Las Vegas')
author.save()

you get this output:

insert into author (id, version, name) values (null, ?, ?)

insert into book (id, version, title) values (null, ?, ?)

update author set version=?, name=? where id=? and version=?

update book set version=?, title=? where id=? and version=?

insert into author_books (author_id, book_id) values (?, ?)

It inserts the author and the book, then versions both rows, and inserts a row into the join table.

If you run this updating code:

def author = Author.get(1)
author.addToBooks(title: "Hell's Angels: A Strange and Terrible Saga")
author.save()

then the output is similar to the output for one-to-many with a join table:

select author0_.id as id0_0_, author0_.version as version0_0_,
author0_.name as name0_0_ from author author0_ where author0_.id=?

select books0_.author_id as author1_0_0_, books0_.book_id as book2_0_
from author_books books0_ where books0_.author_id=?

insert into book (id, version, title) values (null, ?, ?)

update author set version=?, name=? where id=? and version=?

update book set version=?, title=? where id=? and version=?

delete from author_books where author_id=?

insert into author_books (author_id, book_id) values (?, ?)

insert into author_books (author_id, book_id) values (?, ?)

It loads the author, then all of the book ids from the join table (to create proxies, which are lighter-weight than full domain class instances but there will still be N of them in memory), then inserts the new book, versions both rows, and again deletes every row from the join table and reinserts them. Ouch again.


So for the two cases where there are join tables, we have a problem. Hibernate doesn’t worry about duplicates or order in-memory, but the join tables can’t have duplicate records, so it has to pessimistically clear the data and reinsert it. This has all of the negatives of the non-Bag approach and adds another big one.

Even in the first case I described where there’s no join table, there’s still a problem. Since the Author’s version gets incremented when you add a Book (you’re editing a property of the Author, so it’s considered to be updated even though it’s a collection pointing to another table) there’s a high risk that concurrently adding child instances will cause optimistic locking exceptions for the Author, even though you just want to insert rows into the book table. And this is the case for all three scenarios.


So I guess I’m back to advocating the approach from my earlier talks; don’t map a collection of Books in the Author class, but add an Author field to the Book class instead:

class Author {
   String name
}
class Book {
   String title
   Author author
}

And for many-to-many case map the “author_books” table with a domain class:

class Author {
   String name
}
class Book {
   String title
}
class AuthorBook {
   Author author
   Book book
   ...
}

11 Responses to “Hibernate Bags in Grails 2.0”

  1. Nick Vaidyanathan says:

    It seems to me that the Domain Instance for M:N relationship is a Grails Best Practice that seems to keep coming up. Scott Davis wrote about it years ago (http://www.ibm.com/developerworks/java/library/j-grails04158/index.html), and his arguments about manifesting the hidden concepts in the design (join tables represent concepts which may have additional attributes) seem to also have performance implications, as shown here.

    I’ve always advocated to people, when discussing M:N in Grails, to avoid the hasMany/hasMany (which I believe is a sloppy shortcut that short-circuits future design) in favor of mapping the relationship with a class. Nevertheless, people continue to be confused about their options because there are multiple ways of doing M:N, and generally choose hasMany/hasMany because it “seems easier” (though I don’t see what’s so hard about adding another class)

    Maybe it’s time for a highly opinionated framework to reduce the option space? Perhaps remove support for hasMany/hasMany and replace it with a shell command (e.g. grails-create-many-to-many ) that takes care of scaffolding the relationship class like PersonRole in Spring Security Core?

  2. I was wondering lately that we should avoid hasMany property even in 1:N relations. We can always split domain object and link children to parent by belongsTo relation. It is very simple and more straightforward for developers to operate on that kind of relations (especially when there is need to do some HQL).

  3. Fabien7474 says:

    So is it still needed to implement one to many associations with Bag? Is there any advantages left agains Set ?

  4. yovi says:

    Hi there, and thank you for this interesting post.

    I was wondering whether these issues have been solved in the Grails 2.0 final release; looking into my logs I don’t see those DELETE statements…

    Thanks!

  5. Iván says:

    Nice post.

    I have tried the examples in Grails 2.0.4 and the behaviour is the same.

    Burt, do you recomend avoid hasMany (in 1:N and N:M) and always map the intermediate class?

    Thanks and regards, Iván.

    • Burt says:

      If the collection size is reasonably small (and this depends on your app, usage patterns, traffic, etc.) then the cost won’t be an issue or it will be low enough to not be too bothersome. But it’s not that much work mapping the join table (especially since once you’ve done it once it’s easy to copy/paste a previous domain class).

      • Iván says:

        Ok, thanks for your reply. I just have finished to view your talk at SpringOne/2GX and now I have the things a little more clear.

        Please, allow me another question. In a Grails 2.0 application, if the order and the uniqueness is not a problem, is it always better to use Bags instead of the default Sets?

        Thanks and regards, Iván.

        • Burt says:

          No, that was the point of this blog post. At the time of the 2GX video I assumed adding Bag support would fix things but as I show here it makes things worse in most cases.

  6. jay says:

    If I turn off version property (disable optimistic locking), is the bag solution ok then for 1-m relationships?

  7. numan says:

    With your original workaround, is there a way to eagerly fetch the associations?

  8. […] el uso de estas propiedades, pero si que recomiendo echar un vistazo a estas dos entradas ( 1 y 2) de Burt Beckwith sobre las recomendaciones de usar el concepto de Bag de Hibernate para estas […]

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 License.