Skip to content

Commit 547a1c1

Browse files
committed
DATAJDBC-164 - Added support of basic @query annotation.
1 parent 2c2c697 commit 547a1c1

24 files changed

+442
-53
lines changed

README.adoc

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ public void someMethod() {
5757
}
5858
----
5959

60+
=== Query annotation
61+
62+
You can annotate a query method with `@Query` to specify a SQL statement to be used for that method.
63+
You can bind method arguments using named parameters in the SQL statement like in the following example:
64+
65+
[source,java]
66+
----
67+
@Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower")
68+
List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper") String upper);
69+
----
70+
71+
If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations.
72+
6073
=== Id generation
6174

6275
Spring Data JDBC uses the id to identify entities but also to determine if an entity is new or already existing in the database.
@@ -200,9 +213,12 @@ Note that the type used for prefixing the statement name is the name of the aggr
200213

201214
== Features planned for the not to far future
202215

203-
=== Query annotation
216+
=== Advance query annotation support
204217

205-
Just annotate a method with a SQL query to use this whenever the method gets called.
218+
* customizable `RowMapper`
219+
* projections
220+
* modifying queries
221+
* SpEL expressions
206222

207223
=== MyBatis per method support
208224

@@ -218,7 +234,7 @@ Currently you will need to build it locally.
218234
== Getting Help
219235

220236
Right now the best source of information is the source code in this repository.
221-
Especially the integration tests (type `t` and then `IntegrationTests.java`)
237+
Especially the integration tests (When you are reading this on github type `t` and then `IntegrationTests.java`)
222238

223239
We are keeping an eye on the (soon to be created) https://stackoverflow.com/questions/tagged/spring-data-jdbc[spring-data-jdbc tag on stackoverflow].
224240

src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
import org.springframework.dao.EmptyResultDataAccessException;
2525
import org.springframework.dao.InvalidDataAccessApiUsageException;
2626
import org.springframework.dao.NonTransientDataAccessException;
27-
import org.springframework.data.jdbc.mapping.model.*;
27+
import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation;
28+
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
29+
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
30+
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
31+
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
2832
import org.springframework.data.jdbc.support.JdbcUtil;
2933
import org.springframework.data.mapping.PropertyHandler;
3034
import org.springframework.data.mapping.PropertyPath;
@@ -278,7 +282,7 @@ private <S> Optional<Object> getIdFromHolder(KeyHolder holder, JdbcPersistentEnt
278282
}
279283
}
280284

281-
private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
285+
public <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
282286
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context.getConversions(), context, accessStrategy);
283287
}
284288

