diff --git a/pom.xml b/pom.xml index fe60487ec8..3181bc4eb1 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-326-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 1753776a73..d987c51da0 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-326-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 9458230173..9a1158979d 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-326-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-326-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java index 7edb996ce6..f78dc5bd35 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -47,6 +48,15 @@ public Object insert(T instance, Class domainType, Map ad return collect(das -> das.insert(instance, domainType, additionalParameters)); } + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, org.springframework.data.jdbc.core.ParentKeys) + */ + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + return collect(das -> das.insert(instance, domainType, identifier)); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) @@ -149,7 +159,7 @@ public boolean existsById(Object id, Class domainType) { private T collect(Function function) { // Keep as Eclipse fails to compile if <> is used. - return strategies.stream().collect(new FunctionCollector(function)); + return strategies.stream().collect(new FunctionCollector<>(function)); } private void collectVoid(Consumer consumer) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java index a307b8b2d0..1ff274c26d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java @@ -19,6 +19,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.lang.Nullable; /** @@ -39,9 +40,27 @@ public interface DataAccessStrategy { * to get referenced are contained in this map. Must not be {@code null}. * @param the type of the instance. * @return the id generated by the database if any. + * @deprecated since 1.1, use {@link #insert(Object, Class, Identifier)} instead. */ + @Deprecated Object insert(T instance, Class domainType, Map additionalParameters); + /** + * Inserts a the data of a single entity. Referenced entities don't get handled. + * + * @param instance the instance to be stored. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param identifier information about data that needs to be considered for the insert but which is not part of the + * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a + * {@link Map} or {@link java.util.List}. + * @param the type of the instance. + * @return the id generated by the database if any. + * @since 1.1 + */ + default Object insert(T instance, Class domainType, Identifier identifier){ + return insert(instance, domainType, identifier.toMap()); + } + /** * Updates the data of a single entity in the database. Referenced entities don't get handled. * diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index 7ee0cf8390..4b3a48cfc0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -37,6 +37,7 @@ 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.domain.Identifier; import org.springframework.data.util.ClassTypeInformation; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -57,10 +58,6 @@ @RequiredArgsConstructor public class DefaultDataAccessStrategy implements DataAccessStrategy { - private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either" - + " the id property in a BeforeInsert event handler, or ensure the database creates a value and your " - + "JDBC driver returns it."; - private final @NonNull SqlGeneratorSource sqlGeneratorSource; private final @NonNull RelationalMappingContext context; private final @NonNull RelationalConverter converter; @@ -87,10 +84,23 @@ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, Relation */ @Override public Object insert(T instance, Class domainType, Map additionalParameters) { + return insert(instance, domainType, Identifier.from(additionalParameters)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) + */ + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { KeyHolder holder = new GeneratedKeyHolder(); RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - Map parameters = new LinkedHashMap<>(additionalParameters); + + Map parameters = new LinkedHashMap<>(identifier.size()); + identifier.forEach((name, value, type) -> { + parameters.put(name, converter.writeValue(value, ClassTypeInformation.from(type))); + }); MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity, ""); @@ -282,7 +292,8 @@ public boolean existsById(Object id, Class domainType) { return result; } - private MapSqlParameterSource getPropertyMap(final S instance, RelationalPersistentEntity persistentEntity, String prefix) { + private MapSqlParameterSource getPropertyMap(S instance, RelationalPersistentEntity persistentEntity, + String prefix) { MapSqlParameterSource parameters = new MapSqlParameterSource(); @@ -294,23 +305,26 @@ private MapSqlParameterSource getPropertyMap(final S instance, Relational return; } - if(property.isEmbedded()){ + if (property.isEmbedded()) { Object value = propertyAccessor.getProperty(property); - final RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); - final MapSqlParameterSource additionalParameters = getPropertyMap((T)value, (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix()); + RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); + MapSqlParameterSource additionalParameters = getPropertyMap((T) value, + (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix()); parameters.addValues(additionalParameters.getValues()); } else { Object value = propertyAccessor.getProperty(property); Object convertedValue = convertForWrite(property, value); - parameters.addValue(prefix + property.getColumnName(), convertedValue, JdbcUtil.sqlTypeFor(property.getColumnType())); + parameters.addValue(prefix + property.getColumnName(), convertedValue, + JdbcUtil.sqlTypeFor(property.getColumnType())); } }); return parameters; } + @Nullable private Object convertForWrite(RelationalPersistentProperty property, @Nullable Object value) { @@ -327,9 +341,8 @@ private Object convertForWrite(RelationalPersistentProperty property, @Nullable String typeName = JDBCType.valueOf(JdbcUtil.sqlTypeFor(componentType)).getName(); - return operations.getJdbcOperations().execute( - (Connection c) -> c.createArrayOf(typeName, (Object[]) convertedValue) - ); + return operations.getJdbcOperations() + .execute((Connection c) -> c.createArrayOf(typeName, (Object[]) convertedValue)); } @SuppressWarnings("unchecked") @@ -354,22 +367,22 @@ private static boolean isIdPropertyNullOrScalarZero(@Nullable ID idValue @Nullable private Object getIdFromHolder(KeyHolder holder, RelationalPersistentEntity persistentEntity) { - try { - // MySQL just returns one value with a special name - return holder.getKey(); - } catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) { - // Postgres returns a value for each column + try { + // MySQL just returns one value with a special name + return holder.getKey(); + } catch (DataRetrievalFailureException | InvalidDataAccessApiUsageException e) { + // Postgres returns a value for each column // MS SQL Server returns a value that might be null. - Map keys = holder.getKeys(); + Map keys = holder.getKeys(); - if (keys == null || persistentEntity.getIdProperty() == null) { - return null; - } + if (keys == null || persistentEntity.getIdProperty() == null) { + return null; + } - return keys.get(persistentEntity.getIdColumn()); - } - } + return keys.get(persistentEntity.getIdColumn()); + } + } private EntityRowMapper getEntityRowMapper(Class domainType) { return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context, converter, accessStrategy); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java index ea6c6603f0..ab15fd7129 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -18,9 +18,9 @@ import lombok.RequiredArgsConstructor; import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.DbAction.Delete; @@ -36,6 +36,7 @@ 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.domain.Identifier; import org.springframework.lang.Nullable; /** @@ -58,7 +59,7 @@ class DefaultJdbcInterpreter implements Interpreter { @Override public void interpret(Insert insert) { - Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), createAdditionalColumnValues(insert)); + Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), getParentKeys(insert)); insert.setGeneratedId(id); } @@ -101,7 +102,7 @@ public void interpret(Merge merge) { // temporary implementation if (!accessStrategy.update(merge.getEntity(), merge.getEntityType())) { - accessStrategy.insert(merge.getEntity(), merge.getEntityType(), createAdditionalColumnValues(merge)); + accessStrategy.insert(merge.getEntity(), merge.getEntityType(), getParentKeys(merge)); } } @@ -141,27 +142,22 @@ public void interpret(DeleteAllRoot deleteAllRoot) { accessStrategy.deleteAll(deleteAllRoot.getEntityType()); } - private Map createAdditionalColumnValues(DbAction.WithDependingOn action) { - - Map additionalColumnValues = new HashMap<>(); - addDependingOnInformation(action, additionalColumnValues); - additionalColumnValues.putAll(action.getAdditionalValues()); - - return additionalColumnValues; - } - - private void addDependingOnInformation(DbAction.WithDependingOn action, - Map additionalColumnValues) { + private Identifier getParentKeys(DbAction.WithDependingOn action) { DbAction.WithEntity dependingOn = action.getDependingOn(); RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(dependingOn.getEntityType()); - String columnName = getColumnNameForReverseColumn(action); + Object id = getIdFromEntityDependingOn(dependingOn, persistentEntity); + JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder // + .forBackReferences(action.getPropertyPath(), id); - Object identifier = getIdFromEntityDependingOn(dependingOn, persistentEntity); + for (Map.Entry, Object> qualifier : action.getQualifiers() + .entrySet()) { + identifier = identifier.withQualifier(qualifier.getKey(), qualifier.getValue()); + } - additionalColumnValues.put(columnName, identifier); + return identifier.build(); } @Nullable @@ -182,9 +178,4 @@ private Object getIdFromEntityDependingOn(DbAction.WithEntity dependingOn, return persistentEntity.getIdentifierAccessor(entity).getIdentifier(); } - private String getColumnNameForReverseColumn(DbAction.WithPropertyPath action) { - - PersistentPropertyPath path = action.getPropertyPath(); - return path.getRequiredLeafProperty().getReverseColumnName(); - } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java index 0a833f30e4..67382311eb 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java @@ -17,6 +17,7 @@ import java.util.Map; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; @@ -40,6 +41,15 @@ public Object insert(T instance, Class domainType, Map ad return delegate.insert(instance, domainType, additionalParameters); } + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, org.springframework.data.jdbc.core.ParentKeys) + */ + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + return delegate.insert(instance, domainType, identifier); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) 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 5680a51ba2..e94f91bf92 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 @@ -20,17 +20,23 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import java.sql.Array; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * {@link RelationalConverter} that uses a {@link MappingContext} to apply basic conversion of relational values to @@ -122,4 +128,6 @@ public Object writeValue(@Nullable Object value, TypeInformation type) { return super.writeValue(value, type); } + + } 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 new file mode 100644 index 0000000000..1757df32f1 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -0,0 +1,79 @@ +/* + * 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 + * + * http://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 org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; +import org.springframework.lang.Nullable; + +/** + * Builder for {@link Identifier}. Mainly for internal use within the framework + * + * @author Jens Schauder + * @since 1.1 + */ +public class JdbcIdentifierBuilder { + + private Identifier identifier; + + private JdbcIdentifierBuilder(Identifier identifier) { + this.identifier = identifier; + } + + public static JdbcIdentifierBuilder empty() { + return new JdbcIdentifierBuilder(Identifier.empty()); + } + + /** + * Creates ParentKeys with backreference for the given path and value of the parents id. + */ + public static JdbcIdentifierBuilder forBackReferences(PersistentPropertyPath path, + @Nullable Object value) { + + Identifier identifier = Identifier.of( // + path.getRequiredLeafProperty().getReverseColumnName(), // + value, // + getLastIdProperty(path).getColumnType() // + ); + + return new JdbcIdentifierBuilder(identifier); + } + + public JdbcIdentifierBuilder withQualifier(PersistentPropertyPath path, Object value) { + + RelationalPersistentProperty leafProperty = path.getRequiredLeafProperty(); + identifier = identifier.withPart(leafProperty.getKeyColumn(), value, leafProperty.getQualifierColumnType()); + + return this; + } + + public Identifier build() { + return identifier; + } + + private static RelationalPersistentProperty getLastIdProperty( + PersistentPropertyPath path) { + + RelationalPersistentProperty idProperty = path.getRequiredLeafProperty().getOwner().getIdProperty(); + + if (idProperty != null) { + return idProperty; + } + + return getLastIdProperty(path.getParentPath()); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 9618455805..0677a15e3e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -22,6 +22,7 @@ import org.apache.ibatis.session.SqlSession; import org.mybatis.spring.SqlSessionTemplate; + import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; @@ -32,6 +33,7 @@ import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.util.Assert; @@ -123,7 +125,7 @@ public void setNamespaceStrategy(NamespaceStrategy namespaceStrategy) { this.namespaceStrategy = namespaceStrategy; } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) */ @@ -136,7 +138,20 @@ public Object insert(T instance, Class domainType, Map ad return myBatisContext.getId(); } - /* + /* + * (non-Javadoc) + * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, ParentKeys) + */ + @Override + public Object insert(T instance, Class domainType, Identifier identifier) { + + MyBatisContext myBatisContext = new MyBatisContext(null, instance, domainType, identifier.toMap()); + sqlSession().insert(namespace(domainType) + ".insert", myBatisContext); + + return myBatisContext.getId(); + } + + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) */ @@ -147,7 +162,7 @@ public boolean update(S instance, Class domainType) { new MyBatisContext(null, instance, domainType, Collections.emptyMap())) != 0; } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class) */ @@ -158,7 +173,7 @@ public void delete(Object id, Class domainType) { new MyBatisContext(id, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PersistentPropertyPath) */ @@ -171,7 +186,7 @@ public void delete(Object rootId, PersistentPropertyPath void deleteAll(Class domainType) { ); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(org.springframework.data.mapping.PersistentPropertyPath) */ @@ -200,7 +215,7 @@ public void deleteAll(PersistentPropertyPath prope ); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findById(java.lang.Object, java.lang.Class) */ @@ -210,7 +225,7 @@ public T findById(Object id, Class domainType) { new MyBatisContext(id, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAll(java.lang.Class) */ @@ -220,7 +235,7 @@ public Iterable findAll(Class domainType) { new MyBatisContext(null, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllById(java.lang.Iterable, java.lang.Class) */ @@ -230,7 +245,7 @@ public Iterable findAllById(Iterable ids, Class domainType) { new MyBatisContext(ids, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.relational.core.mapping.RelationalPersistentProperty) */ @@ -241,7 +256,7 @@ public Iterable findAllByProperty(Object rootId, RelationalPersistentProp new MyBatisContext(rootId, null, property.getType(), Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#existsById(java.lang.Object, java.lang.Class) */ @@ -251,7 +266,7 @@ public boolean existsById(Object id, Class domainType) { new MyBatisContext(id, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class) */ 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 1029746cea..164a8b3355 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 @@ -16,14 +16,12 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.util.AbstractMap.SimpleEntry; -import java.util.Map; - 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; @@ -31,6 +29,7 @@ import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; /** * Unit tests for {@link DefaultJdbcInterpreter} @@ -67,10 +66,12 @@ public void insertDoesHonourNamingStrategyForBackReference() { interpreter.interpret(insert); - ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(Map.class); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); - assertThat(argumentCaptor.getValue()).containsExactly(new SimpleEntry(BACK_REFERENCE, CONTAINER_ID)); + assertThat(argumentCaptor.getValue().getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } @Test // DATAJDBC-251 @@ -80,10 +81,12 @@ public void idOfParentGetsPassedOnAsAdditionalParameterIfNoIdGotGenerated() { interpreter.interpret(insert); - ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(Map.class); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); - assertThat(argumentCaptor.getValue()).containsExactly(new SimpleEntry(BACK_REFERENCE, CONTAINER_ID)); + assertThat(argumentCaptor.getValue().getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } @Test // DATAJDBC-251 @@ -93,10 +96,12 @@ public void generatedIdOfParentGetsPassedOnAsAdditionalParameter() { interpreter.interpret(insert); - ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(Map.class); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); - assertThat(argumentCaptor.getValue()).containsExactly(new SimpleEntry(BACK_REFERENCE, CONTAINER_ID)); + assertThat(argumentCaptor.getValue().getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } static class Container { 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 new file mode 100644 index 0000000000..d7213940e9 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java @@ -0,0 +1,132 @@ +/* + * 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 + * + * http://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 static org.springframework.data.jdbc.core.PropertyPathUtils.*; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +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; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; + +/** + * Unit tests for the {@link JdbcIdentifierBuilder}. + * + * @author Jens Schauder + */ +public class JdbcIdentifierBuilderUnitTests { + + JdbcMappingContext context = new JdbcMappingContext(); + + @Test // DATAJDBC-326 + public void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { + + Identifier identifier = JdbcIdentifierBuilder.forBackReferences(getPath("child"), "eins").build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple("dummy_entity", "eins", UUID.class) // + ); + } + + @Test // DATAJDBC-326 + public void qualifiersForMaps() { + + PersistentPropertyPath path = getPath("children"); + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(path, "parent-eins") // + .withQualifier(path, "map-key-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple("dummy_entity", "parent-eins", UUID.class), // + tuple("dummy_entity_key", "map-key-eins", String.class) // + ); + } + + @Test // DATAJDBC-326 + public void qualifiersForLists() { + + PersistentPropertyPath path = getPath("moreChildren"); + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(path, "parent-eins") // + .withQualifier(path, "list-index-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple("dummy_entity", "parent-eins", UUID.class), // + tuple("dummy_entity_key", "list-index-eins", Integer.class) // + ); + } + + @Test // DATAJDBC-326 + public void backreferenceAcrossEmbeddable() { + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(getPath("embeddable.child"), "parent-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple("embeddable", "parent-eins", UUID.class) // + ); + } + + @NotNull + private PersistentPropertyPath getPath(String dotPath) { + return toPath(dotPath, DummyEntity.class, context); + } + + @SuppressWarnings("unused") + static class DummyEntity { + + @Id UUID id; + String one; + Long two; + Child child; + + Map children; + + List moreChildren; + + Embeddable embeddable; + } + + @SuppressWarnings("unused") + static class Embeddable { + Child child; + } + + static class Child {} +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithMapsIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithMapsIntegrationTests.java index 269a426c5f..0239595f13 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithMapsIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithMapsIntegrationTests.java @@ -96,7 +96,9 @@ public void saveAndLoadEmptyMap() { public void saveAndLoadNonEmptyMap() { Element element1 = new Element(); + element1.content = "element 1"; Element element2 = new Element(); + element2.content = "element 2"; DummyEntity entity = createDummyEntity(); entity.content.put("one", element1); diff --git a/spring-data-jdbc/src/test/resources/logback.xml b/spring-data-jdbc/src/test/resources/logback.xml index e310de95b6..f1bfdbaf39 100644 --- a/spring-data-jdbc/src/test/resources/logback.xml +++ b/spring-data-jdbc/src/test/resources/logback.xml @@ -12,7 +12,7 @@ - + \ No newline at end of file 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 a327aecad8..4edb29701d 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 @@ -8,6 +8,10 @@ CREATE TABLE ONE_TO_ONE_PARENT ( id3 BIGINT GENERATED BY DEFAULT AS IDENTITY(STA 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 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); + 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); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithMapsIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithMapsIntegrationTests-hsql.sql index 33c747b6f0..15d39c175f 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithMapsIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithMapsIntegrationTests-hsql.sql @@ -1,2 +1,6 @@ CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)); CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, content VARCHAR(100), Dummy_Entity_key VARCHAR(100), dummy_entity BIGINT); + +ALTER TABLE ELEMENT + ADD FOREIGN KEY (dummy_entity) + REFERENCES dummy_entity(id); diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 11e7db737c..46306c43a9 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-326-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-326-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java index 86e9bc8acf..037d851930 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java @@ -114,8 +114,7 @@ static void setId(RelationalMappingContext context, RelationalConverter converte if (leafProperty.isQualified()) { - String keyColumn = leafProperty.getKeyColumn(); - Object keyObject = action.getAdditionalValues().get(keyColumn); + Object keyObject = action.getQualifiers().get(propertyPathToEntity); if (List.class.isAssignableFrom(leafProperty.getType())) { setIdInElementOfList(converter, action, generatedId, (List) currentPropertyValue, (int) keyObject); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index 171b392bf2..0daa1d445a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java @@ -75,7 +75,7 @@ class Insert implements WithGeneratedId, WithDependingOn { @NonNull final PersistentPropertyPath propertyPath; @NonNull final WithEntity dependingOn; - Map additionalValues = new HashMap<>(); + Map, Object> qualifiers = new HashMap<>(); private Object generatedId; @@ -154,7 +154,7 @@ class Merge implements WithDependingOn, WithPropertyPath { @NonNull PersistentPropertyPath propertyPath; @NonNull WithEntity dependingOn; - Map additionalValues = new HashMap<>(); + Map, Object> qualifiers = new HashMap<>(); @Override public void doExecuteWith(Interpreter interpreter) { @@ -248,7 +248,7 @@ interface WithDependingOn extends WithPropertyPath, WithEntity { * become available once the parent entity got persisted. * * @return Guaranteed to be not {@code null}. - * @see #getAdditionalValues() + * @see #getQualifiers() */ WithEntity getDependingOn(); @@ -259,7 +259,7 @@ interface WithDependingOn extends WithPropertyPath, WithEntity { * * @return Guaranteed to be not {@code null}. */ - Map getAdditionalValues(); + Map, Object> getQualifiers(); @Override default Class getEntityType() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java index 10b2874050..6de69781e7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java @@ -127,7 +127,7 @@ private List> insertAll(PersistentPropertyPath value = (Pair) node.getValue(); insert = new DbAction.Insert<>(value.getSecond(), path, getAction(node.getParent())); - insert.getAdditionalValues().put(node.getPath().getRequiredLeafProperty().getKeyColumn(), value.getFirst()); + insert.getQualifiers().put(node.getPath(), value.getFirst()); } else { insert = new DbAction.Insert<>(node.getValue(), path, getAction(node.getParent())); 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 001f7fb3ca..b9b70ea4af 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 @@ -195,6 +195,19 @@ public boolean isQualified() { return isMap() || isListLike(); } + @Override + public Class getQualifierColumnType() { + + Assert.isTrue(isQualified(), "The qualifier column type is only defined for properties that are qualified"); + + if (isMap()) { + return getTypeInformation().getComponentType().getType(); + } + + // for lists and arrays + return Integer.class; + } + @Override public boolean isOrdered() { return isListLike(); 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 2653be60f3..4d0f60dbaa 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 @@ -67,6 +67,8 @@ public interface RelationalPersistentProperty extends PersistentProperty getQualifierColumnType(); + /** * Returns whether this property is an ordered property. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java new file mode 100644 index 0000000000..18dc6c6b8d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java @@ -0,0 +1,214 @@ +/* + * 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 + * + * http://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.relational.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@literal Identifier} represents a composite id of an entity that may be composed of one or many parts. Parts or all + * of the entity might not have a representation as a property in the entity but might only be derived from other + * entities referencing it. + * + * @author Jens Schauder + * @author Mark Paluch + * @since 1.1 + */ +@EqualsAndHashCode +@ToString +public final class Identifier { + + private static final Identifier EMPTY = new Identifier(Collections.emptyList()); + + private final List parts; + + private Identifier(List parts) { + this.parts = parts; + } + + /** + * Returns an empty {@link Identifier}. + * + * @return an empty {@link Identifier}. + */ + public static Identifier empty() { + return EMPTY; + } + + /** + * Creates an {@link Identifier} from {@code name}, {@code value}, and a {@link Class target type}. + * + * @param name must not be {@literal null} or empty. + * @param value + * @param targetType must not be {@literal null}. + * @return the {@link Identifier} for {@code name}, {@code value}, and a {@link Class target type}. + */ + public static Identifier of(String name, Object value, Class targetType) { + + Assert.hasText(name, "Name must not be empty!"); + Assert.notNull(targetType, "Target type must not be null!"); + + return new Identifier(Collections.singletonList(new SingleIdentifierValue(name, value, targetType))); + } + + /** + * Creates an {@link Identifier} from a {@link Map} of name to value tuples. + * + * @param map must not be {@literal null}. + * @return the {@link Identifier} from a {@link Map} of name to value tuples. + */ + public static Identifier from(Map map) { + + Assert.notNull(map, "Map must not be null!"); + + if (map.isEmpty()) { + return empty(); + } + + List values = new ArrayList<>(); + + map.forEach((k, v) -> { + + values.add(new SingleIdentifierValue(k, v, v != null ? ClassUtils.getUserClass(v) : Object.class)); + }); + + return new Identifier(Collections.unmodifiableList(values)); + } + + /** + * Creates a new {@link Identifier} from the current instance and sets the value for {@code key}. Existing key + * definitions for {@code name} are overwritten if they already exist. + * + * @param name must not be {@literal null} or empty. + * @param value + * @param targetType must not be {@literal null}. + * @return the {@link Identifier} containing all existing keys and the key part for {@code name}, {@code value}, and a + * {@link Class target type}. + */ + public Identifier withPart(String name, Object value, Class targetType) { + + Assert.hasText(name, "Name must not be empty!"); + Assert.notNull(targetType, "Target type must not be null!"); + + boolean overwritten = false; + List keys = new ArrayList<>(this.parts.size() + 1); + + for (SingleIdentifierValue singleValue : this.parts) { + + if (singleValue.getName().equals(name)) { + overwritten = true; + keys.add(new SingleIdentifierValue(singleValue.getName(), value, targetType)); + } else { + keys.add(singleValue); + } + } + + if (!overwritten) { + keys.add(new SingleIdentifierValue(name, value, targetType)); + } + + return new Identifier(Collections.unmodifiableList(keys)); + } + + /** + * Returns a {@link Map} containing the identifier name to value tuples. + * + * @return a {@link Map} containing the identifier name to value tuples. + */ + public Map toMap() { + + Map result = new LinkedHashMap<>(); + forEach((name, value, type) -> result.put(name, value)); + return result; + } + + /** + * @return the {@link SingleIdentifierValue key parts}. + */ + public Collection getParts() { + return this.parts; + } + + /** + * Performs the given action for each element of the {@link Identifier} until all elements have been processed or the + * action throws an exception. Unless otherwise specified by the implementing class, actions are performed in the + * order of iteration (if an iteration order is specified). Exceptions thrown by the action are relayed to the caller. + * + * @param consumer the action, must not be {@literal null}. + */ + public void forEach(IdentifierConsumer consumer) { + + Assert.notNull(consumer, "IdentifierConsumer must not be null"); + + getParts().forEach(it -> consumer.accept(it.name, it.value, it.targetType)); + } + + /** + * Returns the number of key parts in this collection. + * + * @return the number of key parts in this collection. + */ + public int size() { + return this.parts.size(); + } + + /** + * A single value of an Identifier consisting of the column name, the value and the target type which is to be used to + * store the element in the database. + * + * @author Jens Schauder + */ + @Value + @AllArgsConstructor(access = AccessLevel.PRIVATE) + static class SingleIdentifierValue { + + String name; + Object value; + Class targetType; + } + + /** + * Represents an operation that accepts identifier key parts (name, value and {@link Class target type}) defining a + * contract to consume {@link Identifier} values. + * + * @author Mark Paluch + */ + @FunctionalInterface + public interface IdentifierConsumer { + + /** + * Performs this operation on the given arguments. + * + * @param name + * @param value + * @param targetType + */ + void accept(String name, Object value, Class targetType); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java index d894935b9e..ef07b70461 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java @@ -27,7 +27,10 @@ import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** * Unit tests for the {@link AggregateChange}. @@ -52,7 +55,7 @@ DbAction.Insert createInsert(String propertyName, Object value, Object key) { DbAction.Insert insert = new DbAction.Insert<>(value, context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert); - insert.getAdditionalValues().put("dummy_entity_key", key); + insert.getQualifiers().put(toPath(propertyName, DummyEntity.class), key); return insert; } @@ -112,6 +115,14 @@ public void setIdForSingleElementMap() { .containsExactlyInAnyOrder(tuple("one", 23)); } + PersistentPropertyPath toPath(String path, Class source) { + + PersistentPropertyPaths persistentPropertyPaths = context + .findPersistentPropertyPaths(source, p -> true); + + return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst().orElse(null); + } + private static class DummyEntity { @Id Integer rootId; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java index 1d143329c7..98e2b08f44 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java @@ -30,6 +30,8 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.AggregateChange.Kind; import org.springframework.data.relational.core.conversion.DbAction.Delete; import org.springframework.data.relational.core.conversion.DbAction.Insert; @@ -37,6 +39,7 @@ import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** * Unit tests for the {@link RelationalEntityWriter} @@ -47,8 +50,13 @@ @RunWith(MockitoJUnitRunner.class) public class RelationalEntityWriterUnitTests { - public static final long SOME_ENTITY_ID = 23L; - RelationalEntityWriter converter = new RelationalEntityWriter(new RelationalMappingContext()); + static final long SOME_ENTITY_ID = 23L; + final RelationalMappingContext context = new RelationalMappingContext(); + final RelationalEntityWriter converter = new RelationalEntityWriter(context); + + final PersistentPropertyPath listContainerElements = toPath("elements", ListContainer.class, context); + + private final PersistentPropertyPath mapContainerElements = toPath("elements", MapContainer.class, context); @Test // DATAJDBC-112 public void newEntityGetsConvertedToOneInsert() { @@ -60,8 +68,8 @@ public void newEntityGetsConvertedToOneInsert() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // ); @@ -79,8 +87,8 @@ public void newEntityGetsConvertedToOneInsertByEmbeddedEntities() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(InsertRoot.class, EmbeddedReferenceEntity.class, "", EmbeddedReferenceEntity.class, false) // ); @@ -98,8 +106,8 @@ public void newEntityWithReferenceGetsConvertedToTwoInserts() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(InsertRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), // tuple(Insert.class, Element.class, "other", Element.class, true) // @@ -117,8 +125,8 @@ public void existingEntityGetsConvertedToDeletePlusUpdate() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(Delete.class, Element.class, "other", null, false), // tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) // @@ -131,14 +139,14 @@ public void newReferenceTriggersDeletePlusInsert() { SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); entity.other = new Element(null); - AggregateChange aggregateChange = new AggregateChange<>(Kind.SAVE, SingleReferenceEntity.class, - entity); + AggregateChange aggregateChange = new AggregateChange<>(Kind.SAVE, + SingleReferenceEntity.class, entity); converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(Delete.class, Element.class, "other", null, false), // tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), // @@ -150,14 +158,14 @@ public void newReferenceTriggersDeletePlusInsert() { public void newEntityWithEmptySetResultsInSingleInsert() { SetContainer entity = new SetContainer(null); - AggregateChange aggregateChange = new AggregateChange<>( - Kind.SAVE, SetContainer.class, entity); + AggregateChange aggregateChange = new AggregateChange<>(Kind.SAVE, + SetContainer.class, entity); converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) // - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false)); } @@ -173,8 +181,8 @@ public void newEntityWithSetResultsInAdditionalInsertPerElement() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false), // tuple(Insert.class, Element.class, "elements", Element.class, true), // @@ -203,8 +211,8 @@ public void cascadingReferencesTriggerCascadingActions() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(InsertRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, @@ -239,8 +247,8 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() { converter.write(entity, aggregateChange); assertThat(aggregateChange.getActions()) - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType, - DbActionTestSupport::isWithDependsOn) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, + DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) // .containsExactly( // tuple(Delete.class, Element.class, "other.element", null, false), tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false), @@ -264,7 +272,8 @@ public void newEntityWithEmptyMapResultsInSingleInsert() { converter.write(entity, aggregateChange); - assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath) // + assertThat(aggregateChange.getActions()) + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath) // .containsExactly( // tuple(InsertRoot.class, MapContainer.class, "")); } @@ -341,7 +350,8 @@ public void newEntityWithEmptyListResultsInSingleInsert() { converter.write(entity, aggregateChange); - assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath) // + assertThat(aggregateChange.getActions()) + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath) // .containsExactly( // tuple(InsertRoot.class, ListContainer.class, "")); } @@ -418,17 +428,29 @@ private CascadingReferenceMiddleElement createMiddleElement(Element first, Eleme } private Object getMapKey(DbAction a) { - return a instanceof DbAction.WithDependingOn - ? ((DbAction.WithDependingOn) a).getAdditionalValues().get("map_container_key") + + return a instanceof DbAction.WithDependingOn // + ? ((DbAction.WithDependingOn) a).getQualifiers().get(mapContainerElements) // : null; } private Object getListKey(DbAction a) { - return a instanceof DbAction.WithDependingOn - ? ((DbAction.WithDependingOn) a).getAdditionalValues().get("list_container_key") + + return a instanceof DbAction.WithDependingOn // + ? ((DbAction.WithDependingOn) a).getQualifiers() + .get(listContainerElements) // : null; } + static PersistentPropertyPath toPath(String path, Class source, + RelationalMappingContext context) { + + PersistentPropertyPaths persistentPropertyPaths = context + .findPersistentPropertyPaths(source, p -> true); + + return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst().orElse(null); + } + @RequiredArgsConstructor static class SingleReferenceEntity { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/event/IdentifierTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/event/IdentifierUnitTests.java similarity index 97% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/event/IdentifierTest.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/event/IdentifierUnitTests.java index 992d295663..a740d95334 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/event/IdentifierTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/event/IdentifierUnitTests.java @@ -26,7 +26,7 @@ * * @author Jens Schauder */ -public class IdentifierTest { +public class IdentifierUnitTests { @SuppressWarnings("unchecked") @Test diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java new file mode 100644 index 0000000000..57f58f6a7f --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java @@ -0,0 +1,104 @@ +/* + * 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 + * + * http://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.relational.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.junit.Test; + +/** + * Unit tests for {@link Identifier}. + * + * @author Jens Schauder + * @author Mark Paluch + */ +public class IdentifierUnitTests { + + @Test // DATAJDBC-326 + public void getParametersByName() { + + Identifier identifier = Identifier.of("aName", "aValue", String.class); + + assertThat(identifier.toMap()).hasSize(1).containsEntry("aName", "aValue"); + } + + @Test // DATAJDBC-326 + public void parametersWithStringKeysUseObjectAsTypeForNull() { + + HashMap parameters = new HashMap<>(); + parameters.put("one", null); + + Identifier identifier = Identifier.from(parameters); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple("one", null, Object.class) // + ); + } + + @Test // DATAJDBC-326 + public void createsIdentifierFromMap() { + + Identifier identifier = Identifier.from(Collections.singletonMap("aName", "aValue")); + + assertThat(identifier.toMap()).hasSize(1).containsEntry("aName", "aValue"); + } + + @Test // DATAJDBC-326 + public void withAddsNewEntries() { + + Identifier identifier = Identifier.from(Collections.singletonMap("aName", "aValue")).withPart("foo", "bar", + String.class); + + assertThat(identifier.toMap()).hasSize(2).containsEntry("aName", "aValue").containsEntry("foo", "bar"); + } + + @Test // DATAJDBC-326 + public void withOverridesExistingEntries() { + + Identifier identifier = Identifier.from(Collections.singletonMap("aName", "aValue")).withPart("aName", "bar", + String.class); + + assertThat(identifier.toMap()).hasSize(1).containsEntry("aName", "bar"); + } + + @Test // DATAJDBC-326 + public void forEachIteratesOverKeys() { + + List keys = new ArrayList<>(); + + Identifier.from(Collections.singletonMap("aName", "aValue")).forEach((name, value, targetType) -> keys.add(name)); + + assertThat(keys).containsOnly("aName"); + } + + @Test // DATAJDBC-326 + public void equalsConsidersEquality() { + + Identifier one = Identifier.from(Collections.singletonMap("aName", "aValue")); + Identifier two = Identifier.from(Collections.singletonMap("aName", "aValue")); + Identifier three = Identifier.from(Collections.singletonMap("aName", "different")); + + assertThat(one).isEqualTo(two); + assertThat(one).isNotEqualTo(three); + } +}