001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao.index; 021 022import ca.uhn.fhir.jpa.model.entity.BaseResourceIndex; 023import ca.uhn.fhir.jpa.model.entity.ResourceTable; 024import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 025import ca.uhn.fhir.jpa.util.AddRemoveCount; 026import com.google.common.annotations.VisibleForTesting; 027import jakarta.persistence.EntityManager; 028import jakarta.persistence.PersistenceContext; 029import jakarta.persistence.PersistenceContextType; 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032import org.springframework.stereotype.Service; 033 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.HashSet; 037import java.util.Iterator; 038import java.util.List; 039import java.util.Set; 040 041@Service 042public class DaoSearchParamSynchronizer { 043 private static final Logger ourLog = LoggerFactory.getLogger(DaoSearchParamSynchronizer.class); 044 045 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 046 protected EntityManager myEntityManager; 047 048 public AddRemoveCount synchronizeSearchParamsToDatabase( 049 ResourceIndexedSearchParams theParams, 050 ResourceTable theEntity, 051 ResourceIndexedSearchParams existingParams) { 052 AddRemoveCount retVal = new AddRemoveCount(); 053 054 synchronize(theEntity, retVal, theParams.myStringParams, existingParams.myStringParams); 055 synchronize(theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams); 056 synchronize(theEntity, retVal, theParams.myNumberParams, existingParams.myNumberParams); 057 synchronize(theEntity, retVal, theParams.myQuantityParams, existingParams.myQuantityParams); 058 synchronize(theEntity, retVal, theParams.myQuantityNormalizedParams, existingParams.myQuantityNormalizedParams); 059 synchronize(theEntity, retVal, theParams.myDateParams, existingParams.myDateParams); 060 synchronize(theEntity, retVal, theParams.myUriParams, existingParams.myUriParams); 061 synchronize(theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams); 062 synchronize(theEntity, retVal, theParams.myLinks, existingParams.myLinks); 063 synchronize(theEntity, retVal, theParams.myComboTokenNonUnique, existingParams.myComboTokenNonUnique); 064 065 // make sure links are indexed 066 theEntity.setResourceLinks(theParams.myLinks); 067 068 return retVal; 069 } 070 071 @VisibleForTesting 072 public void setEntityManager(EntityManager theEntityManager) { 073 myEntityManager = theEntityManager; 074 } 075 076 private <T extends BaseResourceIndex> void synchronize( 077 ResourceTable theEntity, 078 AddRemoveCount theAddRemoveCount, 079 Collection<T> theNewParams, 080 Collection<T> theExistingParams) { 081 Collection<T> newParams = theNewParams; 082 for (T next : newParams) { 083 next.setPartitionId(theEntity.getPartitionId()); 084 next.calculateHashes(); 085 } 086 087 /* 088 * It's technically possible that the existing index collection 089 * contains duplicates. Duplicates don't actually cause any 090 * issues for searching since we always deduplicate the PIDs we 091 * get back from the search, but they are wasteful. We don't 092 * enforce uniqueness in the DB for the index tables for 093 * performance reasons (no sense adding a constraint that slows 094 * down writes when dupes don't actually hurt anything other than 095 * a bit of wasted space). 096 * 097 * So we check if there are any dupes, and if we find any we 098 * remove them. 099 */ 100 Set<T> existingParamsAsSet = new HashSet<>(theExistingParams.size()); 101 for (Iterator<T> iterator = theExistingParams.iterator(); iterator.hasNext(); ) { 102 T next = iterator.next(); 103 if (!existingParamsAsSet.add(next)) { 104 iterator.remove(); 105 myEntityManager.remove(next); 106 } 107 } 108 109 /* 110 * HashCodes may have changed as a result of setting the partition ID, so 111 * create a new set that will reflect the new hashcodes 112 */ 113 newParams = new HashSet<>(newParams); 114 115 List<T> paramsToRemove = subtract(theExistingParams, newParams); 116 List<T> paramsToAdd = subtract(newParams, theExistingParams); 117 tryToReuseIndexEntities(paramsToRemove, paramsToAdd); 118 119 for (T next : paramsToRemove) { 120 if (!myEntityManager.contains(next)) { 121 // If a resource is created and deleted in the same transaction, we can end up 122 // in a state where we're deleting entities that don't actually exist. Hibernate 123 // 6 is stricter about this, so we skip here. 124 continue; 125 } 126 myEntityManager.remove(next); 127 } 128 for (T next : paramsToAdd) { 129 myEntityManager.merge(next); 130 } 131 132 // TODO: are there any unintended consequences to fixing this bug? 133 theAddRemoveCount.addToAddCount(paramsToAdd.size()); 134 theAddRemoveCount.addToRemoveCount(paramsToRemove.size()); 135 } 136 137 /** 138 * The logic here is that often times when we update a resource we are dropping 139 * one index row and adding another. This method tries to reuse rows that would otherwise 140 * have been deleted by updating them with the contents of rows that would have 141 * otherwise been added. In other words, we're trying to replace 142 * "one delete + one insert" with "one update" 143 * 144 * @param theIndexesToRemove The rows that would be removed 145 * @param theIndexesToAdd The rows that would be added 146 */ 147 private <T extends BaseResourceIndex> void tryToReuseIndexEntities( 148 List<T> theIndexesToRemove, List<T> theIndexesToAdd) { 149 for (int addIndex = 0; addIndex < theIndexesToAdd.size(); addIndex++) { 150 151 // If there are no more rows to remove, there's nothing we can reuse 152 if (theIndexesToRemove.isEmpty()) { 153 break; 154 } 155 156 T targetEntity = theIndexesToAdd.get(addIndex); 157 if (targetEntity.getId() != null) { 158 continue; 159 } 160 161 // Take a row we were going to remove, and repurpose its ID 162 T entityToReuse = theIndexesToRemove.remove(theIndexesToRemove.size() - 1); 163 entityToReuse.copyMutableValuesFrom(targetEntity); 164 theIndexesToAdd.set(addIndex, entityToReuse); 165 } 166 } 167 168 public static <T> List<T> subtract(Collection<T> theSubtractFrom, Collection<T> theToSubtract) { 169 assert theSubtractFrom != theToSubtract || (theSubtractFrom.isEmpty()); 170 171 if (theSubtractFrom.isEmpty()) { 172 return new ArrayList<>(); 173 } 174 175 ArrayList<T> retVal = new ArrayList<>(); 176 for (T next : theSubtractFrom) { 177 if (!theToSubtract.contains(next)) { 178 retVal.add(next); 179 } 180 } 181 return retVal; 182 } 183}