Jens Schauder
2022-01-14 11:02:22 +01:00
parent 0bdca5b05c
commit 092e31e890
15 changed files with 456 additions and 0 deletions

View File

@@ -23,6 +23,7 @@
<module>bidirectionalinternal</module>
<module>caching</module>
<module>idgeneration</module>
<module>selectiveupdate</module>
</modules>
</project>

View File

@@ -0,0 +1,5 @@
== Spring Data JDBC How To perform selective updates.
Spring Data JDBC normally persists complete aggregates, which is wasteful if only few things have changed.
This project demonstrates alternatives that require a little more work from the developer but are much more efficient.

View File

@@ -0,0 +1,20 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-jdbc-how-to-selective-update</artifactId>
<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-jdbc-how-to</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Spring Data JDBC - How to do selective updates</name>
<description>Sample project for Spring Data JDBC demonstrating how to update only parts of an aggregate.
</description>
<url>https://projects.spring.io/spring-data-jdbc</url>
<inceptionYear>2022</inceptionYear>
</project>

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2022 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
/**
* Simple enum for the color of a minion.
*
* @author Jens Schauder
*/
public enum Color {
YELLOW, PURPLE
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2021 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.Version;
/**
* A minion. The main entity and aggregate root for this example.
*
* @author Jens Schauder
*/
class Minion {
@Id Long id;
String name;
Color color = Color.YELLOW;
Set<Toy> toys = new HashSet<>();
@Version int version;
Minion(String name) {
this.name = name;
}
@PersistenceConstructor
private Minion(Long id, String name, Collection<Toy> toys, int version) {
this.id = id;
this.name = name;
this.toys.addAll(toys);
this.version = version;
}
Minion addToy(Toy toy) {
toys.add(toy);
return this;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2021 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
/**
* The normal MinionRepository.
*
* @author Jens Schauder
*/
interface MinionRepository extends CrudRepository<Minion, Long>, PartyHatRepository {
@Modifying
@Query("UPDATE MINION SET COLOR ='PURPLE', VERSION = VERSION +1 WHERE ID = :id")
void turnPurple(Long id);
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2022 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
/**
* A repository fragment to perform custom logic, including a special way to update minions in the database.
*
* @author Jens Schauder
*/
public interface PartyHatRepository {
void addPartyHat(Minion minion);
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2022 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import java.util.HashMap;
import java.util.Map;
/**
* Implementation of custom logic for a minion repository.
*
* @author Jens Schauder
*/
class PartyHatRepositoryImpl implements PartyHatRepository {
private final NamedParameterJdbcOperations template;
public PartyHatRepositoryImpl(NamedParameterJdbcOperations template) {
this.template = template;
}
@Override
public void addPartyHat(Minion minion) {
Map<String, Object> insertParams = new HashMap<>();
insertParams.put("id", minion.id);
insertParams.put("name", "Party Hat");
template.update("INSERT INTO TOY (MINION, NAME) VALUES (:id, :name)", insertParams);
Map<String, Object> updateParams = new HashMap<>();
updateParams.put("id", minion.id);
updateParams.put("version", minion.version);
final int updateCount = template.update("UPDATE MINION SET VERSION = :version + 1 WHERE ID = :id AND VERSION = :version", updateParams);
if (updateCount != 1) {
throw new OptimisticLockingFailureException("Minion was changed before a Party Hat was given");
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2021 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.relational.core.mapping.Table;
/**
* A simplified minion which only has the attributes
*
* @author Jens Schauder
*/
@Table("MINION")
class PlainMinion {
@Id Long id;
String name;
Color color;
@Version int version;
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2021 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import org.springframework.data.repository.CrudRepository;
/**
* Repository for {@link PlainMinion}.
*
* @author Jens Schauder
*/
interface PlainMinionRepository extends CrudRepository<PlainMinion, Long> {
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2022 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
class SelectiveUpdateApplication {
public static void main(String[] args) {
SpringApplication.run(SelectiveUpdateApplication.class, args);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2021 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import java.util.Objects;
/**
* Toys for minions to play with.
*
* @author Jens Schauder
*/
class Toy {
String name;
Toy(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Toy toy = (Toy) o;
return Objects.equals(name, toy.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}

View File

@@ -0,0 +1 @@
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG

View File

@@ -0,0 +1,13 @@
CREATE TABLE MINION
(
ID IDENTITY PRIMARY KEY,
NAME VARCHAR(255),
COLOR VARCHAR(10),
VERSION INT
);
CREATE TABLE TOY
(
MINION BIGINT NOT NULL,
NAME VARCHAR(255)
);

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2022 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
*
* https://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 example.springdata.jdbc.howto.selectiveupdate;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.OptimisticLockingFailureException;
/**
* Tests demonstrating various was for partial updates of an aggregate.
*/
@SpringBootTest
class SelectiveUpdateApplicationTests {
@Autowired MinionRepository minions;
@Autowired PlainMinionRepository plainMinions;
@Test
void renameWithReducedView() {
Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
minions.save(bob);
PlainMinion plainBob = plainMinions.findById(bob.id).orElseThrow();
plainBob.name = "Bob II.";
plainMinions.save(plainBob);
Minion bob2 = minions.findById(bob.id).orElseThrow();
assertThat(bob2.toys).containsExactly(bob.toys.toArray(new Toy[] {}));
assertThat(bob2.name).isEqualTo("Bob II.");
assertThat(bob2.color).isEqualTo(Color.YELLOW);
}
@Test
void turnPurpleByDirectUpdate() {
Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
minions.save(bob);
minions.turnPurple(bob.id);
Minion bob2 = minions.findById(bob.id).orElseThrow();
assertThat(bob2.toys).containsExactly(bob.toys.toArray(new Toy[] {}));
assertThat(bob2.name).isEqualTo("Bob");
assertThat(bob2.color).isEqualTo(Color.PURPLE);
}
@Test
void grantPartyHat() {
Minion bob = new Minion("Bob").addToy(new Toy("Tiger Duck")).addToy(new Toy("Security blanket"));
minions.save(bob);
minions.addPartyHat(bob);
Minion bob2 = minions.findById(bob.id).orElseThrow();
assertThat(bob2.toys).extracting("name").containsExactlyInAnyOrder("Tiger Duck", "Security blanket", "Party Hat");
assertThat(bob2.name).isEqualTo("Bob");
assertThat(bob2.color).isEqualTo(Color.YELLOW);
assertThat(bob2.version).isEqualTo(bob.version + 1);
assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> minions.addPartyHat(bob));
}
}