From 744ee3242562b5b30fa7e3d571c7bea560e632d4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 6 May 2021 10:55:48 +0200 Subject: [PATCH] Add example for using Immutables with Spring Data JDBC. Original pull request #624 --- README.md | 2 + jdbc/immutables/README.adoc | 27 ++++ jdbc/immutables/pom.xml | 28 ++++ .../jdbc/immutables/Application.java | 138 ++++++++++++++++++ .../springdata/jdbc/immutables/Enigma.java | 44 ++++++ .../jdbc/immutables/EnigmaRepository.java | 28 ++++ .../springdata/jdbc/immutables/Rotor.java | 48 ++++++ jdbc/immutables/src/main/resources/schema.sql | 34 +++++ .../jdbc/immutables/ImmutablesTests.java | 55 +++++++ jdbc/pom.xml | 1 + 10 files changed, 405 insertions(+) create mode 100644 jdbc/immutables/README.adoc create mode 100644 jdbc/immutables/pom.xml create mode 100644 jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java create mode 100644 jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java create mode 100644 jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java create mode 100644 jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java create mode 100644 jdbc/immutables/src/main/resources/schema.sql create mode 100644 jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java diff --git a/README.md b/README.md index 86f8090c..b6671009 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Local Elasticsearch instance must be running to run the tests. ## Spring Data JDBC * `basic` - Basic usage of Spring Data JDBC. +* `immutables` - Showing Spring Data JDBC usage + with [Immutables](https://immutables.github.io/) ## Spring Data JPA diff --git a/jdbc/immutables/README.adoc b/jdbc/immutables/README.adoc new file mode 100644 index 00000000..3553ccc2 --- /dev/null +++ b/jdbc/immutables/README.adoc @@ -0,0 +1,27 @@ +== Spring Data JDBC with Immutables + +This example show how to use https://immutables.github.io/[Immutables] with Spring Data. +The core concept of Immutables is to define an interface (or abstract class) for which Immutables generates an immutable implementation that can then be used by application code. + +Persisting immutable objects and associating the saved object with generated identifiers works out of the box. +The reading side requires a redirection of the to be created object type, see `ImmutablesJdbcConfiguration`. + +Limitations: + +Immutables tends to generate additional constructors when using `@Value.Style(allParameters = true)` or `@Value.Parameter`. +This conflicts with Spring Data's constructor resolution as Spring Data cannot identify reliably a persistence constructor. + +To run the tests, run: + +[indent=0] +---- + $ mvn test +---- + +The code generator is automatically run when executing the tests. +If you want to rerun the code generator manually, just run the following command: + +[indent=0] +---- + $ mvn clean generate-sources +---- diff --git a/jdbc/immutables/pom.xml b/jdbc/immutables/pom.xml new file mode 100644 index 00000000..8527b31d --- /dev/null +++ b/jdbc/immutables/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + spring-data-jdbc-immutables + + + org.springframework.data.examples + spring-data-jdbc-examples + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - Usage with Immutables + Sample project demonstrating Spring Data JDBC features + + + + org.immutables + value + 2.8.8 + provided + + + + + diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java new file mode 100644 index 00000000..073c3c3b --- /dev/null +++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Application.java @@ -0,0 +1,138 @@ +/* + * 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 + * + * 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 example.springdata.jdbc.immutables; + +import java.sql.ResultSet; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory; +import org.springframework.data.jdbc.core.convert.Identifier; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.convert.RelationResolver; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.util.ClassUtils; + +/** + * Configuration stub. + * + * @author Mark Paluch + */ +@SpringBootApplication +class Application { + + /** + * Name scheme how Immutables generates implementations from interface/class definitions. + */ + public static final String IMMUTABLE_IMPLEMENTATION_CLASS = "%s.Immutable%s"; + + @Configuration + static class ImmutablesJdbcConfiguration extends AbstractJdbcConfiguration { + + private final ResourceLoader resourceLoader; + + public ImmutablesJdbcConfiguration(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + /** + * {@link JdbcConverter} that redirects entities to be instantiated towards the implementation. See + * {@link #IMMUTABLE_IMPLEMENTATION_CLASS} and + * {@link #getImplementationEntity(JdbcMappingContext, RelationalPersistentEntity)}. + * + * @param mappingContext + * @param operations + * @param relationResolver + * @param conversions + * @param dialect + * @return + */ + @Override + public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParameterJdbcOperations operations, + RelationResolver relationResolver, JdbcCustomConversions conversions, Dialect dialect) { + + var jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations()); + + return new BasicJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory, + dialect.getIdentifierProcessing()) { + + @Override + public T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) { + return super.mapRow(getImplementationEntity(mappingContext, entity), resultSet, key); + } + + @Override + public T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, + Object key) { + + return super.mapRow(new DelegatePersistentPropertyPathExtension(mappingContext, + path.getRequiredPersistentPropertyPath(), getImplementationEntity(mappingContext, path.getLeafEntity())), + resultSet, identifier, key); + } + }; + } + + @SuppressWarnings("unchecked") + private RelationalPersistentEntity getImplementationEntity(JdbcMappingContext mappingContext, + RelationalPersistentEntity entity) { + + var type = entity.getType(); + if (type.isInterface()) { + + var immutableClass = String.format(IMMUTABLE_IMPLEMENTATION_CLASS, type.getPackageName(), type.getSimpleName()); + if (ClassUtils.isPresent(immutableClass, resourceLoader.getClassLoader())) { + + return (RelationalPersistentEntity) mappingContext + .getPersistentEntity(ClassUtils.resolveClassName(immutableClass, resourceLoader.getClassLoader())); + } + + } + return entity; + } + } + + /** + * Redirect {@link #getLeafEntity()} to a different entity type. + */ + static class DelegatePersistentPropertyPathExtension extends PersistentPropertyPathExtension { + + private final RelationalPersistentEntity leafEntity; + + public DelegatePersistentPropertyPathExtension( + MappingContext, ? extends RelationalPersistentProperty> context, + PersistentPropertyPath path, RelationalPersistentEntity leafEntity) { + super(context, path); + this.leafEntity = leafEntity; + } + + @Override + public RelationalPersistentEntity getLeafEntity() { + return leafEntity; + } + } + +} diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java new file mode 100644 index 00000000..f927fb1a --- /dev/null +++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Enigma.java @@ -0,0 +1,44 @@ +/* + * 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 + * + * 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 example.springdata.jdbc.immutables; + +import java.util.List; + +import org.immutables.value.Value; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.MappedCollection; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.lang.Nullable; + +/** + * @author Mark Paluch + */ +@Value.Immutable +@Table("ENIGMA") +public interface Enigma { + + @Nullable + @Id + Long getId(); + + String getModel(); + + // Explicit keys to not derive key names from the implementation class. + @MappedCollection(idColumn = "ENIGMA_ID", keyColumn = "ROTOR_KEY") + List getRotors(); + +} diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.java new file mode 100644 index 00000000..51611fe4 --- /dev/null +++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/EnigmaRepository.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 + * + * 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 example.springdata.jdbc.immutables; + +import org.springframework.data.repository.CrudRepository; + +/** + * Repository for {@link Enigma} instances. + * + * @author Mark Paluch + */ +public interface EnigmaRepository extends CrudRepository { + + Enigma findByModel(String name); +} diff --git a/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java new file mode 100644 index 00000000..6b9a25b5 --- /dev/null +++ b/jdbc/immutables/src/main/java/example/springdata/jdbc/immutables/Rotor.java @@ -0,0 +1,48 @@ +/* + * 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 + * + * 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 example.springdata.jdbc.immutables; + +import org.immutables.value.Value; + +import org.springframework.data.relational.core.mapping.Table; + +/** + * @author Mark Paluch + */ +@Value.Immutable +@Table("ROTOR") +public interface Rotor { + + String getName(); + + String getWiring(); + + char getNotch(); + + /** + * Factory method for {@link Rotor} as using {@code @Value.Style} and {@code @Value.Parameter} conflicts with Spring + * Data's constructor discovery rules. + * + * @param name + * @param wiring + * @param notch + * @return + */ + static Rotor of(String name, String wiring, char notch) { + return ImmutableRotor.builder().name(name).wiring(wiring).notch(notch).build(); + } + +} diff --git a/jdbc/immutables/src/main/resources/schema.sql b/jdbc/immutables/src/main/resources/schema.sql new file mode 100644 index 00000000..1b397e27 --- /dev/null +++ b/jdbc/immutables/src/main/resources/schema.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS ENIGMA +( + ID + INTEGER + IDENTITY + PRIMARY + KEY, + MODEL + VARCHAR +( + 100 +) + ); + +CREATE TABLE IF NOT EXISTS ROTOR +( + ENIGMA_ID + INTEGER, + ROTOR_KEY + INTEGER, + NAME + VARCHAR +( + 100 +), + WIRING VARCHAR +( + 26 +), + NOTCH CHAR +( + 1 +) + ); diff --git a/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java b/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java new file mode 100644 index 00000000..be11962c --- /dev/null +++ b/jdbc/immutables/src/test/java/example/springdata/jdbc/immutables/ImmutablesTests.java @@ -0,0 +1,55 @@ +/* + * 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 + * + * 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 example.springdata.jdbc.immutables; + +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.autoconfigure.data.jdbc.DataJdbcTest; + +/** + * Integration tests using immutable types assisted by Immutables. Calling code can use the actual immutable + * interface/abstract class while Spring Data materializes the actual implementation. + * + * @author Mark Paluch + * @see https://immutables.github.io/ + */ +@DataJdbcTest +class ImmutablesTests { + + @Autowired EnigmaRepository repository; + + @Test + void shouldInsertAndRetrieveObject() { + + var enigmaA = ImmutableEnigma.builder().model("A") // + .addRotors(Rotor.of("I", "DMTWSILRUYQNKFEJCAZBPGXOHV", 'Q')) // + .addRotors(Rotor.of("II", "HQZGPJTMOBLNCIFDYAWVEUSRXL", 'E')) // + .build(); + + var saved = repository.save(enigmaA); + + assertThat(saved.getId()).isNotNull(); + + var loaded = repository.findByModel("A"); + + assertThat(loaded).isEqualTo(saved).isInstanceOf(ImmutableEnigma.class); + assertThat(loaded.getRotors()).hasSize(2); + assertThat(loaded.getRotors().get(0)).isInstanceOf(ImmutableRotor.class); + } +} diff --git a/jdbc/pom.xml b/jdbc/pom.xml index f7a9dada..4cb80a13 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -19,6 +19,7 @@ basics mybatis + immutables jmolecules jooq