src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
* @author Oliver Gierke
4444
* @since 2.0
4545
*/
46-
class EntityRowMapper<T> implements RowMapper<T> {
46+
public class EntityRowMapper<T> implements RowMapper<T> {
4747

4848
private final JdbcPersistentEntity<T> entity;
4949
private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator();

src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.data.mapping.model.Property;
3838
import org.springframework.data.mapping.model.SimpleTypeHolder;
3939
import org.springframework.data.util.TypeInformation;
40+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4041

4142
/**
4243
* {@link MappingContext} implementation for JDBC.
@@ -53,20 +54,22 @@ public class JdbcMappingContext extends AbstractMappingContext<JdbcPersistentEnt
5354
Temporal.class //
5455
));
5556

56-
private final @Getter NamingStrategy namingStrategy;
57+
@Getter private final NamingStrategy namingStrategy;
58+
@Getter private final NamedParameterJdbcOperations template;
5759
private GenericConversionService conversions = getDefaultConversionService();
5860

59-
public JdbcMappingContext(NamingStrategy namingStrategy, ConversionCustomizer customizer) {
61+
public JdbcMappingContext(NamingStrategy namingStrategy, NamedParameterJdbcOperations template,
62+
ConversionCustomizer customizer) {
6063

6164
this.namingStrategy = namingStrategy;
65+
this.template = template;
6266

6367
customizer.customize(conversions);
64-
6568
setSimpleTypeHolder(new SimpleTypeHolder(CUSTOM_SIMPLE_TYPES, true));
6669
}
6770

68-
public JdbcMappingContext() {
69-
this(new DefaultNamingStrategy(), __ -> {});
71+
public JdbcMappingContext(NamedParameterJdbcOperations template) {
72+
this(new DefaultNamingStrategy(), template, __ -> {});
7073
}
7174

7275
public List<PropertyPath> referencedEntities(Class<?> rootType, PropertyPath path) {
@@ -126,5 +129,4 @@ private static GenericConversionService getDefaultConversionService() {
126129

127130
return conversionService;
128131
}
129-
130132
}

src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
2424
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
2525
import org.springframework.data.jdbc.mapping.model.NamingStrategy;
26+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
2627

2728
/**
2829
* Beans that must be registered for Spring Data JDBC to work.
@@ -33,11 +34,10 @@
3334
public class JdbcConfiguration {
3435

3536
@Bean
36-
JdbcMappingContext jdbcMappingContext(Optional<NamingStrategy> namingStrategy,
37+
JdbcMappingContext jdbcMappingContext(NamedParameterJdbcTemplate template, Optional<NamingStrategy> namingStrategy,
3738
Optional<ConversionCustomizer> conversionCustomizer) {
3839

3940
return new JdbcMappingContext(
40-
namingStrategy.orElse(new DefaultNamingStrategy()),
41-
conversionCustomizer.orElse(conversionService -> {}));
41+
namingStrategy.orElse(new DefaultNamingStrategy()), template, conversionCustomizer.orElse(conversionService -> {}));
4242
}
4343
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.jdbc.repository.query;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.data.annotation.QueryAnnotation;
25+
26+
/**
27+
* Annotation to provide SQL statements that will get used for executing the method.
28+
*
29+
* The SQL statement may contain named parameters as supported by {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}.
30+
* Those parameters will get bound to the arguments of the annotated method.
31+
*
32+
* @author Jens Schauder
33+
*/
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target(ElementType.METHOD)
36+
@QueryAnnotation
37+
@Documented
38+
public @interface Query {
39+
String value();
40+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.jdbc.repository.support;
17+
18+
import java.lang.reflect.Method;
19+
20+
import org.springframework.data.jdbc.core.DataAccessStrategy;
21+
import org.springframework.data.jdbc.core.EntityRowMapper;
22+
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
23+
import org.springframework.data.projection.ProjectionFactory;
24+
import org.springframework.data.repository.core.NamedQueries;
25+
import org.springframework.data.repository.core.RepositoryMetadata;
26+
import org.springframework.data.repository.query.EvaluationContextProvider;
27+
import org.springframework.data.repository.query.QueryLookupStrategy;
28+
import org.springframework.data.repository.query.RepositoryQuery;
29+
import org.springframework.jdbc.core.RowMapper;
30+
31+
/**
32+
* {@link QueryLookupStrategy} for JDBC repositories. Currently only supports annotated queries.
33+
*
34+
* @author Jens Schauder
35+
*/
36+
class JdbcQueryLookupStrategy implements QueryLookupStrategy {
37+
38+
private final JdbcMappingContext context;
39+
private final DataAccessStrategy accessStrategy;
40+
41+
JdbcQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider, JdbcMappingContext context,
42+
DataAccessStrategy accessStrategy) {
43+
44+
this.context = context;
45+
this.accessStrategy = accessStrategy;
46+
}
47+
48+
@Override
49+
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata,
50+
ProjectionFactory projectionFactory, NamedQueries namedQueries) {
51+
52+
JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory);
53+
Class<?> domainType = queryMethod.getReturnedObjectType();
54+
RowMapper<?> rowMapper = new EntityRowMapper<>(context.getRequiredPersistentEntity(domainType),
55+
context.getConversions(), context, accessStrategy);
56+
57+
return new JdbcRepositoryQuery(queryMethod, context, rowMapper);
58+
}
59+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.jdbc.repository.support;
17+
18+
import java.lang.reflect.Method;
19+
20+
import org.springframework.core.annotation.AnnotatedElementUtils;
21+
import org.springframework.data.jdbc.repository.query.Query;
22+
import org.springframework.data.projection.ProjectionFactory;
23+
import org.springframework.data.repository.core.RepositoryMetadata;
24+
import org.springframework.data.repository.query.QueryMethod;
25+
26+
/**
27+
* {@link QueryMethod} implementation that implements a method by executing the query from a {@link Query} annotation on
28+
* that method.
29+
*
30+
* Binds method arguments to named parameters in the SQL statement.
31+
*
32+
* @author Jens Schauder
33+
*/
34+
public class JdbcQueryMethod extends QueryMethod {
35+
36+
private final Method method;
37+
38+
public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
39+
super(method, metadata, factory);
40+
41+
this.method = method;
42+
}
43+
44+
public String getAnnotatedQuery() {
45+
46+
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
47+
48+
return queryAnnotation == null ? null : queryAnnotation.value();
49+
}
50+
}

