fix(home-following): inbox 중복 insert 처리를 보강한다
This commit is contained in:
@@ -12,6 +12,22 @@ interface HomeFollowingNewsInboxJpaRepository : JpaRepository<HomeFollowingNewsI
|
||||
sourceKey: String
|
||||
): Boolean
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select member_id
|
||||
from home_following_news_inbox
|
||||
where news_type = :newsType
|
||||
and source_key = :sourceKey
|
||||
and member_id in :memberIds
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findExistingMemberIds(
|
||||
@Param("newsType") newsType: String,
|
||||
@Param("sourceKey") sourceKey: String,
|
||||
@Param("memberIds") memberIds: Collection<Long>
|
||||
): List<Long>
|
||||
|
||||
@Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
@Query(
|
||||
value = """
|
||||
|
||||
@@ -5,23 +5,33 @@ import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInbo
|
||||
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxRecord
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.transaction.PlatformTransactionManager
|
||||
import org.springframework.transaction.TransactionDefinition
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@Repository
|
||||
class HomeFollowingNewsInboxPersistenceAdapter(
|
||||
private val repository: HomeFollowingNewsInboxJpaRepository,
|
||||
private val entityManager: EntityManager
|
||||
private val entityManager: EntityManager,
|
||||
transactionManager: PlatformTransactionManager? = null
|
||||
) : HomeFollowingNewsInboxPort {
|
||||
@Transactional
|
||||
private val transactionTemplate = transactionManager?.let {
|
||||
TransactionTemplate(it).also { template ->
|
||||
template.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertIgnoreAll(records: List<HomeFollowingNewsInboxRecord>): Int {
|
||||
if (records.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return records
|
||||
val distinctRecords = records
|
||||
.distinctBy { Triple(it.memberId, it.newsType, it.sourceKey) }
|
||||
.sumOf { record -> insertIgnore(record) }
|
||||
|
||||
return insertWithRetry(distinctRecords)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -33,30 +43,52 @@ class HomeFollowingNewsInboxPersistenceAdapter(
|
||||
return repository.findActiveFollowerIds(creatorId)
|
||||
}
|
||||
|
||||
private fun insertIgnore(record: HomeFollowingNewsInboxRecord): Int {
|
||||
val newsType = FollowingNewsType.valueOf(record.newsType)
|
||||
if (repository.existsByMemberIdAndNewsTypeAndSourceKey(record.memberId, newsType, record.sourceKey)) {
|
||||
private fun insertWithRetry(records: List<HomeFollowingNewsInboxRecord>): Int {
|
||||
var lastFailure: DataIntegrityViolationException? = null
|
||||
repeat(MAX_INSERT_ATTEMPTS) {
|
||||
try {
|
||||
return executeInsertAttempt(records)
|
||||
} catch (ex: DataIntegrityViolationException) {
|
||||
lastFailure = ex
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
throw requireNotNull(lastFailure)
|
||||
}
|
||||
|
||||
private fun executeInsertAttempt(records: List<HomeFollowingNewsInboxRecord>): Int {
|
||||
return transactionTemplate?.execute { insertNewRows(records) } ?: insertNewRows(records)
|
||||
}
|
||||
|
||||
private fun insertNewRows(records: List<HomeFollowingNewsInboxRecord>): Int {
|
||||
val entities = records
|
||||
.groupBy { SourceKey(newsType = it.newsType, sourceKey = it.sourceKey) }
|
||||
.flatMap { (sourceKey, sourceRecords) ->
|
||||
FollowingNewsType.valueOf(sourceKey.newsType)
|
||||
val existingMemberIds = repository.findExistingMemberIds(
|
||||
newsType = sourceKey.newsType,
|
||||
sourceKey = sourceKey.sourceKey,
|
||||
memberIds = sourceRecords.map { it.memberId }
|
||||
).toSet()
|
||||
sourceRecords
|
||||
.filterNot { it.memberId in existingMemberIds }
|
||||
.map { it.toEntity() }
|
||||
}
|
||||
|
||||
if (entities.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return try {
|
||||
repository.saveAndFlush(record.toEntity(newsType))
|
||||
1
|
||||
} catch (e: DataIntegrityViolationException) {
|
||||
entityManager.clear()
|
||||
if (repository.existsByMemberIdAndNewsTypeAndSourceKey(record.memberId, newsType, record.sourceKey)) {
|
||||
0
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
repository.saveAll(entities)
|
||||
repository.flush()
|
||||
return entities.size
|
||||
}
|
||||
|
||||
private fun HomeFollowingNewsInboxRecord.toEntity(newsType: FollowingNewsType): HomeFollowingNewsInbox {
|
||||
private fun HomeFollowingNewsInboxRecord.toEntity(): HomeFollowingNewsInbox {
|
||||
return HomeFollowingNewsInbox(
|
||||
memberId = memberId,
|
||||
creatorId = creatorId,
|
||||
newsType = newsType,
|
||||
newsType = FollowingNewsType.valueOf(newsType),
|
||||
sourceKey = sourceKey,
|
||||
targetId = targetId,
|
||||
occurredAtUtc = occurredAtUtc,
|
||||
@@ -70,4 +102,13 @@ class HomeFollowingNewsInboxPersistenceAdapter(
|
||||
isAdult = isAdult
|
||||
)
|
||||
}
|
||||
|
||||
private data class SourceKey(
|
||||
val newsType: String,
|
||||
val sourceKey: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MAX_INSERT_ATTEMPTS = 2
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user