Question

I have read the Hibernate Documentation regarding many-to-many relationships, and have attempted to follow their suggestions, but my solution has not been successful. Hopefully someone can shed some light on the topic.

I have a database structure in which I am trying to map a many-to-many relationship via Hibernate. The idea is that I have a number of Entities which may be in conflict with each other. Table-structure-wise, each of these Entities are similar, so I've abstracted them from a common persisted class, AbstractEntity. Since the Entities can be in conflict, and can be in conflict with any number of other Entities, I've defined a separate Conflict Hibernate Entity to define each of the Conflicts, which should get mapped via a many-to-many relationship to each of the Entities that are in that Conflict.

Here are the Entity declarations. I have included Event as an example of a Concrete implementation of the AbstractEntity Object.

My Classes and Hibernate Configuration:

@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractEntity {
    protected Set<Conflict> conflicts = null;

    protected AbstractEntity() {
        conflicts = new HashSet<Conflict>();
    }

    @ManyToMany(fetch = FetchType.LAZY,
            targetEntity = Conflict.class)
    @JoinTable(name = "conflict_affected_entity",
            joinColumns = { @JoinColumn(name = "affected_entity_id") }, 
            inverseJoinColumns = { @JoinColumn(name = "conflict_id") })
    public Set<Conflict> getConflicts() {
        Hibernate.initialize(conflicts);
        return conflicts;
    }

    public void setConflicts(Set<Conflict> conflicts) {
        this.conflicts.clear();
        this.conflicts.addAll(conflicts);
    }
}

@Entity
@Table(name = "event")
public class Event extends AbstractEntity {

    private String name;

    public Event() {
        super();
    }

    @Column(name = "name", nullable = false)
    public String getName() {
        return name;
    }

    public void setName(String text) {
        this.name = text;
    }
}

@Entity
@Table(name = "conflict")
public class Conflict {

    private Set<AbstractEntity> affectedEntities = null;

    public Conflict() {
        affectedEntities = new HashSet<AbstractEntity>();
    }

    @ManyToMany(fetch = FetchType.LAZY,
            targetEntity = AbstractEntity.class,
            mappedBy = "conflicts")
    public Set<AbstractEntity> getAffectedEntities() {
        Hibernate.initialize(affectedEntities);
        return affectedEntities;
    }

    public void setAffectedEntities(Set<AbstractEntity> affectedEntities) {
        this.affectedEntities.clear();
        this.affectedEntities.addAll(affectedEntities);
    }
}

In the code, I need to be able to create the joining table entries from either side (either adding an AbstractEntity to a Conflict or adding a Conflict to an AbstractEntity). An example of how I'm creating an example is shown here:

Event event1 = EventDAO.getInstance().get(eventID1);
Event event2 = EventDAO.getInstance().get(eventID2);
Conflict conflict = new Conflict();
conflict.getAffectedEntities().add(event1);
conflict.getAffectedEntities().add(event2);

Hibernate seems to have a clue as to what's going on, so I feel like I'm missing something simple. When I create a new Conflict, and add an AbstractEntity to it, the Conflict and AbstractEntity are created, but the joining table remains empty. Can anyone provide me a clue to what I need to do to make Hibernate fill in the blank joining table?

Was it helpful?

Solution 3

It turns out that the issue was not in the Hibernate annotations. Instead, the issue resided in how I was accessing the collections in the annotated methods.

As you can see in the question, the Collection setters clear the collection and then add any items in the new collection. The updated code (which works!) is shown here:

@ManyToMany(targetEntity = AbstractEntity.class,
        fetch = FetchType.LAZY)
@JoinTable(name = "conflict_affected_entity",
        joinColumns = { @JoinColumn(name = "conflict_id", referencedColumnName = "id") },
        inverseJoinColumns = { @JoinColumn(name = "affected_entity_id", referencedColumnName = "id") })
public Set<AbstractEntity> getAffectedEntities()
{
    Hibernate.initialize(affectedEntities);
    return affectedEntities;
}

public void setAffectedEntities(Set<AbstractEntity> affectedEntities)
{
    this.affectedEntities = affectedEntities;
}

and

@ManyToMany(targetEntity = Conflict.class,
        fetch = FetchType.LAZY)
@JoinTable(name = "conflict_affected_entity",
        joinColumns = { @JoinColumn(name = "affected_entity_id", referencedColumnName = "id") }, 
        inverseJoinColumns = { @JoinColumn(name = "conflict_id", referencedColumnName = "id") })
public Set<Conflict> getConflicts()
{
    Hibernate.initialize(conflicts);
    return conflicts;
}

public void setConflicts(Set<Conflict> conflicts)
{
    this.conflicts = conflicts;
}

For future viewers, this Hibernate configuration (mapping each side as a ManyToMany) creates two uni-directional associations: from Conflict -> AbstractEntities and from AbstractEntity -> Conflicts. This means that if you decide to use this configuration, you will have to be careful when adding or deleting items from the collections to make sure the joining table entries get updated to avoid foreign key constraint violations. For example, when deleting a Conflict, we can't just say ConflictDAO.getInstance.delete(toDelete). Instead, we have to make sure the Conflict doesn't retain any associations:

for (AbstractEntity affectedEntity : toDelete.getAffectedEntities()) {
    notifications.add(Notification.forUsersWithAccess(ActionType.UPDATE, affectedEntity));
    // Forcefully remove the associations from the affectedEntity to the Conflict, since we don't want to risk using CascadeType.DELETE
    affectedEntity.getConflicts().remove(toDelete);
}
ConflictDAO.getInstance().delete(toDelete);

OTHER TIPS

Based on the code you have shown, including how you are saving the data, the problem is with what entity owns the relationship. In your code, you have mapped the @ManyToMany in your AbstractEntity class, which means that the AbstractEntity and its subclasses are the ones that own the relationship, and are responsible for the database updates for the join table. In your Conflict class, you have correctly defined the many to many relationship, and by using the

mappedBy = "conflicts"

you have told Hibernate that this second definition of the many to many relationship actually refers to the many to many relationship that is already mapped by the conflicts attribute in the AbstractEntity class. Hence this second definition of the many to many relationship does not need to perform database updates, because the database updates are owned by the AbstractEntity.

In the code you have shown for saving the data, you have Event objects which are already persistent. You then create a new Conflict class, and add the relationships to this class. The problem is that when you persist this new class, Hibernate will not persist the relationship, because the many to many definition says that the Event objects own the database updates. Hence, to fix the problem, you can either change your mapping so that it is declared in the Conflict class, and the AbstractEntity declares the counterpart using the "mappedBy" attribute, or you can persist the Conflict classes, then define the relationships using the Event classes, and update them. Something like:

Event event1 = EventDAO.getInstance().get(eventID1);
Event event2 = EventDAO.getInstance().get(eventID2);
Conflict conflict = new Conflict();
session.save(conflict);
event1.getConflicts().add(conflict);
session.update(event1);
event2.getConflicts().add(conflict);
session.update(event2);

Do your tables have primary keys?

Change your @ManyToMany mapping as such:

@JoinTable(name = "conflict_affected_entity",
        joinColumns = { @JoinColumn(name = "affected_entity_id", referencedColumnName="primaryKeyOfAffectedEntityColumnName") }, 
        inverseJoinColumns = { @JoinColumn(name = "conflict_id", referencedColumnName="primaryKeyOfConflictColumnName") })
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top