src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.support;
1717

18+
import java.util.Optional;
19+
1820
import org.springframework.context.ApplicationEventPublisher;
1921
import org.springframework.data.jdbc.core.DataAccessStrategy;
2022
import org.springframework.data.jdbc.core.JdbcEntityTemplate;
@@ -25,8 +27,12 @@
2527
import org.springframework.data.repository.core.RepositoryInformation;
2628
import org.springframework.data.repository.core.RepositoryMetadata;
2729
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
30+
import org.springframework.data.repository.query.EvaluationContextProvider;
31+
import org.springframework.data.repository.query.QueryLookupStrategy;
2832

2933
/**
34+
* Creates repository implementation based on JDBC.
35+
*
3036
* @author Jens Schauder
3137
* @author Greg Turnquist
3238
* @since 2.0
@@ -67,4 +73,17 @@ protected Class<?> getRepositoryBaseClass(RepositoryMetadata repositoryMetadata)
6773
return SimpleJdbcRepository.class;
6874
}
6975

76+
@Override
77+
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(QueryLookupStrategy.Key key,
78+
EvaluationContextProvider evaluationContextProvider) {
79+
80+
if (key != null //
81+
&& key != QueryLookupStrategy.Key.USE_DECLARED_QUERY //
82+
&& key != QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND //
83+
) {
84+
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
85+
}
86+
87+
return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy));
88+
}
7089
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.jdbc.repository.support;
17+
18+
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
19+
import org.springframework.data.repository.query.RepositoryQuery;
20+
import org.springframework.jdbc.core.RowMapper;
21+
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
22+
23+
/**
24+
* A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the
25+
* method.
26+
*
27+
* @author Jens Schauder
28+
*/
29+
class JdbcRepositoryQuery implements RepositoryQuery {
30+
31+
private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters. Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters.";
32+
33+
private final JdbcQueryMethod queryMethod;
34+
private final JdbcMappingContext context;
35+
private final RowMapper<?> rowMapper;
36+
37+
JdbcRepositoryQuery(JdbcQueryMethod queryMethod, JdbcMappingContext context, RowMapper rowMapper) {
38+
39+
this.queryMethod = queryMethod;
40+
this.context = context;
41+
42+
this.rowMapper = rowMapper;
43+
}
44+
45+
@Override
46+
public Object execute(Object[] objects) {
47+
48+
String query = queryMethod.getAnnotatedQuery();
49+
50+
MapSqlParameterSource parameters = new MapSqlParameterSource();
51+
queryMethod.getParameters().getBindableParameters().forEach(p -> {
52+
53+
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
54+
parameters.addValue(parameterName, objects[p.getIndex()]);
55+
});
56+
57+
return context.getTemplate().query(query, parameters, rowMapper);
58+
}
59+
60+
@Override
61+
public JdbcQueryMethod getQueryMethod() {
62+
return queryMethod;
63+
}
64+
}

0 commit comments

Comments
 (0)