Skip to content

Commit b02fdd9

Browse files
committed
DATAJDBC-318 - Fixed the generation of the select list.
The select list must include columns for 1:1 relationships. The implementation is copied from SqlGenerator and will be unified in the near future. Original pull request: #209.
1 parent 8b0879f commit b02fdd9

File tree

7 files changed

+307
-109
lines changed

7 files changed

+307
-109
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java

Lines changed: 149 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.query;
1717

18-
import java.util.Collections;
18+
import lombok.Value;
19+
20+
import java.util.ArrayList;
1921
import java.util.List;
2022

2123
import org.springframework.data.domain.Pageable;
@@ -26,13 +28,15 @@
2628
import org.springframework.data.relational.core.dialect.Dialect;
2729
import org.springframework.data.relational.core.dialect.RenderContextFactory;
2830
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
31+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
2932
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3033
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
3134
import org.springframework.data.relational.core.query.Criteria;
35+
import org.springframework.data.relational.core.sql.Column;
3236
import org.springframework.data.relational.core.sql.Expression;
3337
import org.springframework.data.relational.core.sql.Select;
3438
import org.springframework.data.relational.core.sql.SelectBuilder;
35-
import org.springframework.data.relational.core.sql.SqlIdentifier;
39+
import org.springframework.data.relational.core.sql.StatementBuilder;
3640
import org.springframework.data.relational.core.sql.Table;
3741
import org.springframework.data.relational.core.sql.render.SqlRenderer;
3842
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
@@ -42,16 +46,19 @@
4246
import org.springframework.data.repository.query.parser.Part;
4347
import org.springframework.data.repository.query.parser.PartTree;
4448
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
49+
import org.springframework.lang.Nullable;
4550
import org.springframework.util.Assert;
4651

4752
/**
4853
* Implementation of {@link RelationalQueryCreator} that creates {@link ParametrizedQuery} from a {@link PartTree}.
4954
*
5055
* @author Mark Paluch
56+
* @author Jens Schauder
5157
* @since 2.0
5258
*/
5359
class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
5460

