DATAJDBC-105 - Test with multiple databases.

Different databases are now supported by means of Maven Profiles and Spring Profiles. There is one Profile for each supported database. The default Profile is equivalent to hql. There is a Maven Profile all-dbs, which runs the integration tests against all databases.

The databases need to be started outside the build. The build assumes the default configuration as I found it after `brew install <database>`

For now we support the databases mysql, postgres and hsqldb.

In order to make the new databases work setting of properties and reading generated ids was improved to do some simple conversions. This might be considered a first step towards DATAJDBC-104.

The project root contains some basic scripts for starting and stopping databases, as well as running a build against all supported databases. Integration tests using a database now use Rules instead of JUnit runners. This gives more flexibility when adding fancy stuff to the Tests in the form of other Rules.

Related issue: DATAJDBC-104.
Original pull request: #5.
This commit is contained in:
Jens Schauder
2017-03-14 15:47:02 +01:00
committed by Oliver Gierke
parent 94d8960558
commit 1582a4d475
36 changed files with 626 additions and 202 deletions

View File

@@ -18,7 +18,8 @@ package org.springframework.data.jdbc.mapping.event;
import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
/**
* Gets published after instantiation and setting of all the properties of an entity. This allows to do some postprocessing of entities.
* Gets published after instantiation and setting of all the properties of an entity. This allows to do some
* postprocessing of entities.
*
* @author Jens Schauder
* @since 2.0

View File

@@ -20,14 +20,13 @@ import java.util.Optional;
import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
/**
* Gets published after deletion of an entity. It will have a {@link Specified} identifier.
*
* If the entity is empty or not depends on the delete method used.
* Gets published after deletion of an entity. It will have a {@link Specified} identifier. If the entity is empty or
* not depends on the delete method used.
*
* @author Jens Schauder
* @since 2.0
*/
public class AfterDelete extends JdbcEventWithId{
public class AfterDelete extends JdbcEventWithId {
/**
* @param id of the entity.

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.data.jdbc.mapping.event;
import java.util.Optional;
import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
/**

View File

@@ -19,9 +19,8 @@ import org.springframework.data.jdbc.mapping.event.Identifier.Unset;
/**
* Gets published before an entity gets inserted into the database. When the id-property of the entity must get set
* manually, an event listener for this event may do so.
*
* The {@link Identifier} is {@link org.springframework.data.jdbc.mapping.event.Identifier.Unset#UNSET}
* manually, an event listener for this event may do so. <br>
* The {@link Identifier} is {@link Unset#UNSET}
*
* @author Jens Schauder
* @since 2.0

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.data.jdbc.mapping.event;
import java.util.Optional;
/**
* Subclasses of this get published before an entity gets saved to the database.
*

View File

@@ -28,12 +28,12 @@ import java.util.Optional;
*/
public interface Identifier {
Optional<Object> getOptionalValue();
static Identifier fromNullable(Object value) {
return (value != null) ? new Specified(value) : Unset.UNSET;
}
Optional<Object> getOptionalValue();
/**
* An unset identifier. Always returns {@link Optional#empty()} as value.
*/
@@ -47,15 +47,13 @@ public interface Identifier {
}
/**
* An {@link Identifier} guaranteed to have a non empty value.
*
* Since it is guaranteed to exist the value can get access directly.
* An {@link Identifier} guaranteed to have a non empty value. Since it is guaranteed to exist the value can get
* access directly.
*/
@Data
class Specified implements Identifier {
@NonNull
private final Object value;
@NonNull private final Object value;
@Override
public Optional<Object> getOptionalValue() {

View File

@@ -15,15 +15,15 @@
*/
package org.springframework.data.jdbc.mapping.event;
import lombok.Getter;
import java.util.Optional;
import org.springframework.context.ApplicationEvent;
import lombok.Getter;
/**
* The common superclass for all events published by JDBC repositories.
* {@link #getSource} contains the {@link Identifier} of the entity triggering the event.
* The common superclass for all events published by JDBC repositories. {@link #getSource} contains the
* {@link Identifier} of the entity triggering the event.
*
* @author Jens Schauder
* @since 2.0
@@ -32,23 +32,20 @@ import lombok.Getter;
public class JdbcEvent extends ApplicationEvent {
/**
* The optional entity for which this event was published. Might be empty in cases of delete events where only the identifier
* was provided to the delete method.
*
* @return The entity triggering this event or empty.
* The optional entity for which this event was published. Might be empty in cases of delete events where only the
* identifier was passed to the delete method.
*/
private final Optional<Object> optionalEntity;
public JdbcEvent(Identifier id, Optional<Object> optionalEntity) {
JdbcEvent(Identifier id, Optional<Object> optionalEntity) {
super(id);
this.optionalEntity = optionalEntity;
}
/**
* The identifier of the entity, triggering this event. Also available via
* {@link #getSource()}.
* The identifier of the entity, triggering this event. Also available via {@link #getSource()}.
*
* @return
* @return the source of the event as an {@link Identifier}.
*/
public Identifier getId() {
return (Identifier) getSource();

View File

@@ -25,7 +25,7 @@ import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
* @author Jens Schauder
* @since 2.0
*/
public class JdbcEventWithId extends JdbcEvent implements WithId{
public class JdbcEventWithId extends JdbcEvent implements WithId {
public JdbcEventWithId(Specified id, Optional<Object> entity) {
super(id, entity);

View File

@@ -16,7 +16,8 @@
package org.springframework.data.jdbc.mapping.event;
/**
* Interface for {@link JdbcEvent}s which are guaranteed to have an entity. Allows direct access to that entity, without going through an {@link java.util.Optional}
* Interface for {@link JdbcEvent}s which are guaranteed to have an entity. Allows direct access to that entity, without
* going through an {@link java.util.Optional}
*
* @author Jens Schauder
* @since 2.0

View File

@@ -18,16 +18,15 @@ package org.springframework.data.jdbc.mapping.event;
import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
/**
* Interface for {@link JdbcEvent}s which are guaranteed to have a {@link Specified} identifier.
*
* Offers direct access to the {@link Specified} identifier.
* Interface for {@link JdbcEvent}s which are guaranteed to have a {@link Specified} identifier. Offers direct access to
* the {@link Specified} identifier.
*
* @author Jens Schauder
* @since 2.0
*/
public interface WithId {
default Specified getSpecifiedId(){
return (Specified) ((JdbcEvent)this).getId();
default Specified getSpecifiedId() {
return (Specified) ((JdbcEvent) this).getId();
}
}

View File

@@ -19,7 +19,7 @@ import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.util.TypeInformation;
/**
* meta data a repository might need for implementing persistence operations for instances of type {@code T}
* Meta data a repository might need for implementing persistence operations for instances of type {@code T}
*
* @author Jens Schauder
* @since 2.0

View File

@@ -18,6 +18,8 @@ package org.springframework.data.jdbc.repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.convert.ClassGeneratingEntityInstantiator;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
@@ -30,7 +32,7 @@ import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.jdbc.core.RowMapper;
/**
* maps a ResultSet to an entity of type {@code T}
* Maps a ResultSet to an entity of type {@code T}
*
* @author Jens Schauder
* @since 2.0
@@ -38,8 +40,8 @@ import org.springframework.jdbc.core.RowMapper;
class EntityRowMapper<T> implements RowMapper<T> {
private final JdbcPersistentEntity<T> entity;
private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator();
private final ConversionService conversions = new DefaultConversionService();
EntityRowMapper(JdbcPersistentEntity<T> entity) {
this.entity = entity;
@@ -58,13 +60,16 @@ class EntityRowMapper<T> implements RowMapper<T> {
}
private T createInstance(ResultSet rs) {
return instantiator.createInstance(entity, new ParameterValueProvider<JdbcPersistentProperty>() {
@SuppressWarnings("unchecked")
@Override
public <T> T getParameterValue(PreferredConstructor.Parameter<T, JdbcPersistentProperty> parameter) {
try {
return (T) rs.getObject(parameter.getName());
return conversions.convert(rs.getObject(parameter.getName()), parameter.getType().getType());
} catch (SQLException e) {
throw new MappingException( //
String.format("Couldn't read column %s from ResultSet.", parameter.getName()) //
);
@@ -76,7 +81,9 @@ class EntityRowMapper<T> implements RowMapper<T> {
private void setProperty(ResultSet rs, T t, PersistentProperty property) {
try {
entity.getPropertyAccessor(t).setProperty(property, rs.getObject(property.getName()));
Object converted = conversions.convert(rs.getObject(property.getName()), property.getType());
entity.getPropertyAccessor(t).setProperty(property, converted);
} catch (Exception e) {
throw new RuntimeException(String.format("Couldn't set property %s.", property.getName()), e);
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.jdbc.repository;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -25,10 +27,9 @@ import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation;
import org.springframework.jdbc.core.RowMapper;
import lombok.RequiredArgsConstructor;
/**
* a RowMapper that publishes events after a delegate, did the actual work of mapping a {@link ResultSet} to an entityInformation.
* A {@link RowMapper} that publishes events after a delegate, did the actual work of mapping a {@link ResultSet} to an
* entityInformation.
*
* @author Jens Schauder
* @since 2.0

View File

@@ -25,6 +25,9 @@ import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.jdbc.mapping.event.AfterDelete;
import org.springframework.data.jdbc.mapping.event.AfterInsert;
@@ -59,12 +62,15 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
private final JdbcPersistentEntityInformation<T, ID> entityInformation;
private final NamedParameterJdbcOperations operations;
private final SqlGenerator sql;
private final EntityRowMapper<T> entityRowMapper;
private final ApplicationEventPublisher publisher;
private final ConversionService conversions = new DefaultConversionService();
public SimpleJdbcRepository(JdbcPersistentEntity<T> persistentEntity, NamedParameterJdbcOperations jdbcOperations,
ApplicationEventPublisher publisher) {
public SimpleJdbcRepository( //
JdbcPersistentEntity<T> persistentEntity, //
NamedParameterJdbcOperations jdbcOperations, //
ApplicationEventPublisher publisher //
) {
Assert.notNull(persistentEntity, "PersistentEntity must not be null.");
Assert.notNull(jdbcOperations, "JdbcOperations must not be null.");
@@ -75,8 +81,8 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
this.operations = jdbcOperations;
this.publisher = publisher;
entityRowMapper = new EntityRowMapper<>(persistentEntity);
sql = new SqlGenerator(persistentEntity);
this.entityRowMapper = new EntityRowMapper<>(persistentEntity);
this.sql = new SqlGenerator(persistentEntity);
}
@Override
@@ -95,9 +101,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
public <S extends T> Iterable<S> save(Iterable<S> entities) {
List<S> savedEntities = new ArrayList<>();
entities.forEach(e -> savedEntities.add(save(e)));
return savedEntities;
}
@@ -141,8 +145,12 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
@Override
public void delete(Iterable<? extends T> entities) {
operations.update(sql.getDeleteByList(), new MapSqlParameterSource("ids", StreamSupport
.stream(entities.spliterator(), false).map(entityInformation::getId).collect(Collectors.toList())));
List<ID> idList = StreamSupport.stream(entities.spliterator(), false) //
.map(entityInformation::getId) //
.collect(Collectors.toList());
MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource("ids", idList);
operations.update(sql.getDeleteByList(), sqlParameterSource);
}
@Override
@@ -154,11 +162,11 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
Map<String, Object> parameters = new HashMap<>();
this.persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) //
property -> parameters.put( //
property.getColumnName(), //
persistentEntity.getPropertyAccessor(instance).getProperty(property)) //
);
this.persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
parameters.put(property.getColumnName(), value);
});
return parameters;
}
@@ -195,16 +203,38 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
}
private <S extends T> void setIdFromJdbc(S instance, KeyHolder holder) {
try {
Number idValueFromJdbc = holder.getKey();
Object idValueFromJdbc = getIdFromHolder(holder);
if (idValueFromJdbc != null) {
entityInformation.setId(instance, idValueFromJdbc);
Class<?> targetType = persistentEntity.getIdProperty().getType();
Object converted = convert(idValueFromJdbc, targetType);
entityInformation.setId(instance, converted);
}
} catch (NonTransientDataAccessException e) {
throw new UnableToSetIdException("Unable to set id of " + instance, e);
}
}
private Object getIdFromHolder(KeyHolder holder) {
try {
// mysql just returns one value with a special name
return holder.getKey();
} catch (InvalidDataAccessApiUsageException e) {
// postgress returns a value for each column
return holder.getKeys().get(persistentEntity.getIdColumn());
}
}
private <V> V convert(Object idValueFromJdbc, Class<V> targetType) {
return conversions.convert(idValueFromJdbc, targetType);
}
private void doDelete(Specified specifiedId, Optional<Object> optionalEntity) {
publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity));

View File

@@ -17,12 +17,15 @@ package org.springframework.data.jdbc.repository;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
/**
* Generates SQL statements to be used by {@Link SimpleJdbcRepository}
*
* @author Jens Schauder
* @since 2.0
*/
@@ -36,29 +39,45 @@ class SqlGenerator {
private final String countSql;
private final String insertSql;
private final String updateSql;
private final String deleteByIdSql;
private final String deleteAllSql;
private final String deleteByListSql;
private final String updateSql;
private final JdbcPersistentEntity<?> entity;
private final List<String> propertyNames = new ArrayList<>();
private final List<String> nonIdPropertyNames = new ArrayList<>();
<T> SqlGenerator(JdbcPersistentEntity<T> entity) {
SqlGenerator(JdbcPersistentEntity<?> entity) {
this.entity = entity;
entity.doWithProperties((PropertyHandler) persistentProperty -> propertyNames.add(persistentProperty.getName()));
initPropertyNames();
findOneSql = createFindOneSelectSql(entity);
findAllSql = createFindAllSql(entity);
findAllInListSql = createFindAllInListSql(entity);
findOneSql = createFindOneSelectSql();
findAllSql = createFindAllSql();
findAllInListSql = createFindAllInListSql();
existsSql = createExistsSql(entity);
countSql = createCountSql(entity);
existsSql = createExistsSql();
countSql = createCountSql();
insertSql = createInsertSql(entity);
updateSql = createUpdateSql(entity);
insertSql = createInsertSql();
deleteByIdSql = createDeleteSql(entity);
deleteAllSql = createDeleteAllSql(entity);
deleteByListSql = createDeleteByListSql(entity);
updateSql = createUpdateSql();
deleteByIdSql = createDeleteSql();
deleteAllSql = createDeleteAllSql();
deleteByListSql = createDeleteByListSql();
}
private <T> void initPropertyNames() {
entity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) p -> {
propertyNames.add(p.getName());
if (!entity.isIdProperty(p)) {
nonIdPropertyNames.add(p.getName());
}
});
}
String getFindAllInList() {
@@ -100,55 +119,56 @@ class SqlGenerator {
String getDeleteByList() {
return deleteByListSql;
}
private String createFindOneSelectSql(JdbcPersistentEntity<?> entity) {
private String createFindOneSelectSql() {
return String.format("select * from %s where %s = :id", entity.getTableName(), entity.getIdColumn());
}
private String createFindAllSql(JdbcPersistentEntity<?> entity) {
private String createFindAllSql() {
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 createFindAllInListSql() {
return String.format("select * from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn());
}
private String createExistsSql(JdbcPersistentEntity<?> entity) {
private String createExistsSql() {
return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn());
}
private <T> String createCountSql(JdbcPersistentEntity<T> entity) {
private <T> String createCountSql() {
return String.format("select count(*) from %s", entity.getTableName());
}
private String createInsertSql(JdbcPersistentEntity<?> entity) {
private String createInsertSql() {
String insertTemplate = "insert into %s (%s) values (%s)";
String tableColumns = propertyNames.stream().collect(Collectors.joining(", "));
String parameterNames = propertyNames.stream().collect(Collectors.joining(", :", ":", ""));
String tableColumns = nonIdPropertyNames.stream().collect(Collectors.joining(", "));
String parameterNames = nonIdPropertyNames.stream().collect(Collectors.joining(", :", ":", ""));
return String.format(insertTemplate, entity.getTableName(), tableColumns, parameterNames);
}
private <T> String createUpdateSql(JdbcPersistentEntity<T> entity) {
private <T> String createUpdateSql() {
String updateTemplate = "update %s set %s where %s = :%s";
String setClause = propertyNames.stream().map(n -> String.format("%s = :%s", n, n)).collect(Collectors.joining(", "));
String setClause = propertyNames.stream().map(n -> String.format("%s = :%s", n, n))
.collect(Collectors.joining(", "));
return String.format(updateTemplate, entity.getTableName(), setClause, entity.getIdColumn(), entity.getIdColumn());
}
private String createDeleteSql(JdbcPersistentEntity entity) {
private String createDeleteSql() {
return String.format("delete from %s where %s = :id", entity.getTableName(), entity.getIdColumn());
}
private String createDeleteAllSql(JdbcPersistentEntity entity) {
private String createDeleteAllSql() {
return String.format("delete from %s", entity.getTableName());
}
private String createDeleteByListSql(JdbcPersistentEntity entity) {
private String createDeleteByListSql() {
return String.format("delete from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn());
}
}