DATAJDBC-130 - Support entities with List valued properties.
This commit is contained in:
committed by
Greg Turnquist
parent
7ed2ed7d7e
commit
f5de195768
@@ -200,11 +200,11 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
Class<?> actualType = property.getActualType();
|
||||
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName(),
|
||||
property.getKeyColumn());
|
||||
property.getKeyColumn(), property.isOrdered());
|
||||
|
||||
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), rootId);
|
||||
|
||||
return (Iterable<T>) operations.query(findAllByProperty, parameter, property.isQualified() //
|
||||
return (Iterable<T>) operations.query(findAllByProperty, parameter, property.isMap() //
|
||||
? getMapEntityRowMapper(property) //
|
||||
: getEntityRowMapper(actualType));
|
||||
}
|
||||
|
||||
@@ -19,10 +19,8 @@ import lombok.NonNull;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ClassGeneratingEntityInstantiator;
|
||||
import org.springframework.data.convert.EntityInstantiator;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
|
||||
@@ -45,6 +43,8 @@ import org.springframework.jdbc.core.RowMapper;
|
||||
*/
|
||||
public class EntityRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
private static final Converter ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter();
|
||||
|
||||
private final JdbcPersistentEntity<T> entity;
|
||||
private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator();
|
||||
private final ConversionService conversions;
|
||||
@@ -79,13 +79,12 @@ public class EntityRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
|
||||
if (Set.class.isAssignableFrom(property.getType())) {
|
||||
if (property.isCollectionLike()) {
|
||||
propertyAccessor.setProperty(property, accessStrategy.findAllByProperty(id, property));
|
||||
} else if (Map.class.isAssignableFrom(property.getType())) {
|
||||
} else if (property.isMap()) {
|
||||
|
||||
Iterable<Object> allByProperty = accessStrategy.findAllByProperty(id, property);
|
||||
IterableOfEntryToMapConverter converter = new IterableOfEntryToMapConverter();
|
||||
propertyAccessor.setProperty(property, converter.convert(allByProperty));
|
||||
propertyAccessor.setProperty(property, ITERABLE_OF_ENTRY_TO_MAP_CONVERTER.convert(allByProperty));
|
||||
} else {
|
||||
propertyAccessor.setProperty(property, readFrom(resultSet, property, ""));
|
||||
}
|
||||
|
||||
@@ -95,17 +95,22 @@ class SqlGenerator {
|
||||
* a referencing entity.
|
||||
*
|
||||
* @param columnName name of the column of the FK back to the referencing entity.
|
||||
* @param keyColumn if the property is of type {@link Map} this column contains the map key.
|
||||
* @param keyColumn if the property is of type {@link Map} this column contains the map key.
|
||||
* @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@literal true}, the keyColumn must not be {@literal null}.
|
||||
* @return a SQL String.
|
||||
*/
|
||||
String getFindAllByProperty(String columnName, String keyColumn) {
|
||||
String getFindAllByProperty(String columnName, String keyColumn, boolean ordered) {
|
||||
|
||||
Assert.isTrue(keyColumn != null || !ordered, "If the SQL statement should be ordered a keyColumn to order by must be provided.");
|
||||
|
||||
String baseSelect = (keyColumn != null) //
|
||||
? createSelectBuilder().column(cb -> cb.tableAlias(entity.getTableName()).column(keyColumn).as(keyColumn))
|
||||
.build()
|
||||
.build()
|
||||
: getFindAll();
|
||||
|
||||
return String.format("%s WHERE %s = :%s", baseSelect, columnName, columnName);
|
||||
String orderBy = ordered ? " ORDER BY " + keyColumn : "";
|
||||
|
||||
return String.format("%s WHERE %s = :%s%s", baseSelect, columnName, columnName, orderBy);
|
||||
}
|
||||
|
||||
String getExists() {
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
package org.springframework.data.jdbc.core.conversion;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.jdbc.core.conversion.DbAction.Insert;
|
||||
import org.springframework.data.jdbc.core.conversion.DbAction.Update;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
|
||||
@@ -33,6 +27,13 @@ import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.util.StreamUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Converts an entity that is about to be saved into {@link DbAction}s inside a {@link AggregateChange} that need to be
|
||||
* executed against the database to recreate the appropriate state in the database.
|
||||
@@ -157,6 +158,10 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
|
||||
Class<?> type = p.getType();
|
||||
|
||||
if (List.class.isAssignableFrom(type)) {
|
||||
return listPropertyAsStream(p, propertyAccessor);
|
||||
}
|
||||
|
||||
if (Collection.class.isAssignableFrom(type)) {
|
||||
return collectionPropertyAsStream(p, propertyAccessor);
|
||||
}
|
||||
@@ -178,6 +183,21 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
: ((Collection<Object>) property).stream();
|
||||
}
|
||||
|
||||
private Stream<Object> listPropertyAsStream(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Object property = propertyAccessor.getProperty(p);
|
||||
|
||||
if (property == null) return Stream.empty();
|
||||
|
||||
List<Object> listProperty = (List<Object>) property;
|
||||
HashMap<Integer, Object> map = new HashMap<>();
|
||||
for (int i = 0; i < listProperty.size(); i++) {
|
||||
map.put(i, listProperty.get(i));
|
||||
}
|
||||
|
||||
return map.entrySet().stream().map(e -> (Object) e);
|
||||
}
|
||||
|
||||
private Stream<Object> mapPropertyAsStream(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Object property = propertyAccessor.getProperty(p);
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.mapping.model;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
|
||||
@@ -29,6 +23,13 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Meta data about a property to be used by repository implementations.
|
||||
*
|
||||
@@ -114,7 +115,16 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper
|
||||
|
||||
@Override
|
||||
public boolean isQualified() {
|
||||
return isMap();
|
||||
return isMap() || isListLike();
|
||||
}
|
||||
|
||||
private boolean isListLike() {
|
||||
return isCollectionLike() && !Set.class.isAssignableFrom(this.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOrdered() {
|
||||
return isListLike();
|
||||
}
|
||||
|
||||
private Class columnTypeIfEntity(Class type) {
|
||||
|
||||
@@ -51,4 +51,9 @@ public interface JdbcPersistentProperty extends PersistentProperty<JdbcPersisten
|
||||
* Returns if this property is a qualified property, i.e. a property referencing multiple elements that can get picked by a key or an index.
|
||||
*/
|
||||
boolean isQualified();
|
||||
|
||||
/**
|
||||
* Returns whether this property is an ordered property.
|
||||
*/
|
||||
boolean isOrdered();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.springframework.util.Assert;
|
||||
public class EntityRowMapperUnitTests {
|
||||
|
||||
public static final long ID_FOR_ENTITY_REFERENCING_MAP = 42L;
|
||||
public static final long ID_FOR_ENTITY_REFERENCING_LIST = 4711L;
|
||||
public static final long ID_FOR_ENTITY_NOT_REFERENCING_MAP = 23L;
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
@@ -116,6 +117,21 @@ public class EntityRowMapperUnitTests {
|
||||
.containsExactly(ID_FOR_ENTITY_REFERENCING_MAP, "alpha", 2);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void listReferenceGetsLoadedWithAdditionalSelect() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name"), //
|
||||
ID_FOR_ENTITY_REFERENCING_LIST, "alpha");
|
||||
rs.next();
|
||||
|
||||
OneToMap extracted = createRowMapper(OneToMap.class).mapRow(rs, 1);
|
||||
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name, e -> e.children.size()) //
|
||||
.containsExactly(ID_FOR_ENTITY_REFERENCING_LIST, "alpha", 2);
|
||||
}
|
||||
|
||||
private <T> EntityRowMapper<T> createRowMapper(Class<T> type) {
|
||||
|
||||
JdbcMappingContext context = new JdbcMappingContext(mock(NamedParameterJdbcOperations.class));
|
||||
@@ -130,6 +146,11 @@ public class EntityRowMapperUnitTests {
|
||||
new SimpleEntry("two", new Trivial()) //
|
||||
))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_MAP), any(JdbcPersistentProperty.class));
|
||||
|
||||
doReturn(new HashSet<>(asList( //
|
||||
new SimpleEntry(1, new Trivial()), //
|
||||
new SimpleEntry(2, new Trivial()) //
|
||||
))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_LIST), any(JdbcPersistentProperty.class));
|
||||
|
||||
GenericConversionService conversionService = new GenericConversionService();
|
||||
conversionService.addConverter(new IterableOfEntryToMapConverter());
|
||||
DefaultConversionService.addDefaultConverters(conversionService);
|
||||
@@ -261,4 +282,13 @@ public class EntityRowMapperUnitTests {
|
||||
String name;
|
||||
Map<String, Trivial> children;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class OneToList {
|
||||
|
||||
@Id
|
||||
Long id;
|
||||
String name;
|
||||
List<Trivial> children;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public class SqlGeneratorUnitTests {
|
||||
public void findAllByProperty() {
|
||||
|
||||
// this would get called when DummyEntity is the element type of a Set
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", null);
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", null, false);
|
||||
|
||||
assertThat(sql).isEqualTo("SELECT DummyEntity.x_id AS x_id, DummyEntity.x_name AS x_name, "
|
||||
+ "ref.x_l1id AS ref_x_l1id, ref.x_content AS ref_x_content, ref.x_further AS ref_x_further "
|
||||
@@ -127,7 +127,7 @@ public class SqlGeneratorUnitTests {
|
||||
public void findAllByPropertyWithKey() {
|
||||
|
||||
// this would get called when DummyEntity is th element type of a Map
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", "key-column");
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", "key-column", false);
|
||||
|
||||
assertThat(sql).isEqualTo("SELECT DummyEntity.x_id AS x_id, DummyEntity.x_name AS x_name, "
|
||||
+ "ref.x_l1id AS ref_x_l1id, ref.x_content AS ref_x_content, ref.x_further AS ref_x_further, "
|
||||
@@ -136,6 +136,26 @@ public class SqlGeneratorUnitTests {
|
||||
+ "WHERE back-ref = :back-ref");
|
||||
}
|
||||
|
||||
@Test (expected = IllegalArgumentException.class) // DATAJDBC-130
|
||||
public void findAllByPropertyOrderedWithoutKey() {
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", null, true);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void findAllByPropertyWithKeyOrdered() {
|
||||
|
||||
// this would get called when DummyEntity is th element type of a Map
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", "key-column", true);
|
||||
|
||||
assertThat(sql).isEqualTo("SELECT DummyEntity.x_id AS x_id, DummyEntity.x_name AS x_name, "
|
||||
+ "ref.x_l1id AS ref_x_l1id, ref.x_content AS ref_x_content, ref.x_further AS ref_x_further, "
|
||||
+ "DummyEntity.key-column AS key-column "
|
||||
+ "FROM DummyEntity LEFT OUTER JOIN ReferencedEntity AS ref ON ref.DummyEntity = DummyEntity.x_id "
|
||||
+ "WHERE back-ref = :back-ref "
|
||||
+ "ORDER BY key-column"
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class DummyEntity {
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -167,7 +169,7 @@ public class JdbcEntityWriterUnitTests {
|
||||
public void newEntityWithEmptyMapResultsInSingleInsert() {
|
||||
|
||||
MapContainer entity = new MapContainer(null);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, MapContainer.class, entity);
|
||||
AggregateChange<MapContainer> aggregateChange = new AggregateChange(Kind.SAVE, MapContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
@@ -183,7 +185,7 @@ public class JdbcEntityWriterUnitTests {
|
||||
entity.elements.put("one", new Element(null));
|
||||
entity.elements.put("two", new Element(null));
|
||||
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity);
|
||||
AggregateChange<MapContainer> aggregateChange = new AggregateChange(Kind.SAVE, MapContainer.class, entity);
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions())
|
||||
@@ -193,15 +195,53 @@ public class JdbcEntityWriterUnitTests {
|
||||
tuple(Insert.class, Element.class, "one", "elements"), //
|
||||
tuple(Insert.class, Element.class, "two", "elements") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, MapContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, "two", "elements") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, MapContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, "one", "elements") //
|
||||
tuple(Insert.class, MapContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, "two", "elements") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, MapContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, "one", "elements") //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
@Test // DATAJDBC-130
|
||||
public void newEntityWithEmptyListResultsInSingleInsert() {
|
||||
|
||||
ListContainer entity = new ListContainer(null);
|
||||
AggregateChange<ListContainer> aggregateChange = new AggregateChange(Kind.SAVE, ListContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType, this::extractPath) //
|
||||
.containsExactly( //
|
||||
tuple(Insert.class, ListContainer.class, ""));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void newEntityWithListResultsInAdditionalInsertPerElement() {
|
||||
|
||||
ListContainer entity = new ListContainer(null);
|
||||
entity.elements.add( new Element(null));
|
||||
entity.elements.add( new Element(null));
|
||||
|
||||
AggregateChange<ListContainer> aggregateChange = new AggregateChange(Kind.SAVE, ListContainer.class, entity);
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions())
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType, this::getListKey, this::extractPath) //
|
||||
.containsExactlyInAnyOrder( //
|
||||
tuple(Insert.class, ListContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, 0, "elements"), //
|
||||
tuple(Insert.class, Element.class, 1, "elements") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, ListContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, 1, "elements") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, ListContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, 0, "elements") //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void mapTriggersDeletePlusInsert() {
|
||||
|
||||
MapContainer entity = new MapContainer(SOME_ENTITY_ID);
|
||||
@@ -217,7 +257,26 @@ public class JdbcEntityWriterUnitTests {
|
||||
tuple(Delete.class, Element.class, null, "elements"), //
|
||||
tuple(Update.class, MapContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, "one", "elements") //
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void listTriggersDeletePlusInsert() {
|
||||
|
||||
ListContainer entity = new ListContainer(SOME_ENTITY_ID);
|
||||
entity.elements.add( new Element(null));
|
||||
|
||||
AggregateChange<ListContainer> aggregateChange = new AggregateChange(Kind.SAVE, ListContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()) //
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType, this::getListKey, this::extractPath) //
|
||||
.containsExactly( //
|
||||
tuple(Delete.class, Element.class, null, "elements"), //
|
||||
tuple(Update.class, ListContainer.class, null, ""), //
|
||||
tuple(Insert.class, Element.class, 0, "elements") //
|
||||
);
|
||||
}
|
||||
|
||||
private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) {
|
||||
@@ -232,6 +291,10 @@ public class JdbcEntityWriterUnitTests {
|
||||
return a.getAdditionalValues().get("MapContainer_key");
|
||||
}
|
||||
|
||||
private Object getListKey(DbAction a) {
|
||||
return a.getAdditionalValues().get("ListContainer_key");
|
||||
}
|
||||
|
||||
private String extractPath(DbAction action) {
|
||||
return action.getPropertyPath().toDotPath();
|
||||
}
|
||||
@@ -273,6 +336,13 @@ public class JdbcEntityWriterUnitTests {
|
||||
Map<String, Element> elements = new HashMap<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class ListContainer {
|
||||
|
||||
@Id final Long id;
|
||||
List< Element> elements = new ArrayList<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class Element {
|
||||
@Id final Long id;
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2017-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.repository;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.jdbc.testing.TestConfiguration;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.rules.SpringClassRule;
|
||||
import org.springframework.test.context.junit4.rules.SpringMethodRule;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Very simple use cases for creation and usage of JdbcRepositories for Entities that contain {@link List}s.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@Transactional
|
||||
public class JdbcRepositoryWithListsIntegrationTests {
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
static class Config {
|
||||
|
||||
@Autowired
|
||||
JdbcRepositoryFactory factory;
|
||||
|
||||
@Bean
|
||||
Class<?> testClass() {
|
||||
return JdbcRepositoryWithListsIntegrationTests.class;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DummyEntityRepository dummyEntityRepository() {
|
||||
return factory.getRepository(DummyEntityRepository.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ClassRule
|
||||
public static final SpringClassRule classRule = new SpringClassRule();
|
||||
@Rule
|
||||
public SpringMethodRule methodRule = new SpringMethodRule();
|
||||
|
||||
@Autowired
|
||||
NamedParameterJdbcTemplate template;
|
||||
@Autowired
|
||||
DummyEntityRepository repository;
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void saveAndLoadEmptyList() {
|
||||
|
||||
DummyEntity entity = repository.save(createDummyEntity());
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull() //
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void saveAndLoadNonEmptyList() {
|
||||
|
||||
Element element1 = new Element();
|
||||
Element element2 = new Element();
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content).allMatch(v -> v.id != null);
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id) //
|
||||
.containsExactlyInAnyOrder(element1.id, element2.id);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void findAllLoadsList() {
|
||||
|
||||
Element element1 = new Element();
|
||||
Element element2 = new Element();
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content).allMatch(v -> v.id != null);
|
||||
|
||||
Iterable<DummyEntity> reloaded = repository.findAll();
|
||||
|
||||
reloaded.forEach(de -> System.out.println("id " + de.id + " content " + de.content.iterator().next().content));
|
||||
|
||||
assertThat(reloaded) //
|
||||
.extracting(e -> e.id, e -> e.content.size()) //
|
||||
.containsExactly(tuple(entity.id, entity.content.size()));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void updateList() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
Element element3 = createElement("three");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
entity.content.remove(element1);
|
||||
element2.content = "two changed";
|
||||
entity.content.add(element3);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content).allMatch(v -> v.id != null);
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
// the elements got properly updated and reloaded
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull();
|
||||
|
||||
assertThat(reloaded.content) //
|
||||
.extracting(e -> e.id, e -> e.content) //
|
||||
.containsExactly( //
|
||||
tuple(element2.id, "two changed"), //
|
||||
tuple(element3.id, "three") //
|
||||
);
|
||||
|
||||
Long count = template.queryForObject("SELECT count(1) FROM Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-130
|
||||
public void deletingWithList() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
repository.deleteById(entity.id);
|
||||
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
|
||||
Long count = template.queryForObject("SELECT count(1) FROM Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
private Element createElement(String content) {
|
||||
|
||||
Element element = new Element();
|
||||
element.content = content;
|
||||
return element;
|
||||
}
|
||||
|
||||
private static DummyEntity createDummyEntity() {
|
||||
|
||||
DummyEntity entity = new DummyEntity();
|
||||
entity.setName("Entity Name");
|
||||
return entity;
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
|
||||
}
|
||||
|
||||
@Data
|
||||
static class DummyEntity {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
String name;
|
||||
List<Element> content = new ArrayList<>();
|
||||
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class Element {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
String content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE dummyentity ( 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), DummyEntity_key BIGINT, dummyentity BIGINT);
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE dummyentity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(100), DummyEntity_key BIGINT,dummyentity BIGINT);
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TABLE element;
|
||||
DROP TABLE dummyentity;
|
||||
CREATE TABLE dummyentity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id SERIAL PRIMARY KEY, content VARCHAR(100),dummyentity_key BIGINT, dummyentity BIGINT);
|
||||
Reference in New Issue
Block a user