JPA 2.0带有额外列的多对多关系

39

我正在尝试在JPA 2.0(JBoss 7.1.1)中建立一个带有额外列(在下面的粗体中)的ManyToMany关系,例如:

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

我不想有重复的属性,所以我想采用在http://giannigar.wordpress.com/2009/09/04/mapping-a-many-to-many-join-table-with-extra-column-using-jpa/中介绍的第二种解决方案。但是我无法让它正常工作,我收到了多个错误消息,例如:

  1. 嵌入式ID类不应包含关系映射(实际上规范也是这样说的);
  2. 在属性“employerDeliveryAgent”中,“mapped by”值“pk.deliveryAgent”无法解析为目标实体上的属性;
  3. 在属性“employerDeliveryAgent”中,“mapped by”值“pk.employer”无法解析为目标实体上的属性;
  4. 不能解析覆盖属性“pk.deliveryAgent”的持久类型;
  5. 不能解析覆盖属性“pk.employer”的持久类型。

那个链接上的很多人都说它运行得很好,所以我想我的环境可能有所不同,也许是JPA或Hibernate版本。因此,我的问题是:如何使用JPA 2.0(Jboss 7.1.1 / 使用Hibernate作为JPA实现)实现这样的场景?为了补充这个问题:我应该避免使用复合键,而是使用普通生成的ID和唯一约束吗?

提前感谢。

注意:我没有在这里复制我的源代码,因为它与上面链接中的代码本质上是相同的,只是类和属性名称不同,所以我认为没有必要。


优雅的解决方案:https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/ - Ismail Yavuz
3个回答

82

来自Eric LucioRenan的回答都有所帮助,但他们在关联表中使用id是多余的。在类中已经有了关联的实体和它们的id,这不是必需的。你可以简单地通过在关联实体字段上使用@Id来映射关联类中的关联实体。

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

关联类
@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;
}

仍需要关联的 PK 类。请注意,字段名称应该与关联类中的字段名称完全对应,但类型应该是关联类型中 id 的类型。

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

    // getters/setters and most importantly equals() and hashCode()
}

2
这是你可以得到的最干净的解决方案,做得好。 - FrancescoM
12
任何人都可以提供插入或删除的示例。 - VK321
非常酷,谢谢谢谢。问题是,如果我在关联类(例如您的EmployerDeliveryAgent示例中)中也放置一个生成的@id列,则会得到NPE。但是我想要一个基于RDBMS的长PK……你怎么说?有没有办法做到这一点?放弃我的替代键? - tom
如何通过额外的字段ID绑定实体,而不是“boolean isProjectLead”? - Zon
2
为什么需要id类? - Abbas Torabi
显示剩余4条评论

64

首先,您需要生成一个 EmployerDeliveryAgentPK 类,因为它具有多个主键:

@Embeddable
public class EmployerDeliveryAgentPK implements Serializable {

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

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

接下来,您需要创建一个EmployerDeliveryAgent类。这个类表示EmployerDeliveryAgent之间的多对多关系:

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

在此之后,在雇主类中,您需要添加:

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

DeliveryAgent 类中,您需要添加:

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

这就是全部!祝好运!


非常感谢,我浪费了两天时间来解决这个问题。 - Paolo
我认为在DeliveryAgent类中,我们应该映射EmployerDeliveryAgent类而不是Employer类。因此,在DeliveryAgent类中的代码应该像这样:private Set<EmployerDeliveryAgent> employerDeliveryAgent = new HashSet<EmployerDeliveryAgent>();,与Employer类中相同。 - Selcuk
需要覆盖 equals()hashcode() 吗?(假设我不打算将持久化类的实例放入 Set 中,并且打算使用分离实例的重新附加) - user3529850
不需要覆盖equals()和hashCode()方法。同时确保实例化@EmbeddedId属性,否则会出现NPE:private EmployerDeliveryAgentPK id = new EmployerDeliveryAgentPK(); - maxeh
嗨,当我运行内连接抓取查询时,会抛出“集合已被清除”的错误。 - withoutOne

14

好的,根据此处提供的解决方案,我已经使其运作。

这个解决方案不会在数据库中生成重复的属性,但是却会在我的JPA实体中生成重复的属性(这很可接受,因为您可以将额外的工作转移给构造函数或方法 - 这最终会变得透明)。 数据库中生成的主键和外键是100%正确的。

正如链接中所述,我无法使用@PrimaryKeyJoinColumn,而是使用了@ JoinColumn(name =“projectId”,updatable = false,insertable = false,referencedColumnName =“id”)。 还有一件值得一提的事情:我必须使用EntityManager.persist(association),这在链接示例中是缺失的。

因此,我的最终解决方案是:

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

}

你好,关于你的工具,addEmployee(和removeEmployee,可能为了简洁而省略)。 是一个实体的责任去创建另一个实体或服务层吗?如果联接表中有多个额外的列怎么办? - Arash
Arash,多个额外的列是一样的,只需添加另一个字段,如isProjectLead。一个实体可以创建另一个实体,这取决于您的架构是否适合,但这超出了此问题的范围,如果是这种情况,我建议您提出新的问题。 - Renan
谢谢Renan,很抱歉我的问题不是很清楚,我英语不太好。我想说我认为最好在ProjectAssociationService之类的地方创建ProjectAssociation实例并将其传递给实用方法。您怎么看?我不知道这是否适合作为新问题。 - Arash
@Arash 这些实用方法旨在维护关联的JPA/Hibernate实体之间的数据一致性,因此将它们保留在实体类中似乎是一个完美的做法。 - Shahin
是的,@Shahin,你说得对。这些方法应该在实体中定义,但我所说的是中介实体(与联接表相关联)应该在相应的服务对象中创建,然后传递给实用方法,因为中介实体可能有5个或更多的实例变量,当您想要传递4个或5个以上的参数时,将它们包装在一个对象中并传递该对象是一个好习惯。那么最好在服务对象中创建中介实体。 - Arash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接