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; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 027import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 028import ca.uhn.fhir.jpa.api.model.ExpungeOptions; 029import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; 030import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 031import ca.uhn.fhir.jpa.dao.expunge.ExpungeService; 032import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 033import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 034import ca.uhn.fhir.jpa.model.dao.JpaPid; 035import ca.uhn.fhir.jpa.model.entity.BaseHasResource; 036import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 037import ca.uhn.fhir.jpa.model.entity.ResourceTable; 038import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 039import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory; 040import ca.uhn.fhir.jpa.search.SearchConstants; 041import ca.uhn.fhir.jpa.util.QueryChunker; 042import ca.uhn.fhir.jpa.util.ResourceCountCache; 043import ca.uhn.fhir.rest.api.server.IBundleProvider; 044import ca.uhn.fhir.rest.api.server.RequestDetails; 045import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 046import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 047import ca.uhn.fhir.util.StopWatch; 048import com.google.common.annotations.VisibleForTesting; 049import jakarta.annotation.Nonnull; 050import jakarta.annotation.Nullable; 051import jakarta.persistence.EntityManager; 052import jakarta.persistence.PersistenceContext; 053import jakarta.persistence.PersistenceContextType; 054import jakarta.persistence.Query; 055import jakarta.persistence.TypedQuery; 056import org.hl7.fhir.instance.model.api.IBaseBundle; 057import org.springframework.beans.factory.annotation.Autowired; 058import org.springframework.context.ApplicationContext; 059import org.springframework.transaction.annotation.Propagation; 060import org.springframework.transaction.annotation.Transactional; 061 062import java.util.Date; 063import java.util.HashMap; 064import java.util.List; 065import java.util.Map; 066import java.util.function.Predicate; 067import java.util.stream.Collectors; 068import java.util.stream.Stream; 069 070public abstract class BaseHapiFhirSystemDao<T extends IBaseBundle, MT> extends BaseStorageDao 071 implements IFhirSystemDao<T, MT> { 072 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirSystemDao.class); 073 074 public ResourceCountCache myResourceCountsCache; 075 076 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 077 protected EntityManager myEntityManager; 078 079 @Autowired 080 private TransactionProcessor myTransactionProcessor; 081 082 @Autowired 083 private ApplicationContext myApplicationContext; 084 085 @Autowired 086 private ExpungeService myExpungeService; 087 088 @Autowired 089 private IResourceTableDao myResourceTableDao; 090 091 @Autowired 092 private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; 093 094 @Autowired 095 private IInterceptorBroadcaster myInterceptorBroadcaster; 096 097 @Autowired 098 private IRequestPartitionHelperSvc myRequestPartitionHelperService; 099 100 @Autowired 101 private IHapiTransactionService myTransactionService; 102 103 @VisibleForTesting 104 public void setTransactionProcessorForUnitTest(TransactionProcessor theTransactionProcessor) { 105 myTransactionProcessor = theTransactionProcessor; 106 } 107 108 @Override 109 @Transactional(propagation = Propagation.NEVER) 110 public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) { 111 validateExpungeEnabled(theExpungeOptions); 112 return myExpungeService.expunge(null, null, theExpungeOptions, theRequestDetails); 113 } 114 115 private void validateExpungeEnabled(ExpungeOptions theExpungeOptions) { 116 if (!getStorageSettings().isExpungeEnabled()) { 117 throw new MethodNotAllowedException(Msg.code(2080) + "$expunge is not enabled on this server"); 118 } 119 120 if (theExpungeOptions.isExpungeEverything() && !getStorageSettings().isAllowMultipleDelete()) { 121 throw new MethodNotAllowedException(Msg.code(2081) + "Multiple delete is not enabled on this server"); 122 } 123 } 124 125 @Transactional(propagation = Propagation.REQUIRED) 126 @Override 127 public Map<String, Long> getResourceCounts() { 128 Map<String, Long> retVal = new HashMap<>(); 129 130 List<Map<?, ?>> counts = myResourceTableDao.getResourceCounts(); 131 for (Map<?, ?> next : counts) { 132 retVal.put( 133 next.get("type").toString(), 134 Long.parseLong(next.get("count").toString())); 135 } 136 137 return retVal; 138 } 139 140 @Nullable 141 @Override 142 public Map<String, Long> getResourceCountsFromCache() { 143 if (myResourceCountsCache == null) { 144 // Lazy load this to avoid a circular dependency 145 myResourceCountsCache = myApplicationContext.getBean("myResourceCountsCache", ResourceCountCache.class); 146 } 147 return myResourceCountsCache.get(); 148 } 149 150 @Override 151 public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { 152 StopWatch w = new StopWatch(); 153 RequestPartitionId requestPartitionId = 154 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 155 theRequestDetails, null, null); 156 IBundleProvider retVal = myTransactionService 157 .withRequest(theRequestDetails) 158 .withRequestPartitionId(requestPartitionId) 159 .execute(() -> myPersistedJpaBundleProviderFactory.history( 160 theRequestDetails, null, null, theSince, theUntil, theOffset, requestPartitionId)); 161 ourLog.info("Processed global history in {}ms", w.getMillisAndRestart()); 162 return retVal; 163 } 164 165 @Override 166 public T transaction(RequestDetails theRequestDetails, T theRequest) { 167 HapiTransactionService.noTransactionAllowed(); 168 return myTransactionProcessor.transaction(theRequestDetails, theRequest, false); 169 } 170 171 @Override 172 public T transactionNested(RequestDetails theRequestDetails, T theRequest) { 173 HapiTransactionService.requireTransaction(); 174 return myTransactionProcessor.transaction(theRequestDetails, theRequest, true); 175 } 176 177 /** 178 * Prefetch entities into the Hibernate session. 179 * 180 * When processing several resources (e.g. transaction bundle, $reindex chunk, etc.) 181 * it would be slow to fetch each piece of a resource (e.g. all token index rows) 182 * one resource at a time. 183 * Instead, we fetch all the linked resources for the entire batch and populate the Hibernate Session. 184 * 185 * @param theResolvedIds the pids 186 * @param thePreFetchIndexes Should resource indexes be loaded 187 */ 188 @SuppressWarnings("rawtypes") 189 @Override 190 public <P extends IResourcePersistentId> void preFetchResources( 191 List<P> theResolvedIds, boolean thePreFetchIndexes) { 192 HapiTransactionService.requireTransaction(); 193 List<Long> pids = theResolvedIds.stream().map(t -> ((JpaPid) t).getId()).collect(Collectors.toList()); 194 195 QueryChunker.chunk(pids, idChunk -> { 196 197 /* 198 * Pre-fetch the resources we're touching in this transaction in mass - this reduced the 199 * number of database round trips. 200 * 201 * The thresholds below are kind of arbitrary. It's not 202 * actually guaranteed that this pre-fetching will help (e.g. if a Bundle contains 203 * a bundle of NOP conditional creates for example, the pre-fetching is actually loading 204 * more data than would otherwise be loaded). 205 * 206 * However, for realistic average workloads, this should reduce the number of round trips. 207 */ 208 if (idChunk.size() >= 2) { 209 List<ResourceTable> entityChunk = prefetchResourceTableHistoryAndProvenance(idChunk); 210 211 if (thePreFetchIndexes) { 212 213 prefetchByField("string", "myParamsString", ResourceTable::isParamsStringPopulated, entityChunk); 214 prefetchByField("token", "myParamsToken", ResourceTable::isParamsTokenPopulated, entityChunk); 215 prefetchByField("date", "myParamsDate", ResourceTable::isParamsDatePopulated, entityChunk); 216 prefetchByField( 217 "quantity", "myParamsQuantity", ResourceTable::isParamsQuantityPopulated, entityChunk); 218 prefetchByField("resourceLinks", "myResourceLinks", ResourceTable::isHasLinks, entityChunk); 219 220 prefetchByJoinClause( 221 "tags", 222 // fetch the TagResources and the actual TagDefinitions 223 "LEFT JOIN FETCH r.myTags t LEFT JOIN FETCH t.myTag", 224 BaseHasResource::isHasTags, 225 entityChunk); 226 227 prefetchByField( 228 "comboStringUnique", 229 "myParamsComboStringUnique", 230 ResourceTable::isParamsComboStringUniquePresent, 231 entityChunk); 232 prefetchByField( 233 "comboTokenNonUnique", 234 "myParamsComboTokensNonUnique", 235 ResourceTable::isParamsComboTokensNonUniquePresent, 236 entityChunk); 237 238 if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.ENABLED) { 239 prefetchByField("searchParamPresence", "mySearchParamPresents", r -> true, entityChunk); 240 } 241 } 242 } 243 }); 244 } 245 246 @Nonnull 247 private List<ResourceTable> prefetchResourceTableHistoryAndProvenance(List<Long> idChunk) { 248 assert idChunk.size() < SearchConstants.MAX_PAGE_SIZE : "assume pre-chunked"; 249 250 Query query = myEntityManager.createQuery("select r, h " 251 + " FROM ResourceTable r " 252 + " LEFT JOIN fetch ResourceHistoryTable h " 253 + " on r.myVersion = h.myResourceVersion and r.id = h.myResourceId " 254 + " left join fetch h.myProvenance " 255 + " WHERE r.myId IN ( :IDS ) "); 256 query.setParameter("IDS", idChunk); 257 258 @SuppressWarnings("unchecked") 259 Stream<Object[]> queryResultStream = query.getResultStream(); 260 return queryResultStream 261 .map(nextPair -> { 262 // Store the matching ResourceHistoryTable in the transient slot on ResourceTable 263 ResourceTable result = (ResourceTable) nextPair[0]; 264 ResourceHistoryTable currentVersion = (ResourceHistoryTable) nextPair[1]; 265 result.setCurrentVersionEntity(currentVersion); 266 return result; 267 }) 268 .collect(Collectors.toList()); 269 } 270 271 /** 272 * Prefetch a join field for the active subset of some ResourceTable entities. 273 * Convenience wrapper around prefetchByJoinClause() for simple fields. 274 * 275 * @param theDescription for logging 276 * @param theJpaFieldName the join field from ResourceTable 277 * @param theEntityPredicate select which ResourceTable entities need this join 278 * @param theEntities the ResourceTable entities to consider 279 */ 280 private void prefetchByField( 281 String theDescription, 282 String theJpaFieldName, 283 Predicate<ResourceTable> theEntityPredicate, 284 List<ResourceTable> theEntities) { 285 286 String joinClause = "LEFT JOIN FETCH r." + theJpaFieldName; 287 288 prefetchByJoinClause(theDescription, joinClause, theEntityPredicate, theEntities); 289 } 290 291 /** 292 * Prefetch a join field for the active subset of some ResourceTable entities. 293 * 294 * @param theDescription for logging 295 * @param theJoinClause the JPA join expression to add to `ResourceTable r` 296 * @param theEntityPredicate selects which entities need this prefetch 297 * @param theEntities the ResourceTable entities to consider 298 */ 299 private void prefetchByJoinClause( 300 String theDescription, 301 String theJoinClause, 302 Predicate<ResourceTable> theEntityPredicate, 303 List<ResourceTable> theEntities) { 304 305 // Which entities need this prefetch? 306 List<Long> idSubset = theEntities.stream() 307 .filter(theEntityPredicate) 308 .map(ResourceTable::getId) 309 .collect(Collectors.toList()); 310 311 if (idSubset.isEmpty()) { 312 // nothing to do 313 return; 314 } 315 316 String jqlQuery = "FROM ResourceTable r " + theJoinClause + " WHERE r.myId IN ( :IDS )"; 317 318 TypedQuery<ResourceTable> query = myEntityManager.createQuery(jqlQuery, ResourceTable.class); 319 query.setParameter("IDS", idSubset); 320 List<ResourceTable> indexFetchOutcome = query.getResultList(); 321 322 ourLog.debug("Pre-fetched {} {} indexes", indexFetchOutcome.size(), theDescription); 323 } 324 325 @Nullable 326 @Override 327 protected String getResourceName() { 328 return null; 329 } 330 331 @Override 332 protected IInterceptorBroadcaster getInterceptorBroadcaster() { 333 return myInterceptorBroadcaster; 334 } 335 336 @Override 337 protected JpaStorageSettings getStorageSettings() { 338 return myStorageSettings; 339 } 340 341 @Override 342 public FhirContext getContext() { 343 return myFhirContext; 344 } 345 346 @VisibleForTesting 347 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 348 myStorageSettings = theStorageSettings; 349 } 350}