繁体   English   中英

如何与现有寄存器建立单向多对多关系?

[英]How to do unidirectional many-to-many relationships with already existing registers?

我是 Spring/JPA 的新手,我正在尝试创建一种与 Vlad 的这篇文章非常相似的关系,但有一个区别。 我的标签已存在于另一个表中。

因此,如果我像 Vlad 在其帖子中所做的那样,创建一个帖子,为其添加一些标签,然后将其持久化,那么一切都会按预期进行。 我在 Post 上有一个注册,两个在 Tag 上,两个在 PostTag 上。

Post newPost = new Post("Title");
newPost.addTag(new Tag("TagName"));
newPost.addTag(new Tag("TagName2"));
this.postRepository.save(newPost);

但是,如果我在创建帖子之前尝试创建标签并保存它,则会出现错误。

Tag tag = new Tag("TagAlreadyCreated");
this.tagRepository.save(tag);
Post newPost = new Post("Title");
newPost.addTag(tag);
this.postRepository.save(newPost);
// Error: detached entity passed to persist: com.***.***.Tag

我知道我不想创建标签,如果它已经存在,并且分离的消息意味着我的标签已经有一个 ID,所以我尝试将 CascadeType 更改为 MERGE,但随后我没有创建注册在 PostTag 上。 类的代码:

