Document how to initialize a database with R2DBC

This commit adds a section to the reference guide on how to initialize
a database using R2DBC. 2 smoke tests are also added to validate this
behaviour with Flyway and Liquibase.

Closes gh-20742
This commit is contained in:
Stephane Nicoll 2020-03-31 13:09:59 +02:00
parent 12123d41e5
commit 960ab159e4
16 changed files with 554 additions and 1 deletions

View File

@ -1937,7 +1937,7 @@ It is a Hibernate feature (and has nothing to do with Spring).
[[howto-initialize-a-database-using-spring-jdbc]]
=== Initialize a Database
=== Initialize a Database using basic SQL scripts
Spring Boot can automatically create the schema (DDL scripts) of your `DataSource` and initialize it (DML scripts).
It loads SQL from the standard root classpath locations: `schema.sql` and `data.sql`, respectively.
In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{platform}.sql` files (if present), where `platform` is the value of `spring.datasource.platform`.
@ -1965,6 +1965,24 @@ Make sure to disable `spring.jpa.hibernate.ddl-auto` if you use `schema.sql`.
[[howto-initialize-a-database-using-r2dc]]
=== Initialize a Database Using R2DBC
If you are using R2DBC, the regular `DataSource` auto-configuration backs off so none of the options described above can be used.
If you are using Spring Data R2DBC, you can initialize the database on startup using simple SQL scripts as shown in the following example:
[source,java,indent=0]
----
include::{code-examples}/r2dbc/R2dbcDatabaseInitializationExample.java[tag=configuration]
----
Alternatively, you can configure either <<howto-execute-flyway-database-migrations-on-startup,Flyway>> or <<howto-execute-liquibase-database-migrations-on-startup,Liquibase>> to configure a `DataSource` for you for the duration of the migration.
Both these libraries offer properties to set the `url`, `username` and `password` of the database to migrate.
NOTE: When choosing this option, `org.springframework:spring-jdbc` is still a required dependency.
[[howto-initialize-a-spring-batch-database]]
=== Initialize a Spring Batch Database
If you use Spring Batch, it comes pre-packaged with SQL initialization scripts for most popular database platforms.

View File

@ -0,0 +1,50 @@
/*
* Copyright 2012-2020 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 org.springframework.boot.docs.r2dbc;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator;
/**
* Example configuration for initializing a database using R2DBC.
*
* @author Stephane Nicoll
*/
public class R2dbcDatabaseInitializationExample {
// tag::configuration[]
@Configuration(proxyBeanMethods = false)
static class DatabaseInitializationConfiguration {
@Autowired
void initializeDatabase(ConnectionFactory connectionFactory) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource[] scripts = new Resource[] { resourceLoader.getResource("classpath:schema.sql"),
resourceLoader.getResource("classpath:data.sql") };
new ResourceDatabasePopulator(scripts).execute(connectionFactory).block();
}
}
// end::configuration[]
}

View File

@ -0,0 +1,22 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot Data R2DBC with Flyway smoke test"
dependencies {
implementation(platform(project(":spring-boot-project:spring-boot-parent")))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-r2dbc"))
runtimeOnly("io.r2dbc:r2dbc-postgresql")
runtimeOnly("org.flywaydb:flyway-core")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("org.springframework:spring-jdbc")
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
testImplementation("io.projectreactor:reactor-test")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import org.springframework.data.annotation.Id;
public class City {
@Id
private Long id;
private String name;
private String state;
private String country;
protected City() {
}
public City(String name, String country) {
this.name = name;
this.country = country;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
public String getCountry() {
return this.country;
}
@Override
public String toString() {
return getName() + "," + getState() + "," + getCountry();
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import reactor.core.publisher.Flux;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface CityRepository extends ReactiveCrudRepository<City, Long> {
Flux<City> findByState(String state);
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleR2dbcFlywayApplication {
public static void main(String[] args) {
SpringApplication.run(SampleR2dbcFlywayApplication.class, args);
}
}

View File

@ -0,0 +1,5 @@
spring.r2dbc.url=r2dbc:postgresql://user:secret@localhost/test_flyway
spring.flyway.url=jdbc:postgresql://localhost/test_flyway
spring.flyway.user=user
spring.flyway.password=secret

View File

@ -0,0 +1,9 @@
CREATE TABLE CITY (
id INTEGER PRIMARY KEY,
name VARCHAR(30),
state VARCHAR(30),
country VARCHAR(30)
);
INSERT INTO CITY (ID, NAME, STATE, COUNTRY) values (2000, 'Washington', 'DC', 'US');
INSERT INTO CITY (ID, NAME, STATE, COUNTRY) values (2001, 'San Francisco', 'CA', 'US');

View File

@ -0,0 +1,68 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CityRepository}.
*/
@Testcontainers(disabledWithoutDocker = true)
@DataR2dbcTest
class CityRepositoryTests {
@Container
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>().withDatabaseName("test_flyway");
@DynamicPropertySource
static void postgresqlProperties(DynamicPropertyRegistry registry) {
registry.add("spring.r2dbc.url", CityRepositoryTests::r2dbcUrl);
registry.add("spring.r2dbc.username", postgresql::getUsername);
registry.add("spring.r2dbc.password", postgresql::getPassword);
// configure flyway to use the same database
registry.add("spring.flyway.url", postgresql::getJdbcUrl);
registry.add("spring.flyway.user", postgresql::getUsername);
registry.add("spring.flyway.password", postgresql::getPassword);
}
@Autowired
private CityRepository repository;
@Test
void databaseHasBeenInitialized() {
StepVerifier.create(this.repository.findByState("DC").filter((city) -> city.getName().equals("Washington")))
.consumeNextWith((city) -> assertThat(city.getId()).isNotNull()).verifyComplete();
}
private static String r2dbcUrl() {
return String.format("r2dbc:postgresql://%s:%s/%s", postgresql.getContainerIpAddress(),
postgresql.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), postgresql.getDatabaseName());
}
}

View File

@ -0,0 +1,22 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot Data R2DBC with Liquibase smoke test"
dependencies {
implementation(platform(project(":spring-boot-project:spring-boot-parent")))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-r2dbc"))
runtimeOnly("io.r2dbc:r2dbc-postgresql")
runtimeOnly("org.liquibase:liquibase-core")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("org.springframework:spring-jdbc")
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
testImplementation("io.projectreactor:reactor-test")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import org.springframework.data.annotation.Id;
public class City {
@Id
private Long id;
private String name;
private String state;
private String country;
protected City() {
}
public City(String name, String country) {
this.name = name;
this.country = country;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
public String getCountry() {
return this.country;
}
@Override
public String toString() {
return getName() + "," + getState() + "," + getCountry();
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import reactor.core.publisher.Flux;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface CityRepository extends ReactiveCrudRepository<City, Long> {
Flux<City> findByState(String state);
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleR2dbcLiquibaseApplication {
public static void main(String[] args) {
SpringApplication.run(SampleR2dbcLiquibaseApplication.class, args);
}
}

View File

@ -0,0 +1,5 @@
spring.r2dbc.url=r2dbc:postgresql://user:secret@localhost/test_liquibase
spring.liquibase.url=jdbc:postgresql://localhost/test_liquibase
spring.liquibase.user=user
spring.liquibase.password=secret

View File

@ -0,0 +1,52 @@
databaseChangeLog:
- changeSet:
id: 1
author: snicoll
changes:
- createTable:
tableName: city
columns:
- column:
name: id
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: varchar(30)
- column:
name: state
type: varchar(30)
- column:
name: country
type: varchar(30)
- changeSet:
id: 2
author: snicoll
changes:
- insert:
tableName: city
columns:
- column:
name: name
value: Washington
- column:
name: state
value: DC
- column:
name: country
value: US
- insert:
tableName: city
columns:
- column:
name: name
value: San Francisco
- column:
name: state
value: CA
- column:
name: country
value: US

View File

@ -0,0 +1,68 @@
/*
* Copyright 2012-2020 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 smoketest.data.r2dbc;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CityRepository}.
*/
@Testcontainers(disabledWithoutDocker = true)
@DataR2dbcTest
class CityRepositoryTests {
@Container
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>().withDatabaseName("test_liquibase");
@DynamicPropertySource
static void postgresqlProperties(DynamicPropertyRegistry registry) {
registry.add("spring.r2dbc.url", CityRepositoryTests::r2dbcUrl);
registry.add("spring.r2dbc.username", postgresql::getUsername);
registry.add("spring.r2dbc.password", postgresql::getPassword);
// configure liquibase to use the same database
registry.add("spring.liquibase.url", postgresql::getJdbcUrl);
registry.add("spring.liquibase.user", postgresql::getUsername);
registry.add("spring.liquibase.password", postgresql::getPassword);
}
@Autowired
private CityRepository repository;
@Test
void databaseHasBeenInitialized() {
StepVerifier.create(this.repository.findByState("DC").filter((city) -> city.getName().equals("Washington")))
.consumeNextWith((city) -> assertThat(city.getId()).isNotNull()).verifyComplete();
}
private static String r2dbcUrl() {
return String.format("r2dbc:postgresql://%s:%s/%s", postgresql.getContainerIpAddress(),
postgresql.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), postgresql.getDatabaseName());
}
}