Browse Source
Immutable entities can now be saved and loaded and the immutability will be honored. This feature is currently limited to a single level of reference. In order to implement this the logic for updating IDs with those generated from the database got moved out of the DefaultDataAccessStrategy into the AggregateChange. As part of that move DataAccessStrategy.save now returns a generated id, if available. See also: DATAJDBC-248.pull/130/head
23 changed files with 645 additions and 118 deletions
@ -0,0 +1,272 @@
@@ -0,0 +1,272 @@
|
||||
/* |
||||
* 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.core; |
||||
|
||||
import static java.util.Collections.*; |
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import lombok.Value; |
||||
import lombok.experimental.Wither; |
||||
|
||||
import org.assertj.core.api.SoftAssertions; |
||||
import org.junit.ClassRule; |
||||
import org.junit.Rule; |
||||
import org.junit.Test; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.context.ApplicationEventPublisher; |
||||
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.testing.TestConfiguration; |
||||
import org.springframework.data.relational.core.conversion.RelationalConverter; |
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||
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; |
||||
|
||||
/** |
||||
* Integration tests for {@link JdbcAggregateTemplate} and it's handling of immutable entities. |
||||
* |
||||
* @author Jens Schauder |
||||
*/ |
||||
@ContextConfiguration |
||||
@Transactional |
||||
public class ImmutableAggregateTemplateHsqlIntegrationTests { |
||||
|
||||
@ClassRule public static final SpringClassRule classRule = new SpringClassRule(); |
||||
@Rule public SpringMethodRule methodRule = new SpringMethodRule(); |
||||
@Autowired JdbcAggregateOperations template; |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveWithGeneratedIdCreatesNewInstance() { |
||||
|
||||
LegoSet legoSet = createLegoSet(createManual()); |
||||
|
||||
LegoSet saved = template.save(legoSet); |
||||
|
||||
SoftAssertions softly = new SoftAssertions(); |
||||
|
||||
softly.assertThat(legoSet).isNotSameAs(saved); |
||||
softly.assertThat(legoSet.getId()).isNull(); |
||||
|
||||
softly.assertThat(saved.getId()).isNotNull(); |
||||
softly.assertThat(saved.name).isNotNull(); |
||||
softly.assertThat(saved.manual).isNotNull(); |
||||
softly.assertThat(saved.manual.content).isNotNull(); |
||||
|
||||
softly.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveAndLoadAnEntityWithReferencedEntityById() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(createManual())); |
||||
|
||||
assertThat(saved.manual.id).describedAs("id of stored manual").isNotNull(); |
||||
|
||||
LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); |
||||
|
||||
assertThat(reloadedLegoSet.manual).isNotNull(); |
||||
|
||||
SoftAssertions softly = new SoftAssertions(); |
||||
|
||||
softly.assertThat(reloadedLegoSet.manual.getId()) //
|
||||
.isEqualTo(saved.getManual().getId()) //
|
||||
.isNotNull(); |
||||
softly.assertThat(reloadedLegoSet.manual.getContent()).isEqualTo(saved.getManual().getContent()); |
||||
|
||||
softly.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveAndLoadManyEntitiesWithReferencedEntity() { |
||||
|
||||
LegoSet legoSet = createLegoSet(createManual()); |
||||
|
||||
LegoSet savedLegoSet = template.save(legoSet); |
||||
|
||||
Iterable<LegoSet> reloadedLegoSets = template.findAll(LegoSet.class); |
||||
|
||||
assertThat(reloadedLegoSets).hasSize(1).extracting("id", "manual.id", "manual.content") |
||||
.contains(tuple(savedLegoSet.getId(), savedLegoSet.getManual().getId(), savedLegoSet.getManual().getContent())); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveAndLoadManyEntitiesByIdWithReferencedEntity() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(createManual())); |
||||
|
||||
Iterable<LegoSet> reloadedLegoSets = template.findAllById(singletonList(saved.getId()), LegoSet.class); |
||||
|
||||
assertThat(reloadedLegoSets).hasSize(1).extracting("id", "manual.id", "manual.content") |
||||
.contains(tuple(saved.getId(), saved.getManual().getId(), saved.getManual().getContent())); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveAndLoadAnEntityWithReferencedNullEntity() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(null)); |
||||
|
||||
LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); |
||||
|
||||
assertThat(reloadedLegoSet.manual).isNull(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveAndDeleteAnEntityWithReferencedEntity() { |
||||
|
||||
LegoSet legoSet = createLegoSet(createManual()); |
||||
|
||||
LegoSet saved = template.save(legoSet); |
||||
|
||||
template.delete(saved, LegoSet.class); |
||||
|
||||
SoftAssertions softly = new SoftAssertions(); |
||||
|
||||
softly.assertThat(template.findAll(LegoSet.class)).isEmpty(); |
||||
softly.assertThat(template.findAll(Manual.class)).isEmpty(); |
||||
|
||||
softly.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void saveAndDeleteAllWithReferencedEntity() { |
||||
|
||||
template.save(createLegoSet(createManual())); |
||||
|
||||
template.deleteAll(LegoSet.class); |
||||
|
||||
SoftAssertions softly = new SoftAssertions(); |
||||
|
||||
assertThat(template.findAll(LegoSet.class)).isEmpty(); |
||||
assertThat(template.findAll(Manual.class)).isEmpty(); |
||||
|
||||
softly.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void updateReferencedEntityFromNull() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(null)); |
||||
|
||||
LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, new Manual(23L, "Some content")); |
||||
|
||||
template.save(changedLegoSet); |
||||
|
||||
LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); |
||||
|
||||
assertThat(reloadedLegoSet.manual.content).isEqualTo("Some content"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void updateReferencedEntityToNull() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(null)); |
||||
|
||||
LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, null); |
||||
|
||||
template.save(changedLegoSet); |
||||
|
||||
LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); |
||||
|
||||
SoftAssertions softly = new SoftAssertions(); |
||||
|
||||
softly.assertThat(reloadedLegoSet.manual).isNull(); |
||||
softly.assertThat(template.findAll(Manual.class)).describedAs("Manuals failed to delete").isEmpty(); |
||||
|
||||
softly.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void replaceReferencedEntity() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(null)); |
||||
|
||||
LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, new Manual(null, "other content")); |
||||
|
||||
template.save(changedLegoSet); |
||||
|
||||
LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); |
||||
|
||||
SoftAssertions softly = new SoftAssertions(); |
||||
|
||||
softly.assertThat(reloadedLegoSet.manual.content).isEqualTo("other content"); |
||||
softly.assertThat(template.findAll(Manual.class)).describedAs("There should be only one manual").hasSize(1); |
||||
|
||||
softly.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void changeReferencedEntity() { |
||||
|
||||
LegoSet saved = template.save(createLegoSet(createManual())); |
||||
|
||||
LegoSet changedLegoSet = saved.withManual(saved.manual.withContent("new content")); |
||||
|
||||
template.save(changedLegoSet); |
||||
|
||||
LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); |
||||
|
||||
Manual manual = reloadedLegoSet.manual; |
||||
assertThat(manual).isNotNull(); |
||||
assertThat(manual.content).isEqualTo("new content"); |
||||
} |
||||
|
||||
private static LegoSet createLegoSet(Manual manual) { |
||||
|
||||
return new LegoSet(null, "Star Destroyer", manual); |
||||
} |
||||
|
||||
private static Manual createManual() { |
||||
return new Manual(null, |
||||
"Accelerates to 99% of light speed. Destroys almost everything. See https://what-if.xkcd.com/1/"); |
||||
} |
||||
|
||||
@Value |
||||
@Wither |
||||
static class LegoSet { |
||||
|
||||
@Id Long id; |
||||
String name; |
||||
Manual manual; |
||||
} |
||||
|
||||
@Value |
||||
@Wither |
||||
static class Manual { |
||||
|
||||
@Id Long id; |
||||
String content; |
||||
} |
||||
|
||||
@Configuration |
||||
@Import(TestConfiguration.class) |
||||
static class Config { |
||||
|
||||
@Bean |
||||
Class<?> testClass() { |
||||
return ImmutableAggregateTemplateHsqlIntegrationTests.class; |
||||
} |
||||
|
||||
@Bean |
||||
JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, |
||||
DataAccessStrategy dataAccessStrategy, RelationalConverter converter) { |
||||
return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
|
||||
/* |
||||
* 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.relational.core.conversion; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.data.annotation.Id; |
||||
import org.springframework.data.mapping.PersistentPropertyAccessor; |
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||
|
||||
/** |
||||
* Unit tests for the {@link AggregateChange}. |
||||
* |
||||
* @author Jens Schauder |
||||
*/ |
||||
public class AggregateChangeUnitTests { |
||||
|
||||
DummyEntity entity = new DummyEntity(); |
||||
Content content = new Content(); |
||||
|
||||
RelationalMappingContext context = new RelationalMappingContext(); |
||||
RelationalConverter converter = new BasicRelationalConverter(context); |
||||
|
||||
PersistentPropertyAccessor<DummyEntity> propertyAccessor = context.getRequiredPersistentEntity(DummyEntity.class) |
||||
.getPropertyAccessor(entity); |
||||
Object id = 23; |
||||
|
||||
DbAction.WithEntity<?> rootInsert = new DbAction.InsertRoot<>(entity); |
||||
|
||||
DbAction.Insert<?> createInsert(String propertyName, Object value, Object key) { |
||||
|
||||
DbAction.Insert<Object> insert = new DbAction.Insert<>(value, |
||||
context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert); |
||||
insert.getAdditionalValues().put("dummy_entity_key", key); |
||||
|
||||
return insert; |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void setIdForSimpleReference() { |
||||
|
||||
entity.single = content; |
||||
|
||||
DbAction.Insert<?> insert = createInsert("single", content, null); |
||||
|
||||
AggregateChange.setId(context, converter, propertyAccessor, insert, id); |
||||
|
||||
DummyEntity result = propertyAccessor.getBean(); |
||||
|
||||
assertThat(result.single.id).isEqualTo(id); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void setIdForSingleElementSet() { |
||||
|
||||
entity.contentSet.add(content); |
||||
|
||||
DbAction.Insert<?> insert = createInsert("contentSet", content, null); |
||||
|
||||
AggregateChange.setId(context, converter, propertyAccessor, insert, id); |
||||
|
||||
DummyEntity result = propertyAccessor.getBean(); |
||||
assertThat(result.contentSet).isNotNull(); |
||||
assertThat(result.contentSet).extracting(c -> c == null ? "null" : c.id).containsExactlyInAnyOrder(23); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void setIdForSingleElementList() { |
||||
|
||||
entity.contentList.add(content); |
||||
|
||||
DbAction.Insert<?> insert = createInsert("contentList", content, 0); |
||||
|
||||
AggregateChange.setId(context, converter, propertyAccessor, insert, id); |
||||
|
||||
DummyEntity result = propertyAccessor.getBean(); |
||||
assertThat(result.contentList).extracting(c -> c.id).containsExactlyInAnyOrder(23); |
||||
} |
||||
|
||||
@Test // DATAJDBC-241
|
||||
public void setIdForSingleElementMap() { |
||||
|
||||
entity.contentMap.put("one", content); |
||||
|
||||
DbAction.Insert<?> insert = createInsert("contentMap", content, "one"); |
||||
|
||||
AggregateChange.setId(context, converter, propertyAccessor, insert, id); |
||||
|
||||
DummyEntity result = propertyAccessor.getBean(); |
||||
assertThat(result.contentMap.entrySet()).extracting(e -> e.getKey(), e -> e.getValue().id) |
||||
.containsExactlyInAnyOrder(tuple("one", 23)); |
||||
} |
||||
|
||||
private static class DummyEntity { |
||||
|
||||
@Id Integer rootId; |
||||
|
||||
Content single; |
||||
|
||||
Set<Content> contentSet = new HashSet<>(); |
||||
|
||||
List<Content> contentList = new ArrayList<>(); |
||||
|
||||
Map<String, Content> contentMap = new HashMap<>(); |
||||
} |
||||
|
||||
private static class Content { |
||||
|
||||
@Id Integer id; |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE LEGO_SET ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(30)); |
||||
CREATE TABLE MANUAL ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, LEGO_SET BIGINT, CONTENT VARCHAR(2000)); |
||||
|
||||
ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) |
||||
REFERENCES LEGO_SET(id); |
||||
Loading…
Reference in new issue