001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2023 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.ReadPartitionIdRequestDetails; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 028import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 029import ca.uhn.fhir.jpa.api.model.ExpungeOptions; 030import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; 031import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 032import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; 033import ca.uhn.fhir.jpa.dao.expunge.ExpungeService; 034import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 035import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 036import ca.uhn.fhir.jpa.model.dao.JpaPid; 037import ca.uhn.fhir.jpa.model.entity.BaseHasResource; 038import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 039import ca.uhn.fhir.jpa.model.entity.ResourceTable; 040import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 041import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory; 042import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 043import ca.uhn.fhir.jpa.util.QueryChunker; 044import ca.uhn.fhir.jpa.util.ResourceCountCache; 045import ca.uhn.fhir.rest.api.server.IBundleProvider; 046import ca.uhn.fhir.rest.api.server.RequestDetails; 047import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 048import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 049import ca.uhn.fhir.util.StopWatch; 050import com.google.common.annotations.VisibleForTesting; 051import org.hl7.fhir.instance.model.api.IBaseBundle; 052import org.springframework.beans.factory.annotation.Autowired; 053import org.springframework.context.ApplicationContext; 054import org.springframework.transaction.annotation.Propagation; 055import org.springframework.transaction.annotation.Transactional; 056 057import java.util.ArrayList; 058import java.util.Date; 059import java.util.HashMap; 060import java.util.List; 061import java.util.Map; 062import java.util.stream.Collectors; 063import javax.annotation.Nullable; 064import javax.persistence.EntityManager; 065import javax.persistence.PersistenceContext; 066import javax.persistence.PersistenceContextType; 067import javax.persistence.TypedQuery; 068import javax.persistence.criteria.CriteriaBuilder; 069import javax.persistence.criteria.CriteriaQuery; 070import javax.persistence.criteria.JoinType; 071import javax.persistence.criteria.Predicate; 072import javax.persistence.criteria.Root; 073 074public abstract class BaseHapiFhirSystemDao<T extends IBaseBundle, MT> extends BaseStorageDao 075 implements IFhirSystemDao<T, MT> { 076 077 public static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[0]; 078 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirSystemDao.class); 079 public ResourceCountCache myResourceCountsCache; 080 081 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 082 protected EntityManager myEntityManager; 083 084 @Autowired 085 private TransactionProcessor myTransactionProcessor; 086 087 @Autowired 088 private ApplicationContext myApplicationContext; 089 090 @Autowired 091 private ExpungeService myExpungeService; 092 093 @Autowired 094 private IResourceTableDao myResourceTableDao; 095 096 @Autowired 097 private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; 098 099 @Autowired 100 private IResourceTagDao myResourceTagDao; 101 102 @Autowired 103 private IInterceptorBroadcaster myInterceptorBroadcaster; 104 105 @Autowired 106 private IRequestPartitionHelperSvc myRequestPartitionHelperService; 107 108 @Autowired 109 private IHapiTransactionService myTransactionService; 110 111 @VisibleForTesting 112 public void setTransactionProcessorForUnitTest(TransactionProcessor theTransactionProcessor) { 113 myTransactionProcessor = theTransactionProcessor; 114 } 115 116 @Override 117 @Transactional(propagation = Propagation.NEVER) 118 public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) { 119 validateExpungeEnabled(theExpungeOptions); 120 return myExpungeService.expunge(null, null, theExpungeOptions, theRequestDetails); 121 } 122 123 private void validateExpungeEnabled(ExpungeOptions theExpungeOptions) { 124 if (!getStorageSettings().isExpungeEnabled()) { 125 throw new MethodNotAllowedException(Msg.code(2080) + "$expunge is not enabled on this server"); 126 } 127 128 if (theExpungeOptions.isExpungeEverything() && !getStorageSettings().isAllowMultipleDelete()) { 129 throw new MethodNotAllowedException(Msg.code(2081) + "Multiple delete is not enabled on this server"); 130 } 131 } 132 133 @Transactional(propagation = Propagation.REQUIRED) 134 @Override 135 public Map<String, Long> getResourceCounts() { 136 Map<String, Long> retVal = new HashMap<>(); 137 138 List<Map<?, ?>> counts = myResourceTableDao.getResourceCounts(); 139 for (Map<?, ?> next : counts) { 140 retVal.put( 141 next.get("type").toString(), 142 Long.parseLong(next.get("count").toString())); 143 } 144 145 return retVal; 146 } 147 148 @Nullable 149 @Override 150 public Map<String, Long> getResourceCountsFromCache() { 151 if (myResourceCountsCache == null) { 152 // Lazy load this to avoid a circular dependency 153 myResourceCountsCache = myApplicationContext.getBean("myResourceCountsCache", ResourceCountCache.class); 154 } 155 return myResourceCountsCache.get(); 156 } 157 158 @Override 159 public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { 160 StopWatch w = new StopWatch(); 161 ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forHistory(null, null); 162 RequestPartitionId requestPartitionId = 163 myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, details); 164 IBundleProvider retVal = myTransactionService 165 .withRequest(theRequestDetails) 166 .withRequestPartitionId(requestPartitionId) 167 .execute(() -> myPersistedJpaBundleProviderFactory.history( 168 theRequestDetails, null, null, theSince, theUntil, theOffset, requestPartitionId)); 169 ourLog.info("Processed global history in {}ms", w.getMillisAndRestart()); 170 return retVal; 171 } 172 173 @Override 174 public T transaction(RequestDetails theRequestDetails, T theRequest) { 175 HapiTransactionService.noTransactionAllowed(); 176 return myTransactionProcessor.transaction(theRequestDetails, theRequest, false); 177 } 178 179 @Override 180 public T transactionNested(RequestDetails theRequestDetails, T theRequest) { 181 HapiTransactionService.requireTransaction(); 182 return myTransactionProcessor.transaction(theRequestDetails, theRequest, true); 183 } 184 185 @Override 186 public <P extends IResourcePersistentId> void preFetchResources( 187 List<P> theResolvedIds, boolean thePreFetchIndexes) { 188 HapiTransactionService.requireTransaction(); 189 List<Long> pids = theResolvedIds.stream().map(t -> ((JpaPid) t).getId()).collect(Collectors.toList()); 190 191 new QueryChunker<Long>().chunk(pids, ids -> { 192 193 /* 194 * Pre-fetch the resources we're touching in this transaction in mass - this reduced the 195 * number of database round trips. 196 * 197 * The thresholds below are kind of arbitrary. It's not 198 * actually guaranteed that this pre-fetching will help (e.g. if a Bundle contains 199 * a bundle of NOP conditional creates for example, the pre-fetching is actually loading 200 * more data than would otherwise be loaded). 201 * 202 * However, for realistic average workloads, this should reduce the number of round trips. 203 */ 204 if (ids.size() >= 2) { 205 List<ResourceTable> loadedResourceTableEntries = new ArrayList<>(); 206 preFetchIndexes(ids, "forcedId", "myForcedId", loadedResourceTableEntries); 207 208 List<Long> entityIds; 209 210 if (thePreFetchIndexes) { 211 entityIds = loadedResourceTableEntries.stream() 212 .filter(ResourceTable::isParamsStringPopulated) 213 .map(ResourceTable::getId) 214 .collect(Collectors.toList()); 215 if (entityIds.size() > 0) { 216 preFetchIndexes(entityIds, "string", "myParamsString", null); 217 } 218 219 entityIds = loadedResourceTableEntries.stream() 220 .filter(ResourceTable::isParamsTokenPopulated) 221 .map(ResourceTable::getId) 222 .collect(Collectors.toList()); 223 if (entityIds.size() > 0) { 224 preFetchIndexes(entityIds, "token", "myParamsToken", null); 225 } 226 227 entityIds = loadedResourceTableEntries.stream() 228 .filter(ResourceTable::isParamsDatePopulated) 229 .map(ResourceTable::getId) 230 .collect(Collectors.toList()); 231 if (entityIds.size() > 0) { 232 preFetchIndexes(entityIds, "date", "myParamsDate", null); 233 } 234 235 entityIds = loadedResourceTableEntries.stream() 236 .filter(ResourceTable::isParamsQuantityPopulated) 237 .map(ResourceTable::getId) 238 .collect(Collectors.toList()); 239 if (entityIds.size() > 0) { 240 preFetchIndexes(entityIds, "quantity", "myParamsQuantity", null); 241 } 242 243 entityIds = loadedResourceTableEntries.stream() 244 .filter(ResourceTable::isHasLinks) 245 .map(ResourceTable::getId) 246 .collect(Collectors.toList()); 247 if (entityIds.size() > 0) { 248 preFetchIndexes(entityIds, "resourceLinks", "myResourceLinks", null); 249 } 250 251 entityIds = loadedResourceTableEntries.stream() 252 .filter(BaseHasResource::isHasTags) 253 .map(ResourceTable::getId) 254 .collect(Collectors.toList()); 255 if (entityIds.size() > 0) { 256 myResourceTagDao.findByResourceIds(entityIds); 257 preFetchIndexes(entityIds, "tags", "myTags", null); 258 } 259 260 entityIds = loadedResourceTableEntries.stream() 261 .map(ResourceTable::getId) 262 .collect(Collectors.toList()); 263 if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.ENABLED) { 264 preFetchIndexes(entityIds, "searchParamPresence", "mySearchParamPresents", null); 265 } 266 } 267 268 new QueryChunker<ResourceTable>() 269 .chunk(loadedResourceTableEntries, SearchBuilder.getMaximumPageSize() / 2, entries -> { 270 Map<Long, ResourceTable> entities = 271 entries.stream().collect(Collectors.toMap(ResourceTable::getId, t -> t)); 272 273 CriteriaBuilder b = myEntityManager.getCriteriaBuilder(); 274 CriteriaQuery<ResourceHistoryTable> q = b.createQuery(ResourceHistoryTable.class); 275 Root<ResourceHistoryTable> from = q.from(ResourceHistoryTable.class); 276 277 from.fetch("myProvenance", JoinType.LEFT); 278 279 List<Predicate> orPredicates = new ArrayList<>(); 280 for (ResourceTable next : entries) { 281 Predicate resId = b.equal(from.get("myResourceId"), next.getId()); 282 Predicate resVer = b.equal(from.get("myResourceVersion"), next.getVersion()); 283 orPredicates.add(b.and(resId, resVer)); 284 } 285 q.where(b.or(orPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 286 List<ResourceHistoryTable> resultList = 287 myEntityManager.createQuery(q).getResultList(); 288 for (ResourceHistoryTable next : resultList) { 289 ResourceTable nextEntity = entities.get(next.getResourceId()); 290 if (nextEntity != null) { 291 nextEntity.setCurrentVersionEntity(next); 292 } 293 } 294 }); 295 } 296 }); 297 } 298 299 private void preFetchIndexes( 300 List<Long> theIds, 301 String typeDesc, 302 String fieldName, 303 @Nullable List<ResourceTable> theEntityListToPopulate) { 304 new QueryChunker<Long>().chunk(theIds, ids -> { 305 TypedQuery<ResourceTable> query = myEntityManager.createQuery( 306 "FROM ResourceTable r LEFT JOIN FETCH r." + fieldName + " WHERE r.myId IN ( :IDS )", 307 ResourceTable.class); 308 query.setParameter("IDS", ids); 309 List<ResourceTable> indexFetchOutcome = query.getResultList(); 310 ourLog.debug("Pre-fetched {} {}} indexes", indexFetchOutcome.size(), typeDesc); 311 if (theEntityListToPopulate != null) { 312 theEntityListToPopulate.addAll(indexFetchOutcome); 313 } 314 }); 315 } 316 317 @Nullable 318 @Override 319 protected String getResourceName() { 320 return null; 321 } 322 323 @Override 324 protected IInterceptorBroadcaster getInterceptorBroadcaster() { 325 return myInterceptorBroadcaster; 326 } 327 328 @Override 329 protected JpaStorageSettings getStorageSettings() { 330 return myStorageSettings; 331 } 332 333 @Override 334 public FhirContext getContext() { 335 return myFhirContext; 336 } 337 338 @VisibleForTesting 339 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 340 myStorageSettings = theStorageSettings; 341 } 342}