From 092e31e890cc5ad226311be0938eb477068905f3 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 14 Jan 2022 11:02:22 +0100 Subject: [PATCH] Adds an example for custom update strategies. See https://spring.io/admin/blog/4540-spring-data-jdbc-how-do-update-just-the-aggregate-root-instead-of-all-the-referenced-entities --- jdbc/howto/pom.xml | 1 + jdbc/howto/selectiveupdate/README.adoc | 5 ++ jdbc/howto/selectiveupdate/pom.xml | 20 +++++ .../jdbc/howto/selectiveupdate/Color.java | 25 ++++++ .../jdbc/howto/selectiveupdate/Minion.java | 58 +++++++++++++ .../selectiveupdate/MinionRepository.java | 34 ++++++++ .../selectiveupdate/PartyHatRepository.java | 27 ++++++ .../PartyHatRepositoryImpl.java | 54 ++++++++++++ .../howto/selectiveupdate/PlainMinion.java | 33 ++++++++ .../PlainMinionRepository.java | 27 ++++++ .../SelectiveUpdateApplication.java | 29 +++++++ .../jdbc/howto/selectiveupdate/Toy.java | 45 ++++++++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/schema.sql | 13 +++ .../SelectiveUpdateApplicationTests.java | 84 +++++++++++++++++++ 15 files changed, 456 insertions(+) create mode 100644 jdbc/howto/selectiveupdate/README.adoc create mode 100644 jdbc/howto/selectiveupdate/pom.xml create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java create mode 100644 jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java create mode 100644 jdbc/howto/selectiveupdate/src/main/resources/application.properties create mode 100644 jdbc/howto/selectiveupdate/src/main/resources/schema.sql create mode 100644 jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java diff --git a/jdbc/howto/pom.xml b/jdbc/howto/pom.xml index 044ae5e1..5da1ec32 100644 --- a/jdbc/howto/pom.xml +++ b/jdbc/howto/pom.xml @@ -23,6 +23,7 @@ bidirectionalinternal caching idgeneration + selectiveupdate diff --git a/jdbc/howto/selectiveupdate/README.adoc b/jdbc/howto/selectiveupdate/README.adoc new file mode 100644 index 00000000..f3d6335e --- /dev/null +++ b/jdbc/howto/selectiveupdate/README.adoc @@ -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. diff --git a/jdbc/howto/selectiveupdate/pom.xml b/jdbc/howto/selectiveupdate/pom.xml new file mode 100644 index 00000000..3fa6ca4d --- /dev/null +++ b/jdbc/howto/selectiveupdate/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + spring-data-jdbc-how-to-selective-update + + + org.springframework.data.examples + spring-data-jdbc-how-to + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - How to do selective updates + Sample project for Spring Data JDBC demonstrating how to update only parts of an aggregate. + + https://projects.spring.io/spring-data-jdbc + 2022 + + diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java new file mode 100644 index 00000000..399e66d8 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Color.java @@ -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 +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java new file mode 100644 index 00000000..78c10529 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Minion.java @@ -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 toys = new HashSet<>(); + @Version int version; + + Minion(String name) { + this.name = name; + } + + @PersistenceConstructor + private Minion(Long id, String name, Collection 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; + } +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java new file mode 100644 index 00000000..0c97d7e8 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/MinionRepository.java @@ -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, PartyHatRepository { + + @Modifying + @Query("UPDATE MINION SET COLOR ='PURPLE', VERSION = VERSION +1 WHERE ID = :id") + void turnPurple(Long id); + +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java new file mode 100644 index 00000000..f2b3a497 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepository.java @@ -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); +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java new file mode 100644 index 00000000..3513338f --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PartyHatRepositoryImpl.java @@ -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 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 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"); + } + } +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java new file mode 100644 index 00000000..4162c4e0 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinion.java @@ -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; +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java new file mode 100644 index 00000000..7d8d8be2 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/PlainMinionRepository.java @@ -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 { + +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java new file mode 100644 index 00000000..9e1b01dd --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplication.java @@ -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); + } + +} diff --git a/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java new file mode 100644 index 00000000..c61a32e5 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/java/example.springdata/jdbc/howto/selectiveupdate/Toy.java @@ -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); + } +} diff --git a/jdbc/howto/selectiveupdate/src/main/resources/application.properties b/jdbc/howto/selectiveupdate/src/main/resources/application.properties new file mode 100644 index 00000000..813de42f --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/resources/application.properties @@ -0,0 +1 @@ +logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG \ No newline at end of file diff --git a/jdbc/howto/selectiveupdate/src/main/resources/schema.sql b/jdbc/howto/selectiveupdate/src/main/resources/schema.sql new file mode 100644 index 00000000..4ebd97e4 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/main/resources/schema.sql @@ -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) +); diff --git a/jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java b/jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java new file mode 100644 index 00000000..5d694349 --- /dev/null +++ b/jdbc/howto/selectiveupdate/src/test/java/example/springdata/jdbc/howto/selectiveupdate/SelectiveUpdateApplicationTests.java @@ -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)); + } + +}