Hibernate中同一列关联双外键的文件夹版本建模问题
我正在开发一款支持文件夹树的简易版本控制系统应用,用户可以创建文件夹树,满意后保存版本并基于新版本继续编辑(类似Git等版本控制系统的逻辑)。当前的数据库设计如下:
CREATE TABLE IF NOT EXISTS folder ( id INT NOT NULL AUTO_INCREMENT, updated_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (id) ); CREATE TABLE IF NOT EXISTS folder_version ( id INT NOT NULL AUTO_INCREMENT, version INT NOT NULL, folder_id INT NOT NULL, name VARCHAR(255) NOT NULL, updated_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (id), UNIQUE KEY (folder_id, version), FOREIGN KEY (folder_id) REFERENCES folder (id) ); CREATE TABLE IF NOT EXISTS folder_relation_version ( id INT NOT NULL AUTO_INCREMENT, parent_id INT NOT NULL, child_id INT NOT NULL, version INT NOT NULL, updated_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (id), UNIQUE KEY (parent_id, child_id, version), FOREIGN KEY (parent_id, version) REFERENCES folder_version (folder_id, version), FOREIGN KEY (child_id, version) REFERENCES folder_version (folder_id, version) );
这个设计的核心目的是禁止跨版本的FolderVersion关联:只允许同版本的文件夹关联(比如Folder_A(version=0)关联Folder_B(version=0)),不允许不同版本的文件夹关联(比如Folder_A(version=0)关联Folder_B(version=1))。
但我在为folder_relation_version表创建Hibernate实体时遇到了问题,因为version列同时被用于两个外键约束。我的尝试代码如下:
@Getter @Setter @SuperBuilder @ToString @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table(name = "folder_relation_version") public class FolderRelationVersion extends BaseEntity { @Column(name = "version") private Integer version; @ManyToOne @JoinColumns({ @JoinColumn(name = "parent_id", referencedColumnName = "folder_id", insertable = false, updatable = false), @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false) }) private FolderVersion parent; @ManyToOne @JoinColumns({ @JoinColumn(name = "child_id", referencedColumnName = "folder_id", insertable = false, updatable = false), @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false) }) private FolderVersion child; }
请问能否在Hibernate中实现该场景?或者有没有其他方式可以保障仅同版本的FolderVersion能建立关联?
解决方案
1. 调整Hibernate实体映射,解决重复列关联问题
你的核心问题是version列被两个@JoinColumn引用,同时又作为独立字段存在,导致Hibernate无法处理重复的列映射。可以通过以下方式调整:
方式一:移除独立的version字段,通过关联对象获取版本号
既然parent和child的版本号必须一致,你可以直接从parent或child对象中获取版本号,不需要在FolderRelationVersion中单独存储:
@Getter @Setter @SuperBuilder @ToString @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table(name = "folder_relation_version") public class FolderRelationVersion extends BaseEntity { @ManyToOne @JoinColumns({ @JoinColumn(name = "parent_id", referencedColumnName = "folder_id"), @JoinColumn(name = "version", referencedColumnName = "version") }) private FolderVersion parent; @ManyToOne @JoinColumns({ @JoinColumn(name = "child_id", referencedColumnName = "folder_id"), @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false) }) private FolderVersion child; // 提供获取版本号的方法 public Integer getVersion() { return parent != null ? parent.getVersion() : null; } }
这里把parent的两个关联列设为可插入/更新,child的version列设为不可插入/更新,确保版本号由parent统一控制,同时数据库的外键约束会自动校验child的版本号是否和parent一致。
方式二:使用复合主键优化映射
如果希望明确管理版本号,可以将parent_id、child_id和version作为复合主键,同时关联到FolderVersion:
首先定义复合主键类:
@Embeddable public class FolderRelationVersionId implements Serializable { private Integer parentId; private Integer childId; private Integer version; // 构造方法、equals、hashCode、getter/setter实现 }
然后修改实体类:
@Getter @Setter @SuperBuilder @ToString @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table(name = "folder_relation_version") public class FolderRelationVersion extends BaseEntity { @EmbeddedId private FolderRelationVersionId id; @ManyToOne @JoinColumns({ @JoinColumn(name = "parent_id", referencedColumnName = "folder_id", insertable = false, updatable = false), @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false) }) private FolderVersion parent; @ManyToOne @JoinColumns({ @JoinColumn(name = "child_id", referencedColumnName = "folder_id", insertable = false, updatable = false), @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false) }) private FolderVersion child; }
这种方式通过复合主键明确约束了关联的唯一性,同时数据库的外键约束依然生效,确保跨版本关联无法插入。
2. 保留现有设计,通过业务层+数据库约束双重保障
如果不想调整实体映射,你可以:
- 保留数据库的外键约束(这已经能从底层阻止跨版本关联)
- 在业务层新增校验逻辑:创建关联时,强制检查
parent和child的版本号是否一致,不一致则抛出异常
这种方式简单直接,数据库约束作为最后一道防线,业务层提前拦截错误请求。
3. 优化数据库设计(可选)
如果希望简化映射,可以考虑修改folder_relation_version表:
- 移除单独的
version列 - 改为通过
parent_id关联到folder_version的id,同时在业务层或数据库触发器中校验parent和child的版本号一致
但这种方式会丢失直接的版本号外键约束,需要额外的触发器或业务逻辑来保障规则,不如原设计严谨。
内容的提问来源于stack exchange,提问作者AmazingSchnitzel

