[ํ๋ก์ ํธ] N:N ๊ด๊ณ → 1:N & N:1 ๋ก ๊ตฌํํ๊ธฐBack-end/Project2024. 9. 9. 19:23
Table of Contents
โ Git
https://github.com/ellaCoo/sns/issues/17
https://github.com/ellaCoo/sns/pull/40
๐ ์ํฉ

[ ํฌ์คํธ : ํด์ํ๊ทธ ]๊ฐ [ N : N ] ๊ด๊ณ๋ก ์ค๊ณ๋ ์ํ์์,
๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ ๊ทํ๋ ํ ์ด๋ธ 2๊ฐ๋ก ๋ค๋๋ค ๊ด๊ณ๋ฅผ ํํํ ์ ์๋ค๋ ํ๊ณ์ ๋ถ๋ช์ณค๋ค.
(@ManyToMany ์ด๋ ธํ ์ด์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ฐ๊ฒฐ ํ ์ด๋ธ์ ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ด ์๋๊ธฐ์..)
⇒ ์ฐ๊ฒฐ ํ ์ด๋ธ๊ณผ ์ฐ๊ฒฐ ํ ์ด๋ธ์ฉ ์ํฐํฐ๋ฅผ ์ถ๊ฐํ์ฌ ์ผ๋๋ค, ๋ค๋์ผ ๊ด๊ณ๋ก ํ์ด๋ด๋ณด์
์ ManyToMany ๋ฅผ ์ง์ํด์ผ ํ๋๊ฐ?
ManyToMany๋ฅผ ์ฌ์ฉํ๋ฉด ์ง์ ํ ์ด๋ธ๊ด๋ฆฌ๋ ์ธ๋ฑ์ค ์ค์ ์ ํ ์ ์๊ณ ,
post_hashtag๊ฐ ์๋์ ์ผ๋ก ๋ง๋ค์ด์ ธ์ ๊ฐ์ ํ๊ณ , ์ง์ ์์ ํ๊ธฐ ์ด๋ ค์์ง.
ํ ์ด๋ธ์ด ์๋์ผ๋ก ๋ง๋ค์ด์ง = ํ ์ด๋ธ์ด ์ค๊ณ์์ ์จ๋๋ค
→ ๊ด๋ฆฌ์์ ๋๋ฝ๋ ์ ์์
๋๋ฉ์ธ ์ค์ ์ฝ๋
// Post
@Getter
@Entity
public class Post extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
@JoinColumn(name = "userId")
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private UserAccount userAccount;
@Setter
@Column(nullable = false)
private String title;
@Setter
@Column(nullable = false, length = 10000)
private String content;
@OrderBy("createdAt DESC")
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY) // ์ฐ๊ด๋ ์ํฐํฐ๋ ๋ชจ๋ ์์์ฑ ์ ์ด
private Set<PostComment> postComments = new LinkedHashSet<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Like> likes = new LinkedHashSet<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<PostHashtag> postHashtags = new LinkedHashSet<>();
// PostHashtag (์ค๊ฐ ์ฐ๊ฒฐ ํ
์ด๋ธ)
@Getter
@Entity
public class PostHashtag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
@JoinColumn(name = "postId")
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Post post;
@Setter
@JoinColumn(name = "hashtagId")
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Hashtag hashtag;
// Hashtag
@Getter
@Entity
public class Hashtag extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
@Column(nullable = false)
private String hashtagName;
@OneToMany(mappedBy = "hashtag", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private Set<PostHashtag> postHashtags = new LinkedHashSet<>();
์์์ฑ ์ ์ด(cascade) ์ต์ = CascadeType
CascadeType.ALL
- Post ์ Set<PostHashtag> postHashtags
์ํฐํฐ์ ์ํ๋ ๋ชจ๋ ์์ (์ ์ฅ, ์ญ์ , ๊ฐฑ์ , ๋ณํฉ ๋ฑ)์ด ๊ด๋ จ๋ ์ฐ๊ด ์ํฐํฐ์๋ ๋์ผํ๊ฒ ์ ์ฉ
Post ์ ์ฅํ๋ฉด ๊ด๋ จ๋ ๋ชจ๋ postHashtags ๋ ํจ๊ป ์ ์ฅ๋๊ณ , Post ์ญ์ ํ๋ฉด ๋ชจ๋ ์์ postHashtags ๋ ์ญ์ ๋จ
- ์๋ ์ ์ด ์ต์ ๋ค ๋ชจ๋ ํฌํจ
PERSIST
REMOVE
MERGE
REFRESH
DETACH
CascadeType.PERSIST
- Hashtag ์ Set<PostHashtag> postHashtags
- ์ ์ฅ(persist) ์์ ์๋ง ์ ์ด๋จ
Hashtag ์ ์ฅํ๋ฉด ๊ด๋ จ๋ ๋ชจ๋ postHashtags ๋ ํจ๊ป ์ ์ฅ๋์ง๋ง, ์ญ์ ์์ ์ ์ ์ด๋์ง ์์
๐ Post๊ฐ ์ ๋ฐ์ดํธ ๋์์ ๋, ํด๋น ํฌ์คํธ์ ํด์ํ๊ทธ๊ฐ ํจ๊ป ์ ๋ฐ์ดํธ ๋์ด์ผ ํ๋ ์ํฉ


์์ค์ฝ๋
๐พ PostService
public void updatePost(Long postId, PostWithHashtagsDto dto) {
try {
Post post = postRepository.getReferenceById(postId);
UserAccount userAccount = userAccountRepository.getReferenceById(dto.userAccountDto().userId());
// ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์์ ๊ฒ์๊ธ ์์ฑ์๊ฐ ๋์ผํ ๊ฒฝ์ฐ(/postId/edit : get์ด๋ฏ๋ก ๋ค๋ฅธ ์ฌ์ฉ์๋ ์ ๊ทผ ๊ฐ๋ฅ)
if (!post.getUserAccount().equals(userAccount)) {
return;
}
if (dto.postDto().title() != null) post.setTitle(dto.postDto().title());
if (dto.postDto().content() != null) post.setContent(dto.postDto().content());
/**
Hashtag LOGIC
*/
Set<Long> originHashtagIds = post.getPostHashtags().stream().map(PostHashtag::getHashtag).map(Hashtag::getId).collect(Collectors.toSet()); // 1
Set<String> newHashtags = dto.hashtagDtos().stream().map(HashtagDto::hashtagName).collect(Collectors.toUnmodifiableSet()); // 2
postHashtagRepository.deleteByPostId(postId); // 3
Set<Hashtag> hashtags = hashtagService.getExistedOrCreatedHashtagsByHashtagNames(newHashtags); // 4
post.addHashtags(hashtags); // Post์ Hashtag ๊ด๊ณ ์ค์ 5
postRepository.flush(); // 6
hashtagService.deleteUnusedHashtags(originHashtagIds); // 7
} catch (EntityNotFoundException e) {
log.warn("ํฌ์คํธ ์
๋ฐ์ดํธ ์คํจ. ํฌ์คํธ๋ฅผ ์์ ํ๋๋ฐ ํ์ํ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
}
}
๐พ PostWithHashtagsDto
public record PostWithHashtagsDto(
PostDto postDto,
UserAccountDto userAccountDto,
Set<HashtagDto> hashtagDtos
) {
public static PostWithHashtagsDto of(PostDto postDto, UserAccountDto userAccountDto, Set<HashtagDto> hashtagDtos) {
return new PostWithHashtagsDto(postDto, userAccountDto, hashtagDtos);
}
/** Hashtag LOGIC */
1 : DB SELECT → ๊ฒ์๊ธ์ ๊ธฐ์กด ํด์ํ๊ทธ Id๋ฅผ ๊ฐ์ ธ์์ Set ํํ๋ก ๊ฐ๊ณต (๋์ค์ ์ฌ์ฉํ์ง ์๋ ํด์ํ๊ทธ ๊ฒ์ฆ์ฉ)
Set<Long> originHashtagIds = post.getPostHashtags().stream().map(PostHashtag::getHashtag).map(Hashtag::getId).collect(Collectors.toSet());
Hibernate:
select
ph1_0.post_id,
ph1_0.id,
ph1_0.hashtag_id
from
post_hashtag ph1_0
where
ph1_0.post_id=?
2024-09-09T18:55:22.736+09:00 TRACE 22129 --- [io-8080-exec-10] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [49]

2 : ์๋ก ์ ๋ฐ์ดํธ ๋ ํด์ํ๊ทธ๋ค์ Set ์ผ๋ก ๊ฐ๊ณต
Set<String> newHashtags = dto.hashtagDtos().stream().map(HashtagDto::hashtagName).collect(Collectors.toUnmodifiableSet());

3 : DB DELETE → ์ฐ๊ฒฐํ ์ด๋ธ(PostHashtag)์์ ํฌ์คํธ์ ํด๋นํ๋ ์ฐ๊ฒฐ ์ญ์
postHashtagRepository.deleteByPostId(postId);
Hibernate:
delete
from
post_hashtag ph1_0
where
ph1_0.post_id=?
2024-09-09T18:58:41.343+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [49]
4 : DB SELECT & INSERT → ์๋ก ์ ๋ฐ์ดํธ ๋ ํด์ํ๊ทธ๊ฐ Hashtag ํ ์ด๋ธ์ ๊ธฐ์กด์ ์์ผ๋ฉด ๊ทธ ์ํฐํฐ ๊ฐ์ ธ์ค๊ณ , ์์ผ๋ฉด INSERT
Set<Hashtag> hashtags = hashtagService.getExistedOrCreatedHashtagsByHashtagNames(newHashtags);
๐พ HashtagService
public Set<Hashtag> getExistedOrCreatedHashtagsByHashtagNames(Set<String> hashtagNames) {
Set<Hashtag> hashtags = new HashSet<>();
for (String hashtagName : hashtagNames) {
Hashtag hashtag = hashtagRepository.findByHashtagName(hashtagName)
.orElseGet(() -> hashtagRepository.save(Hashtag.of(hashtagName)));
hashtags.add(hashtag);
}
return hashtags;
}
Hibernate:
select
h1_0.id,
h1_0.created_at,
h1_0.created_by,
h1_0.hashtag_name,
h1_0.modified_at,
h1_0.modified_by
from
hashtag h1_0
where
h1_0.hashtag_name=?
2024-09-09T18:59:18.471+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (1:VARCHAR) <- [test]
Hibernate:
insert
into
hashtag
(created_at, created_by, hashtag_name, modified_at, modified_by)
values
(?, ?, ?, ?, ?)
returning id

5 : Post ์ํฐํฐ์ private Set<PostHashtag> postHashtags ์ ์ถ๊ฐ
post.addHashtags(hashtags); // Post์ Hashtag ๊ด๊ณ ์ค์
6 : flush ๋ฅผ ํตํด ์ฐ๊ธฐ ์ง์ฐ SQL ์ ์ฅ์์ ์ฟผ๋ฆฌ๋ฅผ DB์ ์ ์ก (๋ณ๊ฒฝ ๊ฐ์ง / ์์ ๋ ์ํฐํฐ ์ฐ๊ธฐ ์ง์ฐ SQL ์ ์ฅ์์ ๋ฑ๋ก)
postRepository.flush();
Hibernate:
insert
into
post_hashtag
(hashtag_id, post_id)
values
(?, ?)
returning id
2024-09-09T19:01:09.389+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [17]
2024-09-09T19:01:09.390+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (2:BIGINT) <- [49]
7 : ์์ ์ ์ ํด์ํ๊ทธ๋ค์ด ์ฌ์ฉ๋์ง ์๋๋ค๋ฉด ์ญ์
hashtagService.deleteUnusedHashtags(originHashtagIds);
๐พ HashtagService
public void deleteUnusedHashtags(Set<Long> hashtagIds) {
// JPQL ์ฟผ๋ฆฌ ์คํ ์ , ์๋ flush
List<Hashtag> unusedHashtags = hashtagRepository.findUnusedHashtagsByIds(hashtagIds);
for (Hashtag unusedHashtag : unusedHashtags) {
hashtagRepository.delete(unusedHashtag);
}
}
๐พ HashtagRepository
// ํด์ํ๊ทธ ์ค์์ PostHashtag์ ์ํ์ง ์๋ ํด์ํ๊ทธ๋ค ์ฐพ๊ธฐ
@Query("SELECT h FROM Hashtag h WHERE h.postHashtags IS EMPTY AND h.id IN :hashtagIds")
List<Hashtag> findUnusedHashtagsByIds(@Param("hashtagIds") Set<Long> hashtagIds);
7-1 : JPQL ์ฟผ๋ฆฌ ์ฌ์ฉํ์ฌ ์ฌ์ฉ๋์ง ์๋ ํด์ํ๊ทธ ์ฐพ๊ธฐ
List<Hashtag> unusedHashtags = hashtagRepository.findUnusedHashtagsByIds(hashtagIds);
Hibernate:
select
h1_0.id,
h1_0.created_at,
h1_0.created_by,
h1_0.hashtag_name,
h1_0.modified_at,
h1_0.modified_by
from
hashtag h1_0
where
not exists(select
1
from
post_hashtag ph1_0
where
h1_0.id=ph1_0.hashtag_id)
and h1_0.id in (?, ?, ?, ?)
2024-09-09T19:02:17.749+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [1]
2024-09-09T19:02:17.750+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (2:BIGINT) <- [7]
2024-09-09T19:02:17.750+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (3:BIGINT) <- [12]
2024-09-09T19:02:17.751+09:00 TRACE 24830 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (4:BIGINT) <- [13]