如何用JPA映射含双复合外键关联同一列的MOVEMENTS表
问题分析与解决方案
你的代码无法生成LOCATIONS对应外键的核心原因是:MOVEMENTS表的外键关联的是LOCATIONS的复合主键(ID, PROBLEM_NAME),但原写法只关联了LOCATIONS的ID字段,未关联PROBLEM_NAME字段,导致JPA无法识别完整的外键约束。
以下是完整的正确实体映射写法:
1. 定义Location的复合主键类
LOCATIONS表采用复合主键,需先创建嵌入式主键类:
@Embeddable public class LocationId implements Serializable { private String id; private String problemName; // JPA要求必须提供无参构造函数 public LocationId() {} public LocationId(String id, String problemName) { this.id = id; this.problemName = problemName; } // 复合主键必须重写equals和hashCode @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LocationId that = (LocationId) o; return Objects.equals(id, that.id) && Objects.equals(problemName, that.problemName); } @Override public int hashCode() { return Objects.hash(id, problemName); } // getter和setter public String getId() { return id; } public void setId(String id) { this.id = id; } public String getProblemName() { return problemName; } public void setProblemName(String problemName) { this.problemName = problemName; } }
2. Location实体类
@Entity @Table(name = "LOCATIONS") public class Location { @EmbeddedId private LocationId id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "PROBLEM_NAME", referencedColumnName = "NAME", insertable = false, updatable = false) private Problem problem; public Location() {} // getter和setter public LocationId getId() { return id; } public void setId(LocationId id) { this.id = id; } public Problem getProblem() { return problem; } public void setProblem(Problem problem) { this.problem = problem; } }
3. Problem实体类
@Entity @Table(name = "PROBLEMS") public class Problem { @Id @Column(name = "NAME") private String name; @Column(name = "AVG_COST") private Double avgCost; public Problem() {} // getter和setter public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getAvgCost() { return avgCost; } public void setAvgCost(Double avgCost) { this.avgCost = avgCost; } }
4. Movement实体类及复合主键
Movement复合主键类
@Embeddable public class MovementId implements Serializable { @Column(name = "START_LOC_ID") private String startLocId; @Column(name = "END_LOC_ID") private String endLocId; @Column(name = "PROBLEM_NAME") private String problemName; public MovementId() {} public MovementId(String startLocId, String endLocId, String problemName) { this.startLocId = startLocId; this.endLocId = endLocId; this.problemName = problemName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MovementId that = (MovementId) o; return Objects.equals(startLocId, that.startLocId) && Objects.equals(endLocId, that.endLocId) && Objects.equals(problemName, that.problemName); } @Override public int hashCode() { return Objects.hash(startLocId, endLocId, problemName); } // getter和setter public String getStartLocId() { return startLocId; } public void setStartLocId(String startLocId) { this.startLocId = startLocId; } public String getEndLocId() { return endLocId; } public void setEndLocId(String endLocId) { this.endLocId = endLocId; } public String getProblemName() { return problemName; } public void setProblemName(String problemName) { this.problemName = problemName; } }
Movement实体核心代码
@Entity @Table(name = "MOVEMENTS") public class Movement { @EmbeddedId private MovementId id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "PROBLEM_NAME", referencedColumnName = "NAME", insertable = false, updatable = false) private Problem problem; @ManyToOne(fetch = FetchType.LAZY) @JoinColumns({ @JoinColumn(name = "START_LOC_ID", referencedColumnName = "ID", insertable = false, updatable = false), @JoinColumn(name = "PROBLEM_NAME", referencedColumnName = "PROBLEM_NAME", insertable = false, updatable = false) }) private Location startLocation; @ManyToOne(fetch = FetchType.LAZY) @JoinColumns({ @JoinColumn(name = "END_LOC_ID", referencedColumnName = "ID", insertable = false, updatable = false), @JoinColumn(name = "PROBLEM_NAME", referencedColumnName = "PROBLEM_NAME", insertable = false, updatable = false) }) private Location endLocation; @Column(name = "DURATION") private int duration; public Movement() {} public Movement(MovementId id) { this.id = id; } // getter和setter public MovementId getId() { return id; } public void setId(MovementId id) { this.id = id; } public Problem getProblem() { return problem; } public void setProblem(Problem problem) { this.problem = problem; } public Location getStartLocation() { return startLocation; } public void setStartLocation(Location startLocation) { this.startLocation = startLocation; } public Location getEndLocation() { return endLocation; } public void setEndLocation(Location endLocation) { this.endLocation = endLocation; } public int getDuration() { return duration; } public void setDuration(int duration) { this.duration = duration; } }
关键注意点
- 所有复合主键类必须实现
Serializable接口,并重写equals和hashCode方法,这是JPA的强制要求。 - 关联Location时使用
@JoinColumns映射完整的复合外键(ID + PROBLEM_NAME),同时设置insertable = false, updatable = false,避免JPA重复操作PROBLEM_NAME字段(实际通过主键类维护该字段)。
内容的提问来源于stack exchange,提问作者Iulian Muntean
相关产品推荐
相关产品推荐

