diff --git a/pom.xml b/pom.xml index cd2bedb301..af364dd3f9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.1.0-SNAPSHOT + 2.1.0.DATAJDBC-352-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index d38dbf88d5..9e51d34d0b 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 - 2.1.0-SNAPSHOT + 2.1.0.DATAJDBC-352-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 899656e2a8..ab174580ee 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.1.0-SNAPSHOT + 2.1.0.DATAJDBC-352-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.1.0-SNAPSHOT + 2.1.0.DATAJDBC-352-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index e4d06f0dd1..c429ed242f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -55,6 +55,7 @@ * @author Jens Schauder * @author Christoph Strobl * @author Myeonghyeon Lee + * @author Yunyoung LEE * @see MappingContext * @see SimpleTypeHolder * @see CustomConversions @@ -436,7 +437,7 @@ private Iterable resolveRelation(@Nullable Object id, RelationalPersiste Identifier identifier = id == null // ? this.identifier.withPart(rootPath.getQualifierColumn(), key, Object.class) // - : Identifier.of(rootPath.extendBy(property).getReverseColumnName(), id, Object.class); + : JdbcIdentifierBuilder.forBackReferences(BasicJdbcConverter.this, rootPath.extendBy(property), id).build(); PersistentPropertyPath propertyPath = path.extendBy(property) .getRequiredPersistentPropertyPath(); @@ -512,6 +513,11 @@ private Object readEntityFrom(RelationalPersistentProperty property) { if (idProperty != null) { idValue = newContext.readFrom(idProperty); + } else if (property.isEmbedded()) { + return createInstance(entity, parameter -> { + RelationalPersistentProperty parameterProperty = entity.getRequiredPersistentProperty(parameter.getName()); + return newContext.propertyValueProvider.getPropertyValue(parameterProperty); + }); } else { idValue = backReferencePropertyValueProvider.getPropertyValue(property); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index afa4b5002a..a8e68f64e2 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -37,7 +37,6 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.relational.core.dialect.LockClause; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -66,6 +65,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Yunyoung LEE * @since 1.1 */ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -117,7 +117,7 @@ public Object insert(T instance, Class domainType, Identifier identifier) if (idValue != null) { RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + addConvertedIdPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); } KeyHolder holder = new GeneratedKeyHolder(); @@ -218,7 +218,18 @@ public void delete(Object rootId, PersistentPropertyPath accessor = rootEntity.getPropertyAccessor(rootId); + for (RelationalPersistentProperty property : rootEntity) { + parameters.addValue(property.getColumnName(), accessor.getProperty(property)); + } + } else { + + parameters.addValue(new PersistentPropertyPathExtension(context, propertyPath).getEffectiveIdColumnName(), + rootId); + } + operations.update(delete, parameters); } @@ -322,13 +333,42 @@ public Iterable findAllById(Iterable ids, Class domainType) { RelationalPersistentProperty idProperty = getRequiredPersistentEntity(domainType).getRequiredIdProperty(); SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(getIdentifierProcessing()); - addConvertedPropertyValuesAsList(parameterSource, idProperty, ids, IDS_SQL_PARAMETER); + if (idProperty.isEmbedded()) { + + // NamedParamterUtils#substituteNamedParameters handles Object[] parameterSource as multiple parameter value + List idParameters = new ArrayList<>(); + for (Object id : ids) { + List parameters = new ArrayList<>(); + extractEmbeddedParameters(parameters, idProperty, id); + idParameters.add(parameters.toArray()); + } + + parameterSource.addValue(IDS_SQL_PARAMETER, idParameters); + } else { + + addConvertedPropertyValuesAsList(parameterSource, idProperty, ids, IDS_SQL_PARAMETER); + } String findAllInListSql = sql(domainType).getFindAllInList(); return operations.query(findAllInListSql, parameterSource, (RowMapper) getEntityRowMapper(domainType)); } + private void extractEmbeddedParameters(List parameters, RelationalPersistentProperty property, Object value) { + + if (property.isEmbedded()) { + + final RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property); + final PersistentPropertyAccessor embeddedAccessor = embeddedEntity.getPropertyAccessor(value); + + embeddedEntity.doWithProperties((PropertyHandler) embeddedProperty -> // + extractEmbeddedParameters(parameters, embeddedProperty, embeddedAccessor.getProperty(embeddedProperty))); + } else { + + parameters.add(value); + } + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.RelationResolver#findAllByPath(org.springframework.data.jdbc.support.Identifier, org.springframework.data.mapping.PersistentPropertyPath) @@ -496,7 +536,7 @@ private SqlIdentifierParameterSource createIdParameterSource(Object id, Clas SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(getIdentifierProcessing()); - addConvertedPropertyValue( // + addConvertedIdPropertyValue( // parameterSource, // getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // id, // @@ -505,6 +545,23 @@ private SqlIdentifierParameterSource createIdParameterSource(Object id, Clas return parameterSource; } + private void addConvertedIdPropertyValue(SqlIdentifierParameterSource parameterSource, + RelationalPersistentProperty property, Object value, SqlIdentifier defaultName) { + + if (property.isEmbedded()) { + final RelationalPersistentEntity embeddedEntity = context.getRequiredPersistentEntity(property); + final String prefix = property.getEmbeddedPrefix(); + + final PersistentPropertyAccessor propertyAccessor = embeddedEntity.getPropertyAccessor(value); + for (final RelationalPersistentProperty embeddedProperty : embeddedEntity) { + addConvertedIdPropertyValue(parameterSource, embeddedProperty, propertyAccessor.getProperty(embeddedProperty), + embeddedProperty.getColumnName().transform(prefix::concat)); + } + } else { + addConvertedPropertyValue(parameterSource, property, value, defaultName); + } + } + private IdentifierProcessing getIdentifierProcessing() { return sqlGeneratorSource.getDialect().getIdentifierProcessing(); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java index b988094a94..10bef7d362 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -15,7 +15,14 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.Iterator; +import java.util.stream.StreamSupport; + +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -23,6 +30,7 @@ * Builder for {@link Identifier}. Mainly for internal use within the framework * * @author Jens Schauder + * @author Yunyoung LEE * @since 1.1 */ public class JdbcIdentifierBuilder { @@ -43,11 +51,29 @@ public static JdbcIdentifierBuilder empty() { public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, PersistentPropertyPathExtension path, @Nullable Object value) { - Identifier identifier = Identifier.of( // - path.getReverseColumnName(), // - value, // - converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) // - ); + Identifier identifier = null; + + RelationalPersistentEntity entity = converter.getMappingContext().getPersistentEntity(value.getClass()); + if (entity != null) { + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + + Iterator reverseColumnNames = path.getReverseColumnNames().iterator(); + + identifier = Identifier.empty(); + for (RelationalPersistentProperty property : entity) { + Assert.state(reverseColumnNames.hasNext(), "no reverse column name for " + property.getName()); + identifier = identifier.withPart(reverseColumnNames.next(), accessor.getProperty(property), + converter.getColumnType(property)); + } + } else { + + identifier = Identifier.of( // + path.getReverseColumnName(), // + value, // + converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) // + ); + } return new JdbcIdentifierBuilder(identifier); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 78a37325b4..3517135a4c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -18,7 +18,7 @@ import lombok.Value; import java.util.*; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -53,6 +53,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Yunyoung LEE */ class SqlGenerator { @@ -110,18 +111,19 @@ class SqlGenerator { * @param path specifies the table and id to select * @param rootCondition the condition on the root of the path determining what to select * @param filterColumn the column to apply the IN-condition to. + * @param rootIndentifier the sql identifier to apply root condition. * @return the IN condition */ private Condition getSubselectCondition(PersistentPropertyPathExtension path, - Function rootCondition, Column filterColumn) { + BiFunction rootCondition, Column filterColumn, SqlIdentifier rootIdentifier) { PersistentPropertyPathExtension parentPath = path.getParentPath(); if (!parentPath.hasIdProperty()) { if (parentPath.getLength() > 1) { - return getSubselectCondition(parentPath, rootCondition, filterColumn); + return getSubselectCondition(parentPath, rootCondition, filterColumn, rootIdentifier); } - return rootCondition.apply(filterColumn); + return rootCondition.apply(filterColumn, rootIdentifier); } Table subSelectTable = Table.create(parentPath.getTableName()); @@ -133,11 +135,11 @@ private Condition getSubselectCondition(PersistentPropertyPathExtension path, if (parentPath.getLength() == 1) { // if the parent is the root of the path // apply the rootCondition - innerCondition = rootCondition.apply(selectFilterColumn); + innerCondition = rootCondition.apply(selectFilterColumn, rootIdentifier); } else { // otherwise we need another layer of subselect - innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); + innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn, rootIdentifier); } Select select = Select.builder() // @@ -357,7 +359,8 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath column.isNotNull()); } /** @@ -368,12 +371,47 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + (filterColumn, sqlIdentifier) -> filterColumn.isEqualTo(getBindMarker(sqlIdentifier))); + } + + private Condition buildIdCondition(BindMarker idMarker) { + final RelationalPersistentProperty idProperty = entity.getIdProperty(); + + if (idProperty.isEmbedded()) { + final Table table = getTable(); + Condition condition = null; + for (final SqlIdentifier sqlIdentifier : columns.idColumnNames) { + final Condition mappedCondition = table.column(sqlIdentifier).isEqualTo(getBindMarker(sqlIdentifier)); + + if (condition != null) { + condition = condition.and(mappedCondition); + } else { + condition = mappedCondition; + } + } + return condition; + } + + return getIdColumn().isEqualTo(idMarker); + } + + private Condition buildIdInListContion() { + final RelationalPersistentProperty idProperty = entity.getIdProperty(); + + if (idProperty.isEmbedded()) { + final Table table = getTable(); + String conditionExpression = "(" + columns.idColumnNames.stream() + .map(identifier -> table.column(identifier).toString()).collect(Collectors.joining(", ")) + ")"; + + return Conditions.in(Expressions.just(conditionExpression), getBindMarker(IDS_SQL_PARAMETER)); + } + + return getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER)); } private String createFindOneSql() { - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + Select select = selectBuilder().where(buildIdCondition(getBindMarker(ID_SQL_PARAMETER))) // .build(); return render(select); @@ -384,11 +422,11 @@ private String createAcquireLockById(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // - .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .lock(lockMode) // - .build(); + .select(getIdColumn()) // + .from(table) // + .where(buildIdCondition(getBindMarker(ID_SQL_PARAMETER))) // + .lock(lockMode) // + .build(); return render(select); } @@ -398,10 +436,10 @@ private String createAcquireLockAll(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // - .from(table) // - .lock(lockMode) // - .build(); + .select(getIdColumn()) // + .from(table) // + .lock(lockMode) // + .build(); return render(select); } @@ -446,7 +484,18 @@ private SelectBuilder.SelectWhere selectBuilder(Collection keyCol SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + SelectBuilder.SelectOn selectOn = baseSelect.leftOuterJoin(join.joinTable); + + SelectBuilder.SelectOnCondition selectOnCondition = null; + for (JoinCondition condition : join.joinConditions) { + if (selectOnCondition == null) { + selectOnCondition = selectOn.on(condition.joinColumn).equals(condition.parentId); + } else { + selectOnCondition = selectOnCondition.and(condition.joinColumn).equals(condition.parentId); + } + } + + baseSelect = selectOnCondition; } return (SelectBuilder.SelectWhere) baseSelect; @@ -527,16 +576,22 @@ Join getJoin(PersistentPropertyPathExtension path) { PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); - return new Join( // - currentTable, // - currentTable.column(path.getReverseColumnName()), // - parentTable.column(idDefiningParentPath.getIdColumnName()) // - ); + Join join = new Join(currentTable); + + Iterator parentColumnNames = idDefiningParentPath.getIdColumnNames().iterator(); + path.getReverseColumnNames().forEach(reverserColumnName -> { + Assert.state(parentColumnNames.hasNext(), + "Relation parent column name not found for reverse column name " + reverserColumnName.getReference()); + join.joinConditions.add( + new JoinCondition(currentTable.column(reverserColumnName), parentTable.column(parentColumnNames.next()))); + }); + + return join; } private String createFindAllInListSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + Select select = selectBuilder().where(buildIdInListContion()).build(); return render(select); } @@ -548,7 +603,7 @@ private String createExistsSql() { Select select = StatementBuilder // .select(Functions.count(getIdColumn())) // .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .where(buildIdCondition(getBindMarker(ID_SQL_PARAMETER))) // .build(); return render(select); @@ -615,7 +670,7 @@ private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { return Update.builder() // .table(table) // .set(assignments) // - .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); + .where(buildIdCondition(getBindMarker(entity.getIdColumn()))); } private String createDeleteSql() { @@ -632,32 +687,32 @@ private String createDeleteByIdAndVersionSql() { } private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { - return Delete.builder().from(table) - .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); + return Delete.builder().from(table) // + .where(buildIdCondition(getBindMarker(ID_SQL_PARAMETER))); } private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, - Function rootCondition) { + BiFunction rootCondition) { Table table = Table.create(path.getTableName()); DeleteBuilder.DeleteWhere builder = Delete.builder() // .from(table); - Delete delete; - - Column filterColumn = table.column(path.getReverseColumnName()); + Condition condition; if (path.getLength() == 1) { - delete = builder // - .where(rootCondition.apply(filterColumn)) // - .build(); + condition = path.getReverseColumnNames().stream() + .map(filterIdentifier -> rootCondition.apply(table.column(filterIdentifier), filterIdentifier)) + .reduce((a, b) -> a.and(b)).orElseThrow(IllegalStateException::new); } else { - Condition condition = getSubselectCondition(path, rootCondition, filterColumn); - delete = builder.where(condition).build(); + SqlIdentifier rootIdentifier = path.getEffectiveIdColumnName(); + condition = getSubselectCondition(path, rootCondition, table.column(rootIdentifier), rootIdentifier); } + Delete delete = builder.where(condition).build(); + return render(delete); } @@ -667,7 +722,7 @@ private String createDeleteByListSql() { Delete delete = Delete.builder() // .from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // + .where(buildIdInListContion()) // .build(); return render(delete); @@ -694,6 +749,10 @@ private Table getTable() { } private Column getIdColumn() { + RelationalPersistentProperty property = entity.getIdProperty(); + if (property.isEmbedded()) { + return getTable().column(columns.idColumnNames.iterator().next()); + } return sqlContext.getIdColumn(); } @@ -725,6 +784,11 @@ private OrderByField orderToOrderByField(Sort.Order order) { @Value static class Join { Table joinTable; + List joinConditions = new ArrayList<>(); + } + + @Value + static class JoinCondition { Column joinColumn; Column parentId; } @@ -754,7 +818,7 @@ static class Columns { this.mappingContext = mappingContext; this.converter = converter; - populateColumnNameCache(entity, ""); + populateColumnNameCache(entity); Set insertable = new LinkedHashSet<>(nonIdColumnNames); insertable.removeAll(readOnlyColumnNames); @@ -769,26 +833,38 @@ static class Columns { this.updateableColumns = Collections.unmodifiableSet(updateable); } - private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { + private void populateColumnNameCache(RelationalPersistentEntity entity) { + entity.doWithProperties((PropertyHandler) property -> { + + // should check isEmbedded before isEntity for work with embedded id. + if (property.isEmbedded()) { + initEmbeddedColumnNames(property, "", property.isIdProperty()); + } else if (!property.isEntity()) { + initSimpleColumnName(property, "", property.isIdProperty()); + } + }); + } + + private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix, boolean isId) { 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); + initSimpleColumnName(property, prefix, isId); } else if (property.isEmbedded()) { - initEmbeddedColumnNames(property, prefix); + initEmbeddedColumnNames(property, prefix, isId); } }); } - private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { + private void initSimpleColumnName(RelationalPersistentProperty property, String prefix, boolean isId) { SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); columnNames.add(columnName); - if (!property.getOwner().isIdProperty(property)) { + if (!isId) { nonIdColumnNames.add(columnName); } else { idColumnNames.add(columnName); @@ -799,14 +875,14 @@ private void initSimpleColumnName(RelationalPersistentProperty property, String } } - private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { + private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix, boolean isId) { String embeddedPrefix = property.getEmbeddedPrefix(); RelationalPersistentEntity embeddedEntity = mappingContext .getRequiredPersistentEntity(converter.getColumnType(property)); - populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); + populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix, isId); } /** diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java index 8834fe8bdc..09cf78fa78 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -90,7 +90,7 @@ public void cascadingDeleteFirstLevel() { assertThat(sql).isEqualTo( // "DELETE FROM " // + user + ".referenced_entity WHERE " // - + user + ".referenced_entity.dummy_entity = :rootId" // + + user + ".referenced_entity.dummy_entity = :dummy_entity" // ); }); } @@ -108,7 +108,7 @@ public void cascadingDeleteAllSecondLevel() { "DELETE FROM " + user + ".second_level_referenced_entity " // + "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)"); + + "WHERE " + user + ".referenced_entity.dummy_entity = :referenced_entity)"); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedIdUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedIdUnitTests.java new file mode 100644 index 0000000000..17156470a3 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedIdUnitTests.java @@ -0,0 +1,219 @@ +/* + * Copyright 2019-2020 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.convert; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +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.Transient; +import org.springframework.data.domain.Persistable; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.MappedCollection; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.sql.SqlIdentifier; + +/** + * Unit tests of {@link SqlGenerator} has {@link Embedded} {@link Id}. + * + * @author Yunyoung LEE + */ +public class SqlGeneratorEmbeddedIdUnitTests { + + private RelationalMappingContext context = new JdbcMappingContext(); + JdbcConverter converter = new BasicJdbcConverter(context, (identifier, path) -> { + throw new UnsupportedOperationException(); + }); + private SqlGenerator sqlGenerator; + private SqlGenerator subSqlGenerator; + + @Before + public void setUp() { + this.context.setForceQuote(false); + this.sqlGenerator = createSqlGenerator(DummyEntity.class); + this.subSqlGenerator = createSqlGenerator(SubEntity.class); + } + + SqlGenerator createSqlGenerator(Class type) { + RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(type); + return new SqlGenerator(context, converter, persistentEntity, NonQuotingDialect.INSTANCE); + } + + @Test // DATAJDBC-352 + public void findOne() { + final String sql = sqlGenerator.getFindOne(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity.dummy_id1 AS dummy_id1") // + .contains("dummy_entity.dummy_id2 AS dummy_id2") // + .contains("dummy_entity.dummy_attr AS dummy_attr") // + .contains("WHERE dummy_entity.dummy_id1 = :dummy_id1") // + .contains("AND dummy_entity.dummy_id2 = :dummy_id2") // + .doesNotContain("JOIN") // + .doesNotContain("dummy_entity.id"); // + }); + } + + @Test // DATAJDBC-352 + public void findAll() { + final String sql = sqlGenerator.getFindAll(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity.dummy_id1 AS dummy_id1") // + .contains("dummy_entity.dummy_id2 AS dummy_id2") // + .contains("dummy_entity.dummy_attr AS dummy_attr") // + .doesNotContain("JOIN") // + .doesNotContain("dummy_entity.id"); + }); + } + + @Test // DATAJDBC-352 + public void findAllInList() { + final String sql = sqlGenerator.getFindAllInList(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity.dummy_id1 AS dummy_id1") // + .contains("dummy_entity.dummy_id2 AS dummy_id2") // + .contains("dummy_entity.dummy_attr AS dummy_attr") // + .contains("WHERE (dummy_entity.dummy_id1, dummy_entity.dummy_id2) IN (:ids)") // + .doesNotContain("JOIN") // + .doesNotContain("dummy_entity.id"); + }); + } + + @Test // DATAJDBC-352 + public void findAllByAttribute() { + final String sql = subSqlGenerator.getFindAllByProperty(Identifier // + .of(SqlIdentifier.quoted("dummy_id1"), null, Long.class) // + .withPart(SqlIdentifier.quoted("dummy_id2"), null, Long.class), // + null, false); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("sub_entity.dummy_id1 AS dummy_id1") // + .contains("sub_entity.dummy_id2 AS dummy_id2") // + .contains("sub_entity.sub_id AS sub_id") // + .contains("sub_entity.sub_attr AS sub_attr") // + .contains("WHERE sub_entity.dummy_id1 = :dummy_id1") // + .contains("AND sub_entity.dummy_id2 = :dummy_id2"); + }); + } + + @Test // DATAJDBC-352 + public void insert() { + final String sql = sqlGenerator.getInsert(Collections.emptySet()); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql) // + .startsWith("INSERT INTO") // + .contains("dummy_entity") // + .contains(":dummy_attr"); + }); + } + + @Test // DATAJDBC-352 + public void update() { + final String sql = sqlGenerator.getUpdate(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql) // + .startsWith("UPDATE") // + .contains("dummy_entity") // + .contains("dummy_attr = :dummy_attr") // + .contains("WHERE") // + .contains("dummy_entity.dummy_id1 = :dummy_id1") // + .contains("dummy_entity.dummy_id2 = :dummy_id2"); + }); + } + + @Test // DATAJDBC-352 + public void deleteById() { + final String sql = sqlGenerator.getDeleteById(); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(sql) // + .startsWith("DELETE FROM") // + .contains("dummy_entity") // + .contains("WHERE") // + .contains("dummy_entity.dummy_id1 = :dummy_id1") // + .contains("dummy_entity.dummy_id2 = :dummy_id2"); + }); + } + + @Data + static class DummyEntity implements Persistable { + @Id @Embedded.Nullable DummyEntityId id; + + String dummyAttr; + + @MappedCollection(idColumns = { "DUMMY_ID1", "DUMMY_ID2" }) Set subEntities; + + @Transient boolean isNew; + + SubEntity createSubEntity(Long subId, String subAttr) { + final SubEntity subEntity = new SubEntity(); + subEntity.setId(new SubEntityId(id, subId)); + subEntity.setSubAttr(subAttr); + + if (subEntities == null) { + subEntities = new HashSet<>(); + } + + subEntities.add(subEntity); + return subEntity; + } + } + + @Data + @AllArgsConstructor + static class DummyEntityId { + Long dummyId1; + Long dummyId2; + } + + @Data + static class SubEntity { + @Id @Embedded.Nullable SubEntityId id; + String subAttr; + } + + @Data + @AllArgsConstructor + static class SubEntityId { + @Embedded.Nullable DummyEntityId dummyEntityId; + Long subId; + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index fb62c75665..6ead8d0b67 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PropertyPathTestingUtils; +import org.springframework.data.jdbc.core.convert.SqlGenerator.JoinCondition; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; @@ -268,12 +269,14 @@ public void joinForEmbeddedWithReference() { SqlGenerator.Join join = generateJoin("embedded.other", DummyEntity2.class); SoftAssertions.assertSoftly(softly -> { + JoinCondition condition = join.getJoinConditions().get(0); softly.assertThat(join.getJoinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.unquoted("id")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); + softly.assertThat(condition.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); + softly.assertThat(condition.getJoinColumn().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); + softly.assertThat(condition.getParentId().getName()).isEqualTo(SqlIdentifier.unquoted("id")); + softly.assertThat(condition.getParentId().getTable().getName()) + .isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index 5fc67d100d..caef8e960d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -122,7 +122,7 @@ public void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref")); assertThat(sql).isEqualTo("DELETE FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"DUMMY_ENTITY\" = :rootId"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"DUMMY_ENTITY\" = :dummy_entity"); } @Test // DATAJDBC-107 @@ -137,7 +137,7 @@ public void cascadingDeleteAllSecondLevel() { + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"REFERENCED_ENTITY\" IN " + "(SELECT \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"DUMMY_ENTITY\" = :rootId)"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"DUMMY_ENTITY\" = :referenced_entity)"); } @Test // DATAJDBC-107 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index ef030fce90..09f6ee4cc1 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -32,6 +32,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.PropertyPathTestingUtils; +import org.springframework.data.jdbc.core.convert.SqlGenerator.JoinCondition; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; @@ -140,7 +141,7 @@ public void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :dummy_entity"); } @Test // DATAJDBC-112 @@ -149,7 +150,7 @@ public void cascadingDeleteByPathSecondLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( - "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)"); + "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 = :referenced_entity)"); } @Test // DATAJDBC-112 @@ -190,7 +191,7 @@ public void deleteMapByPath() { String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :dummy_entity"); } @Test // DATAJDBC-101 @@ -540,7 +541,7 @@ public void deletingLongChain() { "WHERE chain2.chain3 IN (" + // "SELECT chain3.x_three " + // "FROM chain3 " + // - "WHERE chain3.chain4 = :rootId" + // + "WHERE chain3.chain4 = :chain1" + // ")))"); } @@ -549,7 +550,7 @@ public void deletingLongChainNoId() { assertThat(createSqlGenerator(NoIdChain4.class) .createDeleteByPath(getPath("chain3.chain2.chain1.chain0", NoIdChain4.class))) // - .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :rootId"); + .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :no_id_chain4"); } @Test // DATAJDBC-359 @@ -565,7 +566,7 @@ public void deletingLongChainNoIdWithBackreferenceNotReferencingTheRoot() { + "WHERE no_id_chain4.id_no_id_chain IN (" // + "SELECT id_no_id_chain.x_id " // + "FROM id_no_id_chain " // - + "WHERE id_no_id_chain.id_id_no_id_chain = :rootId" // + + "WHERE id_no_id_chain.id_id_no_id_chain = :no_id_chain4" // + "))"); } @@ -580,12 +581,13 @@ public void joinForSimpleReference() { SqlGenerator.Join join = generateJoin("ref", DummyEntity.class); SoftAssertions.assertSoftly(softly -> { + JoinCondition condition = join.getJoinConditions().get(0); softly.assertThat(join.getJoinTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("id1")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); + softly.assertThat(condition.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); + softly.assertThat(condition.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); + softly.assertThat(condition.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("id1")); + softly.assertThat(condition.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); }); } @@ -612,13 +614,14 @@ public void joinForSecondLevelReference() { SqlGenerator.Join join = generateJoin("ref.further", DummyEntity.class); SoftAssertions.assertSoftly(softly -> { + JoinCondition condition = join.getJoinConditions().get(0); softly.assertThat(join.getJoinTable().getName()) .isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_L1ID")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); + softly.assertThat(condition.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); + softly.assertThat(condition.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); + softly.assertThat(condition.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_L1ID")); + softly.assertThat(condition.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); }); } @@ -629,14 +632,15 @@ public void joinForOneToOneWithoutId() { Table joinTable = join.getJoinTable(); SoftAssertions.assertSoftly(softly -> { + JoinCondition condition = join.getJoinConditions().get(0); softly.assertThat(joinTable.getName()).isEqualTo(SqlIdentifier.quoted("NO_ID_CHILD")); softly.assertThat(joinTable).isInstanceOf(Aliased.class); softly.assertThat(((Aliased) joinTable).getAlias()).isEqualTo(SqlIdentifier.quoted("child")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(joinTable); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_ID")); - softly.assertThat(join.getParentId().getTable().getName()) + softly.assertThat(condition.getJoinColumn().getTable()).isEqualTo(joinTable); + softly.assertThat(condition.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); + softly.assertThat(condition.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_ID")); + softly.assertThat(condition.getParentId().getTable().getName()) .isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); }); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedIdIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedIdIntegrationTests.java new file mode 100644 index 0000000000..783371a378 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedIdIntegrationTests.java @@ -0,0 +1,302 @@ +/* + * Copyright 2019-2020 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.repository; + +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.MappedCollection; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; +import org.springframework.transaction.annotation.Transactional; + +/** + * Simple JdbcRepository test has {@link Embedded} {@link Id}. + * + * @author Yunyoung LEE + */ +@ContextConfiguration +@Transactional +public class JdbcRepositoryEmbeddedIdIntegrationTests { + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return JdbcRepositoryEmbeddedIdIntegrationTests.class; + } + + @Bean + DummyEntityRepository dummyEntityRepository() { + return factory.getRepository(DummyEntityRepository.class); + } + + } + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired DummyEntityRepository repository; + + @Test // DATAJDBC-352 + public void savesAnEntity() { + + final DummyEntity entity = createDummyEntity(1L); + entity.createSubEntity(1L, "A"); + + repository.save(entity); + assertThat(repository.count()).isEqualTo(1L); + } + + @Test // DATAJDBC-352 + public void saveAndLoadAnEntity() { + + final DummyEntity newEntity = createDummyEntity(1L); + newEntity.createSubEntity(1L, "A"); + + final DummyEntity entity = repository.save(newEntity); + + assertThat(repository.findById(entity.getId())).hasValueSatisfying(it -> { + assertThat(it.getId()).isEqualTo(entity.getId()); + assertThat(it.getId().getDummyId1()).isEqualTo(entity.getId().getDummyId1()); + assertThat(it.getId().getDummyId2()).isEqualTo(entity.getId().getDummyId2()); + assertThat(it.getDummyAttr()).isEqualTo(entity.getDummyAttr()); + assertThat(it.getSubEntities()).isEqualTo(entity.getSubEntities()); + }); + } + + @Test // DATAJDBC-352 + public void findAllFindsAllEntities() { + + DummyEntity entity = repository.save(createDummyEntity(1L)); + DummyEntity other = repository.save(createDummyEntity(2L)); + + Iterable all = repository.findAll(); + + assertThat(all)// + .extracting(DummyEntity::getId)// + .containsExactlyInAnyOrder(entity.getId(), other.getId()); + } + + @Test // DATAJDBC-352 + public void findByIdReturnsEmptyWhenNoneFound() { + + // NOT saving anything, so DB is empty + assertThat(repository.findById(new DummyEntityId(-1L, -1L))).isEmpty(); + } + + @Test // DATAJDBC-352 + public void findAllById() { + + DummyEntity one = repository.save(createDummyEntity(1L)); + @SuppressWarnings("unused") + DummyEntity two = repository.save(createDummyEntity(2L)); + DummyEntity three = repository.save(createDummyEntity(3L)); + + Iterable entities = repository.findAllById(asList(three.getId(), one.getId())); + + assertThat(entities)// + .extracting(DummyEntity::getId)// + .containsExactlyInAnyOrder(three.getId(), one.getId()); + } + + @Test // DATAJDBC-352 + public void findByAttribute() { + + DummyEntity entity = repository.save(createDummyEntity(1L)); + + assertThat(repository.findByDummyAttr(entity.getDummyAttr())).hasValueSatisfying(it -> { + assertThat(it.getId()).isEqualTo(entity.getId()); + assertThat(it.getId().getDummyId1()).isEqualTo(entity.getId().getDummyId1()); + assertThat(it.getId().getDummyId2()).isEqualTo(entity.getId().getDummyId2()); + assertThat(it.getDummyAttr()).isEqualTo(entity.getDummyAttr()); + }); + + } + + @Test // DATAJDBC-352 + public void update() { + + DummyEntity entity = repository.save(createDummyEntity(1L)); + + entity.setDummyAttr("New Attr"); + entity.setNew(false); + DummyEntity saved = repository.save(entity); + + assertThat(repository.findById(entity.getId())).hasValueSatisfying(it -> { + assertThat(it.getDummyAttr()).isEqualTo(saved.getDummyAttr()); + }); + } + + @Test // DATAJDBC-352 + public void updateMany() { + + DummyEntity entity = repository.save(createDummyEntity(3L)); + DummyEntity other = repository.save(createDummyEntity(1L)); + + entity.setDummyAttr("entity attr"); + entity.setNew(false); + + other.setDummyAttr("other attr"); + other.setNew(false); + + repository.saveAll(asList(entity, other)); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getDummyAttr) // + .containsExactlyInAnyOrder(entity.getDummyAttr(), other.getDummyAttr()); + } + + @Test // DATAJDBC-352 + public void deleteById() { + + DummyEntity one = repository.save(createDummyEntity(1L)); + DummyEntity two = repository.save(createDummyEntity(2L)); + DummyEntity three = repository.save(createDummyEntity(3L)); + + repository.deleteById(two.getId()); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getId) // + .containsExactlyInAnyOrder(one.getId(), three.getId()); + } + + @Test // DATAJDBC-352 + public void deleteByEntity() { + DummyEntity one = repository.save(createDummyEntity(1L)); + DummyEntity two = repository.save(createDummyEntity(2L)); + DummyEntity three = repository.save(createDummyEntity(3L)); + + repository.delete(one); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getId) // + .containsExactlyInAnyOrder(two.getId(), three.getId()); + } + + @Test // DATAJDBC-352 + public void deleteByList() { + + DummyEntity one = repository.save(createDummyEntity(1L)); + DummyEntity two = repository.save(createDummyEntity(2L)); + DummyEntity three = repository.save(createDummyEntity(3L)); + + repository.deleteAll(asList(one, three)); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getId) // + .containsExactlyInAnyOrder(two.getId()); + } + + @Test // DATAJDBC-352 + public void deleteAll() { + + repository.save(createDummyEntity(1L)); + repository.save(createDummyEntity(2L)); + repository.save(createDummyEntity(3L)); + + assertThat(repository.findAll()).isNotEmpty(); + + repository.deleteAll(); + + assertThat(repository.findAll()).isEmpty(); + } + + private static DummyEntity createDummyEntity(Long id) { + DummyEntity entity = new DummyEntity(); + + entity.setId(new DummyEntityId(id, id)); + entity.setDummyAttr("attr"); + entity.setNew(true); + + return entity; + } + + interface DummyEntityRepository extends CrudRepository { + Optional findByDummyAttr(String attr); + } + + @Data + static class DummyEntity implements Persistable { + @Id @Embedded.Nullable DummyEntityId id; + + String dummyAttr; + + @MappedCollection(idColumns = { "DUMMY_ID1", "DUMMY_ID2" }) Set subEntities; + + @Transient boolean isNew; + + SubEntity createSubEntity(Long subId, String subAttr) { + final SubEntity subEntity = new SubEntity(); + subEntity.setId(new SubEntityId(id, subId)); + subEntity.setSubAttr(subAttr); + + if (subEntities == null) { + subEntities = new HashSet<>(); + } + + subEntities.add(subEntity); + return subEntity; + } + } + + @Data + @AllArgsConstructor + static class DummyEntityId { + Long dummyId1; + Long dummyId2; + } + + @Data + static class SubEntity { + @Id @Embedded.Nullable SubEntityId id; + String subAttr; + } + + @Data + @AllArgsConstructor + static class SubEntityId { + @Embedded.Nullable DummyEntityId dummyEntityId; + Long subId; + } + +} diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedIdIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedIdIntegrationTests-hsql.sql new file mode 100644 index 0000000000..c749533cd8 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryEmbeddedIdIntegrationTests-hsql.sql @@ -0,0 +1,13 @@ +CREATE TABLE dummy_entity ( + DUMMY_ID1 BIGINT, + DUMMY_ID2 BIGINT, + DUMMY_ATTR VARCHAR(100), + PRIMARY KEY( DUMMY_ID1, DUMMY_ID2 ) +); +CREATE TABLE sub_entity ( + DUMMY_ID1 BIGINT, + DUMMY_ID2 BIGINT, + SUB_ID BIGINT, + SUB_ATTR VARCHAR(100), + PRIMARY KEY ( DUMMY_ID1, DUMMY_ID2, SUB_ID ) +); diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 5ff3aef680..84d79a77df 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.1.0-SNAPSHOT + 2.1.0.DATAJDBC-352-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.1.0-SNAPSHOT + 2.1.0.DATAJDBC-352-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index d218391118..f303d30ef3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -15,8 +15,12 @@ */ package org.springframework.data.relational.core.mapping; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; @@ -37,12 +41,14 @@ * @author Greg Turnquist * @author Florian Lüdiger * @author Bastian Wilhelm + * @author Yunyoung LEE */ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty implements RelationalPersistentProperty { private final Lazy columnName; private final Lazy> collectionIdColumnName; + private final Lazy>> collectionIdColumnNames; private final Lazy collectionKeyColumnName; private final Lazy isEmbedded; private final Lazy embeddedPrefix; @@ -103,6 +109,12 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity Optional // + .ofNullable(findAnnotation(MappedCollection.class)) // + .map(MappedCollection::idColumns) // + .filter(columns -> columns.length > 0) // + .map(columns -> Arrays.stream(columns).map(this::createSqlIdentifier).collect(Collectors.toList()))); + this.collectionKeyColumnName = Lazy.of(() -> Optionals // .toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)).map(MappedCollection::keyColumn)) // .filter(StringUtils::hasText).findFirst() // @@ -174,6 +186,19 @@ public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) .orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path))); } + @Override + public List getReverseColumnNames(PersistentPropertyPathExtension path) { + + return collectionIdColumnNames.get().orElseGet(() -> { + RelationalPersistentProperty reverseIdProperty = path.getLeafEntity().getIdProperty(); + if (reverseIdProperty != null && reverseIdProperty.isEmbedded()) { + return path.newPathOf(reverseIdProperty).getIdColumnNames(); + } else { + return Collections.singletonList(getReverseColumnName(path)); + } + }); + } + /* * (non-Javadoc) * @see org.springframework.data.relational.core.mapping.RelationalPersistentProperty#getKeyColumn() diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java index fee566c288..0b5515e347 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/MappedCollection.java @@ -30,6 +30,7 @@ * @since 1.1 * @author Bastian Wilhelm * @author Mark Paluch + * @autorh Yunyoung LEE */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @@ -44,6 +45,14 @@ */ String idColumn() default ""; + /** + * Column names for id columns in the corresponding relationship table. Defaults to {@link #idColumn()} if the value + * is empty. + * + * @since 2.1 + */ + String[] idColumns() default {}; + /** * The column name for key columns of {@link List} or {@link Map} collections in the corresponding relationship table. * Defaults to {@link NamingStrategy} usage if the value is empty. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java index 2fe65b8cf2..b27255e3d2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java @@ -17,8 +17,13 @@ import lombok.EqualsAndHashCode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.springframework.data.mapping.PersistentProperty; 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.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -31,6 +36,7 @@ * available used in SQL generation and conversion * * @author Jens Schauder + * @autorh Yunyoung LEE * @since 1.1 */ @EqualsAndHashCode(exclude = { "columnAlias", "context" }) @@ -79,6 +85,11 @@ public PersistentPropertyPathExtension( this.path = path; } + PersistentPropertyPathExtension newPathOf(RelationalPersistentProperty property) { + return new PersistentPropertyPathExtension(context, + context.getPersistentPropertyPath(property.getName(), property.getOwner().getType())); + } + /** * Returns {@literal true} exactly when the path is non empty and the leaf property an embedded one. * @@ -164,6 +175,17 @@ public SqlIdentifier getReverseColumnName() { return path.getRequiredLeafProperty().getReverseColumnName(this); } + /** + * Names of columns used to reference ids in the parent table. + * + * @throws IllegalStateException when called on an empty path. + */ + public List getReverseColumnNames() { + + Assert.state(path != null, "Empty paths don't have a reverse column name"); + return path.getRequiredLeafProperty().getReverseColumnNames(this); + } + /** * The alias used in select for the column used to reference the id in the parent table. * @@ -254,6 +276,27 @@ public SqlIdentifier getIdColumnName() { return getTableOwningAncestor().getRequiredLeafEntity().getIdColumn(); } + public List getIdColumnNames() { + PersistentPropertyPathExtension idPath = newPathOf(getTableOwningAncestor().getRequiredIdProperty()); + if (idPath.isEmbedded()) { + List list = new ArrayList<>(); + extractColumnNames(list, idPath); + return list; + } else { + return Collections.singletonList(getIdColumnName()); + } + } + + private void extractColumnNames(List list, PersistentPropertyPathExtension path) { + if (path.isEmbedded()) { + final RelationalPersistentEntity entity = path.getLeafEntity(); + entity.doWithProperties((PropertyHandler) property -> extractColumnNames(list, + path.extendBy(property))); + } else { + list.add(path.getColumnName()); + } + } + /** * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse * column is returned. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java index 42c20f00a1..e053bf7299 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java @@ -15,6 +15,8 @@ */ package org.springframework.data.relational.core.mapping; +import java.util.List; + import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; @@ -25,6 +27,7 @@ * @author Jens Schauder * @author Oliver Gierke * @author Bastian Wilhelm + * @author Yunyoung LEE */ public interface RelationalPersistentProperty extends PersistentProperty { @@ -42,6 +45,8 @@ public interface RelationalPersistentProperty extends PersistentProperty getReverseColumnNames(PersistentPropertyPathExtension path); + @Nullable SqlIdentifier getKeyColumn(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index 8cc95b2341..f934bbf1e1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -31,6 +31,7 @@ * * @author Mark Paluch * @author Jens Schauder + * @author Yunyoung LEE * @since 1.1 * @see Column * @see SubselectExpression @@ -81,6 +82,8 @@ Delegation enterMatched(Expression segment) { } } else if (segment instanceof Literal) { value = segment.toString(); + } else if (segment instanceof Expression) { + value = segment.toString(); } return Delegation.retain();