001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 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<JpaPid> pids = theResolvedIds.stream().map(t -> ((JpaPid) t)).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.isEmpty()) { 209 List<ResourceTable> entityChunk = null; 210 211 /* 212 * Unless we're in Mass Ingestion mode, we will pre-fetch the current 213 * saved resource text in HFJ_RES_VER (ResourceHistoryTable). If we're 214 * in Mass Ingestion Mode, we don't need to do that because every update 215 * will generate a new version anyway so the system never needs to know 216 * the current contents. 217 */ 218 if (!myStorageSettings.isMassIngestionMode()) { 219 entityChunk = prefetchResourceTableAndHistory(idChunk); 220 } 221 222 if (thePreFetchIndexes) { 223 224 /* 225 * If we're in mass ingestion mode, then we still need to load the resource 226 * entries in HFJ_RESOURCE (ResourceTable). We combine that with the search 227 * for tokens (since token is the most likely kind of index to be populated 228 * for any arbitrary resource type). 229 * 230 * For all other index types, we only load indexes if at least one 231 * HFJ_RESOURCE row indicates that a resource we care about actually has 232 * index rows of the given type. 233 */ 234 if (entityChunk == null) { 235 String jqlQuery = 236 "SELECT r FROM ResourceTable r LEFT JOIN FETCH r.myParamsToken WHERE r.myPid IN ( :IDS )"; 237 TypedQuery<ResourceTable> query = myEntityManager.createQuery(jqlQuery, ResourceTable.class); 238 query.setParameter("IDS", idChunk); 239 entityChunk = query.getResultList(); 240 } else { 241 prefetchByField("token", "myParamsToken", ResourceTable::isParamsTokenPopulated, entityChunk); 242 } 243 244 prefetchByField("string", "myParamsString", ResourceTable::isParamsStringPopulated, entityChunk); 245 prefetchByField("date", "myParamsDate", ResourceTable::isParamsDatePopulated, entityChunk); 246 prefetchByField( 247 "quantity", "myParamsQuantity", ResourceTable::isParamsQuantityPopulated, entityChunk); 248 249 prefetchByJoinClause( 250 "resourceLinks", 251 // fetch the ResourceLink but also the target resource for that link 252 "LEFT JOIN FETCH r.myResourceLinks l LEFT JOIN FETCH l.myTargetResource", 253 ResourceTable::isHasLinks, 254 entityChunk); 255 256 prefetchByJoinClause( 257 "tags", 258 // fetch the TagResources and the actual TagDefinitions 259 "LEFT JOIN FETCH r.myTags t LEFT JOIN FETCH t.myTag", 260 BaseHasResource::isHasTags, 261 entityChunk); 262 263 prefetchByField( 264 "comboStringUnique", 265 "myParamsComboStringUnique", 266 ResourceTable::isParamsComboStringUniquePresent, 267 entityChunk); 268 prefetchByField( 269 "comboTokenNonUnique", 270 "myParamsComboTokensNonUnique", 271 ResourceTable::isParamsComboTokensNonUniquePresent, 272 entityChunk); 273 274 if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.ENABLED) { 275 prefetchByField("searchParamPresence", "mySearchParamPresents", r -> true, entityChunk); 276 } 277 } 278 } 279 }); 280 } 281 282 @Nonnull 283 private List<ResourceTable> prefetchResourceTableAndHistory(List<JpaPid> idChunk) { 284 assert idChunk.size() < SearchConstants.MAX_PAGE_SIZE : "assume pre-chunked"; 285 286 Query query = myEntityManager.createQuery("select r, h " 287 + " FROM ResourceTable r " 288 + " LEFT JOIN fetch ResourceHistoryTable h " 289 + " on r.myVersion = h.myResourceVersion and r = h.myResourceTable " 290 + " WHERE r.myPid IN ( :IDS ) "); 291 query.setParameter("IDS", idChunk); 292 293 @SuppressWarnings("unchecked") 294 Stream<Object[]> queryResultStream = query.getResultStream(); 295 return queryResultStream 296 .map(nextPair -> { 297 // Store the matching ResourceHistoryTable in the transient slot on ResourceTable 298 ResourceTable result = (ResourceTable) nextPair[0]; 299 ResourceHistoryTable currentVersion = (ResourceHistoryTable) nextPair[1]; 300 result.setCurrentVersionEntity(currentVersion); 301 return result; 302 }) 303 .collect(Collectors.toList()); 304 } 305 306 /** 307 * Prefetch a join field for the active subset of some ResourceTable entities. 308 * Convenience wrapper around prefetchByJoinClause() for simple fields. 309 * 310 * @param theDescription for logging 311 * @param theJpaFieldName the join field from ResourceTable 312 * @param theEntityPredicate select which ResourceTable entities need this join 313 * @param theEntities the ResourceTable entities to consider 314 */ 315 private void prefetchByField( 316 String theDescription, 317 String theJpaFieldName, 318 Predicate<ResourceTable> theEntityPredicate, 319 List<ResourceTable> theEntities) { 320 321 String joinClause = "LEFT JOIN FETCH r." + theJpaFieldName; 322 323 prefetchByJoinClause(theDescription, joinClause, theEntityPredicate, theEntities); 324 } 325 326 /** 327 * Prefetch a join field for the active subset of some ResourceTable entities. 328 * 329 * @param theDescription for logging 330 * @param theJoinClause the JPA join expression to add to `ResourceTable r` 331 * @param theEntityPredicate selects which entities need this prefetch 332 * @param theEntities the ResourceTable entities to consider 333 */ 334 private void prefetchByJoinClause( 335 String theDescription, 336 String theJoinClause, 337 Predicate<ResourceTable> theEntityPredicate, 338 List<ResourceTable> theEntities) { 339 340 // Which entities need this prefetch? 341 List<JpaPid> idSubset = theEntities.stream() 342 .filter(theEntityPredicate) 343 .map(ResourceTable::getId) 344 .collect(Collectors.toList()); 345 346 if (idSubset.isEmpty()) { 347 // nothing to do 348 return; 349 } 350 351 String jqlQuery = "FROM ResourceTable r " + theJoinClause + " WHERE r.myPid IN ( :IDS )"; 352 353 TypedQuery<ResourceTable> query = myEntityManager.createQuery(jqlQuery, ResourceTable.class); 354 query.setParameter("IDS", idSubset); 355 List<ResourceTable> indexFetchOutcome = query.getResultList(); 356 357 ourLog.debug("Pre-fetched {} {} indexes", indexFetchOutcome.size(), theDescription); 358 } 359 360 @Nullable 361 @Override 362 protected String getResourceName() { 363 return null; 364 } 365 366 @Override 367 protected IInterceptorBroadcaster getInterceptorBroadcaster() { 368 return myInterceptorBroadcaster; 369 } 370 371 @Override 372 protected JpaStorageSettings getStorageSettings() { 373 return myStorageSettings; 374 } 375 376 @Override 377 public FhirContext getContext() { 378 return myFhirContext; 379 } 380 381 @VisibleForTesting 382 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 383 myStorageSettings = theStorageSettings; 384 } 385}