61+
private final RelationalMappingContext context;
5562
private final PartTree tree;
5663
private final RelationalParameterAccessor accessor;
5764
private final QueryMapper queryMapper;
@@ -62,20 +69,22 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
6269
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
6370
* {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}.
6471
*
72+
* @param context
6573
* @param tree part tree, must not be {@literal null}.
6674
* @param converter must not be {@literal null}.
6775
* @param dialect must not be {@literal null}.
6876
* @param entityMetadata relational entity metadata, must not be {@literal null}.
6977
* @param accessor parameter metadata provider, must not be {@literal null}.
7078
*/
71-
JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect, RelationalEntityMetadata<?> entityMetadata,
72-
RelationalParameterAccessor accessor) {
79+
JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
80+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor) {
7381
super(tree, accessor);
7482

7583
Assert.notNull(converter, "JdbcConverter must not be null");
7684
Assert.notNull(dialect, "Dialect must not be null");
7785
Assert.notNull(entityMetadata, "Relational entity metadata must not be null");
7886

87+
this.context = context;
7988
this.tree = tree;
8089
this.accessor = accessor;
8190

@@ -91,7 +100,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
91100
* @param tree
92101
* @param parameters
93102
*/
94-
public static void validate(PartTree tree, Parameters<?, ?> parameters,
103+
static void validate(PartTree tree, Parameters<?, ?> parameters,
95104
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context) {
96105

97106
RelationalQueryCreator.validate(tree, parameters);
@@ -141,53 +150,167 @@ private static void validateProperty(PersistentPropertyPathExtension path) {
141150
* @return instance of {@link ParametrizedQuery}
142151
*/
143152
@Override
144-
protected ParametrizedQuery complete(Criteria criteria, Sort sort) {
153+
protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {
145154

146155
RelationalPersistentEntity<?> entity = entityMetadata.getTableEntity();
147156
Table table = Table.create(entityMetadata.getTableName());
148157
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
149158

150-
List<? extends Expression> columns = table.columns(getSelectProjection());
151-
if (columns.isEmpty()) {
152-
columns = Collections.singletonList(table.asterisk());
153-
}
159+
SelectBuilder.SelectLimitOffset limitOffsetBuilder = createSelectClause(entity, table);
160+
SelectBuilder.SelectWhere whereBuilder = applyLimitAndOffset(limitOffsetBuilder);
161+
SelectBuilder.SelectOrdered selectOrderBuilder = applyCriteria(criteria, entity, table, parameterSource,
162+
whereBuilder);
163+
selectOrderBuilder = applyOrderBy(sort, entity, table, selectOrderBuilder);
164+
165+
Select select = selectOrderBuilder.build();
166+
167+
String sql = SqlRenderer.create(renderContextFactory.createRenderContext()).render(select);
168+
169+
return new ParametrizedQuery(sql, parameterSource);
170+
}
171+
172+
private SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
173+
SelectBuilder.SelectOrdered selectOrdered) {
174+
175+
return sort.isSorted() ? //
176+
selectOrdered.orderBy(queryMapper.getMappedSort(table, sort, entity)) //
177+
: selectOrdered;
178+
}
179+
180+
private SelectBuilder.SelectOrdered applyCriteria(@Nullable Criteria criteria, RelationalPersistentEntity<?> entity,
181+
Table table, MapSqlParameterSource parameterSource, SelectBuilder.SelectWhere whereBuilder) {
182+
183+
return criteria != null //
184+
? whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)) //
185+
: whereBuilder;
186+
}
154187

155-
SelectBuilder.SelectFromAndJoin builder = Select.builder().select(columns).from(table);
188+
private SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
156189

157190
if (tree.isExistsProjection()) {
158-
builder = builder.limit(1);
191+
limitOffsetBuilder = limitOffsetBuilder.limit(1);
159192
} else if (tree.isLimiting()) {
160-
builder = builder.limit(tree.getMaxResults());
193+
limitOffsetBuilder = limitOffsetBuilder.limit(tree.getMaxResults());
161194
}
162195

163196
Pageable pageable = accessor.getPageable();
164197
if (pageable.isPaged()) {
165-
builder = builder.limit(pageable.getPageSize()).offset(pageable.getOffset());
198+
limitOffsetBuilder = limitOffsetBuilder.limit(pageable.getPageSize()).offset(pageable.getOffset());
166199
}
167200

168-
if (criteria != null) {
169-
builder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity));
201+
return (SelectBuilder.SelectWhere) limitOffsetBuilder;
202+
}
203+
204+
private SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
205+
206+
SelectBuilder.SelectJoin builder;
207+
if (tree.isExistsProjection()) {
208+
209+
Column idColumn = table.column(entity.getIdColumn());
210+
builder = Select.builder().select(idColumn).from(table);
211+
} else {
212+
builder = selectBuilder(table);
170213
}
171214

172-
if (sort.isSorted()) {
173-
builder.orderBy(queryMapper.getMappedSort(table, sort, entity));
215+
return (SelectBuilder.SelectLimitOffset) builder;
216+
}
217+
218+
private SelectBuilder.SelectJoin selectBuilder(Table table) {
219+
220+
List<Expression> columnExpressions = new ArrayList<>();
221+
RelationalPersistentEntity<?> entity = entityMetadata.getTableEntity();
222+
SqlContext sqlContext = new SqlContext(entity);
223+
224+
List<Join> joinTables = new ArrayList<>();
225+
for (PersistentPropertyPath<RelationalPersistentProperty> path : context
226+
.findPersistentPropertyPaths(entity.getType(), p -> true)) {
227+
228+
PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(context, path);
229+
230+
// add a join if necessary
231+
Join join = getJoin(sqlContext, extPath);
232+
if (join != null) {
233+
joinTables.add(join);
234+
}
235+
236+
Column column = getColumn(sqlContext, extPath);
237+
if (column != null) {
238+
columnExpressions.add(column);
239+
}
174240
}
175241

176-
Select select = builder.build();
242+
SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions);
243+
SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table);
177244

178-
String sql = SqlRenderer.create(renderContextFactory.createRenderContext()).render(select);
245+
for (Join join : joinTables) {
246+
baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
247+
}
179248

180-
return new ParametrizedQuery(sql, parameterSource);
249+
return baseSelect;
181250
}
182251

