Add example for using Immutables with Spring Data JDBC.

Original pull request #624
This commit is contained in:
Mark Paluch
2021-05-06 10:55:48 +02:00
committed by Jens Schauder
parent 6e55e3d27a
commit 744ee32425
10 changed files with 405 additions and 0 deletions

View File

@@ -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

View File

@@ -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
----

28
jdbc/immutables/pom.xml Normal file
View File

@@ -0,0 +1,28 @@
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-jdbc-immutables</artifactId>
<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-jdbc-examples</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Spring Data JDBC - Usage with Immutables</name>
<description>Sample project demonstrating Spring Data JDBC features</description>
<dependencies>
<dependency>
<groupId>org.immutables</groupId>
<artifactId>value</artifactId>
<version>2.8.8</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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> T mapRow(RelationalPersistentEntity<T> entity, ResultSet resultSet, Object key) {
return super.mapRow(getImplementationEntity(mappingContext, entity), resultSet, key);
}
@Override
public <T> 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 <T> RelationalPersistentEntity<T> getImplementationEntity(JdbcMappingContext mappingContext,
RelationalPersistentEntity<T> 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<T>) 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 RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
PersistentPropertyPath<? extends RelationalPersistentProperty> path, RelationalPersistentEntity<?> leafEntity) {
super(context, path);
this.leafEntity = leafEntity;
}
@Override
public RelationalPersistentEntity<?> getLeafEntity() {
return leafEntity;
}
}
}

View File

@@ -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<Rotor> getRotors();
}

View File

@@ -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, Long> {
Enigma findByModel(String name);
}

View File

@@ -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();
}
}

View File

@@ -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
)
);

View File

@@ -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 <a href="https://immutables.github.io/">https://immutables.github.io/</a>
*/
@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);
}
}

View File

@@ -19,6 +19,7 @@
<modules>
<module>basics</module>
<module>mybatis</module>
<module>immutables</module>
<module>jmolecules</module>
<module>jooq</module>
</modules>