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

时间:2023-02-02 00:20:35

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.

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

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()?

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

I'm using Hibernate 3.3.1, Hibernate Annotations 3.4.0

我正在使用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:

[编辑]为了解决这个问题,我尝试引入双向映射。首先,我将此代码添加到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.

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

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

作为第二个修复,我尝试按照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> ();

Again, nothing.

2 个解决方案

#1


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.

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

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?".

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

The solution is to force Hibernate to flush its cache:

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

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

#2


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

据我所知,你必须完全指定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,但如果是这样,这将是另一方的映射:

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

#1


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.

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

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?".

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

The solution is to force Hibernate to flush its cache:

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

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

#2


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

据我所知,你必须完全指定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,但如果是这样,这将是另一方的映射:

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