From f348c28a88fe64b049767fec412baa147ad3bfac Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 28 Oct 2026 13:41:56 +0530 Subject: [PATCH 1/7] perf(worklist): batch per-page lookups in getBeneficiaryData Load mapping, details, account, image, address, contact, beneficiary ID, rmnch, born-birth, household, flow, anthropometry, vitals and user lookups once per page instead of once per beneficiary. Read ABHA from m_benhealthidmapping/t_healthid directly instead of calling FHIR-API per beneficiary. Falls back to the existing per-row queries if the batch load fails or a single-result lookup finds more than one row. Co-Authored-By: Claude Opus 5.5 --- .../flw/repo/identity/BeneficiaryRepo.java | 7 + .../service/impl/BeneficiaryServiceImpl.java | 124 +++++++++--- .../service/impl/WorklistPagePrefetch.java | 191 ++++++++++++++++++ 3 files changed, 291 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/iemr/flw/service/impl/WorklistPagePrefetch.java diff --git a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java index a1b0b933..cd7316c8 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -107,6 +107,13 @@ Page getBenDataWithinDates( @Query(nativeQuery = true, value = " SELECT HealthID,HealthIdNumber,isNewAbha FROM db_iemr.t_healthid WHERE HealthIdNumber = :healthIdNumber ") ArrayList getBenHealthDetails(@Param("healthIdNumber") String healthIdNumber); + // Same rows FHIR-API getBenhealthID reads, so the worklist can fill healthId/healthIdNumber without an HTTP call + @Query(nativeQuery = true, value = " SELECT HealthID, HealthIdNumber FROM db_iemr.m_benhealthidmapping WHERE BeneficiaryRegID = :benRegId ") + List getHealthIdMappingRows(@Param("benRegId") Long benRegId); + + @Query(nativeQuery = true, value = " SELECT HealthId FROM db_iemr.t_healthid WHERE HealthIdNumber = :healthIdNumber ") + List getHealthIdByNumber(@Param("healthIdNumber") String healthIdNumber); + // benRegId has no unique constraint on i_beneficiarymapping — a beneficiary can have // more than one mapping row, so this must return a list, not assume a single result. @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId ORDER BY b.benMapId DESC") diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 7136f490..86742c36 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -119,6 +119,9 @@ public class BeneficiaryServiceImpl implements BeneficiaryService { @Autowired private BenPhysicalVitalRepo benPhysicalVitalRepo; + @Autowired + private WorklistPagePrefetch worklistPagePrefetch; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); @@ -159,13 +162,19 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr // Hibernate 6.4.1 bug there: "Binding is multi-valued; illegal call to // #getBindValue" even with a well-formed, non-empty id list (confirmed live // against 192.168.45.40 - the 10 rows returned have no NULLs/type issues, so - // this isn't a data problem). Looping getAddressById() per id sidesteps - // Hibernate's collection-parameter binding entirely - only ever pageSize (10) - // calls per page, same order-preservation as before. - List pageAddresses = addressIds.stream() - .map(beneficiaryRepo::getAddressById) - .filter(java.util.Objects::nonNull) - .collect(Collectors.toList()); + // this isn't a data problem). WorklistPagePrefetch reads them in one native query + // with the ids as numeric literals (no collection binding), keeping page order; + // the per-id loop stays as the fallback. + List pageAddresses; + try { + pageAddresses = worklistPagePrefetch.addressesInOrder(addressIds); + } catch (Exception e) { + logger.warn("Worklist address batch failed, reading one by one: " + e.getMessage()); + pageAddresses = addressIds.stream() + .map(beneficiaryRepo::getAddressById) + .filter(java.util.Objects::nonNull) + .collect(Collectors.toList()); + } if (pageAddresses.isEmpty()) return null; @@ -233,10 +242,20 @@ private String getMappingsForAddressIDs(List addressLi Map resultMap; ArrayList> resultList = new ArrayList<>(); + // One query per table for the whole page; if it fails, every lookup below falls back to its + // original per-beneficiary query. + WorklistPagePrefetch.Page pf = null; + try { + pf = worklistPagePrefetch.load(addressList); + } catch (Exception e) { + logger.warn("Worklist page prefetch failed, using per-beneficiary queries: " + e.getMessage()); + } + for (RMNCHMBeneficiaryaddress a : addressList) { // exception by-passing try { - List mappingsForAddress = beneficiaryRepo.getByAddressID(a.getId()); + List mappingsForAddress = pf != null ? pf.mappings(a.getId()) + : beneficiaryRepo.getByAddressID(a.getId()); if(!mappingsForAddress.isEmpty()){ RMNCHMBeneficiarymapping m = mappingsForAddress.get(0); if (m != null) { @@ -249,38 +268,53 @@ private String getMappingsForAddressIDs(List addressLi benImageOBJ = new RMNCHMBeneficiaryImage(); benAddressOBJ = new RMNCHMBeneficiaryaddress(); benContactOBJ = new RMNCHMBeneficiarycontact(); - Map healthDetails = getBenHealthDetails(m.getBenRegId()); + Long regIdLong = m.getBenRegId() != null ? m.getBenRegId().longValue() : null; + // no m_benhealthidmapping row -> the original lookup returns an empty map too + Map healthDetails = (pf != null && regIdLong != null && !pf.hasHealthId(regIdLong)) + ? new HashMap<>() : getBenHealthDetails(m.getBenRegId()); if (m.getBenDetailsId() != null) { - benDetailsOBJ = beneficiaryRepo.getDetailsById(m.getBenDetailsId()); + benDetailsOBJ = pf != null ? pf.detail(m.getBenDetailsId()) + : beneficiaryRepo.getDetailsById(m.getBenDetailsId()); } if (m.getBenAccountID() != null) { - benAccountOBJ = beneficiaryRepo.getAccountById(m.getBenAccountID()); + benAccountOBJ = pf != null ? pf.account(m.getBenAccountID()) + : beneficiaryRepo.getAccountById(m.getBenAccountID()); } if (m.getBenImageId() != null) { - benImageOBJ = beneficiaryRepo.getImageById(m.getBenImageId().longValue()); + benImageOBJ = pf != null ? pf.image(m.getBenImageId()) + : beneficiaryRepo.getImageById(m.getBenImageId().longValue()); } if (m.getBenAddressId() != null) { - benAddressOBJ = beneficiaryRepo.getAddressById(m.getBenAddressId()); + benAddressOBJ = pf != null ? pf.address(m.getBenAddressId()) + : beneficiaryRepo.getAddressById(m.getBenAddressId()); } if (m.getBenContactsId() != null) { - benContactOBJ = beneficiaryRepo.getContactById(m.getBenContactsId()); + benContactOBJ = pf != null ? pf.contact(m.getBenContactsId()) + : beneficiaryRepo.getContactById(m.getBenContactsId()); } BigInteger benID = null; - if (m.getBenRegId() != null) - benID = beneficiaryRepo.getBenIdFromRegID(m.getBenRegId().longValue()); + if (regIdLong != null) { + // more than one row: let the original single-result query decide, as before + benID = (pf != null && pf.benIds(regIdLong).size() <= 1) + ? pf.benIds(regIdLong).stream().findFirst().orElse(null) + : beneficiaryRepo.getBenIdFromRegID(regIdLong); + } - if (m.getBenRegId() != null) { - List detailsByRegID = beneficiaryRepo - .getDetailsByRegID((m.getBenRegId()).longValue()); + if (regIdLong != null) { + List detailsByRegID = pf != null ? pf.rmnch(regIdLong) + : beneficiaryRepo.getDetailsByRegID(regIdLong); if(!detailsByRegID.isEmpty()){ benDetailsRMNCH_OBJ = detailsByRegID.get(0); } - benBotnBirthRMNCH_ROBJ = beneficiaryRepo.getBornBirthByRegID((m.getBenRegId()).longValue()); + benBotnBirthRMNCH_ROBJ = (pf != null && pf.bornBirth(regIdLong).size() <= 1) + ? pf.bornBirth(regIdLong).stream().findFirst().orElse(null) + : beneficiaryRepo.getBornBirthByRegID(regIdLong); if (benDetailsRMNCH_OBJ != null && benDetailsRMNCH_OBJ.getHouseoldId() != null) { - List householdsById = houseHoldRepo - .getByHouseHoldID(benDetailsRMNCH_OBJ.getHouseoldId()); + List householdsById = pf != null + ? pf.households(benDetailsRMNCH_OBJ.getHouseoldId()) + : houseHoldRepo.getByHouseHoldID(benDetailsRMNCH_OBJ.getHouseoldId()); if(!householdsById.isEmpty()){ benHouseHoldRMNCH_ROBJ = householdsById.get(0); } @@ -392,7 +426,8 @@ private String getMappingsForAddressIDs(List addressLi } else { // i_beneficiarydetails.dob is null for Stop TB mobile registrations // (Identity-API mapper commented out) — fall back to i_ben_flow_outreach.ben_dob - List flows = benFlowStatusRepo.findByBeneficiaryRegID(m.getBenRegId().longValue()); + List flows = pf != null ? pf.flows(regIdLong) + : benFlowStatusRepo.findByBeneficiaryRegID(m.getBenRegId().longValue()); if (!flows.isEmpty() && flows.get(0).getDob() != null) benDetailsRMNCH_OBJ.setDob(flows.get(0).getDob()); } @@ -535,10 +570,10 @@ private String getMappingsForAddressIDs(List addressLi // Identity-API started writing these tables). Long benRegIdLong = m.getBenRegId() != null ? m.getBenRegId().longValue() : null; if (benRegIdLong != null) { - List anthroList = - benAnthropometryRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); - List vitalList = - benPhysicalVitalRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); + List anthroList = pf != null ? pf.anthropometry(benRegIdLong) + : benAnthropometryRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); + List vitalList = pf != null ? pf.vitals(benRegIdLong) + : benPhysicalVitalRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); Map anthropometry = new HashMap<>(); for (BenAnthropometryDetail anthro : anthroList) { @@ -594,13 +629,24 @@ private String getMappingsForAddressIDs(List addressLi // adding asha id / created by - user id if (benAddressOBJ.getCreatedBy() != null) { - Integer userID = beneficiaryRepo.getUserIDByUserName(benAddressOBJ.getCreatedBy()); + String createdBy = benAddressOBJ.getCreatedBy(); + Integer userID; + if (pf != null && pf.userIdCache().containsKey(createdBy)) { + userID = pf.userIdCache().get(createdBy); + } else { + userID = beneficiaryRepo.getUserIDByUserName(createdBy); + if (pf != null) pf.userIdCache().put(createdBy, userID); + } if (userID != null && userID > 0) resultMap.put("ashaId", userID); } - // get HealthID of ben - if (m.getBenRegId() != null) { - fetchHealthIdByBenRegID(m.getBenRegId().longValue(), authorisation, resultMap); + // get HealthID of ben: read locally (same table FHIR-API reads), only when a mapping exists + if (regIdLong != null) { + if (pf != null) { + if (pf.hasHealthId(regIdLong)) putLocalHealthId(regIdLong, resultMap); + } else { + fetchHealthIdByBenRegID(regIdLong, authorisation, resultMap); + } } resultList.add(resultMap); @@ -670,6 +716,22 @@ private Map getBenBenVisitDetails(BigInteger benRegId) { return healthDetails; } + // Mirrors FHIR-API HealthIDServiceImpl.getBenHealthID: every m_benhealthidmapping row, with healthId + // replaced by t_healthid.HealthId when that is filled; the last row wins, as in fetchHealthIdByBenRegID. + private void putLocalHealthId(Long benRegID, Map resultMap) { + for (Object[] row : beneficiaryRepo.getHealthIdMappingRows(benRegID)) { + String healthId = row[0] != null ? row[0].toString() : null; + String healthIdNumber = row[1] != null ? row[1].toString() : null; + if (healthIdNumber != null) { + List fromHealthIdTable = beneficiaryRepo.getHealthIdByNumber(healthIdNumber); + if (!fromHealthIdTable.isEmpty() && fromHealthIdTable.get(0) != null && !fromHealthIdTable.get(0).isEmpty()) + healthId = fromHealthIdTable.get(0); + } + if (healthId != null) resultMap.put("healthId", healthId); + if (healthIdNumber != null) resultMap.put("healthIdNumber", healthIdNumber); + } + } + public void fetchHealthIdByBenRegID(Long benRegID, String authorization, Map resultMap) { Map requestMap = new HashMap(); requestMap.put("beneficiaryRegID", benRegID); diff --git a/src/main/java/com/iemr/flw/service/impl/WorklistPagePrefetch.java b/src/main/java/com/iemr/flw/service/impl/WorklistPagePrefetch.java new file mode 100644 index 00000000..b34acf9a --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/WorklistPagePrefetch.java @@ -0,0 +1,191 @@ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import com.iemr.flw.domain.iemr.BenFlowStatus; +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; +import com.iemr.flw.domain.identity.*; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Component; + +import java.math.BigInteger; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Loads everything the worklist builder needs for one page with one query per table, instead of + * ~16 queries per beneficiary. Each query keeps the ordering of the per-row query it replaces, so + * the response is unchanged. + * + * IDs are written into the SQL as numeric literals rather than bound as a collection: Hibernate 6.4.1 + * in this service throws "Binding is multi-valued" for every IN (:list) parameter. The values are + * Long/BigInteger read from the database, so the literal list cannot carry anything but digits. + */ +@Component +public class WorklistPagePrefetch { + + @PersistenceContext(unitName = "db_identity") + private EntityManager identityEm; + + @PersistenceContext(unitName = "db_iemr") + private EntityManager iemrEm; + + public static class Page { + final Map> mappingsByAddress = new HashMap<>(); + final Map details = new HashMap<>(); + final Map accounts = new HashMap<>(); + final Map images = new HashMap<>(); + final Map addresses = new HashMap<>(); + final Map contacts = new HashMap<>(); + final Map> benIdsByRegId = new HashMap<>(); + final Map> rmnchByRegId = new HashMap<>(); + final Map> bornBirthByRegId = new HashMap<>(); + final Map> householdsById = new HashMap<>(); + final Map> flowsByRegId = new HashMap<>(); + final Map> anthropometryByRegId = new HashMap<>(); + final Map> vitalsByRegId = new HashMap<>(); + final Set regIdsWithHealthId = new HashSet<>(); + final Map userIdByName = new HashMap<>(); + + public List mappings(BigInteger addressId) { + return mappingsByAddress.getOrDefault(addressId, Collections.emptyList()); + } + public RMNCHMBeneficiarydetail detail(BigInteger id) { return details.get(id); } + public RMNCHMBeneficiaryAccount account(BigInteger id) { return accounts.get(id); } + public RMNCHMBeneficiaryImage image(BigInteger id) { return images.get(id); } + public RMNCHMBeneficiaryaddress address(BigInteger id) { return addresses.get(id); } + public RMNCHMBeneficiarycontact contact(BigInteger id) { return contacts.get(id); } + public List benIds(Long regId) { return benIdsByRegId.getOrDefault(regId, Collections.emptyList()); } + public List rmnch(Long regId) { return rmnchByRegId.getOrDefault(regId, Collections.emptyList()); } + public List bornBirth(Long regId) { return bornBirthByRegId.getOrDefault(regId, Collections.emptyList()); } + public List households(Long householdId) { return householdsById.getOrDefault(householdId, Collections.emptyList()); } + public List flows(Long regId) { return flowsByRegId.getOrDefault(regId, Collections.emptyList()); } + public List anthropometry(Long regId) { return anthropometryByRegId.getOrDefault(regId, Collections.emptyList()); } + public List vitals(Long regId) { return vitalsByRegId.getOrDefault(regId, Collections.emptyList()); } + public boolean hasHealthId(Long regId) { return regIdsWithHealthId.contains(regId); } + public Map userIdCache() { return userIdByName; } + } + + /** The page's addresses in the order of {@code ids} (the worklist page order); ids with no row are skipped, as before. */ + public List addressesInOrder(List ids) { + Map byId = new HashMap<>(); + putAll(byId, entities(identityEm, RMNCHMBeneficiaryaddress.class, + "SELECT * FROM db_identity.i_beneficiaryaddress WHERE BenAddressID IN (%s)", + new LinkedHashSet<>(ids)), RMNCHMBeneficiaryaddress::getBenAddressID); + return ids.stream().map(byId::get).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public Page load(List pageAddresses) { + Page page = new Page(); + pageAddresses.forEach(a -> page.addresses.put(a.getBenAddressID(), a)); + // The builder looks mappings up by address.getId(), which is the VanSerialNo column, not the + // address primary key - keep that exactly. + Set lookupIds = pageAddresses.stream().map(RMNCHMBeneficiaryaddress::getId) + .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); + if (lookupIds.isEmpty()) return page; + + // getByAddressID: ORDER BY BenMapId DESC + for (RMNCHMBeneficiarymapping m : entities(identityEm, RMNCHMBeneficiarymapping.class, + "SELECT * FROM db_identity.i_beneficiarymapping WHERE BenAddressId IN (%s) ORDER BY BenMapId DESC", lookupIds)) { + page.mappingsByAddress.computeIfAbsent(m.getBenAddressId(), k -> new ArrayList<>()).add(m); + } + // the builder only uses the first (newest) mapping of each address + List used = page.mappingsByAddress.values().stream() + .map(l -> l.get(0)).collect(Collectors.toList()); + + putAll(page.details, entities(identityEm, RMNCHMBeneficiarydetail.class, + "SELECT * FROM db_identity.i_beneficiarydetails WHERE BeneficiaryDetailsId IN (%s)", + ids(used, RMNCHMBeneficiarymapping::getBenDetailsId)), RMNCHMBeneficiarydetail::getBeneficiaryDetailsId); + putAll(page.accounts, entities(identityEm, RMNCHMBeneficiaryAccount.class, + "SELECT * FROM db_identity.i_beneficiaryaccount WHERE BenAccountID IN (%s)", + ids(used, RMNCHMBeneficiarymapping::getBenAccountID)), RMNCHMBeneficiaryAccount::getBenAccountID); + putAll(page.images, entities(identityEm, RMNCHMBeneficiaryImage.class, + "SELECT * FROM db_identity.i_beneficiaryimage WHERE BenImageId IN (%s)", + ids(used, RMNCHMBeneficiarymapping::getBenImageId)), RMNCHMBeneficiaryImage::getBenImageId); + putAll(page.contacts, entities(identityEm, RMNCHMBeneficiarycontact.class, + "SELECT * FROM db_identity.i_beneficiarycontacts WHERE BenContactsID IN (%s)", + ids(used, RMNCHMBeneficiarymapping::getBenContactsId)), RMNCHMBeneficiarycontact::getBenContactsID); + Set missingAddressIds = ids(used, RMNCHMBeneficiarymapping::getBenAddressId); + missingAddressIds.removeAll(page.addresses.keySet()); + putAll(page.addresses, entities(identityEm, RMNCHMBeneficiaryaddress.class, + "SELECT * FROM db_identity.i_beneficiaryaddress WHERE BenAddressID IN (%s)", missingAddressIds), + RMNCHMBeneficiaryaddress::getBenAddressID); + + Set regIds = ids(used, RMNCHMBeneficiarymapping::getBenRegId); + if (regIds.isEmpty()) return page; + + for (Object[] r : rows(identityEm, + "SELECT BenRegId, BeneficiaryID FROM db_identity.m_beneficiaryregidmapping WHERE BenRegId IN (%s)", regIds)) { + page.benIdsByRegId.computeIfAbsent(toLong(r[0]), k -> new ArrayList<>()).add(toBigInteger(r[1])); + } + // getDetailsByRegID: ORDER BY beneficiaryDetails_RmnchId DESC + group(page.rmnchByRegId, entities(identityEm, RMNCHBeneficiaryDetailsRmnch.class, + "SELECT * FROM db_identity.i_beneficiarydetails_rmnch WHERE BeneficiaryRegID IN (%s) ORDER BY beneficiaryDetails_RmnchId DESC", + regIds), RMNCHBeneficiaryDetailsRmnch::getBenRegId); + group(page.bornBirthByRegId, entities(identityEm, RMNCHBornBirthDetails.class, + "SELECT * FROM db_identity.i_bornbirthdeatils WHERE BeneficiaryRegID IN (%s) ORDER BY BornBirthDeatilsId", + regIds), RMNCHBornBirthDetails::getBenRegId); + + // households of the newest rmnch row (the one the builder uses) + Set householdIds = page.rmnchByRegId.values().stream().map(l -> l.get(0).getHouseoldId()) + .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); + group(page.householdsById, entities(identityEm, RMNCHHouseHoldDetails.class, + "SELECT * FROM db_identity.i_householddetails WHERE houseoldId IN (%s) ORDER BY houseHoldDetailsId", + householdIds), RMNCHHouseHoldDetails::getHouseoldId); + + group(page.flowsByRegId, entities(iemrEm, BenFlowStatus.class, + "SELECT * FROM db_iemr.i_ben_flow_outreach WHERE beneficiary_reg_id IN (%s) ORDER BY ben_flow_id", + regIds), BenFlowStatus::getBeneficiaryRegID); + // findByBeneficiaryRegIDOrderByCreatedDateDesc + group(page.anthropometryByRegId, entities(iemrEm, BenAnthropometryDetail.class, + "SELECT * FROM db_iemr.t_phy_anthropometry WHERE BeneficiaryRegID IN (%s) ORDER BY CreatedDate DESC", + regIds), BenAnthropometryDetail::getBeneficiaryRegID); + group(page.vitalsByRegId, entities(iemrEm, BenPhysicalVitalDetail.class, + "SELECT * FROM db_iemr.t_phy_vitals WHERE BeneficiaryRegID IN (%s) ORDER BY CreatedDate DESC", + regIds), BenPhysicalVitalDetail::getBeneficiaryRegID); + + for (Object id : iemrEm.createNativeQuery(String.format( + "SELECT DISTINCT BeneficiaryRegID FROM db_iemr.m_benhealthidmapping WHERE BeneficiaryRegID IN (%s)", + literalList(regIds))).getResultList()) { + page.regIdsWithHealthId.add(toLong(id)); + } + return page; + } + + @SuppressWarnings("unchecked") + private static List entities(EntityManager em, Class type, String sql, Collection ids) { + if (ids.isEmpty()) return Collections.emptyList(); + return em.createNativeQuery(String.format(sql, literalList(ids)), type).getResultList(); + } + + @SuppressWarnings("unchecked") + private static List rows(EntityManager em, String sql, Collection ids) { + if (ids.isEmpty()) return Collections.emptyList(); + return em.createNativeQuery(String.format(sql, literalList(ids))).getResultList(); + } + + private static Set ids(List rows, Function getter) { + return rows.stream().map(getter).filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static void putAll(Map target, List rows, Function key) { + rows.forEach(r -> target.putIfAbsent(key.apply(r), r)); + } + + private static void group(Map> target, List rows, Function key) { + rows.forEach(r -> target.computeIfAbsent(key.apply(r), k -> new ArrayList<>()).add(r)); + } + + // Only whole numbers ever reach the SQL text: anything else is rejected rather than quoted. + private static String literalList(Collection ids) { + return ids.stream().map(id -> { + if (id instanceof Number) return new BigInteger(id.toString()).toString(); + throw new IllegalArgumentException("Non-numeric id in worklist prefetch: " + id); + }).collect(Collectors.joining(",")); + } + + private static Long toLong(Object o) { return o == null ? null : ((Number) o).longValue(); } + + private static BigInteger toBigInteger(Object o) { return o == null ? null : new BigInteger(o.toString()); } +} From 1a629f6c7e7d0b09a5217607d67d75b77c04f297 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 28 Oct 2026 13:53:58 +0530 Subject: [PATCH 2/7] perf(worklist): page size 50 and per-page timing log door-to-door-page-size 10 -> 50 so a village pull needs 5x fewer requests. Log one [WORKLIST-TIMING] line per Stop TB village worklist page with the time spent in count, ids, address, prefetch, build and JSON. Co-Authored-By: Claude Opus 5.5 --- .../service/impl/BeneficiaryServiceImpl.java | 34 ++++++++++++++++--- src/main/resources/application.properties | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 86742c36..b0d799f0 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -144,8 +144,10 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr // O(village size), on every page including page 0. See BeneficiaryRepo for the // underlying query. if (request.getProviderServiceMapID() != null && request.getVillageID() != null) { + long tStart = System.currentTimeMillis(); long totalCount = beneficiaryRepo.countVillageWorklist( request.getProviderServiceMapID(), request.getVillageID()); + long tCount = System.currentTimeMillis(); if (totalCount == 0) return null; @@ -155,6 +157,7 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr List addressIds = beneficiaryRepo.getVillageWorklistAddressIds( request.getProviderServiceMapID(), request.getVillageID(), pageSize, offset); + long tIds = System.currentTimeMillis(); if (addressIds.isEmpty()) return null; @@ -177,8 +180,13 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr } if (pageAddresses.isEmpty()) return null; + long tAddr = System.currentTimeMillis(); - return getMappingsForAddressIDs(pageAddresses, totalPage, authorisation); + String timingLabel = "[WORKLIST-TIMING] psm=" + request.getProviderServiceMapID() + + " village=" + request.getVillageID() + " page=" + request.getPageNo() + "/" + totalPage + + " pageSize=" + pageSize + " villageTotal=" + totalCount + + " | countMs=" + (tCount - tStart) + " idsMs=" + (tIds - tCount) + " addressMs=" + (tAddr - tIds); + return getMappingsForAddressIDs(pageAddresses, totalPage, authorisation, timingLabel, tStart); } // Normal FLW/ASHA path @@ -222,13 +230,18 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr return getMappingsForAddressIDs( pageResult.getContent(), pageResult.getTotalPages(), - authorisation + authorisation, + null, + 0L ); } + // timingLabel != null logs one [WORKLIST-TIMING] line per page (Stop TB village path) so camp-laptop + // logs show where each page's time goes. private String getMappingsForAddressIDs(List addressList, int totalPage, - String authorisation) { + String authorisation, String timingLabel, long tStart) { + long tPrefetchStart = System.currentTimeMillis(); RMNCHHouseHoldDetails benHouseHoldRMNCH_ROBJ; RMNCHBeneficiaryDetailsRmnch benDetailsRMNCH_OBJ; RMNCHBornBirthDetails benBotnBirthRMNCH_ROBJ; @@ -250,6 +263,8 @@ private String getMappingsForAddressIDs(List addressLi } catch (Exception e) { logger.warn("Worklist page prefetch failed, using per-beneficiary queries: " + e.getMessage()); } + long tPrefetch = System.currentTimeMillis(); + int failedRows = 0; for (RMNCHMBeneficiaryaddress a : addressList) { // exception by-passing @@ -657,18 +672,29 @@ private String getMappingsForAddressIDs(List addressLi } } catch (Exception e) { + failedRows++; logger.info("Error for ben :"+e.getMessage()); logger.info("Error for ben :"+e); logger.error("error for addressID :" + e.getMessage() + a.getId() + " and vanID : " + a.getVanID()); } } + long tBuild = System.currentTimeMillis(); Map response = new HashMap<>(); response.put("data", resultList); response.put("pageSize", Integer.parseInt(door_to_door_page_size)); response.put("totalPage", totalPage); Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy h:mm:ss a").create(); - return gson.toJson(response); + String json = gson.toJson(response); + + if (timingLabel != null) { + long tEnd = System.currentTimeMillis(); + logger.info(timingLabel + " prefetchMs=" + (tPrefetch - tPrefetchStart) + " buildMs=" + (tBuild - tPrefetch) + + " jsonMs=" + (tEnd - tBuild) + " totalMs=" + (tEnd - tStart) + + " rows=" + resultList.size() + "/" + addressList.size() + " failedRows=" + failedRows + + " batch=" + (pf != null) + " bytes=" + json.length()); + } + return json; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 84cca481..cf38a017 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -35,7 +35,7 @@ spring.datasource.tomcat.remove-abandoned=true spring.datasource.tomcat.logAbandoned=true spring.datasource.continue-on-error=true spring.datasource.tomcat.remove-abandoned-timeout=600 -door-to-door-page-size=10 +door-to-door-page-size=50 #Get-HRP-Status get-HRP-Status=ANC/getHRPStatus From 2cd5912846c0bb1065e6be4c89bf900c233999e7 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 28 Oct 2026 19:37:28 +0530 Subject: [PATCH 3/7] perf(worklist): index-only count and page query; per-request timing log Count village flow rows without the mapping join, and page with a deferred join so OFFSET skips on idx_flow_psm_village_reg only and just the page's rows are joined to the mapping. Ties on registrationDate are ordered by ben_flow_id so a page boundary cannot skip or repeat a beneficiary. An in-range page with no mapped rows returns an empty page instead of "No record found". Add ApiTimingFilter: one [API-TIMING] line per request with method, path, status and time. Co-Authored-By: Claude Opus 5.5 --- .../flw/repo/identity/BeneficiaryRepo.java | 26 ++++++++---- .../service/impl/BeneficiaryServiceImpl.java | 6 ++- .../com/iemr/flw/utils/ApiTimingFilter.java | 40 +++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/iemr/flw/utils/ApiTimingFilter.java diff --git a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java index cd7316c8..8a84687a 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -152,13 +152,24 @@ Page getBenDataWithinDates( // replacing the old approach of loading every flow row for the whole village into // memory and slicing it in Java. Cost is now O(pageSize) instead of O(village size) // on every page request, including page 0. See BeneficiaryServiceImpl.getBenData(). + // Deferred join: the OFFSET skip runs on idx_flow_psm_village_reg alone (index-only, no row or + // mapping lookups for skipped rows), then only this page's flow rows are joined to the mapping. + // Rows sharing a registrationDate are ordered newest ben_flow_id first; the old query left their + // order to the plan (filesort), so a tie could move across a page boundary and be skipped or sent + // twice. A flow row with no mapping row still takes its slot, so such a page can come back short, + // never shifted. @Query(value = """ SELECT bm.BenAddressId - FROM db_iemr.i_ben_flow_outreach bfs + FROM ( + SELECT ben_flow_id, registrationDate + FROM db_iemr.i_ben_flow_outreach + WHERE providerServiceMapID = :psmId AND villageID = :villageId AND deleted = 0 + ORDER BY registrationDate DESC, ben_flow_id DESC + LIMIT :pageSize OFFSET :offset + ) p + JOIN db_iemr.i_ben_flow_outreach bfs ON bfs.ben_flow_id = p.ben_flow_id JOIN db_identity.i_beneficiarymapping bm ON bm.BenRegId = bfs.beneficiary_reg_id - WHERE bfs.providerServiceMapID = :psmId AND bfs.villageID = :villageId AND bfs.deleted = 0 - ORDER BY bfs.registrationDate DESC - LIMIT :pageSize OFFSET :offset + ORDER BY p.registrationDate DESC, p.ben_flow_id DESC """, nativeQuery = true) List getVillageWorklistAddressIds( @Param("psmId") Integer psmId, @@ -166,11 +177,12 @@ List getVillageWorklistAddressIds( @Param("pageSize") int pageSize, @Param("offset") int offset); + // Counts flow rows (index-only), the same rows getVillageWorklistAddressIds pages over, so + // totalPage always covers every flow row. @Query(value = """ SELECT COUNT(*) - FROM db_iemr.i_ben_flow_outreach bfs - JOIN db_identity.i_beneficiarymapping bm ON bm.BenRegId = bfs.beneficiary_reg_id - WHERE bfs.providerServiceMapID = :psmId AND bfs.villageID = :villageId AND bfs.deleted = 0 + FROM db_iemr.i_ben_flow_outreach + WHERE providerServiceMapID = :psmId AND villageID = :villageId AND deleted = 0 """, nativeQuery = true) long countVillageWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index b0d799f0..d8082fe4 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -159,7 +159,11 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr request.getProviderServiceMapID(), request.getVillageID(), pageSize, offset); long tIds = System.currentTimeMillis(); - if (addressIds.isEmpty()) return null; + // In range but every flow row on this page lacks a mapping: return an empty page, not + // "No record found", so the app keeps paging to totalPage. + if (addressIds.isEmpty()) { + return getMappingsForAddressIDs(new ArrayList<>(), totalPage, authorisation, null, tStart); + } // Was findAddressesByIds() - a single "IN :ids" JPQL query. Hit a reproducible // Hibernate 6.4.1 bug there: "Binding is multi-valued; illegal call to diff --git a/src/main/java/com/iemr/flw/utils/ApiTimingFilter.java b/src/main/java/com/iemr/flw/utils/ApiTimingFilter.java new file mode 100644 index 00000000..d9315963 --- /dev/null +++ b/src/main/java/com/iemr/flw/utils/ApiTimingFilter.java @@ -0,0 +1,40 @@ +package com.iemr.flw.utils; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +// One [API-TIMING] line per request (method, path, status, total ms incl. auth filters) so camp-laptop +// logs show every API's time: journalctl -u amrit-flw-api | grep API-TIMING +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ApiTimingFilter implements Filter { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + long start = System.currentTimeMillis(); + try { + chain.doFilter(servletRequest, servletResponse); + } finally { + logger.info("[API-TIMING] {} {} status={} ms={}", request.getMethod(), request.getRequestURI(), + response.getStatus(), System.currentTimeMillis() - start); + } + } +} From 7e9837b273673d85598b8e469f3e628c7cdf6cf0 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 28 Oct 2026 19:52:48 +0530 Subject: [PATCH 4/7] perf(stoptb): faster TB suspected and Stop TB getAll responses TB suspected getAll looked up the full user detail (role + facility queries) for every confirmed row only to read the state for the incentive check; look it up once per user. The incentive rules and calls are unchanged. Stop TB general exam, screening, OPD and diagnostics getAll built the JSON text, parsed it back and wrote it again; build the tree once via OutputResponse.setResponse(JsonElement). Load those rows read-only. Responses are byte-identical to before (Stop TB getAlls and the FLW app worklist/suspected paths compared on the 3307 test DB). Co-Authored-By: Claude Opus 5.5 --- .../iemr/flw/controller/StopTBController.java | 8 ++--- .../flw/repo/iemr/StopTBDiagnosticsRepo.java | 4 +++ .../iemr/StopTBGeneralExaminationRepo.java | 4 +++ .../flw/repo/iemr/StopTBGeneralOpdRepo.java | 4 +++ .../iemr/flw/repo/iemr/TBScreeningRepo.java | 4 +++ .../flw/service/IncentiveLogicService.java | 3 ++ .../flw/service/impl/IncentiveLogicImpl.java | 12 ++++++- .../service/impl/TBSuspectedServiceImpl.java | 36 +++++++++++++++---- .../flw/utils/response/OutputResponse.java | 9 +++++ 9 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 49cb691b..e9a38513 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -52,7 +52,7 @@ public String getAllGeneralExaminations(@RequestBody Map body) { if (raw == null) throw new Exception("providerServiceMapID is required"); Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; Map data = stopTBService.getAllGeneralExaminations(Integer.parseInt(raw.toString()), villageID); - response.setResponse(gson.toJson(data)); + response.setResponse(gson.toJsonTree(data)); } catch (Exception e) { logger.error("Error in getAllGeneralExaminations: " + e); response.setError(5000, "Error fetching general examinations: " + e.getMessage()); @@ -85,7 +85,7 @@ public String getAllNurseTBScreenings(@RequestBody Map body) { if (raw == null) throw new Exception("providerServiceMapID is required"); Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; Map data = stopTBService.getAllNurseTBScreenings(Integer.parseInt(raw.toString()), villageID); - response.setResponse(gson.toJson(data)); + response.setResponse(gson.toJsonTree(data)); } catch (Exception e) { logger.error("Error in getAllNurseTBScreenings: " + e); response.setError(5000, "Error fetching TB screenings: " + e.getMessage()); @@ -122,7 +122,7 @@ public String getAllGeneralOpd(@RequestBody Map body) { if (raw == null) throw new Exception("providerServiceMapID is required"); Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; Map data = stopTBService.getAllGeneralOpd(Integer.parseInt(raw.toString()), villageID); - response.setResponse(gson.toJson(data)); + response.setResponse(gson.toJsonTree(data)); } catch (Exception e) { logger.error("Error in getAllGeneralOpd: " + e); response.setError(5000, "Error fetching general OPD records: " + e.getMessage()); @@ -155,7 +155,7 @@ public String getAllDiagnostics(@RequestBody Map body) { if (raw == null) throw new Exception("providerServiceMapID is required"); Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; Map data = stopTBService.getAllDiagnostics(Integer.parseInt(raw.toString()), villageID); - response.setResponse(gson.toJson(data)); + response.setResponse(gson.toJsonTree(data)); } catch (Exception e) { logger.error("Error in getAllDiagnostics: " + e); response.setError(5000, "Error fetching diagnostics records: " + e.getMessage()); diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java index de3fa909..19bb9acf 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java @@ -4,6 +4,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import jakarta.persistence.QueryHint; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +21,8 @@ public interface StopTBDiagnosticsRepo extends JpaRepository findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + // Read-only: getAll only reads these rows, so Hibernate keeps no change-tracking snapshot per row + @QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true")) @Query("SELECT d FROM StopTBDiagnostics d WHERE d.benRegID IN " + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + "AND d.deleted = false ORDER BY d.visitDate DESC") diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java index 944e99a6..798fcaf8 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -5,6 +5,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import jakarta.persistence.QueryHint; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +34,8 @@ StopTBGeneralExamination findByBeneficiaryRegIDAndVisitCode(@Param("beneficiaryR @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.providerServiceMapID = :psmId AND e.deleted = false ORDER BY e.createdDate DESC") List findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + // Read-only: getAll only reads these rows, so Hibernate keeps no change-tracking snapshot per row + @QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true")) @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.beneficiaryRegID IN " + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + "AND e.deleted = false ORDER BY e.createdDate DESC") diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java index 3cb8868f..d35925a2 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java @@ -5,6 +5,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import jakarta.persistence.QueryHint; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +32,8 @@ public interface StopTBGeneralOpdRepo extends JpaRepository findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + // Read-only: getAll only reads these rows, so Hibernate keeps no change-tracking snapshot per row + @QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true")) @Query("SELECT o FROM StopTBGeneralOpd o WHERE o.benRegID IN " + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + "AND o.deleted = false ORDER BY o.createdDate DESC") diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java index f656dc91..cfb0665d 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java @@ -5,6 +5,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import jakarta.persistence.QueryHint; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -37,6 +39,8 @@ public interface TBScreeningRepo extends JpaRepository { @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.providerServiceMapID = :psmId AND tbs.deleted = false ORDER BY tbs.visitDate DESC") List findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + // Read-only: getAll only reads these rows, so Hibernate keeps no change-tracking snapshot per row + @QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true")) @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.benRegID IN " + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + "AND tbs.deleted = false ORDER BY tbs.visitDate DESC") diff --git a/src/main/java/com/iemr/flw/service/IncentiveLogicService.java b/src/main/java/com/iemr/flw/service/IncentiveLogicService.java index 8becce19..1d15fd4d 100644 --- a/src/main/java/com/iemr/flw/service/IncentiveLogicService.java +++ b/src/main/java/com/iemr/flw/service/IncentiveLogicService.java @@ -32,4 +32,7 @@ public interface IncentiveLogicService { IncentiveActivityRecord incentiveForGiveingIFA(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId); IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId); + + // Same as above with the user's state already looked up, for callers handling many rows of one user. + IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId, Integer stateCode); } diff --git a/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java b/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java index b6e4b7c7..108822e8 100644 --- a/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java @@ -87,9 +87,19 @@ public IncentiveActivityRecord incentiveForLeprosyPaucibacillaryConfirmed( @Override public IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId) { + Integer stateCode; try { - Integer stateCode = userService.getUserDetail(userId).getStateId(); + stateCode = userService.getUserDetail(userId).getStateId(); + } catch (Exception e) { + logger.error("Check Tb Suspected Incentive Exception: ", e); + return null; + } + return incentiveForTbSuspected(benId, treatmentStartDate, treatmentEndDate, userId, stateCode); + } + @Override + public IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId, Integer stateCode) { + try { if (stateCode == null) { logger.warn("State code is null for user: {}", userId); return null; diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index 99db4fcb..34840ef1 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -13,6 +13,7 @@ import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.TBStopVisitService; import com.iemr.flw.service.TBSuspectedService; +import com.iemr.flw.service.UserService; import org.modelmapper.ModelMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +22,11 @@ import java.sql.Timestamp; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; @Service public class TBSuspectedServiceImpl implements TBSuspectedService { @@ -40,6 +45,9 @@ public class TBSuspectedServiceImpl implements TBSuspectedService { @Autowired private IncentiveLogicService incentiveLogicService; + @Autowired + private UserService userService; + @Override public String getByBenId(Long benId, String authorisation) throws Exception { return null; @@ -143,6 +151,10 @@ public String getByUserId(GetBenRequestHandler request) { List tbSuspectedList = request.getProviderServiceMapID() != null ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID()) : tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); + // The incentive check needs the user's state; look it up once per user instead of loading the + // full user detail (role + facility queries) for every confirmed row. + Map stateByUser = new HashMap<>(); + Set usersWithoutDetail = new HashSet<>(); for (TBSuspected tbSuspected : tbSuspectedList) { TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); dto.setUpdateDate(tbSuspected.getLastModDate()); @@ -150,12 +162,24 @@ public String getByUserId(GetBenRequestHandler request) { dtos.add(dto); if (tbSuspected != null && Boolean.TRUE.equals(tbSuspected.getIsConfirmed())) { - incentiveLogicService.incentiveForTbSuspected( - tbSuspected.getBenId(), - tbSuspected.getVisitDate(), - tbSuspected.getVisitDate(), - tbSuspected.getUserId() - ); + Integer userId = tbSuspected.getUserId(); + if (!stateByUser.containsKey(userId) && !usersWithoutDetail.contains(userId)) { + try { + stateByUser.put(userId, userService.getUserDetail(userId).getStateId()); + } catch (Exception e) { + logger.error("Check Tb Suspected Incentive Exception: ", e); + usersWithoutDetail.add(userId); + } + } + if (stateByUser.containsKey(userId)) { + incentiveLogicService.incentiveForTbSuspected( + tbSuspected.getBenId(), + tbSuspected.getVisitDate(), + tbSuspected.getVisitDate(), + userId, + stateByUser.get(userId) + ); + } } } TBSuspectedRequestDTO tbSuspectedRequestDTO = new TBSuspectedRequestDTO(); diff --git a/src/main/java/com/iemr/flw/utils/response/OutputResponse.java b/src/main/java/com/iemr/flw/utils/response/OutputResponse.java index d4cb3670..7b5f7f8e 100644 --- a/src/main/java/com/iemr/flw/utils/response/OutputResponse.java +++ b/src/main/java/com/iemr/flw/utils/response/OutputResponse.java @@ -83,6 +83,15 @@ public void setResponse(String message) { } + // Same result as setResponse(gson.toJson(x)) for an object/array, without writing the JSON text + // and parsing it back: pass gson.toJsonTree(x). + public void setResponse(JsonElement tree) { + this.data = tree; + statusCode = SUCCESS; + errorMessage = "Success"; + status = "Success"; + } + public void setError(Throwable thrown) { Date currDate = Calendar.getInstance().getTime(); logger.info("error happened due to " + thrown.getClass().getSimpleName() + " at " + currDate.toString()); From 6248cad34442fdcfbf9cdd610443bfc8a17d5847 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 28 Oct 2026 20:04:14 +0530 Subject: [PATCH 5/7] perf(stoptb): one-query diagnostic status summary; leaner suspected and OPD lists Diagnostic status summary read each beneficiary's latest order five times, once per status bucket; read beneficiaryId + status once and bucket in Java with the same status groups (case-insensitive, like the database comparison). Bucket contents are unchanged; order inside a bucket was never fixed by the query. TB suspected getAll copies the DTO fields directly instead of ModelMapper per row, and its village subquery hints idx_flow_psm_village_reg (MySQL ignores the hint where that index is absent). Standard OPD records for a village start from the village's beneficiaries instead of every visit of the PSM; the same visits match. Co-Authored-By: Claude Opus 5.5 --- .../flw/repo/iemr/DiagnosticOrderRepo.java | 12 +++++++ .../iemr/flw/repo/iemr/TBSuspectedRepo.java | 7 ++-- .../service/StopTBPrescriptionService.java | 15 +++++---- .../impl/DiagnosticOrderServiceImpl.java | 30 +++++++++++------ .../service/impl/TBSuspectedServiceImpl.java | 32 ++++++++++++++++++- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/iemr/flw/repo/iemr/DiagnosticOrderRepo.java b/src/main/java/com/iemr/flw/repo/iemr/DiagnosticOrderRepo.java index bad44ff2..7c2c7d4d 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/DiagnosticOrderRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/DiagnosticOrderRepo.java @@ -105,4 +105,16 @@ List findBeneficiaryIdsClosed(@Param("orderType") String orderType, "AND (:providerServiceMapId IS NULL OR b.providerServiceMapId = :providerServiceMapId))") List findBeneficiaryIdsAwaitingManualEntry(@Param("orderType") String orderType, @Param("villageId") Integer villageId, @Param("providerServiceMapId") Integer providerServiceMapId); + + // The five queries above in one pass: beneficiaryId + status of each beneficiary's latest order, + // same filters, for the caller to bucket by status. + @Query("SELECT o.beneficiaryId, o.status FROM DiagnosticOrder o WHERE o.orderType = :orderType AND o.deleted = false " + + "AND o.status IN ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED', 'CLOSED', 'CANCELLED', 'EXPIRED', 'REFUSED', 'MANUAL_ENTRY') " + + "AND o.id = (SELECT MAX(o2.id) FROM DiagnosticOrder o2 " + + "WHERE o2.beneficiaryId = o.beneficiaryId AND o2.orderType = :orderType AND o2.deleted = false) " + + "AND o.beneficiaryId IN (SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.deleted = false " + + "AND (:villageId IS NULL OR b.villageID = :villageId) " + + "AND (:providerServiceMapId IS NULL OR b.providerServiceMapId = :providerServiceMapId))") + List findLatestOrderStatusByBeneficiary(@Param("orderType") String orderType, + @Param("villageId") Integer villageId, @Param("providerServiceMapId") Integer providerServiceMapId); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java index 3df494fa..277d21fd 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -29,8 +29,11 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.userId = :userId and tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") List getByUserId(@Param("userId") Integer userId, @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); - @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId IN " + - "(SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + // Same query as before in SQL, with an optimizer hint so the village subquery reads + // idx_flow_psm_village_reg instead of the PSM-only index (ignored if that index is missing). + @Query(nativeQuery = true, value = "SELECT t.* FROM db_iemr.tb_suspected t WHERE t.ben_id IN " + + "(SELECT /*+ INDEX(b idx_flow_psm_village_reg) */ b.beneficiary_id FROM db_iemr.i_ben_flow_outreach b " + + "WHERE b.providerServiceMapID = :psmId AND b.villageID = :villageId)") List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); @Transactional diff --git a/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java b/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java index b846bc02..919a6ab6 100644 --- a/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java +++ b/src/main/java/com/iemr/flw/service/StopTBPrescriptionService.java @@ -316,18 +316,21 @@ public Map prescribeAndDispense(Map prescription */ @SuppressWarnings("unchecked") public List> getStandardOpdRecords(Integer providerServiceMapID, Integer villageID) { - String villageFilter = villageID != null - ? "AND v.BeneficiaryRegID IN (SELECT f.beneficiary_reg_id FROM db_iemr.i_ben_flow_outreach f " - + "WHERE f.providerServiceMapID = :psm AND f.villageID = :villageID) " - : ""; + // With a village, start from the village's beneficiaries (bounded by village size) instead of + // every visit of the PSM; the same visits match either way. + String visitSource = villageID != null + ? "(SELECT /*+ INDEX(f idx_flow_psm_village_reg) */ DISTINCT f.beneficiary_reg_id " + + "FROM db_iemr.i_ben_flow_outreach f WHERE f.providerServiceMapID = :psm AND f.villageID = :villageID) fv " + + "JOIN db_iemr.t_benvisitdetail v ON v.BeneficiaryRegID = fv.beneficiary_reg_id " + : "db_iemr.t_benvisitdetail v "; Query visitQuery = em.createNativeQuery("SELECT v.BenVisitID, v.BeneficiaryRegID, v.VisitCode, v.CreatedBy, " - + "v.CreatedDate, v.LastModDate FROM db_iemr.t_benvisitdetail v " + + "v.CreatedDate, v.LastModDate FROM " + visitSource + "WHERE v.ProviderServiceMapID = :psm AND v.Deleted = false " + "AND (EXISTS (SELECT 1 FROM db_iemr.t_benchiefcomplaint c WHERE c.BeneficiaryRegID = v.BeneficiaryRegID " + " AND c.VisitCode = v.VisitCode AND c.ChiefComplaintID IS NOT NULL AND c.Deleted = false) " + " OR EXISTS (SELECT 1 FROM db_iemr.t_prescribeddrug d WHERE d.BeneficiaryRegID = v.BeneficiaryRegID " + " AND d.VisitCode = v.VisitCode AND d.DrugID IS NOT NULL AND d.Deleted = false)) " - + villageFilter + "ORDER BY v.CreatedDate DESC") + + "ORDER BY v.CreatedDate DESC") .setParameter("psm", providerServiceMapID); if (villageID != null) visitQuery.setParameter("villageID", villageID); List visits = visitQuery.getResultList(); diff --git a/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java index 25747002..dcefe0b9 100644 --- a/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java @@ -38,6 +38,7 @@ import org.springframework.stereotype.Service; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; @@ -682,16 +683,25 @@ private DiagnosticOrderResultDto toResultDto(DiagnosticOrder order) { public DiagnosticOrderStatusSummaryDto getOrderStatusSummary(String orderType, Integer villageId, Integer providerServiceMapId) { DiagnosticOrderType type = DiagnosticOrderType.fromCode(orderType); - List awaitingProviderResult = diagnosticOrderRepo - .findBeneficiaryIdsAwaitingProviderResult(type.name(), villageId, providerServiceMapId); - List completed = diagnosticOrderRepo - .findBeneficiaryIdsCompleted(type.name(), villageId, providerServiceMapId); - List failed = diagnosticOrderRepo - .findBeneficiaryIdsFailed(type.name(), villageId, providerServiceMapId); - List closed = diagnosticOrderRepo - .findBeneficiaryIdsClosed(type.name(), villageId, providerServiceMapId); - List awaitingManualEntry = diagnosticOrderRepo - .findBeneficiaryIdsAwaitingManualEntry(type.name(), villageId, providerServiceMapId); + List awaitingProviderResult = new ArrayList<>(); + List completed = new ArrayList<>(); + List failed = new ArrayList<>(); + List closed = new ArrayList<>(); + List awaitingManualEntry = new ArrayList<>(); + // One query instead of one per bucket; buckets are the same status groups the per-bucket + // queries used (matched case-insensitively, like the database comparison was). + for (Object[] row : diagnosticOrderRepo.findLatestOrderStatusByBeneficiary(type.name(), villageId, providerServiceMapId)) { + Long beneficiaryId = row[0] != null ? ((Number) row[0]).longValue() : null; + String status = row[1] != null ? row[1].toString().toUpperCase(java.util.Locale.ROOT) : ""; + switch (status) { + case "PENDING", "IN_PROGRESS" -> awaitingProviderResult.add(beneficiaryId); + case "COMPLETED" -> completed.add(beneficiaryId); + case "FAILED" -> failed.add(beneficiaryId); + case "CLOSED", "CANCELLED", "EXPIRED", "REFUSED" -> closed.add(beneficiaryId); + case "MANUAL_ENTRY" -> awaitingManualEntry.add(beneficiaryId); + default -> { } + } + } return new DiagnosticOrderStatusSummaryDto(awaitingProviderResult, completed, failed, closed, awaitingManualEntry); } diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index 34840ef1..f92e4fba 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -156,7 +156,7 @@ public String getByUserId(GetBenRequestHandler request) { Map stateByUser = new HashMap<>(); Set usersWithoutDetail = new HashSet<>(); for (TBSuspected tbSuspected : tbSuspectedList) { - TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); + TBSuspectedDTO dto = toDto(tbSuspected); dto.setUpdateDate(tbSuspected.getLastModDate()); dto.setUpdatedBy(tbSuspected.getModifiedBy()); dtos.add(dto); @@ -189,4 +189,34 @@ public String getByUserId(GetBenRequestHandler request) { return gson.toJson(tbSuspectedRequestDTO); } + // Field-by-field copy of the same-named fields ModelMapper copied (updateDate/updatedBy are set by + // the caller); ModelMapper's reflection per row was most of this list's time at village scale. + private static TBSuspectedDTO toDto(TBSuspected s) { + TBSuspectedDTO dto = new TBSuspectedDTO(); + dto.setId(s.getId()); + dto.setBenId(s.getBenId()); + dto.setVisitLabel(s.getVisitLabel()); + dto.setVisitDate(s.getVisitDate()); + dto.setTypeOfTBCase(s.getTypeOfTBCase()); + dto.setReasonForSuspicion(s.getReasonForSuspicion()); + dto.setIsSputumCollected(s.getIsSputumCollected()); + dto.setSputumSubmittedAt(s.getSputumSubmittedAt()); + dto.setNikshayId(s.getNikshayId()); + dto.setSputumTestResult(s.getSputumTestResult()); + dto.setIsChestXRayDone(s.getIsChestXRayDone()); + dto.setChestXRayResult(s.getChestXRayResult()); + dto.setReasonForRefusalXray(s.getReasonForRefusalXray()); + dto.setReasonForRefusalMTB(s.getReasonForRefusalMTB()); + dto.setReasonForRefusalMDRRIF(s.getReasonForRefusalMDRRIF()); + dto.setReasonForRefusalSputum(s.getReasonForRefusalSputum()); + dto.setMdrRifResult(s.getMdrRifResult()); + dto.setReferralFacility(s.getReferralFacility()); + dto.setIsTBConfirmed(s.getIsTBConfirmed()); + dto.setIsConfirmed(s.getIsConfirmed()); + dto.setIsDRTBConfirmed(s.getIsDRTBConfirmed()); + dto.setReferred(s.getReferred()); + dto.setFollowUps(s.getFollowUps()); + return dto; + } + } From 0ebec13707f29db2415994ec65be59ae1c953f2a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 28 Oct 2026 20:19:40 +0530 Subject: [PATCH 6/7] perf(stoptb): keep TB suspected speed-up on the Stop TB village path only Restore IncentiveLogicService/IncentiveLogicImpl to their original code and run the FLW (ashaId) path of TB suspected getAll exactly as before. The Stop TB village path reads each user's state once and calls the unchanged incentiveForTbSuspected only for CG users, the only state where it acts. Co-Authored-By: Claude Opus 5.5 --- .../flw/service/IncentiveLogicService.java | 3 -- .../flw/service/impl/IncentiveLogicImpl.java | 12 +---- .../service/impl/TBSuspectedServiceImpl.java | 52 ++++++++++++++----- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/IncentiveLogicService.java b/src/main/java/com/iemr/flw/service/IncentiveLogicService.java index 1d15fd4d..8becce19 100644 --- a/src/main/java/com/iemr/flw/service/IncentiveLogicService.java +++ b/src/main/java/com/iemr/flw/service/IncentiveLogicService.java @@ -32,7 +32,4 @@ public interface IncentiveLogicService { IncentiveActivityRecord incentiveForGiveingIFA(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId); IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId); - - // Same as above with the user's state already looked up, for callers handling many rows of one user. - IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId, Integer stateCode); } diff --git a/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java b/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java index 108822e8..b6e4b7c7 100644 --- a/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/IncentiveLogicImpl.java @@ -87,19 +87,9 @@ public IncentiveActivityRecord incentiveForLeprosyPaucibacillaryConfirmed( @Override public IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId) { - Integer stateCode; try { - stateCode = userService.getUserDetail(userId).getStateId(); - } catch (Exception e) { - logger.error("Check Tb Suspected Incentive Exception: ", e); - return null; - } - return incentiveForTbSuspected(benId, treatmentStartDate, treatmentEndDate, userId, stateCode); - } + Integer stateCode = userService.getUserDetail(userId).getStateId(); - @Override - public IncentiveActivityRecord incentiveForTbSuspected(Long benId, Timestamp treatmentStartDate, Timestamp treatmentEndDate, Integer userId, Integer stateCode) { - try { if (stateCode == null) { logger.warn("State code is null for user: {}", userId); return null; diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index f92e4fba..7809343b 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -14,6 +14,7 @@ import com.iemr.flw.service.TBStopVisitService; import com.iemr.flw.service.TBSuspectedService; import com.iemr.flw.service.UserService; +import com.iemr.flw.masterEnum.StateCode; import org.modelmapper.ModelMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -148,11 +149,42 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { @Override public String getByUserId(GetBenRequestHandler request) { List dtos = new ArrayList<>(); - List tbSuspectedList = request.getProviderServiceMapID() != null + boolean stopTbVillage = request.getProviderServiceMapID() != null; + List tbSuspectedList = stopTbVillage ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID()) : tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); - // The incentive check needs the user's state; look it up once per user instead of loading the - // full user detail (role + facility queries) for every confirmed row. + if (stopTbVillage) { + dtos = stopTbVillageDtos(tbSuspectedList); + } else { + for (TBSuspected tbSuspected : tbSuspectedList) { + TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); + dto.setUpdateDate(tbSuspected.getLastModDate()); + dto.setUpdatedBy(tbSuspected.getModifiedBy()); + dtos.add(dto); + + if (tbSuspected != null && Boolean.TRUE.equals(tbSuspected.getIsConfirmed())) { + incentiveLogicService.incentiveForTbSuspected( + tbSuspected.getBenId(), + tbSuspected.getVisitDate(), + tbSuspected.getVisitDate(), + tbSuspected.getUserId() + ); + } + } + } + TBSuspectedRequestDTO tbSuspectedRequestDTO = new TBSuspectedRequestDTO(); + tbSuspectedRequestDTO.setTbSuspectedList(dtos); + tbSuspectedRequestDTO.setUserId(request.getAshaId()); + Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy h:mm:ss a").create(); + return gson.toJson(tbSuspectedRequestDTO); + } + + // Stop TB village list (psm + village). Same DTOs and incentive calls as the per-user path, but the + // user's state is read once per user: IncentiveLogicImpl.incentiveForTbSuspected loads the full user + // detail on every call only to check for CG, and does nothing for any other state, so for other + // states the call is skipped and for CG users it is made exactly as before. + private List stopTbVillageDtos(List tbSuspectedList) { + List dtos = new ArrayList<>(); Map stateByUser = new HashMap<>(); Set usersWithoutDetail = new HashSet<>(); for (TBSuspected tbSuspected : tbSuspectedList) { @@ -161,7 +193,7 @@ public String getByUserId(GetBenRequestHandler request) { dto.setUpdatedBy(tbSuspected.getModifiedBy()); dtos.add(dto); - if (tbSuspected != null && Boolean.TRUE.equals(tbSuspected.getIsConfirmed())) { + if (Boolean.TRUE.equals(tbSuspected.getIsConfirmed())) { Integer userId = tbSuspected.getUserId(); if (!stateByUser.containsKey(userId) && !usersWithoutDetail.contains(userId)) { try { @@ -171,22 +203,18 @@ public String getByUserId(GetBenRequestHandler request) { usersWithoutDetail.add(userId); } } - if (stateByUser.containsKey(userId)) { + Integer stateCode = stateByUser.get(userId); + if (stateCode != null && stateCode == StateCode.CG.getStateCode()) { incentiveLogicService.incentiveForTbSuspected( tbSuspected.getBenId(), tbSuspected.getVisitDate(), tbSuspected.getVisitDate(), - userId, - stateByUser.get(userId) + userId ); } } } - TBSuspectedRequestDTO tbSuspectedRequestDTO = new TBSuspectedRequestDTO(); - tbSuspectedRequestDTO.setTbSuspectedList(dtos); - tbSuspectedRequestDTO.setUserId(request.getAshaId()); - Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy h:mm:ss a").create(); - return gson.toJson(tbSuspectedRequestDTO); + return dtos; } // Field-by-field copy of the same-named fields ModelMapper copied (updateDate/updatedBy are set by From b4e083122c32b8e95e2f66de0b28b0a13b1582b7 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 29 Oct 2026 12:35:47 +0530 Subject: [PATCH 7/7] perf(worklist): restore original village count and page queries The index-only count also counted flow rows that have no i_beneficiarymapping row (423 on the reference DB, mostly PSM 1725), so villages with such rows got extra or empty pages and a fully unmapped village got an empty success instead of "No record found". Put countVillageWorklist, getVillageWorklistAddressIds and the empty-page handling back to the release-3.12.0 behaviour. The batch loader, page size and timing logs stay. Co-Authored-By: Claude Opus 5.5 --- .../flw/repo/identity/BeneficiaryRepo.java | 26 +++++-------------- .../service/impl/BeneficiaryServiceImpl.java | 6 +---- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java index 8a84687a..cd7316c8 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -152,24 +152,13 @@ Page getBenDataWithinDates( // replacing the old approach of loading every flow row for the whole village into // memory and slicing it in Java. Cost is now O(pageSize) instead of O(village size) // on every page request, including page 0. See BeneficiaryServiceImpl.getBenData(). - // Deferred join: the OFFSET skip runs on idx_flow_psm_village_reg alone (index-only, no row or - // mapping lookups for skipped rows), then only this page's flow rows are joined to the mapping. - // Rows sharing a registrationDate are ordered newest ben_flow_id first; the old query left their - // order to the plan (filesort), so a tie could move across a page boundary and be skipped or sent - // twice. A flow row with no mapping row still takes its slot, so such a page can come back short, - // never shifted. @Query(value = """ SELECT bm.BenAddressId - FROM ( - SELECT ben_flow_id, registrationDate - FROM db_iemr.i_ben_flow_outreach - WHERE providerServiceMapID = :psmId AND villageID = :villageId AND deleted = 0 - ORDER BY registrationDate DESC, ben_flow_id DESC - LIMIT :pageSize OFFSET :offset - ) p - JOIN db_iemr.i_ben_flow_outreach bfs ON bfs.ben_flow_id = p.ben_flow_id + FROM db_iemr.i_ben_flow_outreach bfs JOIN db_identity.i_beneficiarymapping bm ON bm.BenRegId = bfs.beneficiary_reg_id - ORDER BY p.registrationDate DESC, p.ben_flow_id DESC + WHERE bfs.providerServiceMapID = :psmId AND bfs.villageID = :villageId AND bfs.deleted = 0 + ORDER BY bfs.registrationDate DESC + LIMIT :pageSize OFFSET :offset """, nativeQuery = true) List getVillageWorklistAddressIds( @Param("psmId") Integer psmId, @@ -177,12 +166,11 @@ List getVillageWorklistAddressIds( @Param("pageSize") int pageSize, @Param("offset") int offset); - // Counts flow rows (index-only), the same rows getVillageWorklistAddressIds pages over, so - // totalPage always covers every flow row. @Query(value = """ SELECT COUNT(*) - FROM db_iemr.i_ben_flow_outreach - WHERE providerServiceMapID = :psmId AND villageID = :villageId AND deleted = 0 + FROM db_iemr.i_ben_flow_outreach bfs + JOIN db_identity.i_beneficiarymapping bm ON bm.BenRegId = bfs.beneficiary_reg_id + WHERE bfs.providerServiceMapID = :psmId AND bfs.villageID = :villageId AND bfs.deleted = 0 """, nativeQuery = true) long countVillageWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index d8082fe4..b0d799f0 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -159,11 +159,7 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr request.getProviderServiceMapID(), request.getVillageID(), pageSize, offset); long tIds = System.currentTimeMillis(); - // In range but every flow row on this page lacks a mapping: return an empty page, not - // "No record found", so the app keeps paging to totalPage. - if (addressIds.isEmpty()) { - return getMappingsForAddressIDs(new ArrayList<>(), totalPage, authorisation, null, tStart); - } + if (addressIds.isEmpty()) return null; // Was findAddressesByIds() - a single "IN :ids" JPQL query. Hit a reproducible // Hibernate 6.4.1 bug there: "Binding is multi-valued; illegal call to