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

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