001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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<Long> pids = theResolvedIds.stream().map(t -> ((JpaPid) t).getId()).collect(Collectors.toList());
194
195                new QueryChunker<Long>().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.size() >= 2) {
209                                List<ResourceTable> entityChunk = prefetchResourceTableHistoryAndProvenance(idChunk);
210
211                                if (thePreFetchIndexes) {
212
213                                        prefetchByField("string", "myParamsString", ResourceTable::isParamsStringPopulated, entityChunk);
214                                        prefetchByField("token", "myParamsToken", ResourceTable::isParamsTokenPopulated, entityChunk);
215                                        prefetchByField("date", "myParamsDate", ResourceTable::isParamsDatePopulated, entityChunk);
216                                        prefetchByField(
217                                                        "quantity", "myParamsQuantity", ResourceTable::isParamsQuantityPopulated, entityChunk);
218                                        prefetchByField("resourceLinks", "myResourceLinks", ResourceTable::isHasLinks, entityChunk);
219
220                                        prefetchByJoinClause(
221                                                        "tags",
222                                                        // fetch the TagResources and the actual TagDefinitions
223                                                        "LEFT JOIN FETCH r.myTags t LEFT JOIN FETCH t.myTag",
224                                                        BaseHasResource::isHasTags,
225                                                        entityChunk);
226
227                                        if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.ENABLED) {
228                                                prefetchByField("searchParamPresence", "mySearchParamPresents", r -> true, entityChunk);
229                                        }
230                                }
231                        }
232                });
233        }
234
235        @Nonnull
236        private List<ResourceTable> prefetchResourceTableHistoryAndProvenance(List<Long> idChunk) {
237                assert idChunk.size() < SearchConstants.MAX_PAGE_SIZE : "assume pre-chunked";
238
239                Query query = myEntityManager.createQuery("select r, h "
240                                + " FROM ResourceTable r "
241                                + " LEFT JOIN fetch ResourceHistoryTable h "
242                                + "      on r.myVersion = h.myResourceVersion and r.id = h.myResourceId "
243                                + " left join fetch h.myProvenance "
244                                + " WHERE r.myId IN ( :IDS ) ");
245                query.setParameter("IDS", idChunk);
246
247                @SuppressWarnings("unchecked")
248                Stream<Object[]> queryResultStream = query.getResultStream();
249                return queryResultStream
250                                .map(nextPair -> {
251                                        // Store the matching ResourceHistoryTable in the transient slot on ResourceTable
252                                        ResourceTable result = (ResourceTable) nextPair[0];
253                                        ResourceHistoryTable currentVersion = (ResourceHistoryTable) nextPair[1];
254                                        result.setCurrentVersionEntity(currentVersion);
255                                        return result;
256                                })
257                                .collect(Collectors.toList());
258        }
259
260        /**
261         * Prefetch a join field for the active subset of some ResourceTable entities.
262         * Convenience wrapper around prefetchByJoinClause() for simple fields.
263         *
264         * @param theDescription             for logging
265         * @param theJpaFieldName            the join field from ResourceTable
266         * @param theEntityPredicate         select which ResourceTable entities need this join
267         * @param theEntities                the ResourceTable entities to consider
268         */
269        private void prefetchByField(
270                        String theDescription,
271                        String theJpaFieldName,
272                        Predicate<ResourceTable> theEntityPredicate,
273                        List<ResourceTable> theEntities) {
274
275                String joinClause = "LEFT JOIN FETCH r." + theJpaFieldName;
276
277                prefetchByJoinClause(theDescription, joinClause, theEntityPredicate, theEntities);
278        }
279
280        /**
281         * Prefetch a join field for the active subset of some ResourceTable entities.
282         *
283         * @param theDescription             for logging
284         * @param theJoinClause              the JPA join expression to add to `ResourceTable r`
285         * @param theEntityPredicate         selects which entities need this prefetch
286         * @param theEntities                the ResourceTable entities to consider
287         */
288        private void prefetchByJoinClause(
289                        String theDescription,
290                        String theJoinClause,
291                        Predicate<ResourceTable> theEntityPredicate,
292                        List<ResourceTable> theEntities) {
293
294                // Which entities need this prefetch?
295                List<Long> idSubset = theEntities.stream()
296                                .filter(theEntityPredicate)
297                                .map(ResourceTable::getId)
298                                .collect(Collectors.toList());
299
300                if (idSubset.isEmpty()) {
301                        // nothing to do
302                        return;
303                }
304
305                String jqlQuery = "FROM ResourceTable r " + theJoinClause + " WHERE r.myId IN ( :IDS )";
306
307                TypedQuery<ResourceTable> query = myEntityManager.createQuery(jqlQuery, ResourceTable.class);
308                query.setParameter("IDS", idSubset);
309                List<ResourceTable> indexFetchOutcome = query.getResultList();
310
311                ourLog.debug("Pre-fetched {} {} indexes", indexFetchOutcome.size(), theDescription);
312        }
313
314        @Nullable
315        @Override
316        protected String getResourceName() {
317                return null;
318        }
319
320        @Override
321        protected IInterceptorBroadcaster getInterceptorBroadcaster() {
322                return myInterceptorBroadcaster;
323        }
324
325        @Override
326        protected JpaStorageSettings getStorageSettings() {
327                return myStorageSettings;
328        }
329
330        @Override
331        public FhirContext getContext() {
332                return myFhirContext;
333        }
334
335        @VisibleForTesting
336        public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) {
337                myStorageSettings = theStorageSettings;
338        }
339}