6

I have an issue with an hibernate many-to-many relation: when I remove one item from my set, it is not removed in my database. I know there are tons of similar issues, but I did not succeed in fixing mine by reading them.

I have written a JUnit test case for it. My association is between Buildings and Users:

@Test
public void testBuildingManyToMany(){
    //Create 2 buildings
    Building building = createBuilding("b1");
    Building building2 = createBuilding("b2");
    //Create 1 user
    User user = createUser("u1");

    //Associate the 2 buildings to that user
    user.getBuildings().add(building);
    building.getUsers().add(user);

    user.getBuildings().add(building2);
    building2.getUsers().add(user);

    userController.save(user);
    user = userController.retrieve(user.getId());
    Assert.assertEquals(2, user.getBuildings().size());//Test OK

    //Test 1: remove 1 building from the list
    user.getBuildings().remove(building);
    building.getUsers().remove(user);
    userController.save(user);

    //Test 2: clear and add
    //user.getBuildings().clear();
    //user.getBuildings().add(building);
    //userController.save(user);
    //user = userController.retrieve(user.getId());
    //Assert.assertEquals(1, user.getBuildings().size());
}

Here is the error I got:

...
Hibernate: insert into building_useraccount (userid, buildingid) values (?, ?)
Hibernate: insert into building_useraccount (userid, buildingid) values (?, ?)
Hibernate: delete from building_useraccount where userid=? and buildingid=?
Hibernate: insert into building_useraccount (userid, buildingid) values (?, ?)
4113 [main] WARN org.hibernate.util.JDBCExceptionReporter - SQL Error: 23505, SQLState: 23505
4113 [main] ERROR org.hibernate.util.JDBCExceptionReporter - Unique index or primary key violation: "PRIMARY_KEY_23 ON PUBLIC.BUILDING_USERACCOUNT(BUILDINGID, USERID) VALUES ( /* key:0 */ 201, 201)"; SQL statement:
insert into building_useraccount (userid, buildingid) values (?, ?) [23505-176]

When I comment the "Test 1" and uncomment the "Test 2" lines, I go the following error:

junit.framework.AssertionFailedError: 
Expected :1
Actual   :2

Here are my hbm.xml classes:

<hibernate-mapping default-lazy="true">
    <class name="my.model.pojo.Building" table="building">
    <cache usage="read-write" />
    <id name="id" column="id" type="java.lang.Long">
        <generator class="sequence">
            <param name="sequence">building_id_sequence</param>
        </generator>
    </id>
    <property name="name" type="java.lang.String" column="name" not-null="true" />
    ...
    <set name="users" cascade="none" lazy="true" inverse="true" table="building_useraccount">
        <key column="buildingid" />
        <many-to-many class="my.model.pojo.User" column="userid" />
    </set>
</class>
</hibernate-mapping>

and

<hibernate-mapping default-lazy="true">
<class name="my.model.pojo.User" table="useraccount">
    <cache usage="read-write" />
    <id name="id" column="id" type="java.lang.Long">
        <generator class="sequence">
            <param name="sequence">useraccount_id_sequence</param>
        </generator>
    </id>
    <property name="login" type="java.lang.String" column="login" not-null="true" unique="true" length="40" />

    ...
    <set name="buildings" cascade="none" lazy="false" fetch="join" table="building_useraccount">
        <key column="userid" />
        <many-to-many class="my.model.pojo.Building" column="buildingid" />
    </set>
</class>
</hibernate-mapping>

and the classes

