Skip to content

Commit bc209b1

Browse files
schaudergregturn
authored andcommitted
DATAJDBC-164 - Add support for basic @query annotation.
1 parent 2163a26 commit bc209b1

24 files changed

+442
-53
lines changed

README.adoc

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ But the default mapping itself will stay limited.
111111
If you want highly customizable mappings which support almost everything one can imagine you will probably be much happier with (Spring Data) JPA.
112112
Which is a very powerful and mature technology.
113113

114+
=== Query annotation
115+
116+
You can annotate a query method with `@Query` to specify a SQL statement to be used for that method.
117+
You can bind method arguments using named parameters in the SQL statement like in the following example:
118+
119+
[source,java]
120+
----
121+
@Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower")
122+
List<DummyEntity> findByNameRange(@Param("lower") String lower, @Param("upper") String upper);
123+
----
124+
125+
If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations.
126+
114127
=== Id generation
115128

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

255268
== Features planned for the not to far future
256269

257-
=== Query annotation
270+
=== Advance query annotation support
258271

259-
Just annotate a method with a SQL query to use this whenever the method gets called.
272+
* customizable `RowMapper`
273+
* projections
274+
* modifying queries
275+
* SpEL expressions
260276

261277
=== MyBatis per method support
262278

@@ -274,7 +290,7 @@ Currently you will need to build it locally.
274290
== Getting Help
275291

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

279295
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].
280296

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)