views:

718

answers:

2

I have a simple many-to-many mapping:

@Entity
@Table(name="ITEM")
public static class Item
{
    @Id
    @GeneratedValue
    long id;
    @Column
    String name;
    @ManyToMany(cascade={ CascadeType.ALL })
    Set<Category> categories = new HashSet<Category> ();
}

@Entity
@Table(name="CATEGORY")
public static class Category
{
    @Id
    @GeneratedValue
    long id;
    @Column
    String name;
}

And this test case:

public void testPersist() throws Throwable
{
    Category one = new Category ();
    one.name = "one";
    em.persist (one);
    System.out.println ("one.id="+one.id);

    Category two = new Category ();
    two.name = "two";
    em.persist (two);
    System.out.println ("two.id="+two.id);

    Item item = new Item ();
    item.name = "item";
    item.categories.add (one);
    item.categories.add (two);
    em.persist (item);
    long id = item.id;
    System.out.println ("item.id="+item.id);
    System.out.println ("item.categories="+item.categories);

    em.clear ();

    item = em.find (Item.class, id);
    System.out.println ("item.categories="+item.categories);
    assertEquals (item.categories.toString (), 2, item.categories.size ());
}

The test fails. In the log, I can see:

SchemaUpdate - create table CATEGORY (id bigint generated by default as identity (start with 1), name varchar(255), primary key (id))
SchemaUpdate - create table ITEM (id bigint generated by default as identity (start with 1), name varchar(255), primary key (id))
SchemaUpdate - create table ITEM_CATEGORY (ITEM_id bigint not null, categories_id bigint not null, primary key (ITEM_id, categories_id))
SchemaUpdate - alter table ITEM_CATEGORY add constraint FK5C7030AA7C924DF7 foreign key (categories_id) references CATEGORY
SchemaUpdate - alter table ITEM_CATEGORY add constraint FK5C7030AA66304535 foreign key (ITEM_id) references ITEM

So the correct tables are created, the dual primary key is OK, the foreign keys are correct.

The persisting the item doesn't do the right thing:

[DEBUG] org.hibernate.SQL - insert into CATEGORY (id, name) values (null, ?)
[TRACE] org.hibernate.type.StringType - binding 'one' to parameter: 1
[DEBUG] org.hibernate.SQL - call identity()
one.id=1
[DEBUG] org.hibernate.SQL - insert into CATEGORY (id, name) values (null, ?)
[TRACE] org.hibernate.type.StringType - binding 'two' to parameter: 1
[DEBUG] org.hibernate.SQL - call identity()
two.id=2
[DEBUG] org.hibernate.SQL - insert into ITEM (id, name) values (null, ?)
[TRACE] org.hibernate.type.StringType - binding 'item' to parameter: 1
[DEBUG] org.hibernate.SQL - call identity()
item.id=1
item.categories=[ch.globus.bonus.ManyToManyTest$Category@1d532ae, ch.globus.bonus.ManyToManyTest$Category@42a6eb]

As you can see, the item itself is persisted but not the set with the categories (the inserts into the join table are missing). But there are values in the set!

When it reads the item, it does query the join table:

[DEBUG] org.hibernate.SQL - select manytomany0_.id as id9_0_, manytomany0_.name as name9_0_ from ITEM manytomany0_ where manytomany0_.id=?
[TRACE] org.hibernate.type.LongType - binding '1' to parameter: 1
[TRACE] org.hibernate.type.StringType - returning 'item' as column: name9_0_
[DEBUG] org.hibernate.SQL - select categories0_.ITEM_id as ITEM1_1_, categories0_.categories_id as categories2_1_, manytomany1_.id as id10_0_, manytomany1_.name as name10_0_ from ITEM_CATEGORY categories0_ left outer join CATEGORY manytomany1_ on categories0_.categories_id=manytomany1_.id where categories0_.ITEM_id=?
[TRACE] org.hibernate.type.LongType - binding '1' to parameter: 1
item.categories=[]

but of course, it can't find anything. What's wrong? Why is hibernate considering the join table for find() but not for persist()?

I'm using Hibernate 3.3.1, Hibernate Annotations 3.4.0

[EDIT] To fix the issue, I tried to introduce a bidirectional mapping. First, I added this code to Category:

    /* BEGIN FIX1: made join bidrectional */
    @ManyToMany(cascade={ CascadeType.ALL })
    Set<Item> items = new HashSet<Item> ();
    /* END FIX1: made join bidrectional */

