【问题标题】:OneToMany relation where the `many` side can join with more than one entityOneToMany 关系,其中“多”方可以与多个实体连接
【发布时间】:2020-12-04 14:31:36
【问题描述】:

我想要一个能够将标签应用于各种实体的Tag 表。在 SQL 中它看起来像这样:

CREATE TABLE tag (
  id number GENERATED ALWAYS AS IDENTITY NOT NULL,
  resource_type varchar2(64) NOT NULL,
  resource_id varchar2(256),
  namespace_id varchar2(256),
  tag varchar2(128),
  time_created timestamp with time zone NOT NULL,
  PRIMARY KEY (resource_type, namespace_id, tag),
  CHECK (resource_type in ('post', 'story'))
);

如果resource_typepost,则resource_id 旨在加入Post 表的id 字段(同样适用于Story)。 (namespace_id 字段之所以存在,是因为虽然允许两个 Posts 具有相同的标签字符串,但我的所有实体都分组到命名空间中,并且同一命名空间中的两个实体不能具有相同的标签。希望这无关紧要。)

我不确定实体的外观。我尝试过这样的事情:

@Entity
@Table(name = "post")
public class Post {
    @Id
    private String id;

...

    @NonNull
    @Default
    @OneToMany(fetch = FetchType.EAGER, targetEntity=Tag.class)
//    @JoinColumn(name = "resource_id")
    @Where(clause = "resource_id=post.id and resource_type='post'")
    @ElementCollection
    private List<Tag> tags = new ArrayList<>();
}

我确定这是不对的,而且我不确定是否有办法做到这一点。在Tag 实体方面,我没有@ManyToOne,因为它与各种不同的实体连接。

【问题讨论】:

    标签: hibernate jpa


    【解决方案1】:

    我知道您想要一个代表多个不同实体标签的 tag而不是一个 tag 表 + 特定实体类型的连接表(post_tagsstory_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_idresource_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 需要您自己填充resourceIdresourceType。如果您想将 PostTag 视为单独的聚合根,这种方法很有意义,否则它非常麻烦且容易出错,因为 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 可以有一个 tagtag 列的给定值 - 当然对于给定的 namespace_id。我假设这是一个错字,而您真正想要的是 PRIMARY KEY(id) 加上 UNIQUE(resource_type, resource_id, namespace_id, tag))

    【讨论】:

    • 谢谢!我认为应该是@Where(clause = "resource_type='blueprint'")?我试过了,当我查询我的帖子时,它们都没有标签。 Tag 实体应该是什么样的?我没有在那里放置任何与加入相关的注释。 (当然,它有一个@Column(name="resource_id"))。
    • 有一点可能是错误的,Java 字段名称是驼峰式,但这些注释中的 clausename 字段使用下划线。但现在我都试过了,所以也许不是这样。
    • 嗯,这很奇怪,它应该开箱即用。你甚至不需要@Column(name="resource_id"),因为Hibernate 将使用@JoinColumn 中的列定义。无论如何,我强烈推荐@ManyToAny 而不是@Where,因为保存实体的潜在问题
    • 另外,关于驼峰式与下划线,clausename 使用物理列名,而不是 Java 字段名(@Where 正好代表将附加到本机 SELECT 查询的字符串接孩子时)
    • 仅供参考,Post 上的 @JoinColumn 还需要一个 foreignKey=@ForeignKey(value=ConstraintMode.NO_CONSTRAINT) 以防止 Tag 生成外键约束(这会阻止它加入多个实体)。跨度>
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-25
    • 2018-06-07
    相关资源
    最近更新 更多