使用Hibernate 3.3.1GA无法获得多对多功能

时间:2009-05-26 16:35:00

标签: hibernate many-to-many

我有一个简单的多对多映射:

@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
{
    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 ());
}

测试失败。在日志中,我可以看到:

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

因此创建了正确的表,双主键是OK,外键是正确的。

坚持item并没有做正确的事情:

[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]

正如您所看到的,项目本身是持久的,但不是带有类别的集合(缺少对连接表的插入)。但是集合中有值!

读取项目时,它会查询连接表:

[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=[]

但当然,它找不到任何东西。怎么了?为什么hibernate会考虑find()的连接表而不考虑persist()的连接表?

我正在使用Hibernate 3.3.1,Hibernate Annotations 3.4.0

[编辑]为了解决这个问题,我试图引入双向映射。首先,我将此代码添加到Category

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

然后,我更改了测试以更新双方:

    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 */

我甚至再去坚持这些类别。结果:没什么。 Hibernate愉快地忽略了多对多映射。

作为第二个修复,我尝试按照zoidbeck的建议添加@JoinTable注释:

    @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> ();

再一次,没有。

2 个答案:

答案 0 :(得分:2)

原因是Hibernate中的优化:测试永远不会刷新会话。所有这一切都发生在Hibernate会记住它在内部缓存中需要做的更改,但由于事务永远不会被提交,所以它永远不会看到生成SQL来更新连接表的理由。

但为什么我会看到项目的插入呢?因为Hibernate需要其ID用于连接。所以它必须询问数据库“这个对象会得到什么ID?”。

解决方案是强制Hibernate刷新其缓存:

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

以下是完整的工作测试用例:

@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;
}

答案 1 :(得分:0)

据我所知,您必须完全指定链接表:

@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> ();
}

我不确定你需要以双向方式指定ManyToMany,但如果是这样,那么这将是另一方的映射:

@ManyToMany(mappedBy="categories")  // map info is in item class
private Set<Item> items;