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));
+ }
+
+}