1 /*
2  * Copyright 2008-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.jpa.repository.query;
17
18 import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;
19
20 import javax.persistence.EntityManager;
21 import javax.persistence.Query;
22 import javax.persistence.Tuple;
23 import javax.persistence.TypedQuery;
24
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 import org.springframework.data.jpa.provider.QueryExtractor;
29 import org.springframework.data.repository.query.Parameters;
30 import org.springframework.data.repository.query.QueryCreationException;
31 import org.springframework.data.repository.query.RepositoryQuery;
32 import org.springframework.data.repository.query.ResultProcessor;
33 import org.springframework.data.repository.query.ReturnedType;
34 import org.springframework.lang.Nullable;
35
36 /**
37  * Implementation of {@link RepositoryQuery} based on {@link javax.persistence.NamedQuery}s.
38  *
39  * @author Oliver Gierke
40  * @author Thomas Darimont
41  * @author Mark Paluch
42  */

43 final class NamedQuery extends AbstractJpaQuery {
44
45     private static final String CANNOT_EXTRACT_QUERY = "Your persistence provider does not support extracting the JPQL query from a "
46             + "named query thus you can't use Pageable inside your query method. Make sure you "
47             + "have a JpaDialect configured at your EntityManagerFactoryBean as this affects "
48             + "discovering the concrete persistence provider.";
49
50     private static final Logger LOG = LoggerFactory.getLogger(NamedQuery.class);
51
52     private final String queryName;
53     private final String countQueryName;
54     private final @Nullable String countProjection;
55     private final QueryExtractor extractor;
56     private final boolean namedCountQueryIsPresent;
57     private final DeclaredQuery declaredQuery;
58     private final QueryParameterSetter.QueryMetadataCache metadataCache;
59
60     /**
61      * Creates a new {@link NamedQuery}.
62      */

63     private NamedQuery(JpaQueryMethod method, EntityManager em) {
64
65         super(method, em);
66
67         this.queryName = method.getNamedQueryName();
68         this.countQueryName = method.getNamedCountQueryName();
69         this.extractor = method.getQueryExtractor();
70         this.countProjection = method.getCountQueryProjection();
71
72         Parameters<?, ?> parameters = method.getParameters();
73
74         if (parameters.hasSortParameter()) {
75             throw new IllegalStateException(String.format("Finder method %s is backed " + "by a NamedQuery and must "
76                     + "not contain a sort parameter as we cannot modify the query! Use @Query instead!", method));
77         }
78
79         this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName);
80
81         Query query = em.createNamedQuery(queryName);
82         String queryString = extractor.extractQueryString(query);
83
84         this.declaredQuery = DeclaredQuery.of(queryString);
85
86         boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasPageableParameter();
87         boolean cantExtractQuery = !this.extractor.canExtractQuery();
88
89         if (weNeedToCreateCountQuery && cantExtractQuery) {
90             throw QueryCreationException.create(method, CANNOT_EXTRACT_QUERY);
91         }
92
93         if (parameters.hasPageableParameter()) {
94             LOG.warn("Finder method {} is backed by a NamedQuery" + " but contains a Pageable parameter! Sorting delivered "
95                     + "via this Pageable will not be applied!", method);
96         }
97
98         this.metadataCache = new QueryParameterSetter.QueryMetadataCache();
99     }
100
101     /**
102      * Returns whether the named query with the given name exists.
103      *
104      * @param em must not be {@literal null}.
105      * @param queryName must not be {@literal null}.
106      * @return
107      */

108     private static boolean hasNamedQuery(EntityManager em, String queryName) {
109
110         /*
111          * See DATAJPA-617, we have to use a dedicated em for the lookups to avoid a
112          * potential rollback of the running tx.
113          */

114         EntityManager lookupEm = em.getEntityManagerFactory().createEntityManager();
115
116         try {
117             lookupEm.createNamedQuery(queryName);
118             return true;
119         } catch (IllegalArgumentException e) {
120             LOG.debug("Did not find named query {}", queryName);
121             return false;
122         } finally {
123             lookupEm.close();
124         }
125     }
126
127     /**
128      * Looks up a named query for the given {@link org.springframework.data.repository.query.QueryMethod}.
129      *
130      * @param method must not be {@literal null}.
131      * @param em must not be {@literal null}.
132      * @return
133      */

134     @Nullable
135     public static RepositoryQuery lookupFrom(JpaQueryMethod method, EntityManager em) {
136
137         final String queryName = method.getNamedQueryName();
138
139         LOG.debug("Looking up named query {}", queryName);
140
141         if (!hasNamedQuery(em, queryName)) {
142             return null;
143         }
144
145         try {
146             RepositoryQuery query = new NamedQuery(method, em);
147             LOG.debug("Found named query {}!", queryName);
148             return query;
149         } catch (IllegalArgumentException e) {
150             return null;
151         }
152     }
153
154     /*
155      * (non-Javadoc)
156      * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateQuery(JpaParametersParameterAccessor)
157      */

158     @Override
159     protected Query doCreateQuery(JpaParametersParameterAccessor accessor) {
160
161         EntityManager em = getEntityManager();
162
163         JpaQueryMethod queryMethod = getQueryMethod();
164         ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(accessor);
165
166         Class<?> typeToRead = getTypeToRead(processor.getReturnedType());
167
168         Query query = typeToRead == null //
169                 ? em.createNamedQuery(queryName) //
170                 : em.createNamedQuery(queryName, typeToRead);
171
172         QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryName, query);
173
174         return parameterBinder.get().bindAndPrepare(query, metadata, accessor);
175     }
176
177     /*
178      * (non-Javadoc)
179      * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#doCreateCountQuery(JpaParametersParameterAccessor)
180      */

181     @Override
182     protected TypedQuery<Long> doCreateCountQuery(JpaParametersParameterAccessor accessor) {
183
184         EntityManager em = getEntityManager();
185         TypedQuery<Long> countQuery;
186
187         String cacheKey;
188         if (namedCountQueryIsPresent) {
189             cacheKey = countQueryName;
190             countQuery = em.createNamedQuery(countQueryName, Long.class);
191
192         } else {
193
194             String countQueryString = declaredQuery.deriveCountQuery(null, countProjection).getQueryString();
195             cacheKey = countQueryString;
196             countQuery = em.createQuery(countQueryString, Long.class);
197         }
198
199         QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(cacheKey, countQuery);
200
201         return parameterBinder.get().bind(countQuery, metadata, accessor);
202     }
203
204     /*
205      * (non-Javadoc)
206      * @see org.springframework.data.jpa.repository.query.AbstractJpaQuery#getTypeToRead()
207      */

208     @Override
209     protected Class<?> getTypeToRead(ReturnedType returnedType) {
210
211         if (getQueryMethod().isNativeQuery()) {
212
213             Class<?> type = returnedType.getReturnedType();
214             Class<?> domainType = returnedType.getDomainType();
215
216             // Domain or subtype -> use return type
217             if (domainType.isAssignableFrom(type)) {
218                 return type;
219             }
220
221             // Domain type supertype -> use domain type
222             if (type.isAssignableFrom(domainType)) {
223                 return domainType;
224             }
225
226             // Tuples for projection interfaces or explicit SQL mappings for everything else
227             return type.isInterface() ? Tuple.class : null;
228         }
229
230         return declaredQuery.hasConstructorExpression() //
231                 ? null //
232                 : super.getTypeToRead(returnedType);
233     }
234 }
235