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}