双复合主键Tree与Node多对多关联的JPA中间表注解配置
JPA复合主键下Tree与Node的多对多关联实现方案
核心思路
由于Tree和Node均使用复合主键,无法直接使用JPA原生@ManyToMany注解,必须显式创建中间关联实体,通过@OneToMany + @ManyToOne的组合模拟多对多关系,中间表的主键由双方复合主键共同组成。
步骤1:创建中间关联实体及其复合主键
TreeNodeId(中间表复合主键)
该类封装Tree和Node的复合主键作为中间表的联合主键:
@Embeddable @AllArgsConstructor @NoArgsConstructor @Getter @Setter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class TreeNodeId implements Serializable { private TreeIdentifier treeIdentifier; private NodeIdentifier nodeIdentifier; @Override public int hashCode() { return Objects.hash(treeIdentifier, nodeIdentifier); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; TreeNodeId that = (TreeNodeId) obj; return Objects.equals(treeIdentifier, that.treeIdentifier) && Objects.equals(nodeIdentifier, that.nodeIdentifier); } private static final long serialVersionUID = 1L; }
TreeNode实体(中间表对应实体)
通过@ManyToOne关联Tree和Node,并用@MapsId映射到复合主键的对应部分:
@Entity @Table(name = "tree_node") @AllArgsConstructor @NoArgsConstructor @Getter @Setter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class TreeNode { @EmbeddedId private TreeNodeId id; @ManyToOne(fetch = FetchType.LAZY) @MapsId("treeIdentifier") @JoinColumns({ @JoinColumn(name = "tree_id", referencedColumnName = "tree_id"), @JoinColumn(name = "site_id", referencedColumnName = "site_id"), @JoinColumn(name = "prod_version", referencedColumnName = "prod_version") }) private Tree tree; @ManyToOne(fetch = FetchType.LAZY) @MapsId("nodeIdentifier") @JoinColumns({ @JoinColumn(name = "node_id", referencedColumnName = "node_id"), @JoinColumn(name = "node_version_id", referencedColumnName = "node_version_id") }) private Node node; // 可添加中间表自定义字段,比如关联时间 private LocalDateTime associationDate; }
步骤2:修改Tree实体添加关联集合
在Tree类中添加与TreeNode的一对多关联,并提供便捷操作方法:
@Entity @Table(name = "tree") @AllArgsConstructor @NoArgsConstructor @Getter @Setter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class Tree { @EmbeddedId private TreeIdentifier treeIdentifier; private LocalDateTime creationDate; @OneToMany(mappedBy = "tree", cascade = CascadeType.ALL, orphanRemoval = true) private List<TreeNode> treeNodes = new ArrayList<>(); // 便捷方法:添加Node关联 public void addNode(Node node) { TreeNode treeNode = new TreeNode(); TreeNodeId id = new TreeNodeId(this.treeIdentifier, node.getNodeIdentifier()); treeNode.setId(id); treeNode.setTree(this); treeNode.setNode(node); treeNode.setAssociationDate(LocalDateTime.now()); this.treeNodes.add(treeNode); node.getTreeNodes().add(treeNode); } // 便捷方法:移除Node关联 public void removeNode(Node node) { this.treeNodes.removeIf(tn -> tn.getNode().getNodeIdentifier().equals(node.getNodeIdentifier())); node.getTreeNodes().removeIf(tn -> tn.getTree().getTreeIdentifier().equals(this.treeIdentifier)); } }
步骤3:修改Node实体添加关联集合
在Node类中添加与TreeNode的一对多关联:
@Entity @Table(name = "node") @AllArgsConstructor @NoArgsConstructor @Getter @Setter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class Node { @EmbeddedId private NodeIdentifier nodeIdentifier; private Long nodePosition; @OneToMany(mappedBy = "node", cascade = CascadeType.ALL, orphanRemoval = true) private List<TreeNode> treeNodes = new ArrayList<>(); }
关键注解说明
- @MapsId:将关联实体的主键映射到当前实体嵌入式主键的指定属性,保证中间表主键与关联表主键一致性。
- @JoinColumns:针对复合主键,需逐个指定外键列与关联表主键字段的对应关系。
- fetch = FetchType.LAZY:默认懒加载避免性能损耗,可根据业务需求调整为
EAGER。 - cascade = CascadeType.ALL:对TreeNode的操作会级联到关联的Tree/Node,可根据场景调整为
PERSIST等细分类型。
内容的提问来源于stack exchange,提问作者BugsOverflow
相关产品推荐
相关产品推荐