183-
private SqlIdentifier[] getSelectProjection() {
252+
/**
253+
* Create a {@link Column} for {@link PersistentPropertyPathExtension}.
254+
*
255+
* @param sqlContext
256+
* @param path the path to the column in question.
257+
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
258+
*/
259+
@Nullable
260+
private Column getColumn(SqlContext sqlContext, PersistentPropertyPathExtension path) {
261+
262+
// an embedded itself doesn't give an column, its members will though.
263+
// if there is a collection or map on the path it won't get selected at all, but it will get loaded with a separate
264+
// select
265+
// only the parent path is considered in order to handle arrays that get stored as BINARY properly
266+
if (path.isEmbedded() || path.getParentPath().isMultiValued()) {
267+
return null;
268+
}
184269

185-
RelationalPersistentEntity<?> tableEntity = entityMetadata.getTableEntity();
270+
if (path.isEntity()) {
186271

187-
if (tree.isExistsProjection()) {
188-
return new SqlIdentifier[] { tableEntity.getIdColumn() };
272+
// Simple entities without id include there backreference as an synthetic id in order to distinguish null entities
273+
// from entities with only null values.
274+
275+
if (path.isQualified() //
276+
|| path.isCollectionLike() //
277+
|| path.hasIdProperty() //
278+
) {
279+
return null;
280+
}
281+
282+
return sqlContext.getReverseColumn(path);
283+
}
284+
285+
return sqlContext.getColumn(path);
286+
}
287+
288+
@Nullable
289+
Join getJoin(SqlContext sqlContext, PersistentPropertyPathExtension path) {
290+
291+
if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) {
292+
return null;
189293
}
190294

191-
return new SqlIdentifier[0];
295+
Table currentTable = sqlContext.getTable(path);
296+
297+
PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath();
298+
Table parentTable = sqlContext.getTable(idDefiningParentPath);
299+
300+
return new Join( //
301+
currentTable, //
302+
currentTable.column(path.getReverseColumnName()), //
303+
parentTable.column(idDefiningParentPath.getIdColumnName()) //
304+
);
305+
}
306+
307+
/**
308+
* Value object representing a {@code JOIN} association.
309+
*/
310+
@Value
311+
static private class Join {
312+
Table joinTable;
313+
Column joinColumn;
314+
Column parentId;
192315
}
193316
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.springframework.data.domain.Sort;
1919
import org.springframework.data.jdbc.core.convert.JdbcConverter;
2020
import org.springframework.data.relational.core.dialect.Dialect;
21+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
2122
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
2223
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
2324
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
@@ -31,10 +32,12 @@
3132
* An {@link AbstractJdbcQuery} implementation based on a {@link PartTree}.
3233
*
3334
* @author Mark Paluch
35+
* @author Jens Schauder
3436
* @since 2.0
3537
*/
3638
public class PartTreeJdbcQuery extends AbstractJdbcQuery {
3739

40+
private final RelationalMappingContext context;
3841
private final Parameters<?, ?> parameters;
3942
private final Dialect dialect;
4043
private final JdbcConverter converter;
@@ -43,22 +46,25 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
4346

4447
/**
4548
* Creates a new {@link PartTreeJdbcQuery}.
46-
*
49+
*
50+
* @param context must not be {@literal null}.
4751
* @param queryMethod must not be {@literal null}.
4852
* @param dialect must not be {@literal null}.
4953
* @param converter must not be {@literal null}.
5054
* @param operations must not be {@literal null}.
5155
* @param rowMapper must not be {@literal null}.
5256
*/
53-
public PartTreeJdbcQuery(JdbcQueryMethod queryMethod, Dialect dialect, JdbcConverter converter,
54-
NamedParameterJdbcOperations operations, RowMapper<Object> rowMapper) {
57+
public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect, JdbcConverter converter,
58+
NamedParameterJdbcOperations operations, RowMapper<Object> rowMapper) {
5559

5660
super(queryMethod, operations, rowMapper);
5761

62+
Assert.notNull(context, "RelationalMappingContext must not be null");
5863
Assert.notNull(queryMethod, "JdbcQueryMethod must not be null");
5964
Assert.notNull(dialect, "Dialect must not be null");
6065
Assert.notNull(converter, "JdbcConverter must not be null");
6166

67+
this.context = context;
6268
this.parameters = queryMethod.getParameters();
6369
this.dialect = dialect;
6470
this.converter = converter;
@@ -90,7 +96,7 @@ public Object execute(Object[] values) {
9096
protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) {
9197

9298
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
93-
JdbcQueryCreator queryCreator = new JdbcQueryCreator(tree, converter, dialect, entityMetadata, accessor);
99+
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor);
94100
return queryCreator.createQuery(getDynamicSort(accessor));
95101
}
96102
}

0 commit comments

Comments
 (0)