diff --git a/mvnw b/mvnw index 5551fde8e7..8b9da3b8b6 100755 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an diff --git a/mvnw.cmd b/mvnw.cmd index e5cfb0ae9e..b3f995811c 100755 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -122,7 +122,7 @@ set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central diff --git a/pom.xml b/pom.xml index 680dddf736..563ccf1284 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 1.1.0.BUILD-SNAPSHOT + 1.1.0.DATAJDBC-340-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index f6d4373844..08fe7e38ea 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 1.1.0.BUILD-SNAPSHOT + 1.1.0.DATAJDBC-340-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index cfb1e9443b..baa35dffb8 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -5,7 +5,7 @@ 4.0.0 spring-data-jdbc - 1.1.0.BUILD-SNAPSHOT + 1.1.0.DATAJDBC-340-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 1.1.0.BUILD-SNAPSHOT + 1.1.0.DATAJDBC-340-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtension.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtension.java new file mode 100644 index 0000000000..a9a5e757c1 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtension.java @@ -0,0 +1,302 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core; + +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A wrapper around a {@link org.springframework.data.mapping.PersistentPropertyPath} for making common operations + * available used in SQL generation. + * + * @author Jens Schauder + * @since 1.1 + */ +class PersistentPropertyPathExtension { + + private final RelationalPersistentEntity entity; + private final @Nullable PersistentPropertyPath path; + private final MappingContext, RelationalPersistentProperty> context; + + PersistentPropertyPathExtension(MappingContext, RelationalPersistentProperty> context, + RelationalPersistentEntity entity) { + + Assert.notNull(context, "Context must not be null."); + Assert.notNull(entity, "Entity must not be null."); + + this.context = context; + this.entity = entity; + this.path = null; + } + + PersistentPropertyPathExtension(MappingContext, RelationalPersistentProperty> context, + PersistentPropertyPath path) { + + Assert.notNull(context, "Context must not be null."); + Assert.notNull(path, "Path must not be null."); + Assert.notNull(path.getBaseProperty(), "Path must not be empty."); + + this.context = context; + this.entity = path.getBaseProperty().getOwner(); + this.path = path; + } + + /** + * Returns {@literal true} exactly when the path is non empty and the leaf property an embedded one. + * + * @return if the leaf property is embedded. + */ + boolean isEmbedded() { + return path != null && path.getRequiredLeafProperty().isEmbedded(); + } + + /** + * Returns the path that has the same beginning but is one segment shorter than this path. + * + * @return the parent path. Guaranteed to be not {@literal null}. + * @throws IllegalStateException when called on an empty path. + */ + PersistentPropertyPathExtension getParentPath() { + + if (path == null) { + throw new IllegalStateException("The parent path of a root path is not defined."); + } + + if (path.getLength() == 1) { + return new PersistentPropertyPathExtension(context, entity); + } + + return new PersistentPropertyPathExtension(context, path.getParentPath()); + } + + /** + * Returns {@literal true} if there are multiple values for this path, i.e. if the path contains at least one element + * that is a collection and array or a map. + * + * @return {@literal true} if the path contains a multivalued element. + */ + boolean isMultiValued() { + + return path != null && // + (path.getRequiredLeafProperty().isCollectionLike() // + || path.getRequiredLeafProperty().isQualified() // + || getParentPath().isMultiValued() // + ); + } + + /** + * The {@link RelationalPersistentEntity} associated with the leaf of this path. + * + * @return Might return {@literal null} when called on a path that does not represent an entity. + */ + @Nullable + RelationalPersistentEntity getLeafEntity() { + return path == null ? entity : context.getPersistentEntity(path.getRequiredLeafProperty().getActualType()); + } + + /** + * @return {@literal true} when this is an empty path or the path references an entity. + */ + boolean isEntity() { + return path == null || path.getRequiredLeafProperty().isEntity(); + } + + /** + * @return {@literal true} when this is references a {@link java.util.List} or {@link java.util.Map}. + */ + boolean isQualified() { + return path != null && path.getRequiredLeafProperty().isQualified(); + } + + /** + * @return {@literal true} when this is references a {@link java.util.Collection} or an array. + */ + boolean isCollectionLike() { + return path != null && path.getRequiredLeafProperty().isCollectionLike(); + } + + /** + * The name of the column used to reference the id in the parent table. + * + * @throws IllegalStateException when called on an empty path. + */ + String getReverseColumnName() { + + Assert.state(path != null, "Path is null"); + + return path.getRequiredLeafProperty().getReverseColumnName(); + } + + /** + * The alias used in select for the column used to reference the id in the parent table. + * + * @throws IllegalStateException when called on an empty path. + */ + String getReverseColumnNameAlias() { + + return prefixWithTableAlias(getReverseColumnName()); + } + + /** + * The name of the column used to represent this property in the database. + * + * @throws IllegalStateException when called on an empty path. + */ + String getColumnName() { + + Assert.state(path != null, "Path is null"); + + return assembleColumnName(path.getRequiredLeafProperty().getColumnName()); + } + + /** + * The alias for the column used to represent this property in the database. + * + * @throws IllegalStateException when called on an empty path. + */ + String getColumnAlias() { + + return prefixWithTableAlias(getColumnName()); + } + + /** + * @return {@literal true} if this path represents an entity which has an Id attribute. + */ + boolean hasIdProperty() { + + RelationalPersistentEntity leafEntity = getLeafEntity(); + return leafEntity != null && leafEntity.hasIdProperty(); + } + + PersistentPropertyPathExtension getIdDefiningParentPath() { + + PersistentPropertyPathExtension parent = getParentPath(); + if (parent.path == null) { + return parent; + } + if (parent.isEmbedded()) { + return getParentPath().getIdDefiningParentPath(); + } + return parent; + } + + /** + * The name of the table this path is tied to or of the longest ancestor path that is actually tied to a table. + * + * @return the name of the table. Guaranteed to be not {@literal null}. + */ + String getTableName() { + return getTableOwningAncestor().getRequiredLeafEntity().getTableName(); + } + + /** + * The alias used for the table on which this path is based. + * + * @return a table alias, {@literal null} if the table owning path is the empty path. + */ + @Nullable + String getTableAlias() { + + PersistentPropertyPathExtension tableOwner = getTableOwningAncestor(); + + return tableOwner.path == null ? null : tableOwner.assembleTableAlias(); + + } + + /** + * The column name of the id column of the ancestor path that represents an actual table. + */ + String getIdColumnName() { + return getTableOwningAncestor().getRequiredLeafEntity().getIdColumn(); + } + + /** + * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse + * column is returned. + */ + String getEffectiveIdColumnName() { + + PersistentPropertyPathExtension owner = getTableOwningAncestor(); + return owner.path == null ? owner.getRequiredLeafEntity().getIdColumn() : owner.getReverseColumnName(); + } + + /** + * The length of the path. + */ + int getLength() { + return path == null ? 0 : path.getLength(); + } + + /** + * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a + * table. + * + * @return a path. Guaranteed to be not {@literal null}. + */ + private PersistentPropertyPathExtension getTableOwningAncestor() { + + return isEntity() && !isEmbedded() ? this : getParentPath().getTableOwningAncestor(); + } + + private String assembleTableAlias() { + + Assert.state(path != null, "Path is null"); + + RelationalPersistentProperty leafProperty = path.getRequiredLeafProperty(); + String prefix = isEmbedded() ? leafProperty.getEmbeddedPrefix() : leafProperty.getName(); + + if (path.getLength() == 1) { + Assert.notNull(prefix, "Prefix mus not be null."); + return prefix; + } + + PersistentPropertyPathExtension parentPath = getParentPath(); + return parentPath.isEmbedded() ? parentPath.assembleTableAlias() + prefix + : parentPath.assembleTableAlias() + "_" + prefix; + } + + private String assembleColumnName(String suffix) { + + Assert.state(path != null, "Path is null"); + + if (path.getLength() <= 1) { + return suffix; + } + PersistentPropertyPath parentPath = path.getParentPath(); + RelationalPersistentProperty parentLeaf = parentPath.getRequiredLeafProperty(); + if (!parentLeaf.isEmbedded()) { + return suffix; + } + String embeddedPrefix = parentLeaf.getEmbeddedPrefix(); + return getParentPath().assembleColumnName(embeddedPrefix + suffix); + } + + @SuppressWarnings("unchecked") + private RelationalPersistentEntity getRequiredLeafEntity() { + return path == null ? entity : context.getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType()); + } + + private String prefixWithTableAlias(String columnName) { + + String tableAlias = getTableAlias(); + return tableAlias == null ? columnName : tableAlias + "_" + columnName; + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java deleted file mode 100644 index 7dcc11e61a..0000000000 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2017-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jdbc.core; - -import lombok.Builder; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * Builder for creating Select-statements. Not intended for general purpose, but only for the needs of the - * {@link JdbcAggregateTemplate}. - * - * @author Jens Schauder - */ -class SelectBuilder { - - private final List columns = new ArrayList<>(); - private final String tableName; - private final List joins = new ArrayList<>(); - private final List conditions = new ArrayList<>(); - - /** - * Creates a {@link SelectBuilder} using the given table name. - * - * @param tableName the table name. Must not be {@code null}. - */ - SelectBuilder(String tableName) { - - this.tableName = tableName; - } - - /** - * Adds a column to the select list. - * - * @param columnSpec a function that specifies the column to add. The passed in {@link Column.ColumnBuilder} allows to - * specify details like alias and the source table. Must not be {@code null}. - * @return {@code this}. - */ - SelectBuilder column(Function columnSpec) { - - columns.add(columnSpec.apply(Column.builder()).build()); - return this; - } - - /** - * Adds a where clause to the select - * - * @param whereSpec a function specifying the details of the where clause by manipulating the passed in - * {@link WhereConditionBuilder}. Must not be {@code null}. - * @return {@code this}. - */ - SelectBuilder where(Function whereSpec) { - - conditions.add(whereSpec.apply(new WhereConditionBuilder()).build()); - return this; - } - - /** - * Adds a join to the select. - * - * @param joinSpec a function specifying the details of the join by manipulating the passed in - * {@link Join.JoinBuilder}. Must not be {@code null}. - * @return {@code this}. - */ - SelectBuilder join(Function joinSpec) { - - joins.add(joinSpec.apply(Join.builder()).build()); - return this; - } - - /** - * Builds the actual SQL statement. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String build() { - - return selectFrom() + joinClause() + whereClause(); - } - - private String whereClause() { - - if (conditions.isEmpty()) { - return ""; - } - - return conditions.stream() // - .map(WhereCondition::toSql) // - .collect(Collectors.joining("AND", " WHERE ", "") // - ); - } - - private String joinClause() { - return joins.stream().map(j -> joinTable(j) + joinConditions(j)).collect(Collectors.joining(" ")); - } - - private String joinTable(Join j) { - return String.format("%s JOIN %s AS %s", j.outerJoinModifier(), j.table, j.as); - } - - private String joinConditions(Join j) { - - return j.conditions.stream() // - .map(w -> String.format("%s %s %s", w.fromExpression, w.operation, w.toExpression)) // - .collect(Collectors.joining(" AND ", " ON ", "")); - } - - private String selectFrom() { - - return columns.stream() // - .map(Column::columnDefinition) // - .collect(Collectors.joining(", ", "SELECT ", " FROM " + tableName)); - } - - static class WhereConditionBuilder { - - private String fromTable; - private String fromColumn; - - private String operation = "="; - private String expression; - - WhereConditionBuilder() {} - - WhereConditionBuilder eq() { - - this.operation = "="; - return this; - } - - public WhereConditionBuilder in() { - - this.operation = "in"; - return this; - } - - WhereConditionBuilder tableAlias(String fromTable) { - - this.fromTable = fromTable; - return this; - } - - WhereConditionBuilder column(String fromColumn) { - - this.fromColumn = fromColumn; - return this; - } - - WhereConditionBuilder variable(String var) { - - this.expression = ":" + var; - return this; - } - - WhereCondition build() { - return new WhereCondition(fromTable + "." + fromColumn, operation, expression); - } - - } - - static class Join { - - private final String table; - private final String as; - private final Outer outer; - private final List conditions = new ArrayList<>(); - - Join(String table, String as, List conditions, Outer outer) { - - this.table = table; - this.as = as; - this.outer = outer; - this.conditions.addAll(conditions); - } - - static JoinBuilder builder() { - return new JoinBuilder(); - } - - private String outerJoinModifier() { - - switch (outer) { - case NONE: - return ""; - default: - return String.format(" %s OUTER", outer.name()); - - } - } - - public static class JoinBuilder { - - private String table; - private String as; - private List conditions = new ArrayList<>(); - private Outer outer = Outer.NONE; - - JoinBuilder() {} - - public Join.JoinBuilder table(String table) { - - this.table = table; - return this; - } - - public Join.JoinBuilder as(String as) { - - this.as = as; - return this; - } - - WhereConditionBuilder where(String column) { - return new WhereConditionBuilder(this, column); - } - - private JoinBuilder where(WhereCondition condition) { - - conditions.add(condition); - return this; - } - - Join build() { - return new Join(table, as, conditions, outer); - } - - public String toString() { - return "org.springframework.data.jdbc.core.SelectBuilder.Join.JoinBuilder(table=" + this.table + ", as=" - + this.as + ")"; - } - - JoinBuilder rightOuter() { - - outer = Outer.RIGHT; - return this; - } - - JoinBuilder leftOuter() { - outer = Outer.LEFT; - return this; - } - - static class WhereConditionBuilder { - - private final JoinBuilder joinBuilder; - private final String fromColumn; - - private String operation = "="; - - WhereConditionBuilder(JoinBuilder joinBuilder, String column) { - - this.joinBuilder = joinBuilder; - this.fromColumn = column; - } - - WhereConditionBuilder eq() { - operation = "="; - return this; - } - - JoinBuilder column(String table, String column) { - - return joinBuilder.where(new WhereCondition( // - joinBuilder.as + "." + fromColumn, // - operation, // - table + "." + column // - )); - - } - - } - - } - - private enum Outer { - NONE, RIGHT, LEFT - } - } - - static class WhereCondition { - - private final String operation; - private final String fromExpression; - private final String toExpression; - - WhereCondition(String fromExpression, String operation, String toExpression) { - - this.fromExpression = fromExpression; - this.toExpression = toExpression; - this.operation = operation; - } - - String toSql() { - - if (operation.equals("in")) { - return String.format("%s %s(%s)", fromExpression, operation, toExpression); - } - - return String.format("%s %s %s", fromExpression, operation, toExpression); - } - } - - @Builder - static class Column { - - private final String tableAlias; - private final String column; - private final String as; - - String columnDefinition() { - StringBuilder b = new StringBuilder(); - if (tableAlias != null) - b.append(tableAlias).append('.'); - b.append(column); - if (as != null) - b.append(" AS ").append(as); - return b.toString(); - } - } -} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlContext.java new file mode 100644 index 0000000000..aabb3707f8 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core; + +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.SQL; +import org.springframework.data.relational.core.sql.Table; + +/** + * Utility to get from path to SQL DSL elements. + * + * @author Jens Schauder + * @author Mark Paluch + * @since 1.1 + */ +class SqlContext { + + private final RelationalPersistentEntity entity; + private final Table table; + + SqlContext(RelationalPersistentEntity entity) { + this.entity = entity; + this.table = SQL.table(entity.getTableName()); + } + + Column getIdColumn() { + return table.column(entity.getIdColumn()); + } + + Table getTable() { + return table; + } + + Table getTable(PersistentPropertyPathExtension path) { + + String tableAlias = path.getTableAlias(); + Table table = SQL.table(path.getTableName()); + return tableAlias == null ? table : table.as(tableAlias); + } + + Column getColumn(PersistentPropertyPathExtension path) { + return getTable(path).column(path.getColumnName()).as(path.getColumnAlias()); + } + + Column getReverseColumn(PersistentPropertyPathExtension path) { + return getTable(path).column(path.getReverseColumnName()).as(path.getReverseColumnNameAlias()); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java index 4a28b59ded..bfa8fe67be 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java @@ -15,26 +15,48 @@ */ package org.springframework.data.jdbc.core; +import lombok.Value; + import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.AssignValue; +import org.springframework.data.relational.core.sql.Assignments; +import org.springframework.data.relational.core.sql.BindMarker; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Delete; +import org.springframework.data.relational.core.sql.DeleteBuilder; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.Expressions; +import org.springframework.data.relational.core.sql.Functions; +import org.springframework.data.relational.core.sql.Insert; +import org.springframework.data.relational.core.sql.InsertBuilder; +import org.springframework.data.relational.core.sql.SQL; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.SelectBuilder; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.Update; +import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.util.Lazy; -import org.springframework.data.util.StreamUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -45,16 +67,19 @@ * @author Yoichi Imai * @author Bastian Wilhelm * @author Oleksandr Kucher + * @author Mark Paluch */ class SqlGenerator { + private static final Pattern parameterPattern = Pattern.compile("\\W"); + private final RelationalPersistentEntity entity; - private final RelationalMappingContext context; - private final List columnNames = new ArrayList<>(); - private final List nonIdColumnNames = new ArrayList<>(); - private final Set readOnlyColumnNames = new HashSet<>(); + private final MappingContext, RelationalPersistentProperty> mappingContext; + + private final SqlContext sqlContext; + private final Columns columns; - private final Lazy findOneSql = Lazy.of(this::createFindOneSelectSql); + private final Lazy findOneSql = Lazy.of(this::createFindOneSql); private final Lazy findAllSql = Lazy.of(this::createFindAllSql); private final Lazy findAllInListSql = Lazy.of(this::createFindAllInListSql); @@ -65,53 +90,19 @@ class SqlGenerator { private final Lazy deleteByIdSql = Lazy.of(this::createDeleteSql); private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); - private final SqlGeneratorSource sqlGeneratorSource; - private final Pattern parameterPattern = Pattern.compile("\\W"); - - SqlGenerator(RelationalMappingContext context, RelationalPersistentEntity entity, - SqlGeneratorSource sqlGeneratorSource) { + /** + * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. + * + * @param mappingContext must not be {@literal null}. + * @param entity must not be {@literal null}. + */ + SqlGenerator(RelationalMappingContext mappingContext, RelationalPersistentEntity entity) { - this.context = context; + this.mappingContext = mappingContext; this.entity = entity; - this.sqlGeneratorSource = sqlGeneratorSource; - initColumnNames(entity, ""); - } - - private void initColumnNames(RelationalPersistentEntity entity, String prefix) { - - entity.doWithProperties((PropertyHandler) property -> { - - // the referencing column of referenced entity is expected to be on the other side of the relation - if (!property.isEntity()) { - initSimpleColumnName(property, prefix); - } else if (property.isEmbedded()) { - initEmbeddedColumnNames(property, prefix); - } - }); - } - - private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { - - String columnName = prefix + property.getColumnName(); - - columnNames.add(columnName); - - if (!entity.isIdProperty(property)) { - nonIdColumnNames.add(columnName); - } - if (property.isAnnotationPresent(ReadOnlyProperty.class)) { - readOnlyColumnNames.add(columnName); - } - } - - private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { - - final String embeddedPrefix = property.getEmbeddedPrefix(); - - final RelationalPersistentEntity embeddedEntity = context.getRequiredPersistentEntity(property.getColumnType()); - - initColumnNames(embeddedEntity, prefix + embeddedPrefix); + this.sqlContext = new SqlContext(entity); + this.columns = new Columns(entity, mappingContext); } /** @@ -150,14 +141,21 @@ String getFindAllByProperty(String columnName, @Nullable String keyColumn, boole Assert.isTrue(keyColumn != null || !ordered, "If the SQL statement should be ordered a keyColumn to order by must be provided."); - String baseSelect = (keyColumn != null) // - ? createSelectBuilder().column(cb -> cb.tableAlias(entity.getTableName()).column(keyColumn).as(keyColumn)) - .build() - : getFindAll(); + SelectBuilder.SelectWhere builder = selectBuilder( + keyColumn == null ? Collections.emptyList() : Collections.singleton(keyColumn)); - String orderBy = ordered ? " ORDER BY " + keyColumn : ""; + Table table = getTable(); + SelectBuilder.SelectWhereAndOr withWhereClause = builder + .where(table.column(columnName).isEqualTo(getBindMarker(columnName))); - return String.format("%s WHERE %s = :%s%s", baseSelect, columnName, columnName, orderBy); + Select select; + if (ordered) { + select = withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build(); + } else { + select = withWhereClause.build(); + } + + return render(select); } String getExists() { @@ -188,277 +186,392 @@ String getDeleteByList() { return deleteByListSql.get(); } - private String createFindOneSelectSql() { + private String createFindOneSql() { - return createSelectBuilder() // - .where(wb -> wb.tableAlias(entity.getTableName()).column(entity.getIdColumn()).eq().variable("id")) // + Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker("id"))) // .build(); - } - private SelectBuilder createSelectBuilder() { + return render(select); + } - SelectBuilder builder = new SelectBuilder(entity.getTableName()); - addColumnsForSimpleProperties(entity, "", "", entity, builder); - addColumnsForEmbeddedProperties(entity, "", "", entity, builder); - addColumnsAndJoinsForOneToOneReferences(entity, "", "", entity, builder); + private String createFindAllSql() { + return render(selectBuilder().build()); + } - return builder; + private SelectBuilder.SelectWhere selectBuilder() { + return selectBuilder(Collections.emptyList()); } - /** - * Adds the columns to the provided {@link SelectBuilder} representing simple properties, including those from - * one-to-one relationships. - * - * @param rootEntity the root entity for which to add the columns. - * @param builder The {@link SelectBuilder} to be modified. - */ - private void addColumnsAndJoinsForOneToOneReferences(RelationalPersistentEntity entity, String prefix, - String tableAlias, RelationalPersistentEntity rootEntity, SelectBuilder builder) { - - for (RelationalPersistentProperty property : entity) { - if (!property.isEntity() // - || property.isEmbedded() // - || Collection.class.isAssignableFrom(property.getType()) // - || Map.class.isAssignableFrom(property.getType()) // - ) { - continue; - } + private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { - final RelationalPersistentEntity refEntity = context.getRequiredPersistentEntity(property.getActualType()); - final String joinAlias; + Table table = getTable(); - if (tableAlias.isEmpty()) { - if (prefix.isEmpty()) { - joinAlias = property.getName(); - } else { - joinAlias = prefix + property.getName(); - } - } else { - if (prefix.isEmpty()) { - joinAlias = tableAlias + "_" + property.getName(); - } else { - joinAlias = tableAlias + "_" + prefix + property.getName(); - } - } + List columnExpressions = new ArrayList<>(); - // final String joinAlias = tableAlias.isEmpty() ? property.getName() : tableAlias + "_" + property.getName(); - builder.join(jb -> jb.leftOuter().table(refEntity.getTableName()).as(joinAlias) // - .where(property.getReverseColumnName()).eq().column(rootEntity.getTableName(), rootEntity.getIdColumn())); + List joinTables = new ArrayList<>(); + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { - addColumnsForSimpleProperties(refEntity, "", joinAlias, refEntity, builder); - addColumnsForEmbeddedProperties(refEntity, "", joinAlias, refEntity, builder); - addColumnsAndJoinsForOneToOneReferences(refEntity, "", joinAlias, refEntity, builder); + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - // if the referenced property doesn't have an id, include the back reference in the select list. - // this enables determining if the referenced entity is present or null. - if (!refEntity.hasIdProperty()) { + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + joinTables.add(join); + } - builder.column( // - cb -> cb.tableAlias(joinAlias) // - .column(property.getReverseColumnName()) // - .as(joinAlias + "_" + property.getReverseColumnName()) // - ); + Column column = getColumn(extPath); + if (column != null) { + columnExpressions.add(column); } } - } - private void addColumnsForEmbeddedProperties(RelationalPersistentEntity currentEntity, String prefix, - String tableAlias, RelationalPersistentEntity rootEntity, SelectBuilder builder) { - for (RelationalPersistentProperty property : currentEntity) { - if (!property.isEmbedded()) { - continue; - } + for (String keyColumn : keyColumns) { + columnExpressions.add(table.column(keyColumn).as(keyColumn)); + } - final String embeddedPrefix = prefix + property.getEmbeddedPrefix(); - final RelationalPersistentEntity embeddedEntity = context - .getRequiredPersistentEntity(property.getColumnType()); + SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); + SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); - addColumnsForSimpleProperties(embeddedEntity, embeddedPrefix, tableAlias, rootEntity, builder); - addColumnsForEmbeddedProperties(embeddedEntity, embeddedPrefix, tableAlias, rootEntity, builder); - addColumnsAndJoinsForOneToOneReferences(embeddedEntity, embeddedPrefix, tableAlias, rootEntity, builder); + for (Join join : joinTables) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); } + + return (SelectBuilder.SelectWhere) baseSelect; } - private void addColumnsForSimpleProperties(RelationalPersistentEntity currentEntity, String prefix, - String tableAlias, RelationalPersistentEntity rootEntity, SelectBuilder builder) { + @Nullable + Column getColumn(PersistentPropertyPathExtension path) { - for (RelationalPersistentProperty property : currentEntity) { + // an embedded itself doesn't give an column, its members will though. + // 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 + // select + // only the parent path is considered in order to handle arrays that get stored as BINARY properly + if (path.isEmbedded() || path.getParentPath().isMultiValued()) { + return null; + } - if (property.isEntity()) { - continue; - } + if (path.isEntity()) { - final String column = prefix + property.getColumnName(); - final String as = tableAlias.isEmpty() ? column : tableAlias + "_" + column; + // Simple entities without id include there backreference as an synthetic id in order to distinguish null entities + // from entities with only null values. - builder.column(cb -> cb // - .tableAlias(tableAlias.isEmpty() ? rootEntity.getTableName() : tableAlias) // - .column(column) // - .as(as)); - } - } + if (path.isQualified() // + || path.isCollectionLike() // + || path.hasIdProperty() // + ) { + return null; + } - private Stream getColumnNameStream(String prefix) { + return sqlContext.getReverseColumn(path); + } - return StreamUtils.createStreamFromIterator(entity.iterator()) // - .flatMap(p -> getColumnNameStream(p, prefix)); + return sqlContext.getColumn(path); } - private Stream getColumnNameStream(RelationalPersistentProperty p, String prefix) { + @Nullable + Join getJoin(PersistentPropertyPathExtension path) { - if (p.isEntity()) { - return sqlGeneratorSource.getSqlGenerator(p.getType()).getColumnNameStream(prefix + p.getColumnName() + "_"); - } else { - return Stream.of(prefix + p.getColumnName()); + if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { + return null; } - } - private String createFindAllSql() { - return createSelectBuilder().build(); + Table currentTable = sqlContext.getTable(path); + + PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); + Table parentTable = sqlContext.getTable(idDefiningParentPath); + + return new Join( // + currentTable, // + currentTable.column(path.getReverseColumnName()), // + parentTable.column(idDefiningParentPath.getIdColumnName()) // + ); } private String createFindAllInListSql() { - return createSelectBuilder() // - .where(wb -> wb.tableAlias(entity.getTableName()).column(entity.getIdColumn()).in().variable("ids")) // - .build(); + Select select = selectBuilder().where(getIdColumn().in(getBindMarker("ids"))).build(); + + return render(select); } private String createExistsSql() { - return String.format("SELECT COUNT(*) FROM %s WHERE %s = :id", entity.getTableName(), entity.getIdColumn()); + + Table table = getTable(); + + Select select = StatementBuilder // + .select(Functions.count(getIdColumn())) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker("id"))) // + .build(); + + return render(select); } private String createCountSql() { - return String.format("select count(*) from %s", entity.getTableName()); + + Table table = getTable(); + + Select select = StatementBuilder // + .select(Functions.count(Expressions.asterisk())) // + .from(table) // + .build(); + + return render(select); } private String createInsertSql(Set additionalColumns) { - String insertTemplate = "INSERT INTO %s (%s) VALUES (%s)"; + Table table = getTable(); - LinkedHashSet columnNamesForInsert = new LinkedHashSet<>(nonIdColumnNames); + Set columnNamesForInsert = new LinkedHashSet<>(columns.getInsertableColumns()); columnNamesForInsert.addAll(additionalColumns); - columnNamesForInsert.removeIf(readOnlyColumnNames::contains); - String tableColumns = String.join(", ", columnNamesForInsert); + InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); - String parameterNames = columnNamesForInsert.stream()// - .map(this::columnNameToParameterName) - .map(n -> String.format(":%s", n))// - .collect(Collectors.joining(", ")); + for (String cn : columnNamesForInsert) { + insert = insert.column(table.column(cn)); + } + + InsertBuilder.InsertValuesWithBuild insertWithValues = null; + for (String cn : columnNamesForInsert) { + insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); + } - return String.format(insertTemplate, entity.getTableName(), tableColumns, parameterNames); + return render(insertWithValues == null ? insert.build() : insertWithValues.build()); } private String createUpdateSql() { - String updateTemplate = "UPDATE %s SET %s WHERE %s = :%s"; + Table table = getTable(); - String setClause = columnNames.stream() // - .filter(s -> !s.equals(entity.getIdColumn())) // - .filter(s -> !readOnlyColumnNames.contains(s)) // - .map(n -> String.format("%s = :%s", n, columnNameToParameterName(n))) // - .collect(Collectors.joining(", ")); + List assignments = columns.getUpdateableColumns() // + .stream() // + .map(columnName -> Assignments.value( // + table.column(columnName), // + getBindMarker(columnName))) // + .collect(Collectors.toList()); - return String.format( // - updateTemplate, // - entity.getTableName(), // - setClause, // - entity.getIdColumn(), // - columnNameToParameterName(entity.getIdColumn()) // - ); + Update update = Update.builder() // + .table(table) // + .set(assignments) // + .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))) // + .build(); + + return render(update); } private String createDeleteSql() { - return String.format("DELETE FROM %s WHERE %s = :id", entity.getTableName(), entity.getIdColumn()); + + Table table = getTable(); + + Delete delete = Delete.builder().from(table).where(getIdColumn().isEqualTo(SQL.bindMarker(":id"))) // + .build(); + + return render(delete); } String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - if (path == null) { - return String.format("DELETE FROM %s", entity.getTableName()); - } + Table table = getTable(); - RelationalPersistentEntity entityToDelete = context - .getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType()); + DeleteBuilder.DeleteWhere deleteAll = Delete.builder().from(table); - final String innerMostCondition1 = createInnerMostCondition("%s IS NOT NULL", path); - String condition = cascadeConditions(innerMostCondition1, getSubPath(path)); + if (path == null) { + return render(deleteAll.build()); + } - return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition); + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), Column::isNotNull); } private String createDeleteByListSql() { - return String.format("DELETE FROM %s WHERE %s IN (:ids)", entity.getTableName(), entity.getIdColumn()); + + Table table = getTable(); + + Delete delete = Delete.builder() // + .from(table) // + .where(getIdColumn().in(getBindMarker("ids"))) // + .build(); + + return render(delete); } String createDeleteByPath(PersistentPropertyPath path) { + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), + filterColumn -> filterColumn.isEqualTo(getBindMarker("rootId"))); + } - RelationalPersistentEntity entityToDelete = context - .getRequiredPersistentEntity(path.getRequiredLeafProperty().getActualType()); + private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, + Function rootCondition) { - final String innerMostCondition = createInnerMostCondition("%s = :rootId", path); - String condition = cascadeConditions(innerMostCondition, getSubPath(path)); + Table table = SQL.table(path.getTableName()); - return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition); - } + DeleteBuilder.DeleteWhere builder = Delete.builder() // + .from(table); + Delete delete; + + Column filterColumn = table.column(path.getReverseColumnName()); + + if (path.getLength() == 1) { - private String createInnerMostCondition(String template, PersistentPropertyPath path) { - PersistentPropertyPath currentPath = path; - while (!currentPath.getParentPath().isEmpty() - && !currentPath.getParentPath().getRequiredLeafProperty().isEmbedded()) { - currentPath = currentPath.getParentPath(); + delete = builder // + .where(rootCondition.apply(filterColumn)) // + .build(); + } else { + + Condition condition = getSubselectCondition(path, rootCondition, filterColumn); + delete = builder.where(condition).build(); } - RelationalPersistentProperty property = currentPath.getRequiredLeafProperty(); - return String.format(template, property.getReverseColumnName()); + return render(delete); } - private PersistentPropertyPath getSubPath( - PersistentPropertyPath path) { + private static Condition getSubselectCondition(PersistentPropertyPathExtension path, + Function rootCondition, Column filterColumn) { - int pathLength = path.getLength(); + PersistentPropertyPathExtension parentPath = path.getParentPath(); - PersistentPropertyPath ancestor = path; + Table subSelectTable = SQL.table(parentPath.getTableName()); + Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); + Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); - int embeddedDepth = 0; - while (!ancestor.getParentPath().isEmpty() && ancestor.getParentPath().getRequiredLeafProperty().isEmbedded()) { - embeddedDepth++; - ancestor = ancestor.getParentPath(); - } + Condition innerCondition = parentPath.getLength() == 1 ? rootCondition.apply(selectFilterColumn) + : getSubselectCondition(parentPath, rootCondition, selectFilterColumn); - ancestor = path; + Select select = Select.builder() // + .select(idColumn) // + .from(subSelectTable) // + .where(innerCondition).build(); - for (int i = pathLength - 1 + embeddedDepth; i > 0; i--) { - ancestor = ancestor.getParentPath(); - } + return filterColumn.in(select); + } + + private String render(Select select) { + return SqlRenderer.create().render(select); + } + + private String render(Insert insert) { + return SqlRenderer.create().render(insert); + } + + private String render(Update update) { + return SqlRenderer.create().render(update); + } + + private String render(Delete delete) { + return SqlRenderer.create().render(delete); + } + + private Table getTable() { + return sqlContext.getTable(); + } + + private Column getIdColumn() { + return sqlContext.getIdColumn(); + } + + private static BindMarker getBindMarker(String columnName) { + return SQL.bindMarker(":" + parameterPattern.matcher(columnName).replaceAll("")); + } - return path.getExtensionForBaseOf(ancestor); + /** + * Value object representing a {@code JOIN} association. + */ + @Value + static class Join { + Table joinTable; + Column joinColumn; + Column parentId; } - private String cascadeConditions(String innerCondition, PersistentPropertyPath path) { + /** + * Value object encapsulating column name caches. + * + * @author Mark Paluch + */ + static class Columns { + + private final MappingContext, RelationalPersistentProperty> mappingContext; + + private final List columnNames = new ArrayList<>(); + private final List idColumnNames = new ArrayList<>(); + private final List nonIdColumnNames = new ArrayList<>(); + private final Set readOnlyColumnNames = new HashSet<>(); + private final Set insertableColumns; + private final Set updateableColumns; + + Columns(RelationalPersistentEntity entity, + MappingContext, RelationalPersistentProperty> mappingContext) { + + this.mappingContext = mappingContext; + + populateColumnNameCache(entity, ""); + + Set insertable = new LinkedHashSet<>(nonIdColumnNames); + insertable.removeAll(readOnlyColumnNames); + + this.insertableColumns = Collections.unmodifiableSet(insertable); + + Set updateable = new LinkedHashSet<>(columnNames); + + updateable.removeAll(idColumnNames); + updateable.removeAll(readOnlyColumnNames); + + this.updateableColumns = Collections.unmodifiableSet(updateable); + } + + private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { - if (path.getLength() == 0) { - return innerCondition; + entity.doWithProperties((PropertyHandler) property -> { + + // the referencing column of referenced entity is expected to be on the other side of the relation + if (!property.isEntity()) { + initSimpleColumnName(property, prefix); + } else if (property.isEmbedded()) { + initEmbeddedColumnNames(property, prefix); + } + }); } - PersistentPropertyPath rootPath = path; - while (rootPath.getLength() > 1) { - rootPath = rootPath.getParentPath(); + private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { + + String columnName = prefix + property.getColumnName(); + + columnNames.add(columnName); + + if (!property.getOwner().isIdProperty(property)) { + nonIdColumnNames.add(columnName); + } else { + idColumnNames.add(columnName); + } + + if (!property.isWritable() || property.isAnnotationPresent(ReadOnlyProperty.class)) { + readOnlyColumnNames.add(columnName); + } } - RelationalPersistentEntity entity = context - .getRequiredPersistentEntity(rootPath.getBaseProperty().getOwner().getTypeInformation()); - RelationalPersistentProperty property = path.getRequiredLeafProperty(); + private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { - return String.format("%s IN (SELECT %s FROM %s WHERE %s)", // - property.getReverseColumnName(), // - entity.getIdColumn(), // - entity.getTableName(), innerCondition // - ); - } + String embeddedPrefix = property.getEmbeddedPrefix(); - private String columnNameToParameterName(String columnName){ - return parameterPattern.matcher(columnName).replaceAll(""); + RelationalPersistentEntity embeddedEntity = mappingContext + .getRequiredPersistentEntity(property.getColumnType()); + + populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); + } + + /** + * @return Column names that can be used for {@code INSERT}. + */ + Set getInsertableColumns() { + return insertableColumns; + } + + /** + * @return Column names that can be used for {@code UPDATE}. + */ + Set getUpdateableColumns() { + return updateableColumns; + } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java index b66273a9e5..732d9807ec 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java @@ -17,7 +17,6 @@ import lombok.RequiredArgsConstructor; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -38,7 +37,6 @@ public class SqlGeneratorSource { SqlGenerator getSqlGenerator(Class domainType) { return sqlGeneratorCache.computeIfAbsent(domainType, - t -> new SqlGenerator(context, context.getRequiredPersistentEntity(t), this)); - + t -> new SqlGenerator(context, context.getRequiredPersistentEntity(t))); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java index 0e089c6bc1..11efc3ffb8 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java @@ -21,7 +21,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.DbAction.Insert; @@ -55,7 +54,7 @@ public String getReverseColumnName(RelationalPersistentProperty property) { Element element = new Element(); InsertRoot containerInsert = new InsertRoot<>(container); - Insert insert = new Insert<>(element, PropertyPathUtils.toPath("element", Container.class, context), + Insert insert = new Insert<>(element, PropertyPathTestingUtils.toPath("element", Container.class, context), containerInsert); @Test // DATAJDBC-145 @@ -104,6 +103,7 @@ public void generatedIdOfParentGetsPassedOnAsAdditionalParameter() { .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } + @SuppressWarnings("unused") static class Container { @Id Long id; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index 6bab2e0c02..dcbb0855d8 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -20,6 +20,8 @@ import lombok.Data; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -31,7 +33,6 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -44,6 +45,9 @@ import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.annotation.ProfileValueSourceConfiguration; import org.springframework.test.annotation.ProfileValueUtils; @@ -67,7 +71,8 @@ public class JdbcAggregateTemplateIntegrationTests { @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); @Rule public SpringMethodRule methodRule = new SpringMethodRule(); @Autowired JdbcAggregateOperations template; - + @Autowired + NamedParameterJdbcOperations jdbcTemplate; LegoSet legoSet = createLegoSet(); @Test // DATAJDBC-112 @@ -422,6 +427,7 @@ public void saveAndLoadAnEntityWithSet() { @Test // DATAJDBC-327 public void saveAndLoadAnEntityWithByteArray() { + ByteArrayOwner owner = new ByteArrayOwner(); owner.binaryData = new byte[] { 1, 23, 42 }; @@ -434,6 +440,35 @@ public void saveAndLoadAnEntityWithByteArray() { assertThat(reloaded.binaryData).isEqualTo(new byte[] { 1, 23, 42 }); } + @Test + public void saveAndLoadLongChain() { + + Chain4 chain4 = new Chain4(); + chain4.fourValue = "omega"; + chain4.chain3 = new Chain3(); + chain4.chain3.threeValue = "delta"; + chain4.chain3.chain2 = new Chain2(); + chain4.chain3.chain2.twoValue = "gamma"; + chain4.chain3.chain2.chain1 = new Chain1(); + chain4.chain3.chain2.chain1.oneValue = "beta"; + chain4.chain3.chain2.chain1.chain0 = new Chain0(); + chain4.chain3.chain2.chain1.chain0.zeroValue = "alpha"; + + template.save(chain4); + + Chain4 reloaded = template.findById(chain4.four, Chain4.class); + + assertThat(reloaded).isNotNull(); + + assertThat(reloaded.four).isEqualTo(chain4.four); + assertThat(reloaded.chain3.chain2.chain1.chain0.zeroValue).isEqualTo(chain4.chain3.chain2.chain1.chain0.zeroValue); + + template.delete(chain4, Chain4.class); + + String countSelect = "SELECT COUNT(*) FROM %s"; + jdbcTemplate.queryForObject(String.format(countSelect, "CHAIN0"),emptyMap(), Long.class); + } + private static void assumeNot(String dbProfileName) { Assume.assumeTrue("true" @@ -537,4 +572,33 @@ JdbcAggregateOperations operations(ApplicationEventPublisher publisher, Relation return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); } } + + static class Chain0 { + @Id Long zero; + String zeroValue; + } + + static class Chain1 { + @Id Long one; + String oneValue; + Chain0 chain0; + } + + static class Chain2 { + @Id Long two; + String twoValue; + Chain1 chain1; + } + + static class Chain3 { + @Id Long three; + String threeValue; + Chain2 chain2; + } + + static class Chain4 { + @Id Long four; + String fourValue; + Chain3 chain3; + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java index 13ec82a143..90db074d64 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java @@ -16,7 +16,7 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.jdbc.core.PropertyPathUtils.*; +import static org.springframework.data.jdbc.core.PropertyPathTestingUtils.*; import java.util.List; import java.util.Map; @@ -24,7 +24,6 @@ import org.jetbrains.annotations.NotNull; import org.junit.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -49,8 +48,8 @@ public void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactly( // - tuple("dummy_entity", "eins", UUID.class) // - ); + tuple("dummy_entity", "eins", UUID.class) // + ); } @Test // DATAJDBC-326 @@ -66,9 +65,9 @@ public void qualifiersForMaps() { assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactlyInAnyOrder( // - tuple("dummy_entity", "parent-eins", UUID.class), // - tuple("dummy_entity_key", "map-key-eins", String.class) // - ); + tuple("dummy_entity", "parent-eins", UUID.class), // + tuple("dummy_entity_key", "map-key-eins", String.class) // + ); } @Test // DATAJDBC-326 @@ -84,9 +83,9 @@ public void qualifiersForLists() { assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactlyInAnyOrder( // - tuple("dummy_entity", "parent-eins", UUID.class), // - tuple("dummy_entity_key", "list-index-eins", Integer.class) // - ); + tuple("dummy_entity", "parent-eins", UUID.class), // + tuple("dummy_entity_key", "list-index-eins", Integer.class) // + ); } @Test // DATAJDBC-326 @@ -99,8 +98,8 @@ public void backreferenceAcrossEmbeddable() { assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactly( // - tuple("embeddable", "parent-eins", UUID.class) // - ); + tuple("embeddable", "parent-eins", UUID.class) // + ); } @NotNull diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java index b4fa8263f6..7e5ccab554 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java @@ -47,11 +47,7 @@ public class MyBatisDataAccessStrategyUnitTests { MyBatisDataAccessStrategy accessStrategy = new MyBatisDataAccessStrategy(session); - PersistentPropertyPath path(String path, Class source) { - - RelationalMappingContext context = this.context; - return PropertyPathUtils.toPath(path, source, context); - } + PersistentPropertyPath path = PropertyPathTestingUtils.toPath("one.two", DummyEntity.class, context); @Before public void before() { @@ -128,7 +124,7 @@ public void delete() { @Test // DATAJDBC-123 public void deleteAllByPath() { - accessStrategy.deleteAll(path("one.two", DummyEntity.class)); + accessStrategy.deleteAll(path); verify(session).delete( eq("org.springframework.data.jdbc.core.MyBatisDataAccessStrategyUnitTests$DummyEntityMapper.deleteAll-one-two"), @@ -174,7 +170,7 @@ public void deleteAllByType() { @Test // DATAJDBC-123 public void deleteByPath() { - accessStrategy.delete("rootid", path("one.two", DummyEntity.class)); + accessStrategy.delete("rootid", path); verify(session).delete( eq("org.springframework.data.jdbc.core.MyBatisDataAccessStrategyUnitTests$DummyEntityMapper.delete-one-two"), @@ -334,10 +330,12 @@ public void count() { ); } + @SuppressWarnings("unused") private static class DummyEntity { ChildOne one; } + @SuppressWarnings("unused") private static class ChildOne { ChildTwo two; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java new file mode 100644 index 0000000000..27350c3ef4 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core; + +import java.util.List; + +import org.assertj.core.api.SoftAssertions; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * @author Jens Schauder + */ +public class PersistentPropertyPathExtensionUnitTests { + + JdbcMappingContext context = new JdbcMappingContext(); + private RelationalPersistentEntity entity = context.getRequiredPersistentEntity(DummyEntity.class); + + @Test + public void isEmbedded() { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath(entity).isEmbedded()).isFalse(); + softly.assertThat(extPath("second").isEmbedded()).isFalse(); + softly.assertThat(extPath("second.third").isEmbedded()).isTrue(); + }); + } + + @Test + public void isMultiValued() { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath(entity).isMultiValued()).isFalse(); + softly.assertThat(extPath("second").isMultiValued()).isFalse(); + softly.assertThat(extPath("second.third").isMultiValued()).isFalse(); + softly.assertThat(extPath("secondList.third").isMultiValued()).isTrue(); + softly.assertThat(extPath("secondList").isMultiValued()).isTrue(); + }); + } + + @Test + public void leafEntity() { + + RelationalPersistentEntity second = context.getRequiredPersistentEntity(Second.class); + RelationalPersistentEntity third = context.getRequiredPersistentEntity(Third.class); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath(entity).getLeafEntity()).isEqualTo(entity); + softly.assertThat(extPath("second").getLeafEntity()).isEqualTo(second); + softly.assertThat(extPath("second.third").getLeafEntity()).isEqualTo(third); + softly.assertThat(extPath("secondList.third").getLeafEntity()).isEqualTo(third); + softly.assertThat(extPath("secondList").getLeafEntity()).isEqualTo(second); + }); + } + + @Test + public void isEntity() { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath(entity).isEntity()).isTrue(); + String path = "second"; + softly.assertThat(extPath(path).isEntity()).isTrue(); + softly.assertThat(extPath("second.third").isEntity()).isTrue(); + softly.assertThat(extPath("second.third.value").isEntity()).isFalse(); + softly.assertThat(extPath("secondList.third").isEntity()).isTrue(); + softly.assertThat(extPath("secondList.third.value").isEntity()).isFalse(); + softly.assertThat(extPath("secondList").isEntity()).isTrue(); + }); + } + + @Test + public void getTableName() { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath(entity).getTableName()).isEqualTo("dummy_entity"); + softly.assertThat(extPath("second").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("second.third").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("second.third.value").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("secondList.third").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("secondList.third.value").getTableName()).isEqualTo("second"); + softly.assertThat(extPath("secondList").getTableName()).isEqualTo("second"); + }); + } + + @Test + public void getTableAlias() { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath(entity).getTableAlias()).isEqualTo(null); + softly.assertThat(extPath("second").getTableAlias()).isEqualTo("second"); + softly.assertThat(extPath("second.third").getTableAlias()).isEqualTo("second"); + softly.assertThat(extPath("second.third.value").getTableAlias()).isEqualTo("second"); + softly.assertThat(extPath("second.third2").getTableAlias()).isEqualTo("second_third2"); + softly.assertThat(extPath("second.third2.value").getTableAlias()).isEqualTo("second_third2"); + softly.assertThat(extPath("secondList.third").getTableAlias()).isEqualTo("secondList"); + softly.assertThat(extPath("secondList.third.value").getTableAlias()).isEqualTo("secondList"); + softly.assertThat(extPath("secondList.third2").getTableAlias()).isEqualTo("secondList_third2"); + softly.assertThat(extPath("secondList.third2.value").getTableAlias()).isEqualTo("secondList_third2"); + softly.assertThat(extPath("secondList").getTableAlias()).isEqualTo("secondList"); + softly.assertThat(extPath("second2.third2").getTableAlias()).isEqualTo("secthird2"); + }); + } + + @Test + public void getColumnName() { + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(extPath("second.third.value").getColumnName()).isEqualTo("thrdvalue"); + softly.assertThat(extPath("second.third2.value").getColumnName()).isEqualTo("value"); + softly.assertThat(extPath("secondList.third.value").getColumnName()).isEqualTo("thrdvalue"); + softly.assertThat(extPath("secondList.third2.value").getColumnName()).isEqualTo("value"); + softly.assertThat(extPath("second2.third.value").getColumnName()).isEqualTo("secthrdvalue"); + softly.assertThat(extPath("second2.third2.value").getColumnName()).isEqualTo("value"); + }); + } + + @NotNull + private PersistentPropertyPathExtension extPath(RelationalPersistentEntity entity) { + return new PersistentPropertyPathExtension(context, entity); + } + + @NotNull + private PersistentPropertyPathExtension extPath(String path) { + return new PersistentPropertyPathExtension(context, createSimplePath(path)); + } + + PersistentPropertyPath createSimplePath(String path) { + return PropertyPathTestingUtils.toPath(path, DummyEntity.class, context); + } + + @SuppressWarnings("unused") + static class DummyEntity { + Second second; + List secondList; + @Embedded("sec") Second second2; + } + + @SuppressWarnings("unused") + static class Second { + @Embedded("thrd") Third third; + Third third2; + } + + @SuppressWarnings("unused") + static class Third { + String value; + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java similarity index 97% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java index 6db1a82031..0dcf7f4210 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathUtils.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PropertyPathTestingUtils.java @@ -29,7 +29,7 @@ * @author Jens Schauder */ @UtilityClass -class PropertyPathUtils { +class PropertyPathTestingUtils { static PersistentPropertyPath toPath(String path, Class source, RelationalMappingContext context) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java deleted file mode 100644 index 9b3e044565..0000000000 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2017-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jdbc.core; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.Test; - -/** - * Unit tests for the {@link SelectBuilder}. - * - * @author Jens Schauder - */ -public class SelectBuilderUnitTests { - - @Test // DATAJDBC-112 - public void simplestSelect() { - - String sql = new SelectBuilder("mytable") // - .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // - .build(); - - assertThat(sql).isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable"); - } - - @Test // DATAJDBC-112 - public void columnWithoutTableAlias() { - - String sql = new SelectBuilder("mytable") // - .column(cb -> cb.column("mycolumn").as("myalias")) // - .build(); - - assertThat(sql).isEqualTo("SELECT mycolumn AS myalias FROM mytable"); - } - - @Test // DATAJDBC-112 - public void whereClause() { - - String sql = new SelectBuilder("mytable") // - .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // - .where(cb -> cb.tableAlias("mytable").column("mycolumn").eq().variable("var")).build(); - - assertThat(sql).isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable WHERE mytable.mycolumn = :var"); - } - - @Test // DATAJDBC-112 - public void multipleColumnsSelect() { - - String sql = new SelectBuilder("mytable") // - .column(cb -> cb.tableAlias("mytable").column("one").as("oneAlias")) // - .column(cb -> cb.tableAlias("mytable").column("two").as("twoAlias")) // - .build(); - - assertThat(sql).isEqualTo("SELECT mytable.one AS oneAlias, mytable.two AS twoAlias FROM mytable"); - } - - @Test // DATAJDBC-112 - public void join() { - String sql = new SelectBuilder("mytable") // - .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // - .join(jb -> jb.table("other").as("o").where("oid").eq().column("mytable", "id")).build(); - - assertThat(sql).isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable JOIN other AS o ON o.oid = mytable.id"); - } - - @Test // DATAJDBC-112 - public void outerJoin() { - String sql = new SelectBuilder("mytable") // - .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // - .join(jb -> jb.rightOuter().table("other").as("o").where("oid").eq().column("mytable", "id")).build(); - - assertThat(sql) - .isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable RIGHT OUTER JOIN other AS o ON o.oid = mytable.id"); - } - -} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java index 3e435e2b6f..4bf0a59788 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -84,9 +84,10 @@ public void cascadingDeleteFirstLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref")); - assertThat(sql).isEqualTo("DELETE FROM " + user + ".referenced_entity WHERE " + "dummy_entity = :rootId"); + assertThat(sql).isEqualTo( + "DELETE FROM " + user + ".referenced_entity WHERE " + user + ".referenced_entity.dummy_entity = :rootId"); }); } @@ -97,13 +98,13 @@ public void cascadingDeleteAllSecondLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref.further")); assertThat(sql).isEqualTo( // "DELETE FROM " + user + ".second_level_referenced_entity " // - + "WHERE " + "referenced_entity IN " // - + "(SELECT l1id FROM " + user + ".referenced_entity " // - + "WHERE " + "dummy_entity = :rootId)"); + + "WHERE " + user + ".second_level_referenced_entity.referenced_entity IN " // + + "(SELECT " + user + ".referenced_entity.l1id FROM " + user + ".referenced_entity " // + + "WHERE " + user + ".referenced_entity.dummy_entity = :rootId)"); }); } @@ -127,10 +128,10 @@ public void cascadingDeleteAllFirstLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteAllSql(getPath("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref")); assertThat(sql).isEqualTo( // - "DELETE FROM " + user + ".referenced_entity WHERE " + "dummy_entity IS NOT NULL"); + "DELETE FROM " + user + ".referenced_entity WHERE " + user + ".referenced_entity.dummy_entity IS NOT NULL"); }); } @@ -141,18 +142,18 @@ public void cascadingDeleteSecondLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); - String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further")); assertThat(sql).isEqualTo( // "DELETE FROM " + user + ".second_level_referenced_entity " // - + "WHERE " + "referenced_entity IN " // - + "(SELECT l1id FROM " + user + ".referenced_entity " // - + "WHERE " + "dummy_entity IS NOT NULL)"); + + "WHERE " + user + ".second_level_referenced_entity.referenced_entity IN " // + + "(SELECT " + user + ".referenced_entity.l1id FROM " + user + ".referenced_entity " // + + "WHERE " + user + ".referenced_entity.dummy_entity IS NOT NULL)"); }); } - private PersistentPropertyPath getPath(String path, Class baseType) { - return PersistentPropertyPathTestUtils.getPath(this.context, path, baseType); + private PersistentPropertyPath getPath(String path) { + return PersistentPropertyPathTestUtils.getPath(this.context, path, DummyEntity.class); } /** @@ -211,9 +212,10 @@ private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { RelationalMappingContext context = new JdbcMappingContext(namingStrategy); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); - return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); + return new SqlGenerator(context, persistentEntity); } + @SuppressWarnings("unused") static class DummyEntity { @Id Long id; @@ -221,6 +223,7 @@ static class DummyEntity { ReferencedEntity ref; } + @SuppressWarnings("unused") static class ReferencedEntity { @Id Long l1id; @@ -228,6 +231,7 @@ static class ReferencedEntity { SecondLevelReferencedEntity further; } + @SuppressWarnings("unused") static class SecondLevelReferencedEntity { @Id Long l2id; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java index e3af893f33..10184c76c9 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorEmbeddedUnitTests.java @@ -16,9 +16,11 @@ package org.springframework.data.jdbc.core; import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; import org.assertj.core.api.SoftAssertions; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -26,6 +28,7 @@ import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.sql.Aliased; /** * Unit tests for the {@link SqlGenerator} in a context of the {@link Embedded} annotation. @@ -34,6 +37,7 @@ */ public class SqlGeneratorEmbeddedUnitTests { + private RelationalMappingContext context = new JdbcMappingContext(); private SqlGenerator sqlGenerator; @Before @@ -42,9 +46,8 @@ public void setUp() { } SqlGenerator createSqlGenerator(Class type) { - RelationalMappingContext context = new JdbcMappingContext(); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(type); - return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); + return new SqlGenerator(context, persistentEntity); } @Test // DATAJDBC-111 @@ -52,44 +55,31 @@ public void findOne() { final String sql = sqlGenerator.getFindOne(); SoftAssertions softAssertions = new SoftAssertions(); - softAssertions.assertThat(sql) - .startsWith("SELECT") - .contains("dummy_entity.id1 AS id1") - .contains("dummy_entity.test AS test") - .contains("dummy_entity.attr1 AS attr1") - .contains("dummy_entity.attr2 AS attr2") - .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") - .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") - .contains("dummy_entity.prefix_test AS prefix_test") - .contains("dummy_entity.prefix_attr1 AS prefix_attr1") - .contains("dummy_entity.prefix_attr2 AS prefix_attr2") + softAssertions.assertThat(sql).startsWith("SELECT").contains("dummy_entity.id1 AS id1") + .contains("dummy_entity.test AS test").contains("dummy_entity.attr1 AS attr1") + .contains("dummy_entity.attr2 AS attr2").contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") + .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2").contains("dummy_entity.prefix_test AS prefix_test") + .contains("dummy_entity.prefix_attr1 AS prefix_attr1").contains("dummy_entity.prefix_attr2 AS prefix_attr2") .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") - .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") - .contains("WHERE dummy_entity.id1 = :id") + .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2").contains("WHERE dummy_entity.id1 = :id") .doesNotContain("JOIN").doesNotContain("embeddable"); softAssertions.assertAll(); } @Test // DATAJDBC-111 - public void findAll() { - final String sql = sqlGenerator.getFindAll(); - - SoftAssertions softAssertions = new SoftAssertions(); - softAssertions.assertThat(sql) - .startsWith("SELECT") - .contains("dummy_entity.id1 AS id1") - .contains("dummy_entity.test AS test") - .contains("dummy_entity.attr1 AS attr1") - .contains("dummy_entity.attr2 AS attr2") - .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") - .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") - .contains("dummy_entity.prefix_test AS prefix_test") - .contains("dummy_entity.prefix_attr1 AS prefix_attr1") - .contains("dummy_entity.prefix_attr2 AS prefix_attr2") - .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") - .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") - .doesNotContain("JOIN").doesNotContain("embeddable"); - softAssertions.assertAll(); + public void findAll() { + final String sql = sqlGenerator.getFindAll(); + + SoftAssertions softAssertions = new SoftAssertions(); + softAssertions.assertThat(sql).startsWith("SELECT").contains("dummy_entity.id1 AS id1") + .contains("dummy_entity.test AS test").contains("dummy_entity.attr1 AS attr1") + .contains("dummy_entity.attr2 AS attr2").contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") + .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2").contains("dummy_entity.prefix_test AS prefix_test") + .contains("dummy_entity.prefix_attr1 AS prefix_attr1").contains("dummy_entity.prefix_attr2 AS prefix_attr2") + .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") + .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2").doesNotContain("JOIN") + .doesNotContain("embeddable"); + softAssertions.assertAll(); } @Test // DATAJDBC-111 @@ -97,21 +87,14 @@ public void findAllInList() { final String sql = sqlGenerator.getFindAllInList(); SoftAssertions softAssertions = new SoftAssertions(); - softAssertions.assertThat(sql) - .startsWith("SELECT") - .contains("dummy_entity.id1 AS id1") - .contains("dummy_entity.test AS test") - .contains("dummy_entity.attr1 AS attr1") - .contains("dummy_entity.attr2 AS attr2") - .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") - .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") - .contains("dummy_entity.prefix_test AS prefix_test") - .contains("dummy_entity.prefix_attr1 AS prefix_attr1") - .contains("dummy_entity.prefix_attr2 AS prefix_attr2") + softAssertions.assertThat(sql).startsWith("SELECT").contains("dummy_entity.id1 AS id1") + .contains("dummy_entity.test AS test").contains("dummy_entity.attr1 AS attr1") + .contains("dummy_entity.attr2 AS attr2").contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") + .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2").contains("dummy_entity.prefix_test AS prefix_test") + .contains("dummy_entity.prefix_attr1 AS prefix_attr1").contains("dummy_entity.prefix_attr2 AS prefix_attr2") .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") - .contains("WHERE dummy_entity.id1 in(:ids)") - .doesNotContain("JOIN").doesNotContain("embeddable"); + .contains("WHERE dummy_entity.id1 IN (:ids)").doesNotContain("JOIN").doesNotContain("embeddable"); softAssertions.assertAll(); } @@ -120,18 +103,9 @@ public void insert() { final String sql = sqlGenerator.getInsert(emptySet()); SoftAssertions softAssertions = new SoftAssertions(); - softAssertions.assertThat(sql) - .startsWith("INSERT INTO") - .contains("dummy_entity") - .contains(":test") - .contains(":attr1") - .contains(":attr2") - .contains(":prefix2_attr1") - .contains(":prefix2_attr2") - .contains(":prefix_test") - .contains(":prefix_attr1") - .contains(":prefix_attr2") - .contains(":prefix_prefix2_attr1") + softAssertions.assertThat(sql).startsWith("INSERT INTO").contains("dummy_entity").contains(":test") + .contains(":attr1").contains(":attr2").contains(":prefix2_attr1").contains(":prefix2_attr2") + .contains(":prefix_test").contains(":prefix_attr1").contains(":prefix_attr2").contains(":prefix_prefix2_attr1") .contains(":prefix_prefix2_attr2"); softAssertions.assertAll(); } @@ -141,48 +115,166 @@ public void update() { final String sql = sqlGenerator.getUpdate(); SoftAssertions softAssertions = new SoftAssertions(); - softAssertions.assertThat(sql) - .startsWith("UPDATE") - .contains("dummy_entity") - .contains("test = :test") - .contains("attr1 = :attr1") - .contains("attr2 = :attr2") - .contains("prefix2_attr1 = :prefix2_attr1") - .contains("prefix2_attr2 = :prefix2_attr2") - .contains("prefix_test = :prefix_test") - .contains("prefix_attr1 = :prefix_attr1") - .contains("prefix_attr2 = :prefix_attr2") + softAssertions.assertThat(sql).startsWith("UPDATE").contains("dummy_entity").contains("test = :test") + .contains("attr1 = :attr1").contains("attr2 = :attr2").contains("prefix2_attr1 = :prefix2_attr1") + .contains("prefix2_attr2 = :prefix2_attr2").contains("prefix_test = :prefix_test") + .contains("prefix_attr1 = :prefix_attr1").contains("prefix_attr2 = :prefix_attr2") .contains("prefix_prefix2_attr1 = :prefix_prefix2_attr1") .contains("prefix_prefix2_attr2 = :prefix_prefix2_attr2"); softAssertions.assertAll(); } + @Test // DATAJDBC-340 + @Ignore // this is just broken right now + public void deleteByPath() { + + final String sql = sqlGenerator + .createDeleteByPath(PropertyPathTestingUtils.toPath("embedded.other", DummyEntity2.class, context)); + + assertThat(sql).containsSequence("DELETE FROM other_entity", // + "WHERE", // + "embedded_with_reference IN (", // + "SELECT ", // + "id ", // + "FROM", // + "dummy_entity2", // + "WHERE", // + "embedded_with_reference = :rootId"); + } + + @Test // DATAJDBC-340 + public void noJoinForEmbedded() { + + SqlGenerator.Join join = generateJoin("embeddable", DummyEntity.class); + + assertThat(join).isNull(); + } + + @Test // DATAJDBC-340 + public void columnForEmbeddedProperty() { + + assertThat(generatedColumn("embeddable.test", DummyEntity.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) + .containsExactly("test", "dummy_entity", null, "test"); + } + + @Test // DATAJDBC-340 + public void noColumnForEmbedded() { + + assertThat(generatedColumn("embeddable", DummyEntity.class)) // + .isNull(); + } + + @Test // DATAJDBC-340 + public void noJoinForPrefixedEmbedded() { + + SqlGenerator.Join join = generateJoin("prefixedEmbeddable", DummyEntity.class); + + assertThat(join).isNull(); + } + + @Test // DATAJDBC-340 + public void columnForPrefixedEmbeddedProperty() { + + assertThat(generatedColumn("prefixedEmbeddable.test", DummyEntity.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) + .containsExactly("prefix_test", "dummy_entity", null, "prefix_test"); + } + + @Test // DATAJDBC-340 + public void noJoinForCascadedEmbedded() { + + SqlGenerator.Join join = generateJoin("embeddable.embeddable", DummyEntity.class); + + assertThat(join).isNull(); + } + + @Test // DATAJDBC-340 + public void columnForCascadedEmbeddedProperty() { + + assertThat(generatedColumn("embeddable.embeddable.attr1", DummyEntity.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) + .containsExactly("attr1", "dummy_entity", null, "attr1"); + } + + @Test // DATAJDBC-340 + public void joinForEmbeddedWithReference() { + + SqlGenerator.Join join = generateJoin("embedded.other", DummyEntity2.class); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(join.getJoinTable().getName()).isEqualTo("other_entity"); + softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); + softly.assertThat(join.getJoinColumn().getName()).isEqualTo("embedded_with_reference"); + softly.assertThat(join.getParentId().getName()).isEqualTo("id"); + softly.assertThat(join.getParentId().getTable().getName()).isEqualTo("dummy_entity2"); + }); + } + + @Test // DATAJDBC-340 + public void columnForEmbeddedWithReferenceProperty() { + + assertThat(generatedColumn("embedded.other.value", DummyEntity2.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) + .containsExactly("value", "other_entity", "prefix_other", "prefix_other_value"); + } + + private SqlGenerator.Join generateJoin(String path, Class type) { + return createSqlGenerator(type) + .getJoin(new PersistentPropertyPathExtension(context, PropertyPathTestingUtils.toPath(path, type, context))); + } + + private String getAlias(Object maybeAliased) { + + if (maybeAliased instanceof Aliased) { + return ((Aliased) maybeAliased).getAlias(); + } + return null; + } + + private org.springframework.data.relational.core.sql.Column generatedColumn(String path, Class type) { + return createSqlGenerator(type) + .getColumn(new PersistentPropertyPathExtension(context, PropertyPathTestingUtils.toPath(path, type, context))); + } + @SuppressWarnings("unused") static class DummyEntity { - @Column("id1") - @Id - Long id; + @Column("id1") @Id Long id; - @Embedded("prefix_") - CascadedEmbedded prefixedEmbeddable; + @Embedded("prefix_") CascadedEmbedded prefixedEmbeddable; - @Embedded - CascadedEmbedded embeddable; + @Embedded CascadedEmbedded embeddable; } @SuppressWarnings("unused") - static class CascadedEmbedded - { + static class CascadedEmbedded { String test; @Embedded("prefix2_") Embeddable prefixedEmbeddable; @Embedded Embeddable embeddable; } @SuppressWarnings("unused") - static class Embeddable - { + static class Embeddable { Long attr1; String attr2; } + + @SuppressWarnings("unused") + static class DummyEntity2 { + + @Id Long id; + + @Embedded("prefix_") EmbeddedWithReference embedded; + } + + static class EmbeddedWithReference { + OtherEntity other; + } + + static class OtherEntity { + String value; + } + } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java index 5fbc08eccf..2dd17b916c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -112,10 +112,10 @@ public void cascadingDeleteFirstLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref")); - assertThat(sql).isEqualTo( - "DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + "WHERE dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + + "WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity.dummy_entity = :rootId"); } @Test // DATAJDBC-107 @@ -123,11 +123,13 @@ public void cascadingDeleteAllSecondLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteByPath(getPath("ref.further")); assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity " - + "WHERE referenced_entity IN " + "(SELECT FixedCustomPropertyPrefix_l1id " - + "FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + "WHERE dummy_entity = :rootId)"); + + "WHERE FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity.referenced_entity IN " + + "(SELECT FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity.FixedCustomPropertyPrefix_l1id " + + "FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + + "WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity.dummy_entity = :rootId)"); } @Test // DATAJDBC-107 @@ -145,10 +147,10 @@ public void cascadingDeleteAllFirstLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteAllSql(getPath("ref", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref")); - assertThat(sql).isEqualTo( - "DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + "WHERE dummy_entity IS NOT NULL"); + assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + + "WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity.dummy_entity IS NOT NULL"); } @Test // DATAJDBC-107 @@ -156,11 +158,13 @@ public void cascadingDeleteSecondLevel() { SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); - String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further", DummyEntity.class)); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further")); assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity " - + "WHERE referenced_entity IN " + "(SELECT FixedCustomPropertyPrefix_l1id " - + "FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + "WHERE dummy_entity IS NOT NULL)"); + + "WHERE FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity.referenced_entity IN " + + "(SELECT FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity.FixedCustomPropertyPrefix_l1id " + + "FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + + "WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity.dummy_entity IS NOT NULL)"); } @Test // DATAJDBC-113 @@ -171,25 +175,24 @@ public void deleteByList() { String sql = sqlGenerator.getDeleteByList(); assertThat(sql).isEqualTo( - "DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_DummyEntity WHERE FixedCustomPropertyPrefix_id IN (:ids)"); + "DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_DummyEntity WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity.FixedCustomPropertyPrefix_id IN (:ids)"); } - private PersistentPropertyPath getPath(String path, Class baseType) { - return PersistentPropertyPathTestUtils.getPath(context, path, baseType); + private PersistentPropertyPath getPath(String path) { + return PersistentPropertyPathTestUtils.getPath(context, path, DummyEntity.class); } /** * Plug in a custom {@link NamingStrategy} for this test case. - * - * @param namingStrategy */ private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { RelationalMappingContext context = new JdbcMappingContext(namingStrategy); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); - return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); + return new SqlGenerator(context, persistentEntity); } + @SuppressWarnings("unused") static class DummyEntity { @Id Long id; @@ -197,6 +200,7 @@ static class DummyEntity { ReferencedEntity ref; } + @SuppressWarnings("unused") static class ReferencedEntity { @Id Long l1id; @@ -204,6 +208,7 @@ static class ReferencedEntity { SecondLevelReferencedEntity further; } + @SuppressWarnings("unused") static class SecondLevelReferencedEntity { @Id Long l2id; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java index 64c613ef7f..a7e6e064b9 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java @@ -24,6 +24,7 @@ import org.assertj.core.api.SoftAssertions; import org.junit.Before; import org.junit.Test; + import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.jdbc.core.mapping.AggregateReference; @@ -35,6 +36,8 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.Table; /** * Unit tests for the {@link SqlGenerator}. @@ -43,25 +46,24 @@ * @author Greg Turnquist * @author Oleksandr Kucher * @author Bastian Wilhelm + * @author Mark Paluch */ public class SqlGeneratorUnitTests { - private SqlGenerator sqlGenerator; - private RelationalMappingContext context = new JdbcMappingContext(); + SqlGenerator sqlGenerator; + NamingStrategy namingStrategy = new PrefixingNamingStrategy(); + RelationalMappingContext context = new JdbcMappingContext(namingStrategy); @Before public void setUp() { - this.sqlGenerator = createSqlGenerator(DummyEntity.class); } SqlGenerator createSqlGenerator(Class type) { - NamingStrategy namingStrategy = new PrefixingNamingStrategy(); - RelationalMappingContext context = new JdbcMappingContext(namingStrategy); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(type); - return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); + return new SqlGenerator(context, persistentEntity); } @Test // DATAJDBC-112 @@ -87,18 +89,18 @@ public void findOne() { @Test // DATAJDBC-112 public void cascadingDeleteFirstLevel() { - String sql = sqlGenerator.createDeleteByPath(getPath("ref")); + String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId"); } @Test // DATAJDBC-112 - public void cascadingDeleteAllSecondLevel() { + public void cascadingDeleteByPathSecondLevel() { - String sql = sqlGenerator.createDeleteByPath(getPath("ref.further")); + String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( - "DELETE FROM second_level_referenced_entity WHERE referenced_entity IN (SELECT x_l1id FROM referenced_entity WHERE dummy_entity = :rootId)"); + "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId)"); } @Test // DATAJDBC-112 @@ -112,57 +114,61 @@ public void deleteAll() { @Test // DATAJDBC-112 public void cascadingDeleteAllFirstLevel() { - String sql = sqlGenerator.createDeleteAllSql(getPath("ref")); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE dummy_entity IS NOT NULL"); + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity IS NOT NULL"); } @Test // DATAJDBC-112 - public void cascadingDeleteSecondLevel() { + public void cascadingDeleteAllSecondLevel() { - String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further")); + String sql = sqlGenerator.createDeleteAllSql(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( - "DELETE FROM second_level_referenced_entity WHERE referenced_entity IN (SELECT x_l1id FROM referenced_entity WHERE dummy_entity IS NOT NULL)"); + "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity IS NOT NULL)"); } @Test // DATAJDBC-227 public void deleteAllMap() { - String sql = sqlGenerator.createDeleteAllSql(getPath("mappedElements")); + String sql = sqlGenerator.createDeleteAllSql(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE dummy_entity IS NOT NULL"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity IS NOT NULL"); } @Test // DATAJDBC-227 public void deleteMapByPath() { - String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements")); + String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :rootId"); } @Test // DATAJDBC-131, DATAJDBC-111 public void findAllByProperty() { // this would get called when ListParent is the element type of a Set - String sql = sqlGenerator.getFindAllByProperty("back-ref", null, false); - - assertThat(sql).isEqualTo("SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name, " // - + "dummy_entity.x_other AS x_other, " // - + "ref.x_l1id AS ref_x_l1id, ref.x_content AS ref_x_content, " - + "ref_further.x_l2id AS ref_further_x_l2id, ref_further.x_something AS ref_further_x_something " // - + "FROM dummy_entity " // - + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1 " // - + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = referenced_entity.x_l1id " // - + "WHERE back-ref = :back-ref"); + String sql = sqlGenerator.getFindAllByProperty("backref", null, false); + + assertThat(sql).contains("SELECT", // + "dummy_entity.id1 AS id1", // + "dummy_entity.x_name AS x_name", // + "dummy_entity.x_other AS x_other", // + "ref.x_l1id AS ref_x_l1id", // + "ref.x_content AS ref_x_content", // + "ref_further.x_l2id AS ref_further_x_l2id", // + "ref_further.x_something AS ref_further_x_something", // + "FROM dummy_entity ", // + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1", // + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = ref.x_l1id", // + "WHERE dummy_entity.backref = :backref"); } @Test // DATAJDBC-131, DATAJDBC-111 public void findAllByPropertyWithKey() { // this would get called when ListParent is th element type of a Map - String sql = sqlGenerator.getFindAllByProperty("back-ref", "key-column", false); + String sql = sqlGenerator.getFindAllByProperty("backref", "key-column", false); assertThat(sql).isEqualTo("SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name, " // + "dummy_entity.x_other AS x_other, " // @@ -170,9 +176,9 @@ public void findAllByPropertyWithKey() { + "ref_further.x_l2id AS ref_further_x_l2id, ref_further.x_something AS ref_further_x_something, " // + "dummy_entity.key-column AS key-column " // + "FROM dummy_entity " // - + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1 " // - + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = referenced_entity.x_l1id " // - + "WHERE back-ref = :back-ref"); + + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1 " // + + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = ref.x_l1id " // + + "WHERE dummy_entity.backref = :backref"); } @Test(expected = IllegalArgumentException.class) // DATAJDBC-130 @@ -184,7 +190,7 @@ public void findAllByPropertyOrderedWithoutKey() { public void findAllByPropertyWithKeyOrdered() { // this would get called when ListParent is th element type of a Map - String sql = sqlGenerator.getFindAllByProperty("back-ref", "key-column", true); + String sql = sqlGenerator.getFindAllByProperty("backref", "key-column", true); assertThat(sql).isEqualTo("SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name, " // + "dummy_entity.x_other AS x_other, " // @@ -192,9 +198,9 @@ public void findAllByPropertyWithKeyOrdered() { + "ref_further.x_l2id AS ref_further_x_l2id, ref_further.x_something AS ref_further_x_something, " // + "dummy_entity.key-column AS key-column " // + "FROM dummy_entity " // - + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1 " // - + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = referenced_entity.x_l1id " // - + "WHERE back-ref = :back-ref " + "ORDER BY key-column"); + + "LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1 " // + + "LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = ref.x_l1id " // + + "WHERE dummy_entity.backref = :backref " + "ORDER BY key-column"); } @Test // DATAJDBC-264 @@ -214,9 +220,8 @@ public void getInsertForQuotedColumnName() { String insert = sqlGenerator.getInsert(emptySet()); - assertThat(insert).isEqualTo("INSERT INTO entity_with_quoted_column_name " + - "(\"test_@123\") " + - "VALUES (:test_123)"); + assertThat(insert) + .isEqualTo("INSERT INTO entity_with_quoted_column_name " + "(\"test_@123\") " + "VALUES (:test_123)"); } @Test // DATAJDBC-266 @@ -249,7 +254,7 @@ public void readOnlyPropertyExcludedFromQuery_when_generateUpdateSql() { assertThat(sqlGenerator.getUpdate()).isEqualToIgnoringCase( // "UPDATE entity_with_read_only_property " // + "SET x_name = :x_name " // - + "WHERE x_id = :x_id" // + + "WHERE entity_with_read_only_property.x_id = :x_id" // ); } @@ -260,9 +265,8 @@ public void getUpdateForQuotedColumnName() { String update = sqlGenerator.getUpdate(); - assertThat(update).isEqualTo("UPDATE entity_with_quoted_column_name " + - "SET \"test_@123\" = :test_123 " + - "WHERE \"test_@id\" = :test_id"); + assertThat(update).isEqualTo("UPDATE entity_with_quoted_column_name " + "SET \"test_@123\" = :test_123 " + + "WHERE entity_with_quoted_column_name.\"test_@id\" = :test_id"); } @Test // DATAJDBC-324 @@ -292,15 +296,15 @@ public void readOnlyPropertyIncludedIntoQuery_when_generateFindAllByPropertySql( final SqlGenerator sqlGenerator = createSqlGenerator(EntityWithReadOnlyProperty.class); - assertThat(sqlGenerator.getFindAllByProperty("back-ref", "key-column", true)).isEqualToIgnoringCase( // + assertThat(sqlGenerator.getFindAllByProperty("backref", "key-column", true)).isEqualToIgnoringCase( // "SELECT " // - + "entity_with_read_only_property.x_id AS x_id, " // - + "entity_with_read_only_property.x_name AS x_name, " // - + "entity_with_read_only_property.x_read_only_value AS x_read_only_value, " // - + "entity_with_read_only_property.key-column AS key-column " // - + "FROM entity_with_read_only_property " // - + "WHERE back-ref = :back-ref " // - + "ORDER BY key-column" // + + "entity_with_read_only_property.x_id AS x_id, " // + + "entity_with_read_only_property.x_name AS x_name, " // + + "entity_with_read_only_property.x_read_only_value AS x_read_only_value, " // + + "entity_with_read_only_property.key-column AS key-column " // + + "FROM entity_with_read_only_property " // + + "WHERE entity_with_read_only_property.backref = :backref " // + + "ORDER BY key-column" // ); } @@ -311,11 +315,11 @@ public void readOnlyPropertyIncludedIntoQuery_when_generateFindAllInListSql() { assertThat(sqlGenerator.getFindAllInList()).isEqualToIgnoringCase( // "SELECT " // - + "entity_with_read_only_property.x_id AS x_id, " // - + "entity_with_read_only_property.x_name AS x_name, " // - + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // - + "FROM entity_with_read_only_property " // - + "WHERE entity_with_read_only_property.x_id in(:ids)" // + + "entity_with_read_only_property.x_id AS x_id, " // + + "entity_with_read_only_property.x_name AS x_name, " // + + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // + + "FROM entity_with_read_only_property " // + + "WHERE entity_with_read_only_property.x_id IN (:ids)" // ); } @@ -326,23 +330,158 @@ public void readOnlyPropertyIncludedIntoQuery_when_generateFindOneSql() { assertThat(sqlGenerator.getFindOne()).isEqualToIgnoringCase( // "SELECT " // - + "entity_with_read_only_property.x_id AS x_id, " // - + "entity_with_read_only_property.x_name AS x_name, " // - + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // - + "FROM entity_with_read_only_property " // - + "WHERE entity_with_read_only_property.x_id = :id" // + + "entity_with_read_only_property.x_id AS x_id, " // + + "entity_with_read_only_property.x_name AS x_name, " // + + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // + + "FROM entity_with_read_only_property " // + + "WHERE entity_with_read_only_property.x_id = :id" // ); } - private PersistentPropertyPath getPath(String path) { - return PersistentPropertyPathTestUtils.getPath(context, path, DummyEntity.class); + @Test // DATAJDBC-340 + public void deletingLongChain() { + + assertThat( + createSqlGenerator(Chain4.class).createDeleteByPath(getPath("chain3.chain2.chain1.chain0", Chain4.class))) // + .isEqualTo("DELETE FROM chain0 " + // + "WHERE chain0.chain1 IN (" + // + "SELECT chain1.x_one " + // + "FROM chain1 " + // + "WHERE chain1.chain2 IN (" + // + "SELECT chain2.x_two " + // + "FROM chain2 " + // + "WHERE chain2.chain3 IN (" + // + "SELECT chain3.x_three " + // + "FROM chain3 " + // + "WHERE chain3.chain4 = :rootId" + // + ")))"); + } + + @Test // DATAJDBC-340 + public void noJoinForSimpleColumn() { + assertThat(generateJoin("id", DummyEntity.class)).isNull(); + } + + @Test // DATAJDBC-340 + public void joinForSimpleReference() { + + SoftAssertions.assertSoftly(softly -> { + + SqlGenerator.Join join = generateJoin("ref", DummyEntity.class); + softly.assertThat(join.getJoinTable().getName()).isEqualTo("referenced_entity"); + softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); + softly.assertThat(join.getJoinColumn().getName()).isEqualTo("dummy_entity"); + softly.assertThat(join.getParentId().getName()).isEqualTo("id1"); + softly.assertThat(join.getParentId().getTable().getName()).isEqualTo("dummy_entity"); + }); + } + + @Test // DATAJDBC-340 + public void noJoinForCollectionReference() { + + SqlGenerator.Join join = generateJoin("elements", DummyEntity.class); + + assertThat(join).isNull(); + + } + + @Test // DATAJDBC-340 + public void noJoinForMappedReference() { + + SqlGenerator.Join join = generateJoin("mappedElements", DummyEntity.class); + + assertThat(join).isNull(); + } + + @Test // DATAJDBC-340 + public void joinForSecondLevelReference() { + + SoftAssertions.assertSoftly(softly -> { + + SqlGenerator.Join join = generateJoin("ref.further", DummyEntity.class); + softly.assertThat(join.getJoinTable().getName()).isEqualTo("second_level_referenced_entity"); + softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); + softly.assertThat(join.getJoinColumn().getName()).isEqualTo("referenced_entity"); + softly.assertThat(join.getParentId().getName()).isEqualTo("x_l1id"); + softly.assertThat(join.getParentId().getTable().getName()).isEqualTo("referenced_entity"); + }); + } + + @Test // DATAJDBC-340 + public void joinForOneToOneWithoutId() { + + SoftAssertions.assertSoftly(softly -> { + + SqlGenerator.Join join = generateJoin("child", ParentOfNoIdChild.class); + Table joinTable = join.getJoinTable(); + softly.assertThat(joinTable.getName()).isEqualTo("no_id_child"); + softly.assertThat(joinTable).isInstanceOf(Aliased.class); + softly.assertThat(((Aliased) joinTable).getAlias()).isEqualTo("child"); + softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(joinTable); + softly.assertThat(join.getJoinColumn().getName()).isEqualTo("parent_of_no_id_child"); + softly.assertThat(join.getParentId().getName()).isEqualTo("x_id"); + softly.assertThat(join.getParentId().getTable().getName()).isEqualTo("parent_of_no_id_child"); + + }); + } + + private SqlGenerator.Join generateJoin(String path, Class type) { + return createSqlGenerator(type) + .getJoin(new PersistentPropertyPathExtension(context, PropertyPathTestingUtils.toPath(path, type, context))); + } + + @Test // DATAJDBC-340 + public void simpleColumn() { + + assertThat(generatedColumn("id", DummyEntity.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) + .containsExactly("id1", "dummy_entity", null, "id1"); + } + + @Test // DATAJDBC-340 + public void columnForIndirectProperty() { + + assertThat(generatedColumn("ref.l1id", DummyEntity.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) // + .containsExactly("x_l1id", "referenced_entity", "ref", "ref_x_l1id"); + } + + @Test // DATAJDBC-340 + public void noColumnForReferencedEntity() { + + assertThat(generatedColumn("ref", DummyEntity.class)).isNull(); + } + + @Test // DATAJDBC-340 + public void columnForReferencedEntityWithoutId() { + + assertThat(generatedColumn("child", ParentOfNoIdChild.class)) // + .extracting(c -> c.getName(), c -> c.getTable().getName(), c -> getAlias(c.getTable()), this::getAlias) // + .containsExactly("parent_of_no_id_child", "no_id_child", "child", "child_parent_of_no_id_child"); + } + + private String getAlias(Object maybeAliased) { + + if (maybeAliased instanceof Aliased) { + return ((Aliased) maybeAliased).getAlias(); + } + return null; + } + + private org.springframework.data.relational.core.sql.Column generatedColumn(String path, Class type) { + + return createSqlGenerator(type) + .getColumn(new PersistentPropertyPathExtension(context, PropertyPathTestingUtils.toPath(path, type, context))); + } + + private PersistentPropertyPath getPath(String path, Class baseType) { + return PersistentPropertyPathTestUtils.getPath(context, path, baseType); } @SuppressWarnings("unused") static class DummyEntity { - @Column("id1") - @Id Long id; + @Column("id1") @Id Long id; String name; ReferencedEntity ref; Set elements; @@ -412,4 +551,38 @@ static class EntityWithQuotedColumnName { @Id @Column("\"test_@id\"") Long id; @Column("\"test_@123\"") String name; } + + @SuppressWarnings("unused") + static class Chain0 { + @Id Long zero; + String zeroValue; + } + + @SuppressWarnings("unused") + static class Chain1 { + @Id Long one; + String oneValue; + Chain0 chain0; + } + + @SuppressWarnings("unused") + static class Chain2 { + @Id Long two; + String twoValue; + Chain1 chain1; + } + + @SuppressWarnings("unused") + static class Chain3 { + @Id Long three; + String threeValue; + Chain2 chain2; + } + + @SuppressWarnings("unused") + static class Chain4 { + @Id Long four; + String fourValue; + Chain3 chain3; + } } diff --git a/spring-data-jdbc/src/test/resources/logback.xml b/spring-data-jdbc/src/test/resources/logback.xml index 6da1bab099..f1bfdbaf39 100644 --- a/spring-data-jdbc/src/test/resources/logback.xml +++ b/spring-data-jdbc/src/test/resources/logback.xml @@ -7,8 +7,8 @@ - - + + diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index 1699ad6c50..43e6772c4e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -1,20 +1,93 @@ -CREATE TABLE LEGO_SET ( id1 BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(30)); -CREATE TABLE MANUAL ( id2 BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, LEGO_SET BIGINT, ALTERNATIVE BIGINT, CONTENT VARCHAR(2000)); +CREATE TABLE LEGO_SET +( + id1 BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(30) +); +CREATE TABLE MANUAL +( + id2 BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + LEGO_SET BIGINT, + ALTERNATIVE BIGINT, + CONTENT VARCHAR(2000) +); -ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) -REFERENCES LEGO_SET(id1); +ALTER TABLE MANUAL + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET (id1); -CREATE TABLE ONE_TO_ONE_PARENT ( id3 BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, content VARCHAR(30)); -CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, content VARCHAR(30)); +CREATE TABLE ONE_TO_ONE_PARENT +( + id3 BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + content VARCHAR(30) +); +CREATE TABLE Child_No_Id +( + ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, + content VARCHAR(30) +); -CREATE TABLE LIST_PARENT ( id4 BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE ELEMENT_NO_ID ( content VARCHAR(100), LIST_PARENT_KEY BIGINT, LIST_PARENT BIGINT); +CREATE TABLE LIST_PARENT +( + id4 BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE ELEMENT_NO_ID +( + content VARCHAR(100), + LIST_PARENT_KEY BIGINT, + LIST_PARENT BIGINT +); ALTER TABLE ELEMENT_NO_ID ADD FOREIGN KEY (LIST_PARENT) - REFERENCES LIST_PARENT(id4); + REFERENCES LIST_PARENT (id4); +CREATE TABLE ARRAY_OWNER +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS VARCHAR(20) ARRAY[10] NOT NULL, + MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL +); -CREATE TABLE ARRAY_OWNER (ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, DIGITS VARCHAR(20) ARRAY[10] NOT NULL, MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL); +CREATE TABLE BYTE_ARRAY_OWNER +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + BINARY_DATA VARBINARY(20) NOT NULL +); -CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL); +CREATE TABLE CHAIN4 +( + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); +CREATE TABLE CHAIN3 +( + THREE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 30) PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) +); + +CREATE TABLE CHAIN2 +( + TWO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 20) PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) +); + +CREATE TABLE CHAIN1 +( + ONE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 10) PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) +); + +CREATE TABLE CHAIN0 +( + ZERO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 0) PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 38957bcc61..cccddb9777 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -1,13 +1,84 @@ -CREATE TABLE LEGO_SET ( id1 BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(30)); -CREATE TABLE MANUAL ( id2 BIGINT AUTO_INCREMENT PRIMARY KEY, LEGO_SET BIGINT, ALTERNATIVE BIGINT, CONTENT VARCHAR(2000)); +CREATE TABLE LEGO_SET +( + id1 BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(30) +); +CREATE TABLE MANUAL +( + id2 BIGINT AUTO_INCREMENT PRIMARY KEY, + LEGO_SET BIGINT, + ALTERNATIVE BIGINT, + CONTENT VARCHAR(2000) +); -ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) -REFERENCES LEGO_SET(id1); +ALTER TABLE MANUAL + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET (id1); -CREATE TABLE ONE_TO_ONE_PARENT ( id3 BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(30)); -CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, content VARCHAR(30)); +CREATE TABLE ONE_TO_ONE_PARENT +( + id3 BIGINT AUTO_INCREMENT PRIMARY KEY, + content VARCHAR(30) +); +CREATE TABLE Child_No_Id +( + ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, + content VARCHAR(30) +); -CREATE TABLE LIST_PARENT ( id4 BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); +CREATE TABLE LIST_PARENT +( + id4 BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE element_no_id +( + content VARCHAR(100), + LIST_PARENT_key BIGINT, + LIST_PARENT BIGINT +); -CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT AUTO_INCREMENT PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL) \ No newline at end of file +CREATE TABLE BYTE_ARRAY_OWNER +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + BINARY_DATA VARBINARY(20) NOT NULL +); + +CREATE TABLE CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + + +CREATE TABLE CHAIN3 +( + THREE BIGINT AUTO_INCREMENT PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4(FOUR) +); + +CREATE TABLE CHAIN2 +( + TWO BIGINT AUTO_INCREMENT PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3(THREE) +); + +CREATE TABLE CHAIN1 +( + ONE BIGINT AUTO_INCREMENT PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2(TWO) +); + +CREATE TABLE CHAIN0 +( + ZERO BIGINT AUTO_INCREMENT PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1(ONE) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index b507634c35..ff06116d2e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -1,18 +1,93 @@ DROP TABLE IF EXISTS MANUAL; DROP TABLE IF EXISTS LEGO_SET; -CREATE TABLE LEGO_SET ( id1 BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(30)); -CREATE TABLE MANUAL ( id2 BIGINT IDENTITY PRIMARY KEY, LEGO_SET BIGINT, ALTERNATIVE BIGINT, CONTENT VARCHAR(2000)); -ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) REFERENCES LEGO_SET(id1); +CREATE TABLE LEGO_SET +( + id1 BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(30) +); +CREATE TABLE MANUAL +( + id2 BIGINT IDENTITY PRIMARY KEY, + LEGO_SET BIGINT, + ALTERNATIVE BIGINT, + CONTENT VARCHAR(2000) +); +ALTER TABLE MANUAL + ADD FOREIGN KEY (LEGO_SET) REFERENCES LEGO_SET (id1); DROP TABLE IF EXISTS Child_No_Id; DROP TABLE IF EXISTS ONE_TO_ONE_PARENT; -CREATE TABLE ONE_TO_ONE_PARENT ( id3 BIGINT IDENTITY PRIMARY KEY, content VARCHAR(30)); -CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT BIGINT PRIMARY KEY, content VARCHAR(30)); +CREATE TABLE ONE_TO_ONE_PARENT +( + id3 BIGINT IDENTITY PRIMARY KEY, + content VARCHAR(30) +); +CREATE TABLE Child_No_Id +( + ONE_TO_ONE_PARENT BIGINT PRIMARY KEY, + content VARCHAR(30) +); DROP TABLE IF EXISTS element_no_id; DROP TABLE IF EXISTS LIST_PARENT; -CREATE TABLE LIST_PARENT ( id4 BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); +CREATE TABLE LIST_PARENT +( + id4 BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE element_no_id +( + content VARCHAR(100), + LIST_PARENT_key BIGINT, + LIST_PARENT BIGINT +); DROP TABLE IF EXISTS BYTE_ARRAY_OWNER; -CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT IDENTITY PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL) \ No newline at end of file +CREATE TABLE BYTE_ARRAY_OWNER +( + ID BIGINT IDENTITY PRIMARY KEY, + BINARY_DATA VARBINARY(20) NOT NULL +); + +DROP TABLE IF EXISTS CHAIN4; +CREATE TABLE CHAIN4 +( + FOUR BIGINT IDENTITY PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +DROP TABLE IF EXISTS CHAIN3; +CREATE TABLE CHAIN3 +( + THREE BIGINT IDENTITY PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) +); + +DROP TABLE IF EXISTS CHAIN2; +CREATE TABLE CHAIN2 +( + TWO BIGINT IDENTITY PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) +); + +DROP TABLE IF EXISTS CHAIN1; +CREATE TABLE CHAIN1 +( + ONE BIGINT IDENTITY PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) +); + +DROP TABLE IF EXISTS CHAIN0; +CREATE TABLE CHAIN0 +( + ZERO BIGINT IDENTITY PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 38957bcc61..3000a7a0a8 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -1,13 +1,84 @@ -CREATE TABLE LEGO_SET ( id1 BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(30)); -CREATE TABLE MANUAL ( id2 BIGINT AUTO_INCREMENT PRIMARY KEY, LEGO_SET BIGINT, ALTERNATIVE BIGINT, CONTENT VARCHAR(2000)); +CREATE TABLE LEGO_SET +( + id1 BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(30) +); +CREATE TABLE MANUAL +( + id2 BIGINT AUTO_INCREMENT PRIMARY KEY, + LEGO_SET BIGINT, + ALTERNATIVE BIGINT, + CONTENT VARCHAR(2000) +); -ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) -REFERENCES LEGO_SET(id1); +ALTER TABLE MANUAL + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET (id1); -CREATE TABLE ONE_TO_ONE_PARENT ( id3 BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(30)); -CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, content VARCHAR(30)); +CREATE TABLE ONE_TO_ONE_PARENT +( + id3 BIGINT AUTO_INCREMENT PRIMARY KEY, + content VARCHAR(30) +); +CREATE TABLE Child_No_Id +( + ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, + content VARCHAR(30) +); -CREATE TABLE LIST_PARENT ( id4 BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT BIGINT); +CREATE TABLE LIST_PARENT +( + id4 BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE element_no_id +( + content VARCHAR(100), + LIST_PARENT_key BIGINT, + LIST_PARENT BIGINT +); -CREATE TABLE BYTE_ARRAY_OWNER (ID BIGINT AUTO_INCREMENT PRIMARY KEY, BINARY_DATA VARBINARY(20) NOT NULL) \ No newline at end of file +CREATE TABLE BYTE_ARRAY_OWNER +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + BINARY_DATA VARBINARY(20) NOT NULL +); + +CREATE TABLE CHAIN4 +( + FOUR BIGINT AUTO_INCREMENT PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + + +CREATE TABLE CHAIN3 +( + THREE BIGINT AUTO_INCREMENT PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4(FOUR) +); + +CREATE TABLE CHAIN2 +( + TWO BIGINT AUTO_INCREMENT PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3(THREE) +); + +CREATE TABLE CHAIN1 +( + ONE BIGINT AUTO_INCREMENT PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2(TWO) +); + +CREATE TABLE CHAIN0 +( + ZERO BIGINT AUTO_INCREMENT PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1(ONE) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index 4f39362347..f996800a45 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -6,19 +6,99 @@ DROP TABLE LIST_PARENT; DROP TABLE element_no_id; DROP TABLE ARRAY_OWNER; DROP TABLE BYTE_ARRAY_OWNER; +DROP TABLE CHAIN4; +DROP TABLE CHAIN3; +DROP TABLE CHAIN2; +DROP TABLE CHAIN1; +DROP TABLE CHAIN0; -CREATE TABLE LEGO_SET ( id1 SERIAL PRIMARY KEY, NAME VARCHAR(30)); -CREATE TABLE MANUAL ( id2 SERIAL PRIMARY KEY, LEGO_SET BIGINT, ALTERNATIVE BIGINT, CONTENT VARCHAR(2000)); +CREATE TABLE LEGO_SET +( + id1 SERIAL PRIMARY KEY, + NAME VARCHAR(30) +); +CREATE TABLE MANUAL +( + id2 SERIAL PRIMARY KEY, + LEGO_SET BIGINT, + ALTERNATIVE BIGINT, + CONTENT VARCHAR(2000) +); -ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) -REFERENCES LEGO_SET(id1); +ALTER TABLE MANUAL + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET (id1); -CREATE TABLE ONE_TO_ONE_PARENT ( id3 SERIAL PRIMARY KEY, content VARCHAR(30)); -CREATE TABLE Child_No_Id (ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, content VARCHAR(30)); +CREATE TABLE ONE_TO_ONE_PARENT +( + id3 SERIAL PRIMARY KEY, + content VARCHAR(30) +); +CREATE TABLE Child_No_Id +( + ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, + content VARCHAR(30) +); -CREATE TABLE LIST_PARENT ( id4 SERIAL PRIMARY KEY, NAME VARCHAR(100)); -CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, LIST_PARENT INTEGER); +CREATE TABLE LIST_PARENT +( + id4 SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE element_no_id +( + content VARCHAR(100), + LIST_PARENT_key BIGINT, + LIST_PARENT INTEGER +); -CREATE TABLE ARRAY_OWNER (ID SERIAL PRIMARY KEY, DIGITS VARCHAR(20)[10], MULTIDIMENSIONAL VARCHAR(20)[10][10]); +CREATE TABLE ARRAY_OWNER +( + ID SERIAL PRIMARY KEY, + DIGITS VARCHAR(20)[10], + MULTIDIMENSIONAL VARCHAR(20)[10][10] +); -CREATE TABLE BYTE_ARRAY_OWNER (ID SERIAL PRIMARY KEY, BINARY_DATA BYTEA NOT NULL) \ No newline at end of file +CREATE TABLE BYTE_ARRAY_OWNER +( + ID SERIAL PRIMARY KEY, + BINARY_DATA BYTEA NOT NULL +); + +CREATE TABLE CHAIN4 +( + FOUR SERIAL PRIMARY KEY, + FOUR_VALUE VARCHAR(20) +); + +CREATE TABLE CHAIN3 +( + THREE SERIAL PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) +); + +CREATE TABLE CHAIN2 +( + TWO SERIAL PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) +); + +CREATE TABLE CHAIN1 +( + ONE SERIAL PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) +); + +CREATE TABLE CHAIN0 +( + ZERO SERIAL PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) +); \ No newline at end of file diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index ba0ffea143..11717415c2 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -5,7 +5,7 @@ 4.0.0 spring-data-relational - 1.1.0.BUILD-SNAPSHOT + 1.1.0.DATAJDBC-340-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -13,7 +13,7 @@ org.springframework.data spring-data-relational-parent - 1.1.0.BUILD-SNAPSHOT + 1.1.0.DATAJDBC-340-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java index 1dc41820e5..97e71b2a6d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java @@ -24,6 +24,7 @@ * Renders to: {@code } or {@code .}. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 */ public class Column extends AbstractSegment implements Expression, Named { @@ -182,6 +183,16 @@ public In in(Expression... expression) { return Conditions.in(this, expression); } + /** + * Creates a new {@link In} {@link Condition} given a subselects. + * + * @param subselect right side of the comparison. + * @return the {@link In} condition. + */ + public In in(Select subselect) { + return Conditions.in(this, subselect); + } + /** * Creates a {@code IS NULL} condition. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java index a3c7a2f15e..ea71b0c63c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java @@ -139,7 +139,7 @@ public static Like like(Expression leftColumnOrExpression, Expression rightColum * @param arg IN argument. * @return the {@link In} condition. */ - public static Condition in(Expression columnOrExpression, Expression arg) { + public static In in(Expression columnOrExpression, Expression arg) { Assert.notNull(columnOrExpression, "Comparison column or expression must not be null"); Assert.notNull(arg, "Expression argument must not be null"); @@ -184,7 +184,7 @@ public static In in(Expression columnOrExpression, Expression... expressions) { * @param subselect the subselect. * @return the {@link In} condition. */ - public static Condition in(Column column, Select subselect) { + public static In in(Column column, Select subselect) { Assert.notNull(column, "Column must not be null"); Assert.notNull(subselect, "Subselect must not be null"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultInsertBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultInsertBuilder.java index f477f762ef..f263796f83 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultInsertBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultInsertBuilder.java @@ -40,7 +40,7 @@ class DefaultInsertBuilder * @see org.springframework.data.relational.core.sql.InsertBuilder#into(org.springframework.data.relational.core.sql.Table) */ @Override - public InsertIntoColumnsAndValues into(Table table) { + public InsertIntoColumnsAndValuesWithBuild into(Table table) { Assert.notNull(table, "Insert Into Table must not be null!"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java index 84383b8795..9571dd07c8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java @@ -30,6 +30,7 @@ * Default {@link SelectBuilder} implementation. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 */ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAndJoin, SelectWhereAndOr { @@ -249,6 +250,15 @@ public SelectOn join(Table table) { return new JoinBuilder(table, this); } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(org.springframework.data.relational.core.sql.Table) + */ + @Override + public SelectOn leftOuterJoin(Table table) { + return new JoinBuilder(table, this, JoinType.LEFT_OUTER_JOIN); + } + public DefaultSelectBuilder join(Join join) { this.joins.add(join); @@ -273,13 +283,21 @@ static class JoinBuilder implements SelectOn, SelectOnConditionComparison, Selec private final Table table; private final DefaultSelectBuilder selectBuilder; + private final JoinType joinType; private Expression from; private Expression to; private @Nullable Condition condition; - JoinBuilder(Table table, DefaultSelectBuilder selectBuilder) { + + JoinBuilder(Table table, DefaultSelectBuilder selectBuilder, JoinType joinType) { this.table = table; this.selectBuilder = selectBuilder; + + this.joinType = joinType; + } + + JoinBuilder(Table table, DefaultSelectBuilder selectBuilder) { + this(table, selectBuilder, JoinType.JOIN); } /* @@ -328,7 +346,7 @@ private void finishCondition() { private Join finishJoin() { finishCondition(); - return new Join(JoinType.JOIN, table, condition); + return new Join(joinType, table, condition); } /* @@ -391,6 +409,16 @@ public SelectOn join(Table table) { return selectBuilder.join(table); } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#leftOuterJoin(org.springframework.data.relational.core.sql.Table) + */ + @Override + public SelectOn leftOuterJoin(Table table) { + selectBuilder.join(finishJoin()); + return selectBuilder.leftOuterJoin(table); + } + /* * (non-Javadoc) * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoinCondition#limitOffset(long, long) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java index 431141e6e1..bd14a272af 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java @@ -19,6 +19,7 @@ * Factory for common {@link Expression}s. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 * @see SQL * @see Conditions @@ -55,7 +56,7 @@ public static Expression asterisk(Table table) { // Utility constructor. private Expressions() {} - static class SimpleExpression extends AbstractSegment implements Expression { + static public class SimpleExpression extends AbstractSegment implements Expression { private final String expression; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java index 5fd7699b19..fb49426a41 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java @@ -25,6 +25,7 @@ * Factory for common {@link Expression function expressions}. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 * @see SQL * @see Expressions @@ -38,7 +39,7 @@ public class Functions { * @param columns columns to apply count, must not be {@literal null}. * @return the new {@link SimpleFunction count function} for {@code columns}. */ - public static SimpleFunction count(Column... columns) { + public static SimpleFunction count(Expression... columns) { Assert.notNull(columns, "Columns must not be null!"); Assert.notEmpty(columns, "Columns must contains at least one column"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InsertBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InsertBuilder.java index f76e39657a..3a718b49ab 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InsertBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InsertBuilder.java @@ -21,6 +21,7 @@ * Entry point to construct an {@link Insert} statement. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 * @see StatementBuilder */ @@ -34,7 +35,7 @@ public interface InsertBuilder { * @see Into * @see SQL#table(String) */ - InsertIntoColumnsAndValues into(Table table); + InsertIntoColumnsAndValuesWithBuild into(Table table); /** * Interface exposing {@code WHERE} methods. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java index 5ee62bc524..a658b16520 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java @@ -21,6 +21,7 @@ * Entry point to construct a {@link Select} statement. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 * @see StatementBuilder */ @@ -460,6 +461,16 @@ interface SelectJoin extends BuildSelect { * @see SQL#table(String) */ SelectOn join(Table table); + + /** + * Declare a {@code LEFT OUTER JOIN} {@link Table}. + * + * @param table name of the table, must not be {@literal null}. + * @return {@code this} builder. + * @see Join + * @see SQL#table(String) + */ + SelectOn leftOuterJoin(Table table); } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java index 3cf74fd0d6..da6a3d61ee 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java @@ -26,6 +26,7 @@ * {@code JOIN} clause. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 */ class SelectValidator extends AbstractImportValidator { @@ -93,18 +94,18 @@ public void enter(Visitable segment) { return; } - super.enter(segment); + if (segment instanceof Expression && parent instanceof Select) { + selectFieldCount++; + } if (segment instanceof AsteriskFromTable && parent instanceof Select) { Table table = ((AsteriskFromTable) segment).getTable(); requiredBySelect.add(table); - selectFieldCount++; } if (segment instanceof Column && (parent instanceof Select || parent instanceof SimpleFunction)) { - selectFieldCount++; Table table = ((Column) segment).getTable(); if (table != null) { @@ -124,6 +125,7 @@ public void enter(Visitable segment) { if (segment instanceof Table && parent instanceof Join) { join.add((Table) segment); } + super.enter(segment); } /* diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InsertStatementVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InsertStatementVisitor.java index 5ba4375dc8..2f8af6d086 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InsertStatementVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/InsertStatementVisitor.java @@ -25,6 +25,7 @@ * {@link PartRenderer} for {@link Insert} statements. * * @author Mark Paluch + * @author Jens Schauder * @since 1.1 */ class InsertStatementVisitor extends DelegatingVisitor implements PartRenderer { @@ -94,17 +95,13 @@ public Delegation doLeave(Visitable segment) { builder.append("INSERT"); - if (into.length() != 0) { - builder.append(" INTO ").append(into); - } + builder.append(" INTO ").append(into); if (columns.length() != 0) { builder.append(" (").append(columns).append(")"); } - if (values.length() != 0) { - builder.append(" VALUES(").append(values).append(")"); - } + builder.append(" VALUES (").append(values).append(")"); return Delegation.leave(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java index c98fcd0d01..56a72788b8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java @@ -16,7 +16,9 @@ package org.springframework.data.relational.core.sql.render; import org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.AsteriskFromTable; import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Expression; import org.springframework.data.relational.core.sql.SelectList; import org.springframework.data.relational.core.sql.SimpleFunction; import org.springframework.data.relational.core.sql.Table; @@ -38,6 +40,7 @@ class SelectListVisitor extends TypedSubtreeVisitor implements PartR private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for // subelements. + SelectListVisitor(RenderContext context, RenderTarget target) { this.context = context; this.target = target; @@ -57,8 +60,6 @@ Delegation enterNested(Visitable segment) { if (segment instanceof SimpleFunction) { builder.append(((SimpleFunction) segment).getFunctionName()).append("("); insideFunction = true; - } else { - insideFunction = false; } return super.enterNested(segment); @@ -87,14 +88,26 @@ Delegation leaveNested(Visitable segment) { } if (segment instanceof SimpleFunction) { + builder.append(")"); + if (segment instanceof Aliased) { + builder.append(" AS ").append(((Aliased) segment).getAlias()); + } + + insideFunction = false; requiresComma = true; } else if (segment instanceof Column) { + builder.append(context.getNamingStrategy().getName((Column) segment)); - if (segment instanceof Aliased) { + if (segment instanceof Aliased && !insideFunction) { builder.append(" AS ").append(((Aliased) segment).getAlias()); } requiresComma = true; + } else if (segment instanceof AsteriskFromTable) { + // the toString of AsteriskFromTable includes the table name, which would cause it to appear twice. + builder.append("*"); + } else if (segment instanceof Expression) { + builder.append(segment.toString()); } return super.leaveNested(segment); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/InsertRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/InsertRendererUnitTests.java index c09d1c757a..6b99fc9099 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/InsertRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/InsertRendererUnitTests.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.*; import org.junit.Test; - import org.springframework.data.relational.core.sql.Insert; import org.springframework.data.relational.core.sql.SQL; import org.springframework.data.relational.core.sql.Table; @@ -27,6 +26,7 @@ * Unit tests for {@link SqlRenderer}. * * @author Mark Paluch + * @author Jens Schauder */ public class InsertRendererUnitTests { @@ -37,7 +37,7 @@ public void shouldRenderInsert() { Insert insert = Insert.builder().into(bar).values(SQL.bindMarker()).build(); - assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar VALUES(?)"); + assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar VALUES (?)"); } @Test // DATAJDBC-335 @@ -47,7 +47,7 @@ public void shouldRenderInsertColumn() { Insert insert = Insert.builder().into(bar).column(bar.column("foo")).values(SQL.bindMarker()).build(); - assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar (foo) VALUES(?)"); + assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar (foo) VALUES (?)"); } @Test // DATAJDBC-335 @@ -58,6 +58,17 @@ public void shouldRenderInsertMultipleColumns() { Insert insert = Insert.builder().into(bar).columns(bar.columns("foo", "baz")).value(SQL.bindMarker()) .value(SQL.literalOf("foo")).build(); - assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar (foo, baz) VALUES(?, 'foo')"); + assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar (foo, baz) VALUES (?, 'foo')"); + } + + @Test // DATAJDBC-340 + public void shouldRenderInsertWithZeroColumns() { + + Table bar = SQL.table("bar"); + + Insert insert = Insert.builder().into(bar).build(); + + assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar VALUES ()"); } + } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index 2cb98b0560..f61b17dc59 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java @@ -17,10 +17,11 @@ import static org.assertj.core.api.Assertions.*; +import org.junit.Ignore; import org.junit.Test; - import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.Conditions; +import org.springframework.data.relational.core.sql.Expressions; import org.springframework.data.relational.core.sql.Functions; import org.springframework.data.relational.core.sql.OrderByField; import org.springframework.data.relational.core.sql.SQL; @@ -93,6 +94,17 @@ public void shouldRenderCountFunction() { assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT COUNT(bar.foo), bar.bar FROM bar"); } + @Test // DATAJDBC-340 + public void shouldRenderCountFunctionWithAliasedColumn() { + + Table table = SQL.table("bar"); + Column foo = table.column("foo").as("foo_bar"); + + Select select = Select.builder().select(Functions.count(foo), foo).from(table).build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT COUNT(bar.foo), bar.foo AS foo_bar FROM bar"); + } + @Test // DATAJDBC-309 public void shouldRenderSimpleJoin() { @@ -107,6 +119,21 @@ public void shouldRenderSimpleJoin() { + "JOIN department ON employee.department_id = department.id"); } + @Test // DATAJDBC-340 + public void shouldRenderOuterJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")) // + .from(employee) // + .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "LEFT OUTER JOIN department ON employee.department_id = department.id"); + } + @Test // DATAJDBC-309 public void shouldRenderSimpleJoinWithAnd() { @@ -119,7 +146,7 @@ public void shouldRenderSimpleJoinWithAnd() { .build(); assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // + + "JOIN department ON employee.department_id = department.id " // + "AND employee.tenant = department.tenant"); } @@ -246,7 +273,7 @@ public void shouldRenderInSubselect() { Select subselect = Select.builder().select(bah).from(floo).build(); - Select select = Select.builder().select(bar).from(foo).where(Conditions.in(bar, subselect)).build(); + Select select = Select.builder().select(bar).from(foo).where(bar.in(subselect)).build(); assertThat(SqlRenderer.toString(select)) .isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (SELECT floo.bah FROM floo)"); @@ -272,4 +299,44 @@ public void shouldConsiderNamingStrategy() { assertThat(mapped).isEqualTo("SELECT foo.baR FROM foo WHERE foo.baR = foo.baZ"); } + @Test // DATAJDBC-340 + public void shouldRenderCountStar() { + + Select select = Select.builder() // + .select(Functions.count(Expressions.asterisk())) // + .from(SQL.table("foo")) // + .build(); + + String rendered = SqlRenderer.toString(select); + + assertThat(rendered).isEqualTo("SELECT COUNT(*) FROM foo"); + } + + @Test // DATAJDBC-340 + public void shouldRenderCountTableStar() { + + Table foo = SQL.table("foo"); + Select select = Select.builder() // + .select(Functions.count(foo.asterisk())) // + .from(foo) // + .build(); + + String rendered = SqlRenderer.toString(select); + + assertThat(rendered).isEqualTo("SELECT COUNT(foo.*) FROM foo"); + } + + @Test // DATAJDBC-340 + public void shouldRenderFunctionWithAlias() { + + Table foo = SQL.table("foo"); + Select select = Select.builder() // + .select(Functions.count(foo.asterisk()).as("counter")) // + .from(foo) // + .build(); + + String rendered = SqlRenderer.toString(select); + + assertThat(rendered).isEqualTo("SELECT COUNT(foo.*) AS counter FROM foo"); + } }