#2 - Add R2dbcRepositoryFactory and simple query subsystem.

This commit is contained in:
Mark Paluch
2018-06-20 16:54:27 +02:00
parent 28dee1274f
commit f794bfc3ab
24 changed files with 1983 additions and 42 deletions

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017-2018 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 org.springframework.data.jdbc.degraph;
import static de.schauderhaft.degraph.check.JCheck.*;
import static org.junit.Assert.*;
import de.schauderhaft.degraph.check.JCheck;
import scala.runtime.AbstractFunction1;
import org.junit.Ignore;
import org.junit.Test;
/**
* Test package dependencies for violations.
*
* @author Jens Schauder
*/
public class DependencyTests {
@Test // DATAJDBC-114
public void cycleFree() {
assertThat( //
classpath() //
.noJars() //
.including("org.springframework.data.jdbc.**") //
.filterClasspath("*target/classes") // exclude test code
.printOnFailure("degraph.graphml"),
JCheck.violationFree());
}
@Test // DATAJDBC-220
@Ignore("I don't understand why this fails after adding reactive repos - mp911de")
public void acrossModules() {
assertThat( //
classpath() //
// include only Spring Data related classes (for example no JDK code)
.including("org.springframework.data.**") //
.filterClasspath(new AbstractFunction1<String, Object>() {
@Override
public Object apply(String s) { //
// only the current module + commons
return s.endsWith("target/classes") || s.contains("spring-data-commons");
}
}) // exclude test code
.withSlicing("sub-modules", // sub-modules are defined by any of the following pattern.
"org.springframework.data.jdbc.(**).*", //
"org.springframework.data.(**).*") //
.printTo("degraph-across-modules.graphml"), // writes a graphml to this location
JCheck.violationFree());
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2018 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 org.springframework.data.jdbc.repository;
import static org.assertj.core.api.Assertions.*;
import io.r2dbc.spi.ConnectionFactory;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.Table;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.repository.support.R2dbcRepositoryFactory;
import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory}.
*
* @author Mark Paluch
*/
public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport {
private static JdbcMappingContext mappingContext = new JdbcMappingContext();
private ConnectionFactory connectionFactory;
private DatabaseClient databaseClient;
private LegoSetRepository repository;
private JdbcTemplate jdbc;
@Before
public void before() {
Hooks.onOperatorDebug();
this.connectionFactory = createConnectionFactory();
this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build();
this.jdbc = createJdbcTemplate(createDataSource());
String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n"
+ " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");";
this.jdbc.execute("DROP TABLE IF EXISTS repo_legoset");
this.jdbc.execute(tableToCreate);
this.repository = new R2dbcRepositoryFactory(databaseClient, mappingContext).getRepository(LegoSetRepository.class);
}
@Test
public void shouldInsertNewItems() {
LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12);
LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13);
repository.saveAll(Arrays.asList(legoSet1, legoSet2)) //
.as(StepVerifier::create) //
.expectNextCount(2) //
.verifyComplete();
}
@Test
public void shouldFindItemsByManual() {
shouldInsertNewItems();
repository.findByManual(13) //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.getName()).isEqualTo("FORSCHUNGSSCHIFF");
}) //
.verifyComplete();
}
@Test
public void shouldFindItemsByNameLike() {
shouldInsertNewItems();
repository.findByNameContains("%F%") //
.map(LegoSet::getName) //
.collectList() //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF");
}).verifyComplete();
}
@Test
public void shouldFindApplyingProjection() {
shouldInsertNewItems();
repository.findAsProjection() //
.map(Named::getName) //
.collectList() //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF");
}).verifyComplete();
}
interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {
@Query("SELECT * FROM repo_legoset WHERE name like $1")
Flux<LegoSet> findByNameContains(String name);
@Query("SELECT * FROM repo_legoset")
Flux<Named> findAsProjection();
@Query("SELECT * FROM repo_legoset WHERE manual = $1")
Mono<LegoSet> findByManual(int manual);
}
@Data
@Table("repo_legoset")
@AllArgsConstructor
@NoArgsConstructor
static class LegoSet {
@Id Integer id;
String name;
Integer manual;
}
interface Named {
String getName();
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2018 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 org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* Unit test for {@link R2dbcQueryMethod}.
*
* @author Mark Paluch
*/
public class R2dbcQueryMethodUnitTests {
JdbcMappingContext context;
@Before
public void setUp() {
context = new JdbcMappingContext();
}
@Test
public void detectsCollectionFromReturnTypeIfReturnTypeAssignable() throws Exception {
R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "method");
JdbcEntityMetadata<?> metadata = queryMethod.getEntityInformation();
assertThat(metadata.getJavaType()).isAssignableFrom(Contact.class);
assertThat(metadata.getTableName()).isEqualTo("contact");
}
@Test
public void detectsTableNameFromRepoTypeIfReturnTypeNotAssignable() throws Exception {
R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "differentTable");
JdbcEntityMetadata<?> metadata = queryMethod.getEntityInformation();
assertThat(metadata.getJavaType()).isAssignableFrom(Address.class);
assertThat(metadata.getTableName()).isEqualTo("contact");
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNullMappingContext() throws Exception {
Method method = PersonRepository.class.getMethod("findMonoByLastname", String.class, Pageable.class);
new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
new SpelAwareProxyProjectionFactory(), null);
}
@Test(expected = IllegalStateException.class)
public void rejectsMonoPageableResult() throws Exception {
queryMethod(PersonRepository.class, "findMonoByLastname", String.class, Pageable.class);
}
@Test
public void createsQueryMethodObjectForMethodReturningAnInterface() throws Exception {
queryMethod(SampleRepository.class, "methodReturningAnInterface");
}
@Test(expected = InvalidDataAccessApiUsageException.class)
public void throwsExceptionOnWrappedPage() throws Exception {
queryMethod(PersonRepository.class, "findMonoPageByLastname", String.class, Pageable.class);
}
@Test(expected = InvalidDataAccessApiUsageException.class)
public void throwsExceptionOnWrappedSlice() throws Exception {
queryMethod(PersonRepository.class, "findMonoSliceByLastname", String.class, Pageable.class);
}
@Test
public void fallsBackToRepositoryDomainTypeIfMethodDoesNotReturnADomainType() throws Exception {
R2dbcQueryMethod method = queryMethod(PersonRepository.class, "deleteByUserName", String.class);
assertThat(method.getEntityInformation().getJavaType()).isAssignableFrom(Contact.class);
}
private R2dbcQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
Method method = repository.getMethod(name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context);
}
interface PersonRepository extends Repository<Contact, Long> {
Mono<Contact> findMonoByLastname(String lastname, Pageable pageRequest);
Mono<Page<Contact>> findMonoPageByLastname(String lastname, Pageable pageRequest);
Mono<Slice<Contact>> findMonoSliceByLastname(String lastname, Pageable pageRequest);
void deleteByUserName(String userName);
}
interface SampleRepository extends Repository<Contact, Long> {
List<Contact> method();
List<Address> differentTable();
Customer methodReturningAnInterface();
}
interface Customer {}
static class Contact {}
static class Address {}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2018 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 org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ReflectionUtils;
/**
* Unit tests for {@link StringBasedR2dbcQuery}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class StringBasedR2dbcQueryUnitTests {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@Mock private DatabaseClient databaseClient;
@Mock private GenericExecuteSpec bindSpec;
private JdbcMappingContext mappingContext;
private MappingR2dbcConverter converter;
private ProjectionFactory factory;
private RepositoryMetadata metadata;
@Before
@SuppressWarnings("unchecked")
public void setUp() {
this.mappingContext = new JdbcMappingContext();
this.converter = new MappingR2dbcConverter(this.mappingContext);
this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class);
this.factory = new SpelAwareProxyProjectionFactory();
when(bindSpec.bind(anyString(), any())).thenReturn(bindSpec);
}
@Test
public void bindsSimplePropertyCorrectly() {
StringBasedR2dbcQuery query = getQueryMethod("findByLastname", String.class);
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White");
BindableQuery stringQuery = query.createQuery(accessor);
assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = $1");
assertThat(stringQuery.bind(bindSpec)).isNotNull();
verify(bindSpec).bind("$1", "White");
}
private StringBasedR2dbcQuery getQueryMethod(String name, Class<?>... args) {
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, converter.getMappingContext());
return new StringBasedR2dbcQuery(queryMethod, databaseClient, converter, PARSER,
ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
}
@SuppressWarnings("unused")
private interface SampleRepository extends Repository<Person, String> {
@Query("SELECT * FROM person WHERE lastname = $1")
Person findByLastname(String lastname);
}
static class Person {
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2018 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 org.springframework.data.jdbc.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.Repository;
/**
* Unit test for {@link R2dbcRepositoryFactory}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class R2dbcRepositoryFactoryUnitTests {
@Mock DatabaseClient databaseClient;
@Mock @SuppressWarnings("rawtypes") MappingContext mappingContext;
@Mock @SuppressWarnings("rawtypes") JdbcPersistentEntity entity;
@Before
@SuppressWarnings("unchecked")
public void before() {
when(mappingContext.getRequiredPersistentEntity(Person.class)).thenReturn(entity);
}
@Test
@SuppressWarnings("unchecked")
public void usesMappingJdbcEntityInformationIfMappingContextSet() {
R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext);
JdbcEntityInformation<Person, Long> entityInformation = factory.getEntityInformation(Person.class);
assertThat(entityInformation).isInstanceOf(MappingJdbcEntityInformation.class);
}
@Test
@SuppressWarnings("unchecked")
public void createsRepositoryWithIdTypeLong() {
R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext);
MyPersonRepository repository = factory.getRepository(MyPersonRepository.class);
assertThat(repository).isNotNull();
}
interface MyPersonRepository extends Repository<Person, Long> {}
static class Person {}
}

View File

@@ -40,6 +40,7 @@ import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.Table;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -65,10 +66,13 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS
this.connectionFactory = createConnectionFactory();
this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build();
this.repository = new SimpleR2dbcRepository<>(databaseClient,
new MappingR2dbcConverter(mappingContext),
JdbcEntityInformation<LegoSet, Integer> entityInformation = new MappingJdbcEntityInformation<>(
(JdbcPersistentEntity<LegoSet>) mappingContext.getRequiredPersistentEntity(LegoSet.class));
this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient,
new MappingR2dbcConverter(mappingContext));
this.jdbc = createJdbcTemplate(createDataSource());
String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n"