diff --git a/pom.xml b/pom.xml index 8f9de8fe20..8fc40b10df 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jdbc - 1.0.0.BUILD-SNAPSHOT + 1.0.0.DATAJDBC-97-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -67,6 +67,11 @@ spring-beans + + org.springframework + spring-jdbc + + org.springframework spring-core @@ -84,9 +89,12 @@ 2.2.8 test + - org.springframework - spring-jdbc + org.assertj + assertj-core + 3.6.2 + test diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java index 7e91a166c9..217d8d701f 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java @@ -19,11 +19,35 @@ import org.springframework.data.util.TypeInformation; /** + * meta data a repository might need for implementing persistence operations for instances of type {@code T} * @author Jens Schauder */ public class JdbcPersistentEntity extends BasicPersistentEntity { + private String tableName; + private String idColumn; + public JdbcPersistentEntity(TypeInformation information) { super(information); } + + public String getTableName() { + + if (tableName == null) + tableName = getType().getSimpleName(); + + return tableName; + } + + public String getIdColumn() { + + if (idColumn == null) + idColumn = getIdProperty().getName(); + + return idColumn; + } + + public Object getIdValue(T instance) { + return getPropertyAccessor(instance).getProperty(getIdProperty()); + } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java index 1bcc1be849..841d64c650 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java @@ -23,6 +23,8 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; /** + * meta data about a property to be used by repository implementations. + * * @author Jens Schauder */ public class JdbcPersistentProperty extends AnnotationBasedPersistentProperty { @@ -43,4 +45,8 @@ public JdbcPersistentProperty(Field field, PropertyDescriptor propertyDescriptor protected Association createAssociation() { return null; } + + public String getColumnName() { + return getName(); + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java new file mode 100644 index 0000000000..4bde019f07 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 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.repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import org.springframework.data.convert.ClassGeneratingEntityInstantiator; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.mapping.model.ParameterValueProvider; + +/** + * maps a ResultSet to an entity of type {@code T} + * + * @author Jens Schauder + */ +class EntityRowMapper implements org.springframework.jdbc.core.RowMapper { + + private final JdbcPersistentEntity entity; + + private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator(); + + EntityRowMapper(JdbcPersistentEntity entity) { + this.entity = entity; + } + + @Override + public T mapRow(ResultSet rs, int rowNum) throws SQLException { + + T t = createInstance(rs); + + entity.doWithProperties((PropertyHandler) property -> { + setProperty(rs, t, property); + }); + + return t; + } + + private T createInstance(ResultSet rs) { + return instantiator.createInstance(entity, new ParameterValueProvider() { + @Override + public T getParameterValue(PreferredConstructor.Parameter parameter) { + try { + return (T) rs.getObject(parameter.getName()); + } catch (SQLException e) { + throw new MappingException(String.format("Couldn't read column %s from ResultSet.", parameter.getName())); + } + } + }); + } + + private void setProperty(ResultSet rs, T t, PersistentProperty property) { + + try { + entity.getPropertyAccessor(t).setProperty(property, rs.getObject(property.getName())); + } catch (Exception e) { + throw new RuntimeException(String.format("Couldn't set property %s.", property.getName()), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java index 45bb7ad979..556e2a1eaf 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java @@ -18,9 +18,14 @@ import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import javax.sql.DataSource; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.core.EntityInformation; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -29,74 +34,121 @@ */ public class SimpleJdbcRepository implements CrudRepository { - private final EntityInformation entityInformation; + private final JdbcPersistentEntity entity; private final NamedParameterJdbcOperations template; + private final SqlGenerator sql; - public SimpleJdbcRepository(EntityInformation entityInformation, DataSource dataSource) { - this.entityInformation = entityInformation; + private final EntityRowMapper entityRowMapper; + + public SimpleJdbcRepository(JdbcPersistentEntity entity, DataSource dataSource) { + + this.entity = entity; this.template = new NamedParameterJdbcTemplate(dataSource); + + entityRowMapper = new EntityRowMapper(entity); + sql = new SqlGenerator(entity); } @Override public S save(S entity) { - Map parameters = new HashMap<>(); - parameters.put("id", entityInformation.getId(entity)); - parameters.put("name", "blah blah"); - template.update( - "insert into dummyentity (id, name) values (:id, :name)", - parameters); + template.update(sql.getInsert(), getPropertyMap(entity)); return entity; } @Override public Iterable save(Iterable entities) { - return null; + + Map[] batchValues = StreamSupport + .stream(entities.spliterator(), false) + .map(i -> getPropertyMap(i)) + .toArray(size -> new Map[size]); + + template.batchUpdate(sql.getInsert(), batchValues); + + return entities; } @Override public T findOne(ID id) { - return null; + + return template.queryForObject( + sql.getFindOne(), + new MapSqlParameterSource("id", id), + entityRowMapper + ); } @Override public boolean exists(ID id) { - return false; + + return template.queryForObject( + sql.getExists(), + new MapSqlParameterSource("id", id), + Boolean.class + ); } @Override public Iterable findAll() { - return null; + return template.query(sql.getFindAll(), entityRowMapper); } @Override public Iterable findAll(Iterable ids) { - return null; + return template.query(sql.getFindAllInList(), new MapSqlParameterSource("ids", ids), entityRowMapper); } @Override public long count() { - return 0; + return template.getJdbcOperations().queryForObject(sql.getCount(), Long.class); } @Override public void delete(ID id) { - + template.update(sql.getDeleteById(), new MapSqlParameterSource("id", id)); } @Override - public void delete(T entity) { + public void delete(T instance) { + template.update( + sql.getDeleteById(), + new MapSqlParameterSource("id", + entity.getIdValue(instance))); } @Override public void delete(Iterable entities) { + template.update( + sql.getDeleteByList(), + new MapSqlParameterSource("ids", + StreamSupport + .stream(entities.spliterator(), false) + .map(entity::getIdValue) + .collect(Collectors.toList()) + ) + ); } @Override public void deleteAll() { + template.getJdbcOperations().update(sql.getDeleteAll()); + } + + private Map getPropertyMap(final S instance) { + + Map parameters = new HashMap<>(); + + this.entity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(JdbcPersistentProperty persistentProperty) { + parameters.put(persistentProperty.getColumnName(), entity.getPropertyAccessor(instance).getProperty(persistentProperty)); + } + }); + return parameters; } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java new file mode 100644 index 0000000000..b561029bb5 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017 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.repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.mapping.PropertyHandler; + +/** + * @author Jens Schauder + */ +class SqlGenerator { + + private final String findOneSql; + private final String findAllSql; + private final String findAllInListSql; + + private final String existsSql; + private final String countSql; + + private final String insertSql; + private final String deleteByIdSql; + private final String deleteAllSql; + private final String deleteByListSql; + + SqlGenerator(JdbcPersistentEntity entity) { + + findOneSql = createFindOneSelectSql(entity); + findAllSql = createFindAllSql(entity); + findAllInListSql = createFindAllInListSql(entity); + + existsSql = createExistsSql(entity); + countSql = createCountSql(entity); + + insertSql = createInsertSql(entity); + + deleteByIdSql = createDeleteSql(entity); + deleteAllSql = createDeleteAllSql(entity); + deleteByListSql = createDeleteByListSql(entity); + } + + String getFindAllInList() { + return findAllInListSql; + } + + String getFindAll() { + return findAllSql; + } + + String getExists() { + return existsSql; + } + + String getFindOne() { + return findOneSql; + } + + String getInsert() { + return insertSql; + } + + String getCount() { + return countSql; + } + + String getDeleteById() { + return deleteByIdSql; + } + + String getDeleteAll() { + return deleteAllSql; + } + + String getDeleteByList() { + return deleteByListSql; + } + private String createFindOneSelectSql(JdbcPersistentEntity entity) { + return String.format("select * from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createFindAllSql(JdbcPersistentEntity entity) { + return String.format("select * from %s", entity.getTableName()); + } + + private String createFindAllInListSql(JdbcPersistentEntity entity) { + return String.format(String.format("select * from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn()), entity.getTableName()); + } + + private String createExistsSql(JdbcPersistentEntity entity) { + return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createCountSql(JdbcPersistentEntity entity) { + return String.format("select count(*) from %s", entity.getTableName(), entity.getIdColumn()); + } + + private String createInsertSql(JdbcPersistentEntity entity) { + + List propertyNames = new ArrayList<>(); + entity.doWithProperties((PropertyHandler) persistentProperty -> propertyNames.add(persistentProperty.getName())); + + String insertTemplate = "insert into %s (%s) values (%s)"; + + String tableName = entity.getType().getSimpleName(); + + String tableColumns = propertyNames.stream().collect(Collectors.joining(", ")); + String parameterNames = propertyNames.stream().collect(Collectors.joining(", :", ":", "")); + + return String.format(insertTemplate, tableName, tableColumns, parameterNames); + } + + private String createDeleteSql(JdbcPersistentEntity entity) { + return String.format("delete from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createDeleteAllSql(JdbcPersistentEntity entity) { + return String.format("delete from %s", entity.getTableName()); + } + + private String createDeleteByListSql(JdbcPersistentEntity entity) { + return String.format("delete from %s where id in (:ids)", entity.getTableName()); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index a92d74d6cb..d24598250d 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -39,13 +39,12 @@ public JdbcRepositoryFactory(DataSource dataSource) { @Override public EntityInformation getEntityInformation(Class aClass) { - JdbcPersistentEntity persistentEntity = (JdbcPersistentEntity) context.getPersistentEntity(aClass); - return new JdbcPersistentEntityInformation(persistentEntity); + return new JdbcPersistentEntityInformation((JdbcPersistentEntity) context.getPersistentEntity(aClass)); } @Override protected Object getTargetRepository(RepositoryInformation repositoryInformation) { - return new SimpleJdbcRepository(getEntityInformation(repositoryInformation.getDomainType()), dataSource); + return new SimpleJdbcRepository(context.getPersistentEntity(repositoryInformation.getDomainType()), dataSource); } @Override diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index b938925f62..8af0faf152 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -15,10 +15,13 @@ */ package org.springframework.data.jdbc.repository; +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; import java.sql.SQLException; import org.junit.After; +import org.junit.Assert; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -48,19 +51,18 @@ public class JdbcRepositoryIntegrationTests { private final NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(db); + private final DummyEntityRepository repository = createRepository(db); + + private DummyEntity entity = createDummyEntity(23L); + @After - public void afeter() { + public void after() { db.shutdown(); } @Test - public void canSaveAnEntity() throws SQLException { - DummyEntityRepository repository = createRepository(); - - DummyEntity entity = new DummyEntity(); - entity.setId(23L); - entity.setName("Entity Name"); + public void canSaveAnEntity() { entity = repository.save(entity); @@ -74,17 +76,146 @@ public void canSaveAnEntity() throws SQLException { count); } - private DummyEntityRepository createRepository() throws SQLException { - JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(db); - return jdbcRepositoryFactory.getRepository(DummyEntityRepository.class); + @Test + public void canSaveAndLoadAnEntity() { + + entity = repository.save(entity); + + DummyEntity reloadedEntity = repository.findOne(entity.getId()); + + assertEquals( + entity.getId(), + reloadedEntity.getId()); + assertEquals( + entity.getName(), + reloadedEntity.getName()); + } + + @Test + public void saveMany() { + + DummyEntity other = createDummyEntity(24L); + + repository.save(asList(entity, other)); + + assertThat(repository.findAll()).extracting(DummyEntity::getId).containsExactlyInAnyOrder(23L, 24L); + } + + @Test + public void existsReturnsTrueIffEntityExists() { + + entity = repository.save(entity); + + assertTrue(repository.exists(entity.getId())); + assertFalse(repository.exists(entity.getId() + 1)); + } + + @Test + public void findAllFindsAllEntities() { + + DummyEntity other = createDummyEntity(24L); + + other = repository.save(other); + entity = repository.save(entity); + + Iterable all = repository.findAll(); + + assertThat(all).extracting("id").containsExactlyInAnyOrder(entity.getId(), other.getId()); + } + + @Test + public void findAllFindsAllSpecifiedEntities() { + + repository.save(createDummyEntity(24L)); + DummyEntity other = repository.save(createDummyEntity(25L)); + entity = repository.save(entity); + + Iterable all = repository.findAll(asList(entity.getId(), other.getId())); + + assertThat(all).extracting("id").containsExactlyInAnyOrder(entity.getId(), other.getId()); + } + + @Test + public void count() { + + repository.save(createDummyEntity(24L)); + repository.save(createDummyEntity(25L)); + repository.save(entity); + + assertThat(repository.count()).isEqualTo(3L); + } + + @Test + public void deleteById() { + + repository.save(createDummyEntity(24L)); + repository.save(createDummyEntity(25L)); + repository.save(entity); + + repository.delete(24L); + + assertThat(repository.findAll()).extracting(DummyEntity::getId).containsExactlyInAnyOrder(23L, 25L); + } + + @Test + public void deleteByEntity() { + + repository.save(createDummyEntity(24L)); + repository.save(createDummyEntity(25L)); + repository.save(entity); + + repository.delete(entity); + + assertThat(repository.findAll()).extracting(DummyEntity::getId).containsExactlyInAnyOrder(24L, 25L); + } + + + @Test + public void deleteByList() { + + repository.save(entity); + repository.save(createDummyEntity(24L)); + DummyEntity other = repository.save(createDummyEntity(25L)); + + repository.delete(asList(entity, other)); + + assertThat(repository.findAll()).extracting(DummyEntity::getId).containsExactlyInAnyOrder(24L); + } + + @Test + public void deleteAll() { + + repository.save(entity); + repository.save(createDummyEntity(24L)); + repository.save(createDummyEntity(25L)); + + repository.deleteAll(); + + assertThat(repository.findAll()).isEmpty(); + } + + + + private static DummyEntityRepository createRepository(EmbeddedDatabase db) { + return new JdbcRepositoryFactory(db).getRepository(DummyEntityRepository.class); + } + + + private static DummyEntity createDummyEntity(long id) { + + DummyEntity entity = new DummyEntity(); + entity.setId(id); + entity.setName("Entity Name"); + return entity; } private interface DummyEntityRepository extends CrudRepository { } + // needs to be public in order for the Hamcrest property matcher to work. @Data - private static class DummyEntity { + public static class DummyEntity { @Id Long id;