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}