如何避免在@ManyToMany 中保存重复项,但插入到映射 table 中?
How to avoid saving duplicates in @ManyToMany, but insert into mapping table?
我有 2 个实体 Post 和 Post 具有多对多关系的标签。
我还有 table post_tag_mapping 和 post_id 和 post_tag_id.
@ManyToMany(cascade = {CascadeType.ALL})
@JoinTable(
name = "post_tag_mapping",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
@Getter
@Builder.Default
private Set<PostTag> postTagSet = new HashSet<>();
如果我用 Set<PostTag>
创建 Post - 我会保存 post、1 个或多个 post_tag 和 table post_tag_mapping,例如 post_id tag_id - (1, 1), (1, 2), (1, 3), 等等
但是如果我用数据库中已经存在的 post_tag 名称保存 post - 我不想将它保存到 post_tag(我在 post_tag.name 上有唯一索引), 但为另一个 post.
创建新的 post_tag_mapping
现在我得到异常 SQLIntegrityConstraintViolationException: Duplicate entry 'tag1' for key 'post_tag.idx_post_tag_name'
不太明白如何实现。
如果我很了解你的困境,你的问题是你在保存你的 Post
实体时试图插入新的 PostTag
。
由于 CascadeType.ALL,您正在执行级联保存,因此您的 EntityManager 大致是这样做的:
- 节省Post
- 保存Post标签哪个导致你的异常
- 正在保存 Post <-> Post标签
你应该
- 有一项服务(例如:
PostTag findOrCreateTagByName(String)
)可以按名称获取现有 PostTag
并最终创建它们。因此返回现有的 PostTag
.
- 在与所述 现有 标签关联后保存
Post
。
编辑(作为对评论的回答):
JPA 只是到关系数据库的映射。
在你的代码中,你只显示了一个映射,它表明 Post
链接到多个 PostTag
(并且 PostTag
链接到多个 Post
) .
您添加了一个适用于所有标签的唯一约束:在您的所有数据库中,必须有一个标签“A”,一个标签“B”,依此类推。
如果你像这样填充你的对象(我不使用 lombok,所以我在这里假设一个最小的构造函数):
Post post = new Post();
post.setXXX(...);
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("B"));
这意味着您创建了两个 新标签,名为 A 和 B。
JPA 实现(Hibernate、EclipseLink)并不神奇:它们不会为您获取现有标签,也不会获取失败的标签。如果您违反了 table post_tag
的唯一性约束,这意味着您插入了两次相同的值。要么在同一笔交易中,要么因为标签已经存在于 table.
例如:
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("A"));
如果您没有正确定义 hashCode()
,那么只会使用对象标识哈希码,并且会尝试添加(插入)两个标签 A
.
您在这里唯一可以做的就是通过正确实施 hashCode()/equals
来限制 PostTag
,以便 PostTagSet
确保唯一性 仅适用于相关 Post.
现在假设您首先获取它们并拥有一个新标签 C
:
Post post = new Post();
post.setXXX(...);
for (String tagName : asList("A", "B", "C")) {
post.getPostTagSet().add(tagRepository.findByName(tagName)
.orElseGet(() -> new PostTag(tagName ));
}
postRepository.save(post);
tagRepository
只是一个 Spring JPA 存储库——我想你正在使用它——并且 findByName 签名是:
Optional<String> findByName(String tagName);
代码将执行:
- 找到标签 A:它在数据库中,如
PostTag(1, "A")
- 找到标签 B:它在数据库中,如
PostTag(2, "B")
- 找到标签C:它不在数据库中,创建它。
这应该可以工作,因为级联将在 Post 上执行保存,然后在 Post 标签上执行保存,然后在关系 Post <-> [=138= 上执行保存]标签。
在 SQL 查询方面,您通常应该看到这样的内容:
insert into post_tag (tag_id, name) (3, "C")
insert into post (post_id, ...) (<some id>, ...)
insert into post_tag_mapping (tag_id, post_id) (1, <some id>)
insert into post_tag_mapping (tag_id, post_id) (2, <some id>)
insert into post_tag_mapping (tag_id, post_id) (3, <some id>)
这里的另一个问题是 PostTag
提供的 hashCode()
和 equals()
确保单个 Post
的 PostTag
的唯一性:
如果您在 hashCode()
中使用 id
(等于使用 id
和 name
):
- 如果您使用
id
,则集合将有PostTag(1, "A")
、PostTag(2, "B")
和PostTag("C")
- 保存时,PostTag("C") 将分配一个 ID -> PostTag(3, "C")
- 使用标准
HashSet
,PostTag("C")
将不再位于其有效存储桶中,您将无法再次找到它。
如果您在设置后不使用该对象,这可能没有问题,但我认为最好先保存 PostTag
(为其分配一个 ID),然后将其添加到设置中。
如果在hashCode()
和equals
中使用name
:只要插入集合后不更新名称,就不会出现问题.
就去做吧。
@SpringBootApplication
public class DemoApplication implements ApplicationRunner{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Autowired
PostRepository postRepository;
@Autowired
PostTagRepository postTagRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
init();
testException("name");
addNewPost("name");
addNewPost("other");
readPosts();
}
private void init() {
postTagRepository.save(PostTag.builder().name("name").build());
}
private void testException(String name) {
try {
PostTag postTag = postTagRepository.save(PostTag.builder().name(name).build());
postRepository.save(Post.builder().tags(Collections.singleton(postTag)).build());
} catch ( DataIntegrityViolationException ex ) {
System.out.println("EX: " + ex.getLocalizedMessage());
}
}
private void addNewPost(String name) {
PostTag postTag = postTagRepository.findByName(name)
.orElseGet(()->postTagRepository.save(PostTag.builder().name(name).build()));
postRepository.save(Post.builder().tags(Collections.singleton(postTag)).build());
}
private void readPosts() {
System.out.println(postRepository.findAll());
}
}
并且不要使用你不理解的东西
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {
@Id @GeneratedValue
private Long id;
@ManyToMany
private Set<PostTag> tags;
}
并掌握语法。
并在 repo 中处理 eager fetch。
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = { "tags" })
List<Post> findAll();
}
我有 2 个实体 Post 和 Post 具有多对多关系的标签。 我还有 table post_tag_mapping 和 post_id 和 post_tag_id.
@ManyToMany(cascade = {CascadeType.ALL})
@JoinTable(
name = "post_tag_mapping",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
@Getter
@Builder.Default
private Set<PostTag> postTagSet = new HashSet<>();
如果我用 Set<PostTag>
创建 Post - 我会保存 post、1 个或多个 post_tag 和 table post_tag_mapping,例如 post_id tag_id - (1, 1), (1, 2), (1, 3), 等等
但是如果我用数据库中已经存在的 post_tag 名称保存 post - 我不想将它保存到 post_tag(我在 post_tag.name 上有唯一索引), 但为另一个 post.
创建新的 post_tag_mapping现在我得到异常 SQLIntegrityConstraintViolationException: Duplicate entry 'tag1' for key 'post_tag.idx_post_tag_name'
不太明白如何实现。
如果我很了解你的困境,你的问题是你在保存你的 Post
实体时试图插入新的 PostTag
。
由于 CascadeType.ALL,您正在执行级联保存,因此您的 EntityManager 大致是这样做的:
- 节省Post
- 保存Post标签哪个导致你的异常
- 正在保存 Post <-> Post标签
你应该
- 有一项服务(例如:
PostTag findOrCreateTagByName(String)
)可以按名称获取现有PostTag
并最终创建它们。因此返回现有的PostTag
. - 在与所述 现有 标签关联后保存
Post
。
编辑(作为对评论的回答):
JPA 只是到关系数据库的映射。
在你的代码中,你只显示了一个映射,它表明 Post
链接到多个 PostTag
(并且 PostTag
链接到多个 Post
) .
您添加了一个适用于所有标签的唯一约束:在您的所有数据库中,必须有一个标签“A”,一个标签“B”,依此类推。
如果你像这样填充你的对象(我不使用 lombok,所以我在这里假设一个最小的构造函数):
Post post = new Post();
post.setXXX(...);
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("B"));
这意味着您创建了两个 新标签,名为 A 和 B。
JPA 实现(Hibernate、EclipseLink)并不神奇:它们不会为您获取现有标签,也不会获取失败的标签。如果您违反了 table post_tag
的唯一性约束,这意味着您插入了两次相同的值。要么在同一笔交易中,要么因为标签已经存在于 table.
例如:
post.getPostTagSet().add(new PostTag("A"));
post.getPostTagSet().add(new PostTag("A"));
如果您没有正确定义 hashCode()
,那么只会使用对象标识哈希码,并且会尝试添加(插入)两个标签 A
.
您在这里唯一可以做的就是通过正确实施 hashCode()/equals
来限制 PostTag
,以便 PostTagSet
确保唯一性 仅适用于相关 Post.
现在假设您首先获取它们并拥有一个新标签 C
:
Post post = new Post();
post.setXXX(...);
for (String tagName : asList("A", "B", "C")) {
post.getPostTagSet().add(tagRepository.findByName(tagName)
.orElseGet(() -> new PostTag(tagName ));
}
postRepository.save(post);
tagRepository
只是一个 Spring JPA 存储库——我想你正在使用它——并且 findByName 签名是:
Optional<String> findByName(String tagName);
代码将执行:
- 找到标签 A:它在数据库中,如
PostTag(1, "A")
- 找到标签 B:它在数据库中,如
PostTag(2, "B")
- 找到标签C:它不在数据库中,创建它。
这应该可以工作,因为级联将在 Post 上执行保存,然后在 Post 标签上执行保存,然后在关系 Post <-> [=138= 上执行保存]标签。
在 SQL 查询方面,您通常应该看到这样的内容:
insert into post_tag (tag_id, name) (3, "C")
insert into post (post_id, ...) (<some id>, ...)
insert into post_tag_mapping (tag_id, post_id) (1, <some id>)
insert into post_tag_mapping (tag_id, post_id) (2, <some id>)
insert into post_tag_mapping (tag_id, post_id) (3, <some id>)
这里的另一个问题是 PostTag
提供的 hashCode()
和 equals()
确保单个 Post
的 PostTag
的唯一性:
如果您在 hashCode()
中使用 id
(等于使用 id
和 name
):
- 如果您使用
id
,则集合将有PostTag(1, "A")
、PostTag(2, "B")
和PostTag("C")
- 保存时,PostTag("C") 将分配一个 ID -> PostTag(3, "C")
- 使用标准
HashSet
,PostTag("C")
将不再位于其有效存储桶中,您将无法再次找到它。
如果您在设置后不使用该对象,这可能没有问题,但我认为最好先保存 PostTag
(为其分配一个 ID),然后将其添加到设置中。
如果在hashCode()
和equals
中使用name
:只要插入集合后不更新名称,就不会出现问题.
就去做吧。
@SpringBootApplication
public class DemoApplication implements ApplicationRunner{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Autowired
PostRepository postRepository;
@Autowired
PostTagRepository postTagRepository;
@Override
public void run(ApplicationArguments args) throws Exception {
init();
testException("name");
addNewPost("name");
addNewPost("other");
readPosts();
}
private void init() {
postTagRepository.save(PostTag.builder().name("name").build());
}
private void testException(String name) {
try {
PostTag postTag = postTagRepository.save(PostTag.builder().name(name).build());
postRepository.save(Post.builder().tags(Collections.singleton(postTag)).build());
} catch ( DataIntegrityViolationException ex ) {
System.out.println("EX: " + ex.getLocalizedMessage());
}
}
private void addNewPost(String name) {
PostTag postTag = postTagRepository.findByName(name)
.orElseGet(()->postTagRepository.save(PostTag.builder().name(name).build()));
postRepository.save(Post.builder().tags(Collections.singleton(postTag)).build());
}
private void readPosts() {
System.out.println(postRepository.findAll());
}
}
并且不要使用你不理解的东西
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {
@Id @GeneratedValue
private Long id;
@ManyToMany
private Set<PostTag> tags;
}
并掌握语法。
并在 repo 中处理 eager fetch。
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = { "tags" })
List<Post> findAll();
}