From 28dee1274ffe9bc4b370476bed586ee6dfeb21f5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 12:36:01 +0200 Subject: [PATCH] #2 - Add updates to SimpleR2dbcRepository. --- .../core/function/MappingR2dbcConverter.java | 100 ++++++++++++++++++ .../support/SimpleR2dbcRepository.java | 63 ++++++++++- ...SimpleR2dbcRepositoryIntegrationTests.java | 17 ++- 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java diff --git a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java new file mode 100644 index 0000000..81a90f4 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 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.function; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Converter for R2DBC. + * + * @author Mark Paluch + */ +public class MappingR2dbcConverter { + + private final MappingContext, JdbcPersistentProperty> mappingContext; + + public MappingR2dbcConverter(MappingContext, JdbcPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + + /** + * Returns a {@link Map} that maps column names to an {@link Optional} value. Used {@link Optional#empty()} if the + * underlying property is {@literal null}. + * + * @param object must not be {@literal null}. + * @return + */ + public Map> getFieldsToUpdate(Object object) { + + Assert.notNull(object, "Entity object must not be null!"); + + Class userClass = ClassUtils.getUserClass(object); + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + + Map> update = new LinkedHashMap<>(); + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + + for (JdbcPersistentProperty property : entity) { + update.put(property.getColumnName(), Optional.ofNullable(propertyAccessor.getProperty(property))); + } + + return update; + } + + /** + * Returns a {@link java.util.function.Function} that populates the id property of the {@code object} from a + * {@link Row}. + * + * @param object must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + public BiFunction populateIdIfNecessary(T object) { + + Assert.notNull(object, "Entity object must not be null!"); + + Class userClass = ClassUtils.getUserClass(object); + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + + return (row, metadata) -> { + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + JdbcPersistentProperty idProperty = entity.getRequiredIdProperty(); + + if (propertyAccessor.getProperty(idProperty) == null) { + + propertyAccessor.setProperty(idProperty, row.get(idProperty.getColumnName(), idProperty.getColumnType())); + return (T) propertyAccessor.getBean(); + } + + return object; + }; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java index 5c0a016..955087e 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java @@ -19,6 +19,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -26,6 +28,8 @@ import org.reactivestreams.Publisher; import org.springframework.data.jdbc.core.function.DatabaseClient; import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.jdbc.core.function.FetchSpec; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.repository.reactive.ReactiveCrudRepository; @@ -39,17 +43,22 @@ import org.springframework.util.Assert; public class SimpleR2dbcRepository implements ReactiveCrudRepository { private final DatabaseClient databaseClient; + private final MappingR2dbcConverter converter; private final JdbcPersistentEntity entity; /** * Create a new {@link SimpleR2dbcRepository} given {@link DatabaseClient} and {@link JdbcPersistentEntity}. * * @param databaseClient must not be {@literal null}. + * @param converter must not be {@literal null}. * @param entity must not be {@literal null}. */ - public SimpleR2dbcRepository(DatabaseClient databaseClient, JdbcPersistentEntity entity) { + public SimpleR2dbcRepository(DatabaseClient databaseClient, MappingR2dbcConverter converter, + JdbcPersistentEntity entity) { + this.converter = converter; Assert.notNull(databaseClient, "DatabaseClient must not be null!"); + Assert.notNull(converter, "MappingR2dbcConverter must not be null!"); Assert.notNull(entity, "PersistentEntity must not be null!"); this.databaseClient = databaseClient; @@ -66,13 +75,57 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository it.extract(converter.populateIdIfNecessary(objectToSave)).one()); } - // TODO update + // TODO: Extract in some kind of SQL generator + IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(objectToSave); + Object id = identifierAccessor.getRequiredIdentifier(); - return null; + Map> fields = converter.getFieldsToUpdate(objectToSave); + + String setClause = getSetClause(fields); + + GenericExecuteSpec exec = databaseClient.execute() + .sql(String.format("UPDATE %s SET %s WHERE %s = $1", entity.getTableName(), setClause, getIdColumnName())) // + .bind(0, id); + + int index = 1; + for (Optional setValue : fields.values()) { + + Object value = setValue.orElse(null); + if (value != null) { + exec = exec.bind(index++, value); + } else { + exec = exec.bindNull(index++); + } + } + + return exec.as(entity.getType()) // + .exchange() // + .flatMap(FetchSpec::rowsUpdated) // + .thenReturn(objectToSave); + } + + private static String getSetClause(Map> fields) { + + StringBuilder setClause = new StringBuilder(); + + int index = 2; + for (String field : fields.keySet()) { + + if (setClause.length() != 0) { + setClause.append(", "); + } + + setClause.append(field).append('=').append('$').append(index++); + } + + return setClause.toString(); } /* (non-Javadoc) diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index 4daa5ec..ffe4f8c 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -31,12 +31,12 @@ import java.util.Collections; import java.util.Map; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.core.function.DatabaseClient; import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.Table; @@ -66,6 +66,7 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); this.repository = new SimpleR2dbcRepository<>(databaseClient, + new MappingR2dbcConverter(mappingContext), (JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.jdbc = createJdbcTemplate(createDataSource()); @@ -84,7 +85,10 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS repository.save(legoSet) // .as(StepVerifier::create) // - .expectNextCount(1) // + .consumeNextWith(actual -> { + + assertThat(actual.getId()).isNotNull(); + }) .verifyComplete(); Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); @@ -92,16 +96,11 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS } @Test - @Ignore("Implement me") public void shouldUpdateObject() { - LegoSet legoSet = new LegoSet(null, "SCHAUFELRADBAGGER", 12); - - repository.save(legoSet) // - .as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); legoSet.setManual(14); repository.save(legoSet) //