Skip to content

Commit ab7e2de

Browse files
authored
setting: add querydsl dependency (#31)
* setting: QueryDSL 5.1.0 (Jakarta) 의존성 추가 (#30) * setting: JPAQueryFactory 빈 설정 추가 (#30) * feat: QueryDSL Repository 생성 및 JpaRepository 쿼리를 메서드 체인으로 전환 (#30) * refactor: JpaRepository에서 커스텀 쿼리 제거, CRUD 전용으로 축소 (#30) * refactor: Adapter에서 QueryDSL Repository 주입으로 변경 (#30)
1 parent 734143f commit ab7e2de

16 files changed

Lines changed: 272 additions & 126 deletions

gradle/libs.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ springBoot = "3.3.0"
33
springDependencyManagement = "1.1.5"
44
kotlinPlugin = "2.0.0"
55
jjwt = "0.12.6"
6+
querydsl = "5.1.0"
67
ktLintPlugin = "12.1.1"
78
gradleBuildScanPlugin = "3.18.1"
89
javaVersion = "21"
@@ -37,6 +38,10 @@ spring-boot-starter-jpa = { group = "org.springframework.boot", name = "spring-b
3738
spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis" }
3839
spring-boot-docs = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version = "2.5.0" }
3940

41+
# QueryDSL
42+
querydsl-jpa = { group = "com.querydsl", name = "querydsl-jpa", version.ref = "querydsl" }
43+
querydsl-apt = { group = "com.querydsl", name = "querydsl-apt", version.ref = "querydsl" }
44+
4045
# MySQL
4146
mysql = { group = "com.mysql", name = "mysql-connector-j" }
4247

linktrip-output-persistence/mysql/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ plugins {
55
dependencies {
66
implementation(project(":linktrip-application"))
77
implementation(libs.bundles.adaptor.persistence.mysql)
8+
9+
// QueryDSL (Jakarta)
10+
implementation(variantOf(libs.querydsl.jpa) { classifier("jakarta") })
11+
kapt(variantOf(libs.querydsl.apt) { classifier("jakarta") })
812
}
913

1014
allOpen {

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/PlaceEnrichPersistenceAdapter.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.linktrip.application.domain.video.PlaceEnrichResult
55
import com.linktrip.application.port.output.persistence.PlaceEnrichPersistencePort
66
import com.linktrip.output.persistence.mysql.entity.PlaceEntity
77
import com.linktrip.output.persistence.mysql.repository.PlaceJpaRepository
8-
import com.linktrip.output.persistence.mysql.repository.VideoScheduleItemJpaRepository
8+
import com.linktrip.output.persistence.mysql.repository.VideoScheduleItemQuerydslRepository
99
import mu.KotlinLogging
1010
import org.springframework.stereotype.Component
1111
import org.springframework.transaction.annotation.Transactional
@@ -14,7 +14,7 @@ private val logger = KotlinLogging.logger {}
1414

1515
@Component
1616
class PlaceEnrichPersistenceAdapter(
17-
private val videoScheduleItemJpaRepository: VideoScheduleItemJpaRepository,
17+
private val scheduleItemQuerydslRepository: VideoScheduleItemQuerydslRepository,
1818
private val placeJpaRepository: PlaceJpaRepository,
1919
) : PlaceEnrichPersistencePort {
2020
@Transactional
@@ -25,7 +25,7 @@ class PlaceEnrichPersistenceAdapter(
2525
if (results.isEmpty()) return
2626

2727
val itemMap =
28-
videoScheduleItemJpaRepository
28+
scheduleItemQuerydslRepository
2929
.findByVideoSummaryIdOrderByDayAscItemOrderAsc(videoSummaryId)
3030
.associateBy { it.id }
3131

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/VideoScheduleItemAdapter.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,38 @@ package com.linktrip.output.persistence.mysql.adapter
22

33
import com.linktrip.application.domain.video.VideoScheduleItem
44
import com.linktrip.application.port.output.persistence.VideoScheduleItemPersistencePort
5-
import com.linktrip.output.persistence.mysql.entity.PlaceEntity
65
import com.linktrip.output.persistence.mysql.entity.VideoScheduleItemEntity
76
import com.linktrip.output.persistence.mysql.repository.VideoScheduleItemJpaRepository
7+
import com.linktrip.output.persistence.mysql.repository.VideoScheduleItemQuerydslRepository
88
import org.springframework.stereotype.Component
99

1010
@Component
1111
class VideoScheduleItemAdapter(
12-
private val videoScheduleItemJpaRepository: VideoScheduleItemJpaRepository,
12+
private val jpaRepository: VideoScheduleItemJpaRepository,
13+
private val querydslRepository: VideoScheduleItemQuerydslRepository,
1314
) : VideoScheduleItemPersistencePort {
1415
override fun saveAll(items: List<VideoScheduleItem>) {
1516
val entities = items.map { VideoScheduleItemEntity.from(it) }
16-
videoScheduleItemJpaRepository.saveAll(entities)
17+
jpaRepository.saveAll(entities)
1718
}
1819

1920
override fun findByVideoSummaryId(videoSummaryId: String): List<VideoScheduleItem> =
20-
videoScheduleItemJpaRepository
21+
querydslRepository
2122
.findByVideoSummaryIdOrderByDayAscItemOrderAsc(videoSummaryId)
2223
.map { it.toDomain() }
2324

2425
override fun findByVideoSummaryIdWithPlace(videoSummaryId: String): List<VideoScheduleItem> =
25-
videoScheduleItemJpaRepository
26+
querydslRepository
2627
.findByVideoSummaryIdWithPlace(videoSummaryId)
2728
.map { row ->
28-
val item = (row[0] as VideoScheduleItemEntity).toDomain()
29-
val place = (row[1] as? PlaceEntity)?.toDomain()
30-
item.copy(place = place)
29+
row.item.toDomain().copy(place = row.place?.toDomain())
3130
}
3231

3332
override fun findRetryableItems(videoSummaryId: String): List<VideoScheduleItem> =
34-
videoScheduleItemJpaRepository
33+
querydslRepository
3534
.findRetryableItems(videoSummaryId)
3635
.map { it.toDomain() }
3736

3837
override fun findVideoSummaryIdsWithRetryableItems(): List<String> =
39-
videoScheduleItemJpaRepository.findVideoSummaryIdsWithRetryableItems()
38+
querydslRepository.findVideoSummaryIdsWithRetryableItems()
4039
}

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeChannelPersistenceAdapter.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ import com.linktrip.application.port.output.persistence.YouTubeChannelPersistenc
66
import com.linktrip.output.persistence.mysql.entity.YouTubeChannelEntity
77
import com.linktrip.output.persistence.mysql.entity.YouTubeRecentVideoEntity
88
import com.linktrip.output.persistence.mysql.repository.YouTubeChannelJpaRepository
9+
import com.linktrip.output.persistence.mysql.repository.YouTubeChannelQuerydslRepository
910
import com.linktrip.output.persistence.mysql.repository.YouTubeRecentVideoJpaRepository
11+
import com.linktrip.output.persistence.mysql.repository.YouTubeRecentVideoQuerydslRepository
1012
import org.springframework.stereotype.Component
1113
import org.springframework.transaction.annotation.Transactional
1214

1315
@Component("youtubeChannelDbAdapter")
1416
class YouTubeChannelPersistenceAdapter(
15-
private val youTubeChannelJpaRepository: YouTubeChannelJpaRepository,
16-
private val youTubeRecentVideoJpaRepository: YouTubeRecentVideoJpaRepository,
17+
private val channelJpaRepository: YouTubeChannelJpaRepository,
18+
private val channelQuerydslRepository: YouTubeChannelQuerydslRepository,
19+
private val recentVideoJpaRepository: YouTubeRecentVideoJpaRepository,
20+
private val recentVideoQuerydslRepository: YouTubeRecentVideoQuerydslRepository,
1721
) : YouTubeChannelPersistencePort {
1822
@Transactional
1923
override fun saveAll(channels: List<YouTubeChannelDetail>) {
@@ -23,18 +27,18 @@ class YouTubeChannelPersistenceAdapter(
2327

2428
// 채널 upsert
2529
val existingMap =
26-
youTubeChannelJpaRepository.findAllByChannelIdIn(channelIds)
30+
channelQuerydslRepository.findAllByChannelIdIn(channelIds)
2731
.associateBy { it.channelId }
2832

2933
val entitiesToSave =
3034
channels.map { detail ->
3135
existingMap[detail.channelId]?.apply { updateFrom(detail) }
3236
?: YouTubeChannelEntity.from(IdGenerator.generate(), detail)
3337
}
34-
youTubeChannelJpaRepository.saveAll(entitiesToSave)
38+
channelJpaRepository.saveAll(entitiesToSave)
3539

3640
// 최신 영상: 기존 삭제 후 새로 저장
37-
youTubeRecentVideoJpaRepository.deleteAllByChannelIdIn(channelIds)
41+
recentVideoQuerydslRepository.deleteAllByChannelIdIn(channelIds)
3842

3943
val videoEntities =
4044
channels.flatMap { channel ->
@@ -43,19 +47,19 @@ class YouTubeChannelPersistenceAdapter(
4347
}
4448
}
4549
if (videoEntities.isNotEmpty()) {
46-
youTubeRecentVideoJpaRepository.saveAll(videoEntities)
50+
recentVideoJpaRepository.saveAll(videoEntities)
4751
}
4852
}
4953

5054
@Transactional(readOnly = true)
5155
override fun findAll(): List<YouTubeChannelDetail> {
52-
val channels = youTubeChannelJpaRepository.findAllByOrderBySubscriberCountDesc()
56+
val channels = channelQuerydslRepository.findAllOrderBySubscriberCountDesc()
5357
if (channels.isEmpty()) return emptyList()
5458

5559
// N+1 방지: 채널 ID 목록으로 한번에 조회 후 그룹핑
5660
val channelIds = channels.map { it.channelId }
5761
val recentVideosMap =
58-
youTubeRecentVideoJpaRepository.findAllByChannelIdIn(channelIds)
62+
recentVideoQuerydslRepository.findAllByChannelIdIn(channelIds)
5963
.map { it.toDomain() }
6064
.groupBy { it.channelId }
6165
.mapValues { (_, videos) ->

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import com.linktrip.application.domain.common.CursorPage
44
import com.linktrip.application.domain.youtube.YouTubeVideoDetail
55
import com.linktrip.application.port.output.persistence.YouTubeVideoPersistencePort
66
import com.linktrip.output.persistence.mysql.entity.YouTubeVideoEntity
7-
import com.linktrip.output.persistence.mysql.repository.YouTubeVideoJpaRepository
7+
import com.linktrip.output.persistence.mysql.repository.YouTubeVideoQuerydslRepository
88
import jakarta.persistence.EntityManager
99
import org.springframework.data.domain.PageRequest
1010
import org.springframework.stereotype.Component
@@ -13,7 +13,7 @@ import java.time.LocalDateTime
1313

1414
@Component("youtubeVideoDbAdapter")
1515
class YouTubeVideoPersistenceAdapter(
16-
private val youTubeVideoJpaRepository: YouTubeVideoJpaRepository,
16+
private val querydslRepository: YouTubeVideoQuerydslRepository,
1717
private val entityManager: EntityManager,
1818
) : YouTubeVideoPersistencePort {
1919
@Transactional
@@ -26,21 +26,21 @@ class YouTubeVideoPersistenceAdapter(
2626

2727
@Transactional(readOnly = true)
2828
override fun findExistingVideoIds(videoIds: List<String>): Set<String> =
29-
youTubeVideoJpaRepository.findVideoIdsByVideoIdIn(videoIds).toSet()
29+
querydslRepository.findVideoIdsByVideoIdIn(videoIds).toSet()
3030

3131
@Transactional(readOnly = true)
3232
override fun findAll(): List<YouTubeVideoDetail> =
33-
youTubeVideoJpaRepository.findAllByOrderByViewCountDesc()
33+
querydslRepository.findAllOrderByViewCountDesc()
3434
.map { it.toDomain() }
3535

3636
@Transactional(readOnly = true)
3737
override fun findAllByCountry(country: String): List<YouTubeVideoDetail> =
38-
youTubeVideoJpaRepository.findAllByCountryOrderByViewCountDesc(country)
38+
querydslRepository.findAllByCountryOrderByViewCountDesc(country)
3939
.map { it.toDomain() }
4040

4141
@Transactional(readOnly = true)
4242
override fun findAllByRegion(region: String): List<YouTubeVideoDetail> =
43-
youTubeVideoJpaRepository.findAllByRegionOrderByViewCountDesc(region)
43+
querydslRepository.findAllByRegionOrderByViewCountDesc(region)
4444
.map { it.toDomain() }
4545

4646
@Transactional(readOnly = true)
@@ -53,9 +53,9 @@ class YouTubeVideoPersistenceAdapter(
5353

5454
val entities =
5555
if (cursor != null) {
56-
youTubeVideoJpaRepository.findAllByThemeAndCreatedAtBefore(theme, cursor, pageable)
56+
querydslRepository.findAllByThemeAndCreatedAtBefore(theme, cursor, pageable)
5757
} else {
58-
youTubeVideoJpaRepository.findAllByTheme(theme, pageable)
58+
querydslRepository.findAllByTheme(theme, pageable)
5959
}
6060

6161
val hasNext = entities.size > size
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.linktrip.output.persistence.mysql.config
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory
4+
import jakarta.persistence.EntityManager
5+
import org.springframework.context.annotation.Bean
6+
import org.springframework.context.annotation.Configuration
7+
8+
@Configuration
9+
class QuerydslConfig(
10+
private val entityManager: EntityManager,
11+
) {
12+
@Bean
13+
fun jpaQueryFactory(): JPAQueryFactory = JPAQueryFactory(entityManager)
14+
}
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,6 @@
11
package com.linktrip.output.persistence.mysql.repository
22

3-
import com.linktrip.application.domain.video.Category
43
import com.linktrip.output.persistence.mysql.entity.VideoScheduleItemEntity
54
import org.springframework.data.jpa.repository.JpaRepository
6-
import org.springframework.data.jpa.repository.Query
75

8-
interface VideoScheduleItemJpaRepository : JpaRepository<VideoScheduleItemEntity, String> {
9-
fun findByVideoSummaryIdOrderByDayAscItemOrderAsc(videoSummaryId: String): List<VideoScheduleItemEntity>
10-
11-
@Query(
12-
"""
13-
SELECT i, p FROM VideoScheduleItemEntity i
14-
LEFT JOIN PlaceEntity p ON i.placeId = p.id
15-
WHERE i.videoSummaryId = :videoSummaryId
16-
ORDER BY i.day ASC, i.itemOrder ASC
17-
""",
18-
)
19-
fun findByVideoSummaryIdWithPlace(videoSummaryId: String): List<Array<Any>>
20-
21-
@Query(
22-
"""
23-
SELECT e FROM VideoScheduleItemEntity e
24-
WHERE e.videoSummaryId = :videoSummaryId
25-
AND e.placeId IS NULL
26-
AND e.category <> :excludeCategory
27-
AND e.placeSearchCount < :maxSearchCount
28-
ORDER BY e.day ASC, e.itemOrder ASC
29-
""",
30-
)
31-
fun findRetryableItems(
32-
videoSummaryId: String,
33-
excludeCategory: Category = Category.TRANSPORTATION,
34-
maxSearchCount: Int = 10,
35-
): List<VideoScheduleItemEntity>
36-
37-
@Query(
38-
"""
39-
SELECT DISTINCT e.videoSummaryId FROM VideoScheduleItemEntity e
40-
WHERE e.placeId IS NULL
41-
AND e.category <> :excludeCategory
42-
AND e.placeSearchCount < :maxSearchCount
43-
""",
44-
)
45-
fun findVideoSummaryIdsWithRetryableItems(
46-
excludeCategory: Category = Category.TRANSPORTATION,
47-
maxSearchCount: Int = 10,
48-
): List<String>
49-
}
6+
interface VideoScheduleItemJpaRepository : JpaRepository<VideoScheduleItemEntity, String>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.linktrip.output.persistence.mysql.repository
2+
3+
import com.linktrip.application.domain.video.Category
4+
import com.linktrip.output.persistence.mysql.entity.QPlaceEntity
5+
import com.linktrip.output.persistence.mysql.entity.QVideoScheduleItemEntity
6+
import com.linktrip.output.persistence.mysql.entity.VideoScheduleItemEntity
7+
import com.linktrip.output.persistence.mysql.repository.dto.ScheduleItemWithPlace
8+
import com.querydsl.jpa.impl.JPAQueryFactory
9+
import org.springframework.stereotype.Repository
10+
11+
@Repository
12+
class VideoScheduleItemQuerydslRepository(
13+
private val queryFactory: JPAQueryFactory,
14+
) {
15+
private val scheduleItem = QVideoScheduleItemEntity.videoScheduleItemEntity
16+
private val place = QPlaceEntity.placeEntity
17+
18+
fun findByVideoSummaryIdOrderByDayAscItemOrderAsc(videoSummaryId: String): List<VideoScheduleItemEntity> =
19+
queryFactory
20+
.selectFrom(scheduleItem)
21+
.where(scheduleItem.videoSummaryId.eq(videoSummaryId))
22+
.orderBy(
23+
scheduleItem.day.asc(),
24+
scheduleItem.itemOrder.asc(),
25+
)
26+
.fetch()
27+
28+
fun findByVideoSummaryIdWithPlace(videoSummaryId: String): List<ScheduleItemWithPlace> =
29+
queryFactory
30+
.select(scheduleItem, place)
31+
.from(scheduleItem)
32+
.leftJoin(place).on(scheduleItem.placeId.eq(place.id))
33+
.where(scheduleItem.videoSummaryId.eq(videoSummaryId))
34+
.orderBy(
35+
scheduleItem.day.asc(),
36+
scheduleItem.itemOrder.asc(),
37+
)
38+
.fetch()
39+
.map { tuple ->
40+
ScheduleItemWithPlace(
41+
item = tuple.get(scheduleItem)!!,
42+
place = tuple.get(place),
43+
)
44+
}
45+
46+
fun findRetryableItems(
47+
videoSummaryId: String,
48+
excludeCategory: Category = Category.TRANSPORTATION,
49+
maxSearchCount: Int = 10,
50+
): List<VideoScheduleItemEntity> =
51+
queryFactory
52+
.selectFrom(scheduleItem)
53+
.where(
54+
scheduleItem.videoSummaryId.eq(videoSummaryId),
55+
scheduleItem.placeId.isNull,
56+
scheduleItem.category.ne(excludeCategory),
57+
scheduleItem.placeSearchCount.lt(maxSearchCount),
58+
)
59+
.orderBy(
60+
scheduleItem.day.asc(),
61+
scheduleItem.itemOrder.asc(),
62+
)
63+
.fetch()
64+
65+
fun findVideoSummaryIdsWithRetryableItems(
66+
excludeCategory: Category = Category.TRANSPORTATION,
67+
maxSearchCount: Int = 10,
68+
): List<String> =
69+
queryFactory
70+
.selectDistinct(scheduleItem.videoSummaryId)
71+
.from(scheduleItem)
72+
.where(
73+
scheduleItem.placeId.isNull,
74+
scheduleItem.category.ne(excludeCategory),
75+
scheduleItem.placeSearchCount.lt(maxSearchCount),
76+
)
77+
.fetch()
78+
}

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeChannelJpaRepository.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,4 @@ package com.linktrip.output.persistence.mysql.repository
33
import com.linktrip.output.persistence.mysql.entity.YouTubeChannelEntity
44
import org.springframework.data.jpa.repository.JpaRepository
55

6-
interface YouTubeChannelJpaRepository : JpaRepository<YouTubeChannelEntity, String> {
7-
fun findAllByChannelIdIn(channelIds: List<String>): List<YouTubeChannelEntity>
8-
9-
fun findAllByOrderBySubscriberCountDesc(): List<YouTubeChannelEntity>
10-
}
6+
interface YouTubeChannelJpaRepository : JpaRepository<YouTubeChannelEntity, String>

0 commit comments

Comments
 (0)