Then, I changed the test to update both sides:

    item.categories.add (two);
    /* BEGIN FIX1: made join bidrectional */
    one.items.add (item);
    two.items.add (item);
    /* END FIX1: made join bidrectional */
    em.persist (item);
    /* BEGIN FIX1: made join bidrectional */
    em.persist (one);
    em.persist (two);
    /* END FIX1: made join bidrectional */

I even went to persiste the categories again. Result: Nothing. Hibernate happily ignores the many-to-many mapping.

As a second fix, I tried to add the @JoinTable annotation as suggested by zoidbeck:

    @ManyToMany(cascade={ CascadeType.ALL })
    /* BEGIN FIX2: Added JoinTable */
    @JoinTable(name = "ITEM_CATEGORY",
            joinColumns={@JoinColumn(name="itm_id")},
            inverseJoinColumns={@JoinColumn(name="cat_id")}
    )
    /* END FIX2: Added JoinTable */
    Set<Category> categories = new HashSet<Category> ();

Again, nothing.

A: 

As far as i know you have to fully specify the linktable:

@Entity
@Table(name="ITEM")
public static class Item
{
    @Id
    @GeneratedValue
    long id;

    @Column
    String name;

    @ManyToMany(cascade={ CascadeType.ALL })
    @JoinTable(name = "ITEM_CATEGORY",
      joinColumns = { @JoinColumn(name="item_id") },
      inverseJoinColumns = { @JoinColumn(name="categories_id")}
    Set<Category> categories = new HashSet<Category> ();
}

I am not sure wheather you need to specify the ManyToMany in a bidirectional way, but if so this would be the mapping of the other side:

@ManyToMany(mappedBy="categories")  // map info is in item class
private Set<Item> items;
zoidbeck
Nope, test still fails. See my edits of my question for details.
Aaron Digulla
Sorry that did not work for you, but nice to hear you finally found a solution.
zoidbeck
A: 

The reason is the optimization in Hibernate: The test never flushes the session. So all that happens is that Hibernate remembers the changes it will need to do in its internal cache but since the transaction is never committed, it never sees a reason to generate the SQL to update the join table.

But why do I see the insert of the item, then? Because Hibernate needs its ID for the join. So it has to ask the database "what ID will this object get?".

The solution is to force Hibernate to flush its cache:

    Session session = (Session)em.getDelegate ();
    session.flush ();

Here is the complete working test case:

@Entity
@Table(name="ITEM")
public static class Item
{
    @Id
    @GeneratedValue
    long id;
    @Column
    String name;
    @ManyToMany(cascade={ CascadeType.ALL })
    Set<Category> categories = new HashSet<Category> ();
}

@Entity
@Table(name="CATEGORY")
public static class Category
{
    @Id
    @GeneratedValue
    long id;
    @Column
    String name;
}

public void testPersist() throws Throwable
{
    final Item item = prepareDB ();

    long id = item.id;
    assertTrue ("ID: "+id, id > 0);

    // Clear cache to make sure the objects are loaded again from DB
    em.clear ();

    Item item2 = em.find (Item.class, id);
    assertEquals (item2.categories.toString (), 2, item2.categories.size ());

    delete (item2);

    item2 = em.find (Item.class, id);
    assertNull (item2);
}

public void flush ()
{
    Session session = (Session)em.getDelegate ();
    session.flush ();
}

@Transactional
public void delete (Item item2)
{
    em.remove (item2);
    // Force delete
    flush ();
}

@Transactional
public Item prepareDB ()
{
    final Category one = new Category ();
    one.name = "one";
    em.persist (one);
    System.out.println ("one.id="+one.id);

    final Category two = new Category ();
    two.name = "two";
    em.persist (two);
    System.out.println ("two.id="+two.id);

    final Item item = new Item ();
    item.name = "item";
    item.categories.add (one);
    item.categories.add (two);
    em.persist (item);

    // Force update of join table
    flush ();

    return item;
}
Aaron Digulla
I've had the same problem, with the same solution. But isn't there any other way to get this working? Flushing the session on every commit is not so performant...
Bertvan
No, there is no other way and yes, it is absolutely necessary to flush on commit. At what other time to you want to flush? Actually, Hibernate flushes before every query but in this example, there is no query between persist() and clear() -> the DB update never happens. It's just a consequence how OR mappers work. In an ideal world, the objects would be linked to the database and if the transaction is rolled back, so are the objects (without having to load them again) but in the end, various problems with Java, OR-mappping and performance make that impractical.
Aaron Digulla

related questions