diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt index b5f858e..ff2d24e 100644 --- a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttLectureSyncJobConfig.kt @@ -37,6 +37,7 @@ class SnuttLectureSyncJobConfig( private val semesterLectureRepository: SemesterLectureRepository, private val lectureRepository: LectureRepository, private val snuttLectureIdMapRepository: SnuttLectureIdMapRepository, + private val ratingSyncJob: Job, ) { companion object { private const val JOB_NAME = "SYNC_JOB" @@ -78,6 +79,7 @@ class SnuttLectureSyncJobConfig( ), ), ) + .next(ratingSyncJobStep(jobRepository)) .build() } @@ -93,6 +95,7 @@ class SnuttLectureSyncJobConfig( .toMutableMap() return JobBuilder(JOB_NAME, jobRepository) .start(customReaderStep(jobRepository, Query())) + .next(ratingSyncJobStep(jobRepository)) .build() } @@ -169,6 +172,11 @@ class SnuttLectureSyncJobConfig( snuttLectureIdMapRepository.saveAll(items.map { it.snuttLectureIdMap }) } } + + private fun ratingSyncJobStep(jobRepository: JobRepository): Step = + StepBuilder(SnuttRatingSyncJobConfig.RATING_SYNC_JOB_NAME, jobRepository) + .job(ratingSyncJob) + .build() } data class SyncProcessResult( diff --git a/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt new file mode 100644 index 0000000..c373f50 --- /dev/null +++ b/batch/src/main/kotlin/com/wafflestudio/snuttev/sync/SnuttRatingSyncJobConfig.kt @@ -0,0 +1,85 @@ +package com.wafflestudio.snuttev.sync + +import com.wafflestudio.snuttev.core.domain.lecture.model.SnuttLectureIdMap +import com.wafflestudio.snuttev.core.domain.lecture.repository.LectureRepository +import jakarta.persistence.EntityManagerFactory +import org.springframework.batch.core.Job +import org.springframework.batch.core.Step +import org.springframework.batch.core.job.builder.JobBuilder +import org.springframework.batch.core.repository.JobRepository +import org.springframework.batch.core.step.builder.StepBuilder +import org.springframework.batch.item.ItemWriter +import org.springframework.batch.item.database.JpaPagingItemReader +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.data.mongodb.core.BulkOperations +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.Update +import org.springframework.orm.jpa.JpaTransactionManager + +@Configuration +@Profile(value = ["!test"]) +class SnuttRatingSyncJobConfig( + private val entityManagerFactory: EntityManagerFactory, + private val mongoTemplate: MongoTemplate, + private val lectureRepository: LectureRepository, +) { + companion object { + const val RATING_SYNC_JOB_NAME = "RATING_SYNC_JOB" + private const val CUSTOM_READER_JOB_STEP = RATING_SYNC_JOB_NAME + "_STEP" + private const val CHUNK_SIZE = 1000000 + } + + @Bean + fun ratingSyncJob(jobRepository: JobRepository): Job { + return JobBuilder(RATING_SYNC_JOB_NAME, jobRepository) + .start(customReaderStep(jobRepository)) + .build() + } + + private fun customReaderStep(jobRepository: JobRepository): Step { + return StepBuilder(CUSTOM_READER_JOB_STEP, jobRepository) + .chunk( + CHUNK_SIZE, + JpaTransactionManager().apply { + this.entityManagerFactory = this@SnuttRatingSyncJobConfig.entityManagerFactory + }, + ) + .reader(reader()) + .writer(writer()) + .build() + } + + private fun reader(): JpaPagingItemReader = + JpaPagingItemReaderBuilder() + .name("snuttLectureIdMapReader") + .entityManagerFactory(entityManagerFactory) + .queryString("SELECT s FROM SnuttLectureIdMap s JOIN FETCH s.semesterLecture") + .pageSize(CHUNK_SIZE) + .build() + + private fun writer(): ItemWriter { + return ItemWriter { items -> + val lectureIdtoLectureRatingMap = + lectureRepository.findAllRatingsByLectureIds( + items.mapNotNull { it.semesterLecture.lecture.id }, + ) + .associateBy { it.id } + val bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, "lectures") + items.forEach { + val evInfo = lectureIdtoLectureRatingMap[it.semesterLecture.lecture.id] + bulkOps.updateOne( + Query(Criteria.where("_id").`is`(it.snuttId)), + Update().set("evInfo.evId", evInfo?.id) + .set("evInfo.avgRating", evInfo?.avgRating) + .set("evInfo.count", evInfo?.count), + ) + } + bulkOps.execute() + } + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 9070e49..3395cad 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation("software.amazon.awssdk:secretsmanager:2.20.66") implementation("software.amazon.awssdk:sts:2.20.66") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive") implementation("org.springframework.boot:spring-boot-starter-data-redis") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationService.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationService.kt index 7ed4fc4..0da8334 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationService.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/evaluation/service/EvaluationService.kt @@ -36,6 +36,8 @@ import com.wafflestudio.snuttev.core.domain.evaluation.repository.LectureEvaluat import com.wafflestudio.snuttev.core.domain.lecture.model.SemesterLecture import com.wafflestudio.snuttev.core.domain.lecture.repository.LectureRepository import com.wafflestudio.snuttev.core.domain.lecture.repository.SemesterLectureRepository +import com.wafflestudio.snuttev.core.domain.lecture.repository.SnuttLectureIdMapRepository +import com.wafflestudio.snuttev.core.domain.mongo.MongoService import com.wafflestudio.snuttev.core.domain.tag.repository.TagRepository import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull @@ -51,6 +53,8 @@ class EvaluationService internal constructor( private val evaluationReportRepository: EvaluationReportRepository, private val evaluationLikeRepository: EvaluationLikeRepository, private val cache: Cache, + private val snuttLectureIdMapRepository: SnuttLectureIdMapRepository, + private val mongoService: MongoService, ) { companion object { private const val DEFAULT_PAGE_SIZE = 20 @@ -77,6 +81,8 @@ class EvaluationService internal constructor( cache.deleteAll(CacheKey.EVALUATIONS_BY_TAG_PAGE) + updateEvInfosBySemesterLecture(semesterLecture) + return genLectureEvaluationDto(lectureEvaluation) } @@ -276,6 +282,7 @@ class EvaluationService internal constructor( } cache.deleteAll(CacheKey.EVALUATIONS_BY_TAG_PAGE) + updateEvInfosBySemesterLecture(evaluation.semesterLecture) val isLiked = evaluationLikeRepository.existsByLectureEvaluationAndUserId(evaluation, userId) return EvaluationWithSemesterResponse.of(evaluation, userId, isLiked) @@ -308,6 +315,7 @@ class EvaluationService internal constructor( lectureEvaluation.isHidden = true cache.deleteAll(CacheKey.EVALUATIONS_BY_TAG_PAGE) + updateEvInfosBySemesterLecture(lectureEvaluation.semesterLecture) } fun reportEvaluation( @@ -391,4 +399,10 @@ class EvaluationService internal constructor( content = evaluationReport.content, isHidden = evaluationReport.isHidden, ) + + private fun updateEvInfosBySemesterLecture(semesterLecture: SemesterLecture) { + val evInfo = lectureRepository.findAllRatingsByLectureIds(listOf(semesterLecture.lecture.id!!)).firstOrNull() + val snuttIds = snuttLectureIdMapRepository.findAllBySemesterLecture(semesterLecture).map { it.snuttId } + mongoService.updateEvInfoToSnuttIds(snuttIds, evInfo) + } } diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt index 7df3af5..2ff17dd 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/LectureRepository.kt @@ -32,4 +32,13 @@ interface LectureRepository : JpaRepository, LectureRepositoryCu """, ) fun findAllRatingsByLectureIds(ids: Iterable): List + + @Query( + """ + select new com.wafflestudio.snuttev.core.domain.lecture.model.LectureRatingDao( + sl.lecture.id, avg(le.rating), count(le.id) + ) from LectureEvaluation le right join le.semesterLecture sl where le.isHidden = false group by sl.lecture.id + """, + ) + fun findAllRatings(): List } diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/SnuttLectureIdMapRepository.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/SnuttLectureIdMapRepository.kt index 114245f..dd8566f 100644 --- a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/SnuttLectureIdMapRepository.kt +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/lecture/repository/SnuttLectureIdMapRepository.kt @@ -1,5 +1,6 @@ package com.wafflestudio.snuttev.core.domain.lecture.repository +import com.wafflestudio.snuttev.core.domain.lecture.model.SemesterLecture import com.wafflestudio.snuttev.core.domain.lecture.model.SnuttLectureIdMap import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query @@ -8,4 +9,5 @@ interface SnuttLectureIdMapRepository : JpaRepository { @Query("SELECT ttm FROM SnuttLectureIdMap ttm JOIN FETCH ttm.semesterLecture WHERE ttm.snuttId IN :snuttIds") fun findAllWithSemesterLectureBySnuttIdIn(snuttIds: List): List fun findBySnuttId(snuttId: String): SnuttLectureIdMap? + fun findAllBySemesterLecture(semesterLecture: SemesterLecture): List } diff --git a/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/mongo/MongoService.kt b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/mongo/MongoService.kt new file mode 100644 index 0000000..77d1267 --- /dev/null +++ b/core/src/main/kotlin/com/wafflestudio/snuttev/core/domain/mongo/MongoService.kt @@ -0,0 +1,22 @@ +package com.wafflestudio.snuttev.core.domain.mongo + +import com.wafflestudio.snuttev.core.domain.lecture.model.LectureRatingDao +import org.springframework.data.mongodb.core.ReactiveMongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.Update +import org.springframework.stereotype.Service + +@Service +class MongoService( + private val mongoTemplate: ReactiveMongoTemplate, +) { + fun updateEvInfoToSnuttIds(snuttIds: List, evInfo: LectureRatingDao?) = + mongoTemplate.updateMulti( + Query(Criteria.where("_id").`in`(snuttIds)), + Update().set("evInfo.evId", evInfo?.id) + .set("evInfo.avgRating", evInfo?.avgRating) + .set("evInfo.count", evInfo?.count), + "lectures", + ).subscribe() +}