public class User implements Serializable, Identifiable {

private static final long serialVersionUID = 1L;
private int hashCode;

private Long id;
private String login;

private Set<Building> buildings = new HashSet<Building>();

public boolean equals(Object value) {
    if (value == this)
        return true;
    if (value == null || !(value instanceof User))
        return false;
    if (getId() != null && getId().equals(((User) value).getId()))
        return true;
    return super.equals(value);
}

public int hashCode() {
    if (hashCode == 0) {
        hashCode = (getId() == null) ? super.hashCode() : new HashCodeBuilder().append(getId()).toHashCode();
    }
    return hashCode;
}

/* Getter / Setter ... */

and

public class BuildingBase implements Serializable, Identifiable {

private static final long serialVersionUID = 1L;
private int hashCode;

private Long id;
private String name;

private Set<User> users = new HashSet<User>();

public boolean equals(Object value) {
    if (value == this)
        return true;
    if (value == null || !(value instanceof Building))
        return false;
    if (getId() != null && getId().equals(((Building) value).getId()))
        return true;
    return super.equals(value);
}

public int hashCode() {
    if (hashCode == 0) {
        hashCode = (getId() == null) ? super.hashCode() : new HashCodeBuilder().append(getId()).toHashCode();
    }
    return hashCode;
}

/* Getter / Setter ... */

EDIT: Add userController implementation, for the transaction

@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
public User save(User user) throws ServiceException {
    validate(user);//Validation stuffs
    return userDAO.update(user);
}

The userDAO:

public class UserDAOImpl extends HibernateDAOImpl<User> implements UserDAO {
}

And the HibernateDAOImpl:

public class HibernateDAOImpl<T> implements DAO<T> {

    public T update(T entity) {
        return executeAndCreateSessionIfNeeded(new HibernateAction<T>() {
            @Override
            public T execute(Session session) {
                return (T) session.merge(entity);
            }
        });
    }

    protected <E> E executeAndCreateSessionIfNeeded(HibernateAction<E> action) {
        Session session = null;
        try {
            session = sessionFactory.getCurrentSession();
            return executeAction(action, session);
        } finally {
            if (session != null) {
                session.close();
            }
        }
    }

}
Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
Asterius
  • 2,180
  • 2
  • 19
  • 27
  • Please post the implementation of `userController.save` method. Also, what are the transaction boundaries? – Dragan Bozanovic Dec 20 '15 at 18:19
  • I added some implementations. The transaction works quite well, since it is used everywhere in the code successfully. Note also that clearing the buildings (with user.getBuildings().clear()) also works and empty my many-to-many database table! Just the removal is strangely not working... – Asterius Dec 20 '15 at 22:39

5 Answers5

12

The CascadeType.REMOVE doesn't make sense for many-to-many associations because when set on both sides it could trigger a chain deletion between parents and children and back to parents. If you only set it on the parent side, you could bump into issues when a deleting child is still referenced by some other parents.

To quote the Hibernate docs:

It does not usually make sense to enable cascade on a many-to-one or many-to-many association. In fact the @ManyToOne and @ManyToMany don't even offer a orphanRemoval attribute. Cascading is often useful for one-to-one and one-to-many associations.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
1

Why cascade="none"?

You should use cascade="detached,merge,refresh,persist" (not delete !) instead to update removals in collections.

Felix
  • 17
  • 6
1

Replacing cascade='none' by cascade='all' on the buildings relationship defined on the user should fix the problem.

Since you are saving the user, in order to also update the many-to-many in the DB, you need to cascade the changes on the relationship from the user.

Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117
0

I am afraid that what you are doing is not really a good idea with hibernate even if it is one of the more usual task you would do with a relationship. The way to achieve what you want is using cascades but as Vlad Mihalcea says, this can end up deleting one or the other end of the relationship and not only the relationship itself.

As a proper response I would tell you what a teacher would say... Do you really have a n:m relationship? Are you sure it doesn't have entity by itself? N:M relationships are very rare to find and usually means that the modeling is wrong. Even when this is not the case and you actually have an n:m, this should stay in the model, never forget that you are using an ORM to link the ACTUAL model to your java model so you can actually have an entity in Java with 1:n relationships on each end and store it in the relationship table.

Best regards!

Koalk
  • 59
  • 1
  • 10
0

Changing the cascade property has not fixed my issue. I finally decide to handle the many-to-many relationship myself by creating an object for the intermediate table, and manage it by my own. It is a bit more of code, but provides a consistent behavior for what I wanted to achieve.

Asterius
  • 2,180
  • 2
  • 19
  • 27