邮政

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(
            mappedBy = "post",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<PostTag> tags = new ArrayList<>();

    public Post() {
    }

    public Post(String title) {
        this.title = title;
    }

    public void addTag(Tag tag) {
        PostTag postTag = new PostTag(this, tag);
        tags.add(postTag);
    }

    public void removeTag(Tag tag) {
        for (Iterator<PostTag> iterator = tags.iterator();
             iterator.hasNext(); ) {
            PostTag postTag = iterator.next();

            if (postTag.getPost().equals(this) &&
                    postTag.getTag().equals(tag)) {
                iterator.remove();
                postTag.setPost(null);
                postTag.setTag(null);
            }
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (o == null || getClass() != o.getClass())
            return false;

        Post post = (Post) o;
        return Objects.equals(title, post.title);
    }

    @Override
    public int hashCode() {
        return Objects.hash(title);
    }

    public Long getId() {
        return id;
    }
}

标签

@Entity(name = "Tag")
@Table(name = "tag")
@NaturalIdCache
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Tag {

    @Id
    @GeneratedValue
    private Long id;

    public Long getId() {
        return id;
    }

    @NaturalId
    private String name;

    public Tag() {
    }

    public Tag(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (o == null || getClass() != o.getClass())
            return false;

        Tag tag = (Tag) o;
        return Objects.equals(name, tag.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

邮戳

@Entity(name = "PostTag")
@Table(name = "post_tag")
public class PostTag {

    @EmbeddedId
    private PostTagId id;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("postId")
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("tagId")
    private Tag tag;

    @Column(name = "created_on")
    private Date createdOn = new Date();

    private PostTag() {}

    public void setPost(Post post) {
        this.post = post;
    }

    public void setTag(Tag tag) {
        this.tag = tag;
    }

    public PostTag(Post post, Tag tag) {
        this.post = post;
        this.tag = tag;
        this.id = new PostTagId(post.getId(), tag.getId());
    }

    //Getters and setters omitted for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (o == null || getClass() != o.getClass())
            return false;

        PostTag that = (PostTag) o;
        return Objects.equals(post, that.post) &&
                Objects.equals(tag, that.tag);
    }

    public Post getPost() {
        return post;
    }

    public Tag getTag() {
        return tag;
    }

    @Override
    public int hashCode() {
        return Objects.hash(post, tag);
    }
}

邮政标签ID

@Embeddable
public class PostTagId
        implements Serializable {

    @Column(name = "post_id")
    private Long postId;

    @Column(name = "tag_id")
    private Long tagId;

    private PostTagId() {}

    public PostTagId(
            Long postId,
            Long tagId) {
        this.postId = postId;
        this.tagId = tagId;
    }

    //Getters omitted for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (o == null || getClass() != o.getClass())
            return false;

        PostTagId that = (PostTagId) o;
        return Objects.equals(postId, that.postId) &&
                Objects.equals(tagId, that.tagId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(postId, tagId);
    }
}

spring-data-jpa是JPA之上的一层。 每个实体都有其自己的存储库,您必须处理该存储库。 我已经看过上面提到的教程,它是针对JPA的,并且还将ID设置为null,这似乎有点不正确,可能是导致错误的原因。 我没那么近看。 为了处理spring-data-jpa中的问题,您需要为链接表提供单独的存储库。

@Entity
public class Post {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PostTag> tags;

@Entity
public class Tag {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PostTag> posts;

@Entity
public class PostTag {
    @EmbeddedId
    private PostTagId id = new PostTagId();

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("postId")
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("tagId")
    private Tag tag;

    public PostTag() {}
    public PostTag(Post post, Tag tag) {
        this.post = post;
        this.tag = tag;
    }

@SuppressWarnings("serial")
@Embeddable
public class PostTagId implements Serializable {
    private Long postId;
    private Long tagId;
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        PostTagId that = (PostTagId) o;
        return Objects.equals(postId, that.postId) && Objects.equals(tagId, that.tagId);
    }
    @Override
    public int hashCode() {
        return Objects.hash(postId, tagId);
    }

并使用它,如上所示:

@Transactional
private void update() {
    System.out.println("Step 1");
    Tag tag1 = new Tag();
    Post post1 = new Post();
    PostTag p1t1 = new PostTag(post1, tag1);
    tagRepo.save(tag1);
    postRepo.save(post1);
    postTagRepo.save(p1t1);

    System.out.println("Step 2");
    Tag tag2 = new Tag();
    Post post2 = new Post();
    PostTag p2t2 = new PostTag(post2, tag2);
    postRepo.save(post2);
    tagRepo.save(tag2);
    postTagRepo.save(p2t2);

    System.out.println("Step 3");
    tag2 = tagRepo.getOneWithPosts(2L);
    tag2.getPosts().add(new PostTag(post1, tag2));
    tagRepo.save(tag2);

    System.out.println("Step 4 -- better");
    PostTag p2t1 = new PostTag(post2, tag1);
    postTagRepo.save(p2t1);
}

请注意,更改很少。 我没有明确设置PostTagId ID。 这些由持久层(在这种情况下为休眠)处理。

还请注意,由于设置了CascadeType.ALL因此可以使用其自己的存储库来显式更新PostTag条目,也可以通过从列表中添加和删除PostTag条目来进行显示,如图所示。 CascadeType.ALL用于spring-data-jpa的问题在于,即使您预取了联接表实体,spring-data-jpa仍将再次执行此操作。 尝试通过CascadeType.ALL为新实体更新关系是有问题的。

如果没有CascadeTypepoststags列表(应为Set)都不是该关系的所有者,因此,在持久性方面添加到它们将不会完成任何事情,并且仅用于查询结果。

在读取PostTag关系时,由于没有FetchType.EAGER需要专门获取它们。 这个问题FetchType.EAGER是开销,如果你不想加入,也如果你把它放在俩都TagPost那么你将创建一个递归获取该得到所有TagsPosts的任何查询。

@Query("select t from Tag t left outer join fetch t.posts tps left outer join fetch tps.post where t.id = :id")
Tag getOneWithPosts(@Param("id") Long id);

最后,请始终检查日志。 请注意,创建关联需要spring-data-jpa(我认为是JPA)读取现有表以查看该关系是新建的还是更新的。 无论您自己创建和保存PostTag还是预取列表, PostTag发生这种情况。 JPA有单独的合并,我认为您可以更有效地使用它。

create table post (id bigint generated by default as identity, primary key (id))
create table post_tag (post_id bigint not null, tag_id bigint not null, primary key (post_id, tag_id))
create table tag (id bigint generated by default as identity, primary key (id))
alter table post_tag add constraint FKc2auetuvsec0k566l0eyvr9cs foreign key (post_id) references post
alter table post_tag add constraint FKac1wdchd2pnur3fl225obmlg0 foreign key (tag_id) references tag

Step 1
insert into tag (id) values (null)
insert into post (id) values (null)
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)

Step 2
insert into post (id) values (null)
insert into tag (id) values (null)
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)

Step 3
select tag0_.id as id1_2_0_, posts1_.post_id as post_id1_1_1_, posts1_.tag_id as tag_id2_1_1_, post2_.id as id1_0_2_, posts1_.tag_id as tag_id2_1_0__, posts1_.post_id as post_id1_1_0__ from tag tag0_ left outer join post_tag posts1_ on tag0_.id=posts1_.tag_id left outer join post post2_ on posts1_.post_id=post2_.id where tag0_.id=?
select tag0_.id as id1_2_1_, posts1_.tag_id as tag_id2_1_3_, posts1_.post_id as post_id1_1_3_, posts1_.post_id as post_id1_1_0_, posts1_.tag_id as tag_id2_1_0_ from tag tag0_ left outer join post_tag posts1_ on tag0_.id=posts1_.tag_id where tag0_.id=?
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)

Step 4 -- better
select posttag0_.post_id as post_id1_1_0_, posttag0_.tag_id as tag_id2_1_0_ from post_tag posttag0_ where posttag0_.post_id=? and posttag0_.tag_id=?
insert into post_tag (post_id, tag_id) values (?, ?)

我想我找到了答案。

首先要知道的是,如果您将open-in-view 设置为 true ,Spring 将在请求的整个生命周期内保持 JPA/Hibnernate 会话打开。 这意味着您的代码(比方说在服务方法中)应该可以正常工作。 当然这不是很有效(看看这里为什么):https ://vladmihalcea.com/the-open-session-in-view-anti-pattern/

现在,如果您将open-in-view 设置为 false ,Spring 似乎会为每个存储库调用打开一个新会话。 因此,一个用于获取标签的新会话,然后是一个用于保存帖子的新会话。 这就是为什么在保存帖子时标签实体被分离的原因。

要解决此问题,您需要使用@Transactional注释您的服务调用。 Spring 将尝试在事务内重用相同的会话,因此实体不会分离。 例如,请参见此处: @Transactional 如何影响 Hibernate 中的当前会话?

最后要知道哪个是至关重要的,服务方法必须是公开的 如果您对方法有任何其他可见性,@Transactional 将被忽略,则不会引发任何错误: https : //docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-声明性注释(参见方法可见性和@

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM