39

I'm trying to do a ManyToMany relationship in JPA 2.0 (JBoss 7.1.1) with an extra column (in bold, below) in the relationship, like:

Employer           EmployerDeliveryAgent             DeliveryAgent
(id,...)   (employer_id, deliveryAgent_id, **ref**)  (id,...)

I wouldn't like to have duplicate attributes, so I would like to apply the second solution presented in http://giannigar.wordpress.com/2009/09/04/mapping-a-many-to-many-join-table-with-extra-column-using-jpa/ . But I can't get it to work, I get several errors like:

  1. Embedded ID class should not contain relationship mappings (in fact the spec says so);
  2. In attribute 'employerDeliveryAgent', the "mapped by" value 'pk.deliveryAgent' cannot be resolved to an attribute on the target entity;
  3. In attribute 'employerDeliveryAgent', the "mapped by" value 'pk.employer' cannot be resolved to an attribute on the target entity;
  4. Persistent type of override attribute "pk.deliveryAgent" cannot be resolved;
  5. Persistent type of override attribute "pk.employer" cannot be resolved;

Many people on that link said that it worked fine, so I suppose something is different in my environment, perhaps JPA or Hibernate version. So my question is: how do I achieve such scenario with JPA 2.0 (Jboss 7.1.1 / using Hibernate as JPA implementation)? And to complement that question: should I avoid using composite keys and instead use plain generated id and a unique constraint?

Thanks in advance.

Obs.: I didn't copy my source code here because it is essentially a copy of the one at the link above, just with different classes and attributes names, so I guess it is not necessary.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
Renan
  • 1,705
  • 2
  • 15
  • 32
  • Elegant solution: https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/ – Ismail Yavuz Dec 03 '20 at 12:05

3 Answers3

82

Both answers from Eric Lucio and Renan helped, but their use of the ids in the association table is redundant. You have both the associated entities and their ids in the class. This is not required. You can simply map the associated entity in the association class with the @Id on the associated entity field.

@Entity
public class Employer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @OneToMany(mappedBy = "employer")
    private List<EmployerDeliveryAgent> deliveryAgentAssoc;

    // other properties and getters and setters
}

@Entity
public class DeliveryAgent {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @OneToMany(mappedBy = "deliveryAgent")
    private List<EmployerDeliveryAgent> employerAssoc;

    // other properties and getters and setters
}

The association class

@Entity
@Table(name = "employer_delivery_agent")
@IdClass(EmployerDeliveryAgentId.class)
public class EmployerDeliveryAgent {
    
    @Id
    @ManyToOne
    @JoinColumn(name = "employer_id", referencedColumnName = "id")
    private Employer employer;
    
    @Id
    @ManyToOne
    @JoinColumn(name = "delivery_agent_id", referencedColumnName = "id")
    private DeliveryAgent deliveryAgent;
    
    @Column(name = "is_project_lead")
    private boolean isProjectLead;
}

Still need the association PK class. Notice the field names should correspond exactly to the field names in the association class, but the types should be the type of the id in the associated type.

public class EmployerDeliveryAgentId implements Serializable {
    
    private int employer;
    private int deliveryAgent;

    // getters/setters and most importantly equals() and hashCode()
}
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • 2
    This is the cleanest solution you can get, well done. – FrancescoM Feb 03 '18 at 14:37
  • 12
    anyone can provide insert or delete example too. – VK321 Oct 21 '18 at 07:37
  • Very cool, thank you thank you. Question... ...i get a NPE if i also put a generated @id column in the association class (EmployerDeliveryAgent in your example). But I want a RDBMS long PK... what say you? is there a way to do it? screw my surrogate key? – tom Nov 20 '18 at 18:42
  • How to bind an entity by an extra field id, instead of a `boolean isProjectLead`? – Zon Jul 10 '19 at 17:01
  • Would also like to see a insert or update example as this part is not working for me. See https://stackoverflow.com/questions/61610701/jpa-2-0-many-to-many-with-extra-column-update-collection – isADon May 05 '20 at 10:51
  • 2
    why need the id class? – Abbas Torabi Jul 23 '20 at 09:45
  • I get infinite recursion – farahm May 27 '21 at 20:42
  • How would orphanRemoval be implemented from both sides? – farahm May 27 '21 at 21:11
  • May I ask how to access extra properties defined in the association class like `isProjectLead` from either of `Employer` and `DeliveryAgent`? Thanks – Yufeng Guo Mar 20 '22 at 00:54
64

First of all You need to generate a EmployerDeliveryAgentPK class because It has a multiple PK:

@Embeddable
public class EmployerDeliveryAgentPK implements Serializable {

    @Column(name = "EMPLOYER_ID")
    private Long employer_id;

    @Column(name = "DELIVERY_AGENT_ID")
    private Long deliveryAgent_id;
}

Next, You need to create a EmployerDeliveryAgent class. This class represent the relation many to many between Employer and DeliveryAgent:

@Entity
@Table(name = "EmployerDeliveryAgent")
public class EmployerDeliveryAgent implements Serializable {

    @EmbeddedId
    private EmployerDeliveryAgentPK id;

    @ManyToOne
    @MapsId("employer_id") //This is the name of attr in EmployerDeliveryAgentPK class
    @JoinColumn(name = "EMPLOYER_ID")
    private Employer employer;

    @ManyToOne
    @MapsId("deliveryAgent_id")
    @JoinColumn(name = "DELIVERY_AGENT_ID")
    private DeliveryAgent deliveryAgent;    
}

After that, in Employer class You need to add:

    @OneToMany(mappedBy = "deliveryAgent")
    private Set<EmployerDeliveryAgent> employerDeliveryAgent = new HashSet<EmployerDeliveryAgent>();

And in DeliveryAgent class You need to add:

    @OneToMany(mappedBy = "employer")
    private Set<EmployerDeliveryAgent> employer = new HashSet<EmployerDeliveryAgent>();

This is all! Good luck!!

Anil Bharadia
  • 2,760
  • 6
  • 34
  • 46
Erik Lucio
  • 948
  • 7
  • 8
  • Thank you very much, I wasted two days tryng to figure out this problem. – Paolo Mar 28 '17 at 09:58
  • I think in the `DeliveryAgent` class we should map `EmployerDeliveryAgent` class instead of `Employer` class. So, the code in the `DeliveryAgent` class should look like `private Set employerDeliveryAgent = new HashSet();`, same as in the `Employer` class. – Selcuk Oct 04 '17 at 07:10
  • Is it required to override `equals()` and `hashcode()` ? (assumimng I will not `intend to put instances of persistent classes in a Set AND intend to use reattachment of detached instances ` ) – user3529850 Oct 29 '18 at 21:13
  • It is not required to override equals() and hashCode(). Also make sure to instantiate the @EmbeddedId property, as you will get a NPE otherwise: private EmployerDeliveryAgentPK id = new EmployerDeliveryAgentPK(); – maxeh Oct 20 '21 at 13:07
  • Hi, when I run a inner join fetch query this error throwing "collection was evicted". – withoutOne Mar 18 '23 at 10:06
13

OK, I got it working based on the solution available at

http://en.wikibooks.org/wiki/Java_Persistence/ManyToMany#Mapping_a_Join_Table_with_Additional_Columns.

This solution does not generate duplicate attributes on the database, but it does generate duplicate attributes in my JPA entities (which is very acceptable, since you can relay the extra work to a constructor or method - it ends up being transparent). The primary and foreign keys generated in the database are 100% correct.

As stated on the link, I couldn't use @PrimaryKeyJoinColumn and instead used @JoinColumn(name = "projectId", updatable = false, insertable = false, referencedColumnName = "id"). Another thing worth mentioning: I had to use EntityManager.persist(association), which is missing on the example at the link.

So my final solution is:

@Entity
public class Employee {
  @Id
  private long id;
  ...
  @OneToMany(mappedBy="employee")
  private List<ProjectAssociation> projects;
  ...
}
@Entity
public class Project {

  @PersistenceContext
  EntityManager em;

  @Id
  private long id;
  ...
  @OneToMany(mappedBy="project")
  private List<ProjectAssociation> employees;
  ...
  // Add an employee to the project.
  // Create an association object for the relationship and set its data.
  public void addEmployee(Employee employee, boolean teamLead) {
    ProjectAssociation association = new ProjectAssociation();
    association.setEmployee(employee);
    association.setProject(this);
    association.setEmployeeId(employee.getId());
    association.setProjectId(this.getId());
    association.setIsTeamLead(teamLead);
    em.persist(association);

    this.employees.add(association);
    // Also add the association object to the employee.
    employee.getProjects().add(association);
  }
}
@Entity
@Table(name="PROJ_EMP")
@IdClass(ProjectAssociationId.class)
public class ProjectAssociation {
  @Id
  private long employeeId;
  @Id
  private long projectId;
  @Column(name="IS_PROJECT_LEAD")
  private boolean isProjectLead;
  @ManyToOne
  @JoinColumn(name = "employeeId", updatable = false, insertable = false,
          referencedColumnName = "id")

  private Employee employee;
  @ManyToOne
  @JoinColumn(name = "projectId", updatable = false, insertable = false,
          referencedColumnName = "id")

  private Project project;
  ...
}
public class ProjectAssociationId implements Serializable {

  private long employeeId;

  private long projectId;
  ...

  public int hashCode() {
    return (int)(employeeId + projectId);
  }

  public boolean equals(Object object) {
    if (object instanceof ProjectAssociationId) {
      ProjectAssociationId otherId = (ProjectAssociationId) object;
      return (otherId.employeeId == this.employeeId) 
              && (otherId.projectId == this.projectId);
    }
    return false;
  }

}
Sofia Paixão
  • 309
  • 2
  • 16
Renan
  • 1,705
  • 2
  • 15
  • 32
  • Hi, about your utilities, **`addEmployee`** (and removeEmployee, maybe omitted for brevity). Is it one entity responsibility to create another entity or service layer? What if there are more than one extra column for the join table? – Arash Sep 09 '21 at 06:41
  • Arash, more than one extra column is just the same, just add another field such as isProjectLead. One entity can create another, it depends on your architecture whether this is a good idea or not but it's a bit out of scope for this question, I recommend you start a new question if that's the case. – Renan Sep 09 '21 at 18:02
  • Thanks Renan, Sorry about my question, wasn't clear; I'm not good at English. I wanted to say i think it's better to create `ProjectAssociation` instance some where like in the `ProjectAssociationService` and pass it to the utility method. What you think? and i don't know if this is suitable for a new question. – Arash Sep 11 '21 at 05:02
  • @Arash Such utility methods are meant to maintain data consistency between associated JPA/Hibernate entities, so keeping them in entity classes seems a perfect practice. – Shahin Dec 29 '21 at 16:36
  • Yes your right @Shahin. Those methods should be defined in The entity But i was talking about the intermediary entity (associated with the join table) that should be created in the corresponding service object and then passed to the utility method because the intermediary entity may have 5 or more instance variables and when you want to pass more than 4 or 5 parameters, it's a good practice that wrap them in an object and pass that object. Well then it's better to create the intermediary entity in the service object. – Arash Dec 29 '21 at 17:17