Reputation: 328594
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.
Upvotes: 2
Views: 2094
Reputation: 328594
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;
}
Upvotes: 2
Reputation: 4151
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;
Upvotes: 0