diff --git a/src/main/java/com/learning/mfscreener/repository/UserSchemeDetailsEntityRepository.java b/src/main/java/com/learning/mfscreener/repository/UserSchemeDetailsEntityRepository.java index 25da13c3..963aa5d7 100644 --- a/src/main/java/com/learning/mfscreener/repository/UserSchemeDetailsEntityRepository.java +++ b/src/main/java/com/learning/mfscreener/repository/UserSchemeDetailsEntityRepository.java @@ -29,10 +29,8 @@ public interface UserSchemeDetailsEntityRepository extends JpaRepository findByUserEmailAndName(@Param("email") String email, @Param("name") String name); - @Transactional(readOnly = true) Optional findFirstByAmfi(Long amfi); @Query( @@ -43,6 +41,5 @@ select mf_scheme_id, count(msn.id) from public.user_scheme_details usd join mf_s group by mf_scheme_id having count(msn.id) < 3 """, nativeQuery = true) - @Transactional(readOnly = true) List getHistoricalDataNotLoadedSchemeIdList(); } diff --git a/src/main/java/com/learning/mfscreener/service/CachedNavService.java b/src/main/java/com/learning/mfscreener/service/CachedNavService.java index 0b5e4c14..eceaa945 100644 --- a/src/main/java/com/learning/mfscreener/service/CachedNavService.java +++ b/src/main/java/com/learning/mfscreener/service/CachedNavService.java @@ -8,9 +8,11 @@ import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Loggable +@Transactional(readOnly = true) public class CachedNavService { private static final Logger LOGGER = LoggerFactory.getLogger(CachedNavService.class); @@ -21,8 +23,12 @@ public CachedNavService(SchemeService schemeService) { this.schemeService = schemeService; } - @Cacheable(cacheNames = "getNavForDate", unless = "#result == null") @Loggable(result = false) + @Cacheable( + value = "getNavForDate", + key = + "#root.targetClass.simpleName + ':' + #root.methodName + ':schemeCode:' + #schemeCode + ':navDate:' + #navDate", + unless = "#result == null") public MFSchemeDTO getNavForDate(Long schemeCode, LocalDate navDate) { LOGGER.info("Fetching Nav for AMFISchemeCode: {} for date: {} from Database", schemeCode, navDate); return schemeService diff --git a/src/main/java/com/learning/mfscreener/service/CapitalGainsService.java b/src/main/java/com/learning/mfscreener/service/CapitalGainsService.java index 32d7d57e..85784c27 100644 --- a/src/main/java/com/learning/mfscreener/service/CapitalGainsService.java +++ b/src/main/java/com/learning/mfscreener/service/CapitalGainsService.java @@ -18,8 +18,10 @@ import java.util.Map; import java.util.stream.Collectors; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) public class CapitalGainsService { private static final double MIN_OPEN_BALANCE = 0.01; diff --git a/src/main/java/com/learning/mfscreener/service/FIFOUnitsService.java b/src/main/java/com/learning/mfscreener/service/FIFOUnitsService.java index d2bbc76a..c0c2b408 100644 --- a/src/main/java/com/learning/mfscreener/service/FIFOUnitsService.java +++ b/src/main/java/com/learning/mfscreener/service/FIFOUnitsService.java @@ -23,8 +23,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) public class FIFOUnitsService { private static final Logger LOGGER = LoggerFactory.getLogger(FIFOUnitsService.class); diff --git a/src/main/java/com/learning/mfscreener/service/GainEntryService.java b/src/main/java/com/learning/mfscreener/service/GainEntryService.java index d6b7160d..03bb4caa 100644 --- a/src/main/java/com/learning/mfscreener/service/GainEntryService.java +++ b/src/main/java/com/learning/mfscreener/service/GainEntryService.java @@ -10,8 +10,10 @@ import java.util.HashMap; import java.util.Map; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) public class GainEntryService { private final Map CII = loadCostInflationIndexData(); diff --git a/src/main/java/com/learning/mfscreener/service/HistoricalNavService.java b/src/main/java/com/learning/mfscreener/service/HistoricalNavService.java index 4da5ae2b..f002071e 100644 --- a/src/main/java/com/learning/mfscreener/service/HistoricalNavService.java +++ b/src/main/java/com/learning/mfscreener/service/HistoricalNavService.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; @@ -27,6 +28,7 @@ @Service @Loggable +@Transactional(readOnly = true) public class HistoricalNavService { private static final Logger LOGGER = LoggerFactory.getLogger(HistoricalNavService.class); diff --git a/src/main/java/com/learning/mfscreener/service/MFSchemeNavService.java b/src/main/java/com/learning/mfscreener/service/MFSchemeNavService.java index 35978413..56090316 100644 --- a/src/main/java/com/learning/mfscreener/service/MFSchemeNavService.java +++ b/src/main/java/com/learning/mfscreener/service/MFSchemeNavService.java @@ -17,9 +17,12 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; @Service @Loggable +@Transactional(readOnly = true) public class MFSchemeNavService { private static final Logger LOGGER = LoggerFactory.getLogger(MFSchemeNavService.class); @@ -27,14 +30,18 @@ public class MFSchemeNavService { private final MFSchemeNavEntityRepository mfSchemeNavEntityRepository; private final MFSchemeRepository mfSchemeRepository; private final ResourceLoader resourceLoader; + private final TransactionTemplate transactionTemplate; public MFSchemeNavService( MFSchemeNavEntityRepository mfSchemeNavEntityRepository, MFSchemeRepository mfSchemeRepository, - ResourceLoader resourceLoader) { + ResourceLoader resourceLoader, + TransactionTemplate transactionTemplate) { this.mfSchemeNavEntityRepository = mfSchemeNavEntityRepository; this.mfSchemeRepository = mfSchemeRepository; this.resourceLoader = resourceLoader; + transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRES_NEW"); + this.transactionTemplate = transactionTemplate; } public void loadHistoricalNavOn31Jan2018ForExistingSchemes() { @@ -61,7 +68,9 @@ public void loadHistoricalNavOn31Jan2018ForExistingSchemes() { return mfSchemeNavEntity; }) .toList(); - List persistedEntities = mfSchemeNavEntityRepository.saveAll(mfSchemeNavEntities); + List persistedEntities = + transactionTemplate.execute(status -> mfSchemeNavEntityRepository.saveAll(mfSchemeNavEntities)); + Assert.notNull(persistedEntities, () -> "persistedEntities cant be null"); LOGGER.info("Persisted : {} rows", persistedEntities.size()); } catch (IOException e) { throw new FileNotFoundException(e.getMessage()); @@ -70,12 +79,10 @@ public void loadHistoricalNavOn31Jan2018ForExistingSchemes() { } } - @Transactional(readOnly = true) public boolean navLoadedFor31Jan2018ForExistingSchemes() { return mfSchemeNavEntityRepository.countByNavDate(AppConstants.GRAND_FATHERED_DATE) >= 5908; } - @Transactional(readOnly = true) public boolean navLoadedForClosedOrMergedSchemes() { return mfSchemeNavEntityRepository.countByNavDate(AppConstants.GRAND_FATHERED_DATE) >= 9000; } diff --git a/src/main/java/com/learning/mfscreener/service/NavService.java b/src/main/java/com/learning/mfscreener/service/NavService.java index 16b4b53f..314de171 100644 --- a/src/main/java/com/learning/mfscreener/service/NavService.java +++ b/src/main/java/com/learning/mfscreener/service/NavService.java @@ -11,10 +11,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @Loggable +@Transactional(readOnly = true) public class NavService { private static final Logger LOGGER = LoggerFactory.getLogger(NavService.class); diff --git a/src/main/java/com/learning/mfscreener/service/PortfolioService.java b/src/main/java/com/learning/mfscreener/service/PortfolioService.java index 97cf9668..96499409 100644 --- a/src/main/java/com/learning/mfscreener/service/PortfolioService.java +++ b/src/main/java/com/learning/mfscreener/service/PortfolioService.java @@ -30,10 +30,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @Service @Loggable +@Transactional(readOnly = true) public class PortfolioService { private static final Logger LOGGER = LoggerFactory.getLogger(PortfolioService.class); diff --git a/src/main/java/com/learning/mfscreener/service/PortfolioServiceHelper.java b/src/main/java/com/learning/mfscreener/service/PortfolioServiceHelper.java index a7bfe7cc..0258534d 100644 --- a/src/main/java/com/learning/mfscreener/service/PortfolioServiceHelper.java +++ b/src/main/java/com/learning/mfscreener/service/PortfolioServiceHelper.java @@ -13,9 +13,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Loggable +@Transactional(readOnly = true) public class PortfolioServiceHelper { private static final Logger LOGGER = LoggerFactory.getLogger(PortfolioServiceHelper.class); @@ -45,10 +47,11 @@ public List joinFutures(List> futures) { } public List getPortfolioDetailsByPANAndAsOfDate(String panNumber, LocalDate asOfDate) { - return joinFutures(userCASDetailsService.getPortfolioDetailsByPanAndAsOfDate(panNumber, asOfDate).stream() - .map(portfolioDetails -> CompletableFuture.supplyAsync(() -> { + return userCASDetailsService.getPortfolioDetailsByPanAndAsOfDate(panNumber, asOfDate).stream() + .map(portfolioDetails -> { MFSchemeDTO scheme; try { + LOGGER.debug("fetching Nav for SchemeId :{}", portfolioDetails.getSchemeId()); scheme = navService.getNavByDateWithRetry(portfolioDetails.getSchemeId(), asOfDate); } catch (NavNotFoundException navNotFoundException) { // Will happen in case of NFO where units are allocated but not ready for subscription @@ -67,8 +70,8 @@ public List getPortfolioDetailsByPANAndAsOfDate(String panN scheme.date(), xIRRCalculatorService.calculateXIRRBySchemeId( portfolioDetails.getSchemeId(), portfolioDetails.getSchemeDetailId(), asOfDate)); - })) - .toList()); + }) + .toList(); } public long countTransactionsByUserFolioDTOList(List folioDTOList) { diff --git a/src/main/java/com/learning/mfscreener/service/SchemeService.java b/src/main/java/com/learning/mfscreener/service/SchemeService.java index ac4c0dae..1b8fbd3b 100644 --- a/src/main/java/com/learning/mfscreener/service/SchemeService.java +++ b/src/main/java/com/learning/mfscreener/service/SchemeService.java @@ -30,6 +30,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; @@ -38,6 +39,7 @@ @Service @Loggable +@Transactional(readOnly = true) public class SchemeService { private static final Logger LOGGER = LoggerFactory.getLogger(SchemeService.class); @@ -68,14 +70,10 @@ public SchemeService( this.transactionTemplate = transactionTemplate; } - @Loggable - @Transactional public void fetchSchemeDetails(Long schemeCode) { processResponseEntity(schemeCode, getNavResponseResponseEntity(schemeCode)); } - @Loggable - @Transactional public void fetchSchemeDetails(String oldSchemeCode, Long newSchemeCode) { processResponseEntity(newSchemeCode, getNavResponseResponseEntity(Long.valueOf(oldSchemeCode))); } @@ -95,13 +93,12 @@ public List fetchSchemesByFundName(String fundName) { } // if panKYC is NOT OK then PAN is not set. hence manually setting it. - @Loggable public void setPANIfNotSet(Long userCasID) { // find pan by id UserFolioDetailsPanProjection panProjection = userFolioDetailsService.findFirstByUserCasIdAndPanKyc(userCasID, "OK"); int rowsUpdated = userFolioDetailsService.updatePanByCasId(panProjection.getPan(), userCasID); - LOGGER.debug("Updated {} rows with PAN", rowsUpdated); + LOGGER.debug("Updated {} rows with PAN for casID :{}", rowsUpdated, userCasID); } @Loggable(result = false) @@ -157,7 +154,7 @@ void mergeList(NavResponse navResponse, MFSchemeEntity mfSchemeEntity, Long sche mfSchemeEntity.addSchemeNav(newSchemeNav); } try { - this.mfSchemeRepository.save(mfSchemeEntity); + transactionTemplate.executeWithoutResult(status -> this.mfSchemeRepository.save(mfSchemeEntity)); } catch (ConstraintViolationException | DataIntegrityViolationException exception) { LOGGER.error("ConstraintViolationException or DataIntegrityViolationException ", exception); } @@ -167,42 +164,37 @@ void mergeList(NavResponse navResponse, MFSchemeEntity mfSchemeEntity, Long sche } } - @Transactional(readOnly = true) public Optional findByPayOut(String isin) { return mfSchemeRepository.findByPayOut(isin); } @Cacheable(value = "schemeIdByISIN") - @Transactional(readOnly = true) public List getSchemeIdByISIN(String isin) { return mfSchemeRepository.getSchemeIdByISIN(isin); } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) @Loggable(result = false, params = false) public MFSchemeEntity saveEntity(MFSchemeEntity mfSchemeEntity) { return mfSchemeRepository.save(mfSchemeEntity); } - @Transactional(readOnly = true) @Loggable(result = false) public Optional findBySchemeCode(Long schemeCode) { return mfSchemeRepository.findBySchemeId(schemeCode); } - @Transactional(readOnly = true) public long count() { return mfSchemeRepository.count(); } - @Transactional(readOnly = true) @Loggable(result = false) public List findAllSchemeIds() { return mfSchemeRepository.findAllSchemeIds(); } - @Transactional @Loggable(result = false, params = false) + @Transactional public List saveAllEntities(List mfSchemeEntityList) { return mfSchemeRepository.saveAll(mfSchemeEntityList); } @@ -243,7 +235,7 @@ public void loadHistoricalDataForClosedOrMergedSchemes() { .toList(); List persistedEntities = transactionTemplate.execute(status -> mfSchemeRepository.saveAll(mfSchemeEntities)); - Assert.notEmpty(persistedEntities, () -> "Response cant be empty"); + Assert.notNull(persistedEntities, () -> "persistedEntities cant be null"); LOGGER.info("Persisted : {} rows", persistedEntities.size()); } catch (IOException e) { throw new FileNotFoundException(e.getMessage()); diff --git a/src/main/java/com/learning/mfscreener/service/UserCASDetailsService.java b/src/main/java/com/learning/mfscreener/service/UserCASDetailsService.java index d4843594..9c6ab35b 100644 --- a/src/main/java/com/learning/mfscreener/service/UserCASDetailsService.java +++ b/src/main/java/com/learning/mfscreener/service/UserCASDetailsService.java @@ -7,6 +7,7 @@ import java.time.LocalDate; import java.util.List; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Loggable @@ -20,7 +21,7 @@ public UserCASDetailsService(UserCASDetailsEntityRepository userCASDetailsEntity this.userCASDetailsEntityRepository = userCASDetailsEntityRepository; } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public UserCASDetailsEntity saveEntity(UserCASDetailsEntity casDetailsEntity) { return userCASDetailsEntityRepository.save(casDetailsEntity); } diff --git a/src/main/java/com/learning/mfscreener/service/UserFolioDetailsService.java b/src/main/java/com/learning/mfscreener/service/UserFolioDetailsService.java index 76505d36..60bca5c3 100644 --- a/src/main/java/com/learning/mfscreener/service/UserFolioDetailsService.java +++ b/src/main/java/com/learning/mfscreener/service/UserFolioDetailsService.java @@ -8,6 +8,7 @@ import java.time.LocalDate; import java.util.List; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -33,7 +34,7 @@ public UserFolioDetailsPanProjection findFirstByUserCasIdAndPanKyc(Long userCasI return userFolioDetailsEntityRepository.findFirstByUserCasDetailsEntity_IdAndPanKyc(userCasID, ok); } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public int updatePanByCasId(String pan, Long userCasID) { return userFolioDetailsEntityRepository.updatePanByCasId(pan, userCasID); } diff --git a/src/main/java/com/learning/mfscreener/service/XIRRCalculatorService.java b/src/main/java/com/learning/mfscreener/service/XIRRCalculatorService.java index d52b0255..a77a508a 100644 --- a/src/main/java/com/learning/mfscreener/service/XIRRCalculatorService.java +++ b/src/main/java/com/learning/mfscreener/service/XIRRCalculatorService.java @@ -15,9 +15,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Loggable +@Transactional(readOnly = true) public class XIRRCalculatorService { private static final Logger LOGGER = LoggerFactory.getLogger(XIRRCalculatorService.class); diff --git a/src/test/java/com/learning/mfscreener/archunit/CustomConditions.java b/src/test/java/com/learning/mfscreener/archunit/CustomConditions.java index fee32278..7a52b624 100644 --- a/src/test/java/com/learning/mfscreener/archunit/CustomConditions.java +++ b/src/test/java/com/learning/mfscreener/archunit/CustomConditions.java @@ -1,4 +1,4 @@ -/* Licensed under Apache-2.0 2022. */ +/* Licensed under Apache-2.0 2022-2024. */ package com.learning.mfscreener.archunit; import com.tngtech.archunit.core.domain.JavaClass; @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.springframework.transaction.annotation.Transactional; public class CustomConditions { @@ -34,6 +35,43 @@ public class CustomConditions { private CustomConditions() {} + public static ArchCondition beAnnotatedWithTransactionalReadOnlyTrue = + new ArchCondition<>("be annotated with @Transactional(readOnly = true)") { + @Override + public void check(JavaClass javaClass, ConditionEvents events) { + boolean isAnnotatedCorrectly = javaClass.getAnnotations().stream() + .filter(annotation -> + annotation.getRawType().getFullName().equals(Transactional.class.getName())) + .anyMatch(annotation -> Boolean.TRUE.equals( + annotation.get("readOnly").orElse(false))); + + if (!isAnnotatedCorrectly) { + String message = String.format( + "Class %s is not annotated with @Transactional(readOnly = true)", javaClass.getName()); + events.add(SimpleConditionEvent.violated(javaClass, message)); + } + } + }; + + public static ArchCondition notBeAnnotatedWithTransactionalReadOnlyTrue = + new ArchCondition<>("not be annotated with @Transactional(readOnly = true)") { + @Override + public void check(JavaMethod javaMethod, ConditionEvents events) { + boolean isAnnotatedIncorrectly = javaMethod.getAnnotations().stream() + .filter(annotation -> + annotation.getRawType().getFullName().equals(Transactional.class.getName())) + .anyMatch(annotation -> Boolean.TRUE.equals( + annotation.get("readOnly").orElse(false))); + + if (isAnnotatedIncorrectly) { + String message = String.format( + "Method %s in class %s is annotated with @Transactional(readOnly = true)", + javaMethod.getName(), javaMethod.getOwner().getName()); + events.add(SimpleConditionEvent.violated(javaMethod, message)); + } + } + }; + public static ArchCondition haveGetter(Map exclusions) { return buildFieldHaveGetterAndSetterCondition(false, exclusions); } diff --git a/src/test/java/com/learning/mfscreener/archunit/ServiceRulesTest.java b/src/test/java/com/learning/mfscreener/archunit/ServiceRulesTest.java index 4be1b555..40337f13 100644 --- a/src/test/java/com/learning/mfscreener/archunit/ServiceRulesTest.java +++ b/src/test/java/com/learning/mfscreener/archunit/ServiceRulesTest.java @@ -1,4 +1,4 @@ -/* Licensed under Apache-2.0 2022. */ +/* Licensed under Apache-2.0 2022-2024. */ package com.learning.mfscreener.archunit; import static com.learning.mfscreener.archunit.ArchitectureConstants.ANNOTATED_EXPLANATION; @@ -11,7 +11,10 @@ import static com.learning.mfscreener.archunit.CommonRules.privateMethodsAreNotAllowedRule; import static com.learning.mfscreener.archunit.CommonRules.publicConstructorsRule; import static com.learning.mfscreener.archunit.CommonRules.staticMethodsAreNotAllowedRule; +import static com.learning.mfscreener.archunit.CustomConditions.beAnnotatedWithTransactionalReadOnlyTrue; +import static com.learning.mfscreener.archunit.CustomConditions.notBeAnnotatedWithTransactionalReadOnlyTrue; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.junit.AnalyzeClasses; @@ -34,7 +37,16 @@ class ServiceRulesTest { .areTopLevelClasses() .should() .beAnnotatedWith(Service.class) - .because(ANNOTATED_EXPLANATION.formatted(SERVICE_SUFFIX, "@Service")); + .andShould(beAnnotatedWithTransactionalReadOnlyTrue) + .because(ANNOTATED_EXPLANATION.formatted(SERVICE_SUFFIX, "@Service and @Transactional(readOnly = true)")); + + @ArchTest + static final ArchRule methods_should_not_be_annotated_transactionReadOnly = methods() + .that() + .areDeclaredInClassesThat() + .resideInAPackage(SERVICE_PACKAGE) + .should(notBeAnnotatedWithTransactionalReadOnlyTrue) + .because("Methods in service classes should not be annotated with @Transactional(readOnly = true)"); // Fields @ArchTest