diff --git a/backend/data/src/main/kotlin/io/tolgee/activity/iterceptor/InterceptedEventsManager.kt b/backend/data/src/main/kotlin/io/tolgee/activity/iterceptor/InterceptedEventsManager.kt index fb2b17d610..6062fd7c1f 100644 --- a/backend/data/src/main/kotlin/io/tolgee/activity/iterceptor/InterceptedEventsManager.kt +++ b/backend/data/src/main/kotlin/io/tolgee/activity/iterceptor/InterceptedEventsManager.kt @@ -10,6 +10,7 @@ import io.tolgee.activity.data.EntityDescriptionWithRelations import io.tolgee.activity.data.PropertyModification import io.tolgee.activity.data.RevisionType import io.tolgee.activity.propChangesProvider.PropChangesProvider +import io.tolgee.component.ActivityHolderProvider import io.tolgee.dtos.cacheable.UserAccountDto import io.tolgee.events.OnProjectActivityEvent import io.tolgee.model.EntityWithId @@ -274,10 +275,13 @@ class InterceptedEventsManager( applicationContext.getBean(ActivityService::class.java) } - private val activityHolder: ActivityHolder by lazy { - applicationContext.getBean(ActivityHolder::class.java) + private val activityHolderProvider: ActivityHolderProvider by lazy { + applicationContext.getBean(ActivityHolderProvider::class.java) } + private val activityHolder + get() = activityHolderProvider.getActivityHolder() + private val userAccount: UserAccountDto? get() = authenticationFacade.authenticatedUserOrNull } diff --git a/backend/data/src/main/kotlin/io/tolgee/activity/propChangesProvider/TagsPropChangesProvider.kt b/backend/data/src/main/kotlin/io/tolgee/activity/propChangesProvider/TagsPropChangesProvider.kt index 792f50ec5f..601b399ca1 100644 --- a/backend/data/src/main/kotlin/io/tolgee/activity/propChangesProvider/TagsPropChangesProvider.kt +++ b/backend/data/src/main/kotlin/io/tolgee/activity/propChangesProvider/TagsPropChangesProvider.kt @@ -9,6 +9,10 @@ class TagsPropChangesProvider : PropChangesProvider { override fun getChanges(old: Any?, new: Any?): PropertyModification? { if (old is Collection<*> && new is Collection<*>) { + if(old === new){ + return null + } + val oldTagNames = mapSetToTagNames(old) val newTagNames = mapSetToTagNames(new) if (oldTagNames.containsAll(newTagNames) && newTagNames.containsAll(oldTagNames)) { diff --git a/backend/data/src/main/kotlin/io/tolgee/component/ActivityHolderProvider.kt b/backend/data/src/main/kotlin/io/tolgee/component/ActivityHolderProvider.kt new file mode 100644 index 0000000000..05743cc520 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/component/ActivityHolderProvider.kt @@ -0,0 +1,58 @@ +package io.tolgee.component + +import io.tolgee.activity.ActivityHolder +import jakarta.annotation.PreDestroy +import org.springframework.beans.factory.support.ScopeNotActiveException +import org.springframework.context.ApplicationContext +import org.springframework.stereotype.Component +import org.springframework.transaction.support.TransactionSynchronization +import org.springframework.transaction.support.TransactionSynchronizationManager + +/** + * Class providing Activity Holder, while caching it in ThreadLocal. + * I also registers a transaction synchronization, which clears the ThreadLocal after transaction + * is completed, this enables us to be safe even when running activities in main thred. + */ +@Component +class ActivityHolderProvider(private val applicationContext: ApplicationContext) { + private val threadLocal = ThreadLocal() + + fun getActivityHolder(): ActivityHolder { + // Get the activity holder from ThreadLocal. + var activityHolder = threadLocal.get() + if (activityHolder == null) { + // If no activity holder exists for this thread, fetch one and store it in ThreadLocal. + activityHolder = fetchActivityHolder() + threadLocal.set(activityHolder) + } + return activityHolder + } + + private fun fetchActivityHolder(): ActivityHolder { + return try { + applicationContext.getBean("requestActivityHolder", ActivityHolder::class.java).also { + it.activityRevision + } + } catch (e: ScopeNotActiveException) { + val transactionActivityHolder = + applicationContext.getBean("transactionActivityHolder", ActivityHolder::class.java) + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization( + object : TransactionSynchronization { + override fun afterCompletion(status: Int) { + clearThreadLocal() + } + } + ) + return transactionActivityHolder + } + + throw IllegalStateException("Transaction synchronization is not active.") + } + } + + @PreDestroy + fun clearThreadLocal() { + threadLocal.remove() + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/configuration/ActivityHolderConfig.kt b/backend/data/src/main/kotlin/io/tolgee/configuration/ActivityHolderConfig.kt index 645a146a36..c2e64910d2 100644 --- a/backend/data/src/main/kotlin/io/tolgee/configuration/ActivityHolderConfig.kt +++ b/backend/data/src/main/kotlin/io/tolgee/configuration/ActivityHolderConfig.kt @@ -1,6 +1,7 @@ package io.tolgee.configuration import io.tolgee.activity.ActivityHolder +import io.tolgee.component.ActivityHolderProvider import io.tolgee.configuration.TransactionScopeConfig.Companion.SCOPE_TRANSACTION import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.config.BeanDefinition @@ -31,16 +32,16 @@ class ActivityHolderConfig { return ActivityHolder(applicationContext) } + /** + * This method for getting the activity holder is slow, since it + * needs to create new bean every time holder is requested. Which is pretty often. + * + * Use the activityHolderProvider when possible. + */ @Bean @Primary @Scope(BeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS) - fun activityHolder(applicationContext: ApplicationContext): ActivityHolder { - return try { - applicationContext.getBean("requestActivityHolder", ActivityHolder::class.java).also { - it.activityRevision - } - } catch (e: ScopeNotActiveException) { - return applicationContext.getBean("transactionActivityHolder", ActivityHolder::class.java) - } + fun activityHolder(activityHolderProvider: ActivityHolderProvider): ActivityHolder { + return activityHolderProvider.getActivityHolder() } } diff --git a/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportDeleteService.kt b/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportDeleteService.kt new file mode 100644 index 0000000000..29fc7f5646 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportDeleteService.kt @@ -0,0 +1,123 @@ +package io.tolgee.service.dataImport + +import jakarta.persistence.EntityManager +import org.hibernate.Session +import org.springframework.stereotype.Service + +@Service +class ImportDeleteService( + private val entityManager: EntityManager +) { + fun deleteImport(importId: Long) { + entityManager.unwrap(Session::class.java).doWork { connection -> + deleteImportTranslations(connection, importId) + deleteImportLanguages(connection, importId) + deleteImportKeyMetaTags(connection, importId) + deleteImportKeyMetaComments(connection, importId) + deleteImportKeyMetaCodeReferences(connection, importId) + deleteImportKeyMeta(connection, importId) + deleteImportKeys(connection, importId) + deleteImportFileIssueParams(connection, importId) + deleteImportFileIssues(connection, importId) + deleteImportFiles(connection, importId) + deleteTheImport(connection, importId) + } + } + + fun executeUpdate(connection: java.sql.Connection, query: String, importId: Long) { + @Suppress("SqlSourceToSinkFlow") + connection.prepareStatement(query).use { statement -> + statement.setLong(1, importId) + statement.executeUpdate() + } + } + + fun deleteImportTranslations(connection: java.sql.Connection, importId: Long) { + val query = + "delete from import_translation " + + "where id in (" + + "select it.id from import_translation it " + + "join import_language il on il.id = it.language_id " + + "join import_file if on il.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportLanguages(connection: java.sql.Connection, importId: Long) { + val query = + "delete from import_language " + + "where id in (select il.id from import_language il " + + "join import_file if on il.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportKeys(connection: java.sql.Connection, importId: Long) { + val query = + "delete from import_key " + + "where id in (select ik.id from import_key ik " + + "join import_file if on ik.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportKeyMetaTags(connection: java.sql.Connection, importId: Long) { + val query = + "delete from key_meta_tags " + + "where key_metas_id in (select ikm.id from key_meta ikm " + + "join import_key ik on ikm.import_key_id = ik.id " + + "join import_file if on ik.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportKeyMetaComments(connection: java.sql.Connection, importId: Long) { + val query = + "delete from key_comment " + + "where key_meta_id in (select ikm.id from key_meta ikm " + + "join import_key ik on ikm.import_key_id = ik.id " + + "join import_file if on ik.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportKeyMetaCodeReferences(connection: java.sql.Connection, importId: Long) { + val query = + "delete from key_code_reference " + + "where key_meta_id in (select ikm.id from key_meta ikm " + + "join import_key ik on ikm.import_key_id = ik.id " + + "join import_file if on ik.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportKeyMeta(connection: java.sql.Connection, importId: Long) { + val query = + "delete from key_meta " + + "where id in (select ikm.id from key_meta ikm " + + "join import_key ik on ikm.import_key_id = ik.id " + + "join import_file if on ik.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportFileIssueParams(connection: java.sql.Connection, importId: Long) { + val query = + "delete from import_file_issue_param " + + "where import_file_issue_param.issue_id in (select ifi.id from import_file_issue ifi " + + "join import_file if on ifi.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportFileIssues(connection: java.sql.Connection, importId: Long) { + val query = + "delete from import_file_issue " + + "where id in (select ifi.id from import_file_issue ifi " + + "join import_file if on ifi.file_id = if.id where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteImportFiles(connection: java.sql.Connection, importId: Long) { + val query = "delete from import_file " + + "where id in (select if.id from import_file if where if.import_id = ?)" + executeUpdate(connection, query, importId) + } + + fun deleteTheImport(connection: java.sql.Connection, importId: Long) { + val query = "delete from import where id = ?" + executeUpdate(connection, query, importId) + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportService.kt b/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportService.kt index 2e7ae1a23b..80703eb59e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/dataImport/ImportService.kt @@ -53,7 +53,8 @@ class ImportService( private val keyMetaService: KeyMetaService, private val removeExpiredImportService: RemoveExpiredImportService, private val entityManager: EntityManager, - private val businessEventPublisher: BusinessEventPublisher + private val businessEventPublisher: BusinessEventPublisher, + private val importDeleteService: ImportDeleteService ) { @Transactional fun addFiles( @@ -251,16 +252,9 @@ class ImportService( ) } + @Transactional fun deleteImport(import: Import) { - this.importTranslationRepository.deleteAllByImport(import) - this.importLanguageRepository.deleteAllByImport(import) - val keyIds = this.importKeyRepository.getAllIdsByImport(import) - this.keyMetaService.deleteAllByImportKeyIdIn(keyIds) - this.importKeyRepository.deleteByIdIn(keyIds) - this.importFileIssueParamRepository.deleteAllByImport(import) - this.importFileIssueRepository.deleteAllByImport(import) - this.importFileRepository.deleteAllByImport(import) - this.importRepository.delete(import) + importDeleteService.deleteImport(import.id) } @Transactional diff --git a/backend/data/src/main/kotlin/io/tolgee/service/key/KeyMetaService.kt b/backend/data/src/main/kotlin/io/tolgee/service/key/KeyMetaService.kt index 224fd08dae..44ddd5b564 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/key/KeyMetaService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/key/KeyMetaService.kt @@ -121,13 +121,6 @@ class KeyMetaService( fun save(meta: KeyMeta): KeyMeta = this.keyMetaRepository.save(meta) - fun deleteAllByImportKeyIdIn(importKeyIds: List) { - tagService.deleteAllByImportKeyIdIn(importKeyIds) - keyCommentRepository.deleteAllByImportKeyIds(importKeyIds) - keyCodeReferenceRepository.deleteAllByImportKeyIds(importKeyIds) - this.keyMetaRepository.deleteAllByImportKeyIdIn(importKeyIds) - } - fun deleteAllByKeyIdIn(ids: Collection) { tagService.deleteAllByKeyIdIn(ids) keyCommentRepository.deleteAllByKeyIds(ids)