我知道您想要一个代表多个不同实体标签的 tag 表而不是一个 tag 表 + 特定实体类型的连接表(post_tags、story_tags 等) ,这就是 JPA 在默认情况下映射单向一对多的方式。
在这种情况下,我相信 this 就是您要找的东西。
基本上有三种方法可以解决这个问题:
1。 @Where + @Any
>
使用@Where 限制Post.tags 集合中的匹配实体:
@Entity public class Post {
@Id
private String id;
@OneToMany
@Immutable
@JoinColumn(name = "resource_id", referencedColumnName = "id", insertable = false, updatable = false)
@Where(clause = "resource_type = 'post'")
private Collection<Tag> tags;
}
然后,在Tag 中使用@Any 来定义多目标关联:
@Entity public class Tag {
@Id
private Long id;
private String tag;
@CreationTimestamp
private Instant timeCreated;
@JoinColumn(name = "resource_id")
@Any(metaColumn = @Column(name = "resource_type"), optional = false, fetch = LAZY)
@AnyMetaDef(idType = "string", metaType = "string",
metaValues = {
@MetaValue(value = "post", targetEntity = Post.class),
@MetaValue(value = "story", targetEntity = Story.class),
})
private Object resource;
}
将新的Tag 添加到Post 很简单,只需将Post 分配给Tag.resource 属性(故事和所有其他“可标记”实体也是如此)
(请注意,您可能想要添加一个基类/标记接口,如 Taggable 并使用它而不是 Object 来限制可以分配给 Tag.resource 属性的类型。它应该可以工作,但我没有没有测试过,所以我不是 100% 确定)
2。 @Where + Tag 中的显式连接列映射
对Post 使用与以前相同的方法,并将resource_id 和resource_type 列映射为显式属性:
@Entity public class Tag {
@Id
private Long id;
private String tag;
@CreationTimestamp
private Instant timeCreated;
@Column(name = "resource_id")
private String resourceId;
private String resourceType;
}
现在创建新的Tag 需要您自己填充resourceId 和resourceType。如果您想将 Post 和 Tag 视为单独的聚合根,这种方法很有意义,否则它非常麻烦且容易出错,因为 Hibernate 无法帮助您确保一致性,您需要自己管理。
3。继承+mappedBy
>
使用单一继承策略为帖子标签、故事标签等创建单独的实体,并将resource_type 列视为鉴别器值:
@Entity
@Inheritance(strategy = SINGLE_TABLE)
@DiscriminatorColumn(name = "resource_type")
public abstract class Tag {
@Id
private Long id;
private String tag;
@CreationTimestamp
private Instant timeCreated;
}
@Entity
@DiscriminatorValue("post")
public class PostTag extends Tag {
@JoinColumn(name = "resource_id")
@ManyToOne(optional = false, fetch = LAZY)
private Post post;
}
@Entity
@DiscriminatorValue("story")
public class StoryTag extends Tag {
@JoinColumn(name = "resource_id")
@ManyToOne(optional = false, fetch = LAZY)
private Story story;
}
此解决方案的优势在于,在“可标记”实体中,您不再需要拥有 @OneToMany 关联的“假”,而是可以使用 mappedBy:
@Entity public class Post {
@Id
private String id;
@OneToMany(mappedBy = "post")
private Collection<PostTag> tags;
}
@Entity public class Story {
@Id
private String id;
@OneToMany(mappedBy = "story")
private Collection<StoryTag> tags;
}
添加新的Tag 也被简化(想要一个新的帖子标签?创建一个PostTag 对象。想要一个新的故事标签?创建一个StoryTag 对象)。此外,如果您想切换到使用 Post.tags 关联(即单向一对多)管理 Tags,这种方法将是最容易转换的。
(请注意,在这种情况下,您当然不能依赖 Hibernate 生成模式,因为它会尝试在指向所有候选表的 resource_id 列上创建 FK 约束)
我创建了一个github repo,所有三种方法都表示为单独的提交。对于每种方法,都有一个测试证明它确实有效。请注意,所有三种方案的数据库结构都是相同的。
(作为旁注,我现在才注意到表定义的PRIMARY KEY (resource_type, namespace_id, tag) 部分,所以我不得不问:您确实理解这个问题是在考虑一对多关联的情况下提出和回答的,并且不是多对多,对吧?
我问是因为有了这样的 PK 定义,最多一个 post 可以有一个 tag 与 tag 列的给定值 - 当然对于给定的 namespace_id。我假设这是一个错字,而您真正想要的是 PRIMARY KEY(id) 加上 UNIQUE(resource_type, resource_id, namespace_id, tag))