skarltjr / Memory_Write_Record

나의 모든 학습 기록
0 stars 0 forks source link

무분별한 leftJoin 사용 #59

Open skarltjr opened 2 years ago

skarltjr commented 2 years ago
@Entity
@Table(name = "image_article_unit")
class ImageArticleUnit(

    @field:ManyToOne
    @field:JoinColumn(name = "image_id")
    val image: Image,

    article: Article,

    displayOrder: Int
) : BaseArticleUnit(id = null, article = article, displayOrder = displayOrder)
@Entity
@Table(name = "images")
class Image(
    @field:Id
    @field:GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @field:Column(name = "small_key", nullable = false, updatable = false, unique = true)
    val smallKey: String,

    @field:Column(name = "small_url", nullable = false)
    val smallUrl: String,

    @field:Column(name = "mid_key", nullable = false, updatable = false, unique = true)
    val midKey: String,

    @field:Column(name = "mid_url", nullable = false)
    val midUrl: String,

    @field:Column(name = "large_key", nullable = false, updatable = false, unique = true)
    val largeKey: String,

    @field:Column(name = "large_url", nullable = false)
    val largeUrl: String,
)

상황

이미지유닛 & 관련된 이미지를 delete하는 매서드가 존재 
이미지유닛과 관련이미지를 모두 삭제하기 위해
imageUnit을 불러올 때 image까지 한 번에 불러오려고한다.
그래서 join fetch를 활용할 계획

기존

    override fun getAllByArticle(article: Article): List<ImageArticleUnit> {
        return queryFactory.selectFrom(imageArticleUnit)
            .where(imageArticleUnit.article.eq(article))
            .leftjoin(imageArticleUnit.image, image).fetchJoin()
            .fetch()
    }

변경

    override fun getAllByArticle(article: Article): List<ImageArticleUnit> {
        return queryFactory.selectFrom(imageArticleUnit)
            .where(imageArticleUnit.article.eq(article))
            .join(imageArticleUnit.image, image).fetchJoin()
            .fetch()
    }

기록

기존 코드에서 볼 수있듯이 습관적으로 leftJoin을 사용했다.
과연 leftJoin을 무분별하게 사용해도 될까?
여기서는 imageUnit은 image필드 null허용도 하지 않으며 무조건 image를 갖기 때문에 join으로 변경해도 결과는 동일할것이라고 생각
그러나 굳이 innerjoin으로 변경한 이유는 분명히 알고, 올바르게 수정해놓기 위함.

만약 imageUnit이 image필드에 null을 허용한다면? 
혹은 imaegUnit의 image가 없어진 image라면?
leftjoin은 이런경우에 가져오고자하는 대상 imageUnit외의 이미지유닛들도 가져올것이며
ex) imageService.deleteImage( imageUnit.image )를 할 때 오류가 발생할 것

join 비교

left "outer" join

a |  b
--+-----
1 | null
2 | null
3 |    3
4 |    4
skarltjr commented 2 years ago

실수 복기.

ArticleRepositoryExtensionImpl
->

override fun findByIdWithBoardUserAndUserImage(id: Long): Article? {
    return queryFactory.selectFrom(article)
        .where(article.id.eq(id))
        .join(article.board, board).fetchJoin()
        .join(article.user, user).fetchJoin()
        .join(user.image, image).fetchJoin()
        .fetchOne()
}

처음에 게시글을 가져오면서 해당 게시글의 게시판, 작성자(User)와 유저의 프로필 이미지까지 한 번에 가져오고자 했다. 위 쿼리가 왜 동작하지 않았을까. 해당 id를 가진 게시글의 작성자의 프로필 사진(image)가 없다면?

즉 해당 id를 가진 게시글의 작성자의 프로필 사진(image)가 없다면 위 쿼리로 찾아올 수 없다.

해결

override fun findByIdWithBoardUserAndUserImage(id: Long): Article? {
    return queryFactory.selectFrom(article)
        .where(article.id.eq(id))
        .join(article.board, board).fetchJoin()
        .join(article.user, user).fetchJoin()
        .leftJoin(user.image, image).fetchJoin()
        .fetchOne()
}
skarltjr commented 2 years ago

잘못된 사용을 반성하고 올바르게 사용 실천해보기 상황

- 검색( 게시글 리스트 조회 )을 구현할 계획
- 검색은 해당 키워드를 제목 혹은 본문에 포함한 모든 article을 대상으로
- 이 때 no-offset 기반 페이징을 위해 lastArticleId를 전달받고 동적쿼리로 해당id가 null이면 리스트 첫 조회 / lastArticleId가 존재하면 
  lastArticleId의 게시글보다 이전에 생성된 게시글 최신순으로 n개 가져오기

문제점

회의에서 클라이언트와 기획이 게시글의 내용은 text-image-text-image part로 순서를 보장해주고 싶다고했다.
그래서 백엔드는 article 1 <- N textArticleUnit의 구조를 갖춰야했다. imageUnit도 마찬가지

★문제는 이 상황에서 위 검색을 구현해야하는 것. 
하나의 article이 본문과 제목을 모두 갖고있는게 아니라 
제목에 keyword를 포함한 article +. 본문 content에 keyword를 포함한 textUnit의 article을  대상으로 검색을 구현해야했다.

해결

override fun searchArticlesWithKeywordAndLastArticleId(targetKeyword: String?, lastArticleId: Long?): List

{ return queryFactory.select(article) .from(article) .leftJoin(textArticleUnit) .on(article.id.eq(textArticleUnit.article.id)) .where( checkIfArticleContainKeyword(targetKeyword), checkLastArticleId(lastArticleId) ) .distinct() .fetch() }

private fun checkIfArticleContainKeyword(targetKeyword: String?): BooleanExpression? { if (targetKeyword == null || targetKeyword.equals("")) { return null } return textArticleUnit.content.containsIgnoreCase(targetKeyword) .or(checkIfArticleTitleContainKeyword(targetKeyword)) } private fun checkIfArticleTitleContainKeyword(targetKeyword: String?): BooleanExpression { return article.title.containsIgnoreCase(targetKeyword) } private fun checkLastArticleId(lastArticleId: Long?): BooleanExpression? { if (lastArticleId == null) { return null } return article.id.lt(lastArticleId) }



다만! 
- 여기서 한 가지 짚고갈게있었다.
  - 조건 1. 제목에 keyword를 포함한 article 
  - 조건 2. 본문 content에 keyword를 포함한 textUnit의 article
- 예를들어 제목에도 keyword를 갖고있고 본문에도 keyword가 포함된경우라면?
- 중복발생 -> distinct 활용