
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.search.builder.sql; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 024import ca.uhn.fhir.jpa.search.builder.ISearchQueryExecutor; 025import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; 026import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 027import ca.uhn.fhir.util.IoUtil; 028import org.apache.commons.lang3.Validate; 029import org.hibernate.CacheMode; 030import org.hibernate.ScrollMode; 031import org.hibernate.ScrollableResults; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import java.util.Arrays; 036import javax.persistence.EntityManager; 037import javax.persistence.FlushModeType; 038import javax.persistence.PersistenceContext; 039import javax.persistence.PersistenceContextType; 040import javax.persistence.Query; 041 042public class SearchQueryExecutor implements ISearchQueryExecutor { 043 044 private static final Long NO_MORE = -1L; 045 private static final SearchQueryExecutor NO_VALUE_EXECUTOR = new SearchQueryExecutor(); 046 private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; 047 private static final Logger ourLog = LoggerFactory.getLogger(SearchQueryExecutor.class); 048 private final GeneratedSql myGeneratedSql; 049 050 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 051 private EntityManager myEntityManager; 052 053 private boolean myQueryInitialized; 054 private ScrollableResultsIterator<Number> myResultSet; 055 private Long myNext; 056 057 /** 058 * Constructor 059 */ 060 public SearchQueryExecutor(GeneratedSql theGeneratedSql, Integer theMaxResultsToFetch) { 061 Validate.notNull(theGeneratedSql, "theGeneratedSql must not be null"); 062 myGeneratedSql = theGeneratedSql; 063 myQueryInitialized = false; 064 } 065 066 /** 067 * Internal constructor for empty executor 068 */ 069 private SearchQueryExecutor() { 070 assert NO_MORE != null; 071 072 myGeneratedSql = null; 073 myNext = NO_MORE; 074 } 075 076 @Override 077 public void close() { 078 IoUtil.closeQuietly(myResultSet); 079 } 080 081 @Override 082 public boolean hasNext() { 083 fetchNext(); 084 return !NO_MORE.equals(myNext); 085 } 086 087 @Override 088 public Long next() { 089 fetchNext(); 090 Validate.isTrue(hasNext(), "Can not call next() right now, no data remains"); 091 Long next = myNext; 092 myNext = null; 093 return next; 094 } 095 096 private void fetchNext() { 097 if (myNext == null) { 098 String sql = myGeneratedSql.getSql(); 099 Object[] args = myGeneratedSql.getBindVariables().toArray(EMPTY_OBJECT_ARRAY); 100 101 try { 102 if (!myQueryInitialized) { 103 104 /* 105 * Note that we use the spring managed connection, and the expectation is that a transaction that 106 * is managed by Spring has been started before this method is called. 107 */ 108 HapiTransactionService.requireTransaction(); 109 110 Query nativeQuery = myEntityManager.createNativeQuery(sql); 111 org.hibernate.query.Query<?> hibernateQuery = (org.hibernate.query.Query<?>) nativeQuery; 112 for (int i = 1; i <= args.length; i++) { 113 hibernateQuery.setParameter(i, args[i - 1]); 114 } 115 116 ourLog.trace("About to execute SQL: {}. Parameters: {}", sql, Arrays.toString(args)); 117 118 /* 119 * These settings help to ensure that we use a search cursor 120 * as opposed to loading all search results into memory 121 */ 122 hibernateQuery.setFetchSize(500000); 123 hibernateQuery.setCacheable(false); 124 hibernateQuery.setCacheMode(CacheMode.IGNORE); 125 hibernateQuery.setReadOnly(true); 126 127 // This tells hibernate not to flush when we call scroll(), but rather to wait until the transaction 128 // commits and 129 // only flush then. We need to do this so that any exceptions that happen in the transaction happen 130 // when 131 // we try to commit the transaction, and not here. 132 // See the test called testTransaction_multiThreaded (in FhirResourceDaoR4ConcurrentWriteTest) which 133 // triggers 134 // the following exception if we don't set this flush mode: 135 // java.util.concurrent.ExecutionException: 136 // org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back 137 // because it has been marked as rollback-only 138 hibernateQuery.setFlushMode(FlushModeType.COMMIT); 139 ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); 140 myResultSet = new ScrollableResultsIterator<>(scrollableResults); 141 myQueryInitialized = true; 142 } 143 144 if (myResultSet == null || !myResultSet.hasNext()) { 145 myNext = NO_MORE; 146 } else { 147 Number next = myResultSet.next(); 148 myNext = next.longValue(); 149 } 150 151 } catch (Exception e) { 152 ourLog.error("Failed to create or execute SQL query", e); 153 close(); 154 throw new InternalErrorException(Msg.code(1262) + e, e); 155 } 156 } 157 } 158 159 public static SearchQueryExecutor emptyExecutor() { 160 return NO_VALUE_EXECUTOR; 161 } 162}