diff --git a/jdbc/howto/README.adoc b/jdbc/howto/README.adoc new file mode 100644 index 00000000..d7afa08b --- /dev/null +++ b/jdbc/howto/README.adoc @@ -0,0 +1,5 @@ +== Spring Data JDBC How Tos + +=== ID Generation + +Demonstrates the various ways how the user might control generation of IDs when the default of database generated IDs is not sufficient. \ No newline at end of file diff --git a/jdbc/howto/idgeneration/README.adoc b/jdbc/howto/idgeneration/README.adoc new file mode 100644 index 00000000..57a7f61c --- /dev/null +++ b/jdbc/howto/idgeneration/README.adoc @@ -0,0 +1,24 @@ +== Spring Data JDBC How To ID Generation + +There are multiple ways how the generation of IDs can be controlled. + +1. The default is to let the database create the ID by using a `AUTOINCREMENT`, `SERIAL` or `IDENTITY` column. + If you try to save a new aggregate root with a preset ID you will receive an exception. + See `IdGenerationApplicationTest.cantSaveNewAggregateWithPresetId`. + + The reason is that Spring Data JDBC will inspect the aggregate root, notes that the ID is not `null` and tries to perform an update which will update 0 rows and cause an exception. + +2. You can manually set the id of an aggregate root to a value of your choice if you use `JdbcAggregateTemplate.insert`. + This bypasses the check if an update or insert is to be performed and always performs an insert. + See `IdGenerationApplicationTest.insertNewAggregateWithPresetIdUsingTemplate`. + +3. You may use a `BeforeSaveEntityCallBack` to set the id of aggregate roots with null ID. + This has the benefit of being transparent in to your domain code, as it should be since IDs are normally not relevant to it. + See `IdGenerationApplicationTest.idByCallBack` and `IdGenerationApplication.beforeSaveCallback`. + As long as your entity is mutable you might as well use an `BeforeSaveEntityListener`, but since the callback works for both cases it is the recommended approach. + +4. If you add a version attribute, i.e. one annotated with `@Version` that attribute is used to determine if the aggregate is new or not, leaving you free to set the ID as you see fit. + See `IdGenerationApplicationTest.determineIsNewPerVersion`. + +5. The final option is to let your aggregate root implement `Persistable` which allows you to define your own `isNew` method, which controls if we perform an insert or an update. + See `IdGenerationApplicationTest.determineIsNewPerPersistable`. diff --git a/jdbc/howto/idgeneration/pom.xml b/jdbc/howto/idgeneration/pom.xml new file mode 100644 index 00000000..523a0279 --- /dev/null +++ b/jdbc/howto/idgeneration/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + spring-data-jdbc-how-to-id-generation + + + org.springframework.data.examples + spring-data-jdbc-how-to + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - How to ID generation + Sample project for Spring Data JDBC demonstrating the various options to use user defined IDs in Spring Data JDBC aggregates. + It serves as a source code repository for a How To article on the Spring Blog + https://projects.spring.io/spring-data-jdbc + 2021 + + diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/IdGenerationApplication.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/IdGenerationApplication.java new file mode 100644 index 00000000..3b76e189 --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/IdGenerationApplication.java @@ -0,0 +1,28 @@ +package example.springdata.jdbc.howto.idgeneration; + +import java.util.UUID; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; + +@SpringBootApplication +class IdGenerationApplication { + + public static void main(String[] args) { + SpringApplication.run(IdGenerationApplication.class, args); + } + + @Bean + BeforeSaveCallback beforeSaveCallback() { + + return (minion, mutableAggregateChange) -> { + if (minion.id == null) { + minion.id = UUID.randomUUID().toString(); + } + return minion; + }; + } + +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/Minion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/Minion.java new file mode 100644 index 00000000..bbd921cf --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/Minion.java @@ -0,0 +1,28 @@ +/* + * 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.idgeneration; + +import org.springframework.data.annotation.Id; + +class Minion { + @Id + Long id; + String name; + + Minion(String name) { + this.name = name; + } +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/MinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/MinionRepository.java new file mode 100644 index 00000000..1a2ee77f --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/MinionRepository.java @@ -0,0 +1,22 @@ +/* + * 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.idgeneration; + +import org.springframework.data.repository.CrudRepository; + +interface MinionRepository extends CrudRepository { + +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinion.java new file mode 100644 index 00000000..9b3a88af --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinion.java @@ -0,0 +1,42 @@ +/* + * 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.idgeneration; + +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Persistable; +import org.springframework.data.relational.core.mapping.Table; + +@Table("MINION") +class PersistableMinion implements Persistable { + @Id Long id; + String name; + + PersistableMinion(Long id, String name) { + this.id = id; + this.name = name; + } + + @Override + public Long getId() { + return id; + } + + @Override + public boolean isNew() { + // this implementation is most certainly not suitable for production use + return true; + } +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinionRepository.java new file mode 100644 index 00000000..69311209 --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/PersistableMinionRepository.java @@ -0,0 +1,22 @@ +/* + * 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.idgeneration; + +import org.springframework.data.repository.CrudRepository; + +interface PersistableMinionRepository extends CrudRepository { + +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinion.java new file mode 100644 index 00000000..98bf7c5d --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinion.java @@ -0,0 +1,28 @@ +/* + * 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.idgeneration; + +import org.springframework.data.annotation.Id; + +class StringIdMinion { + @Id + String id; + String name; + + StringIdMinion(String name) { + this.name = name; + } +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinionRepository.java new file mode 100644 index 00000000..884ee86d --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/StringIdMinionRepository.java @@ -0,0 +1,22 @@ +/* + * 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.idgeneration; + +import org.springframework.data.repository.CrudRepository; + +interface StringIdMinionRepository extends CrudRepository { + +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinion.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinion.java new file mode 100644 index 00000000..7104bfe8 --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinion.java @@ -0,0 +1,32 @@ +/* + * 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.idgeneration; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; + +class VersionedMinion { + + @Id Long id; + String name; + @Version Integer version; + + VersionedMinion(long id, String name) { + + this.id = id; + this.name = name; + } +} diff --git a/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinionRepository.java b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinionRepository.java new file mode 100644 index 00000000..778e06f5 --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/java/example.springdata/jdbc/howto/idgeneration/VersionedMinionRepository.java @@ -0,0 +1,22 @@ +/* + * 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.idgeneration; + +import org.springframework.data.repository.CrudRepository; + +interface VersionedMinionRepository extends CrudRepository { + +} diff --git a/jdbc/howto/idgeneration/src/main/resources/schema.sql b/jdbc/howto/idgeneration/src/main/resources/schema.sql new file mode 100644 index 00000000..42496f02 --- /dev/null +++ b/jdbc/howto/idgeneration/src/main/resources/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE MINION +( + ID IDENTITY PRIMARY KEY, + NAME VARCHAR(255), +); + +CREATE TABLE STRING_ID_MINION +( + ID VARCHAR(255) PRIMARY KEY, + NAME VARCHAR(255) +); + +CREATE TABLE VERSIONED_MINION +( + ID INT PRIMARY KEY, + NAME VARCHAR(255), + VERSION INT +); diff --git a/jdbc/howto/idgeneration/src/test/java/example/springdata/jdbc/howto/idgeneration/IdGenerationApplicationTests.java b/jdbc/howto/idgeneration/src/test/java/example/springdata/jdbc/howto/idgeneration/IdGenerationApplicationTests.java new file mode 100644 index 00000000..bf235167 --- /dev/null +++ b/jdbc/howto/idgeneration/src/test/java/example/springdata/jdbc/howto/idgeneration/IdGenerationApplicationTests.java @@ -0,0 +1,100 @@ +package example.springdata.jdbc.howto.idgeneration; + +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.IncorrectUpdateSemanticsDataAccessException; +import org.springframework.data.jdbc.core.JdbcAggregateTemplate; + +@SpringBootTest +class IdGenerationApplicationTests { + + @Autowired + MinionRepository minions; + + @Autowired + StringIdMinionRepository stringions; + + @Autowired + VersionedMinionRepository versionedMinions; + + @Autowired + PersistableMinionRepository persistableMinions; + + @Autowired + JdbcAggregateTemplate template; + + @Test + void saveWithNewIdFromDb() { + + Minion before = new Minion("Bob"); + assertThat(before.id).isNull(); + + Minion after = minions.save(before); + + assertThat(after.id).isNotNull(); + } + + @Test + void cantSaveNewAggregateWithPresetId() { + + Minion before = new Minion("Stuart"); + before.id = 42L; + + // We can't save this because Spring Data JDBC thinks it has to do an update. + assertThatThrownBy(() -> minions.save(before)).getRootCause().isInstanceOf(IncorrectUpdateSemanticsDataAccessException.class); + } + + @Test + void insertNewAggregateWithPresetIdUsingTemplate() { + + Minion before = new Minion("Stuart"); + before.id = 42L; + + template.insert(before); + + Minion reloaded = minions.findById(42L).get(); + assertThat(reloaded.name).isEqualTo("Stuart"); + } + + @Test + void idByCallBack() { + + StringIdMinion before = new StringIdMinion("Kevin"); + + stringions.save(before); + + assertThat(before.id).isNotNull(); + + StringIdMinion reloaded = stringions.findById(before.id).get(); + assertThat(reloaded.name).isEqualTo("Kevin"); + } + + @Test + void determineIsNewPerVersion() { + + VersionedMinion before = new VersionedMinion(23L, "Bob"); + + assertThat(before.id).isNotNull(); + + versionedMinions.save(before); + + // It's saved! + VersionedMinion reloaded = versionedMinions.findById(before.id).get(); + assertThat(reloaded.name).isEqualTo("Bob"); + } + + @Test + void determineIsNewPerPersistable() { + + PersistableMinion before = new PersistableMinion(23L, "Dave"); + + persistableMinions.save(before); + + // It's saved! + PersistableMinion reloaded = persistableMinions.findById(before.id).get(); + assertThat(reloaded.name).isEqualTo("Dave"); + } +} diff --git a/jdbc/howto/pom.xml b/jdbc/howto/pom.xml new file mode 100644 index 00000000..d9cc9ee4 --- /dev/null +++ b/jdbc/howto/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + spring-data-jdbc-how-to + pom + + + org.springframework.data.examples + spring-data-jdbc-examples + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - How to Examples + Sample projects for Spring Data JDBC demonstrating various features and options. + It serves as a source code repository for a series of How To articles on the Spring Blog + https://projects.spring.io/spring-data-jdbc + 2021 + + + idgeneration + + + diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 4cb80a13..3186a31d 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -22,6 +22,7 @@ immutables jmolecules jooq + howto