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