Auto-configure script-based R2DBC database initialization

See gh-24741
This commit is contained in:
Andy Wilkinson 2021-03-26 17:31:44 +00:00
parent 9cc7f0b54d
commit e49a88cb9b
10 changed files with 311 additions and 118 deletions

View File

@ -0,0 +1,56 @@
/*
* Copyright 2012-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
*
* 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.autoconfigure.sql.init;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.util.StringUtils;
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@Import(DataSourceInitializationDependencyConfigurer.class)
class DataSourceInitializationConfiguration {
@Bean
DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties initializationProperties) {
DatabaseInitializationSettings settings = SettingsCreator.createFrom(initializationProperties);
return new DataSourceScriptDatabaseInitializer(determineDataSource(dataSource,
initializationProperties.getUsername(), initializationProperties.getPassword()), settings);
}
private static DataSource determineDataSource(DataSource dataSource, String username, String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
DataSourceBuilder.derivedFrom(dataSource).username(username).password(password)
.type(SimpleDriverDataSource.class).build();
}
return dataSource;
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2012-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
*
* 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.autoconfigure.sql.init;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.r2dbc.ConnectionFactoryBuilder;
import org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
/**
* Configuration for initializing an SQL database accessed via an R2DBC
* {@link ConnectionFactory}.
*
* @author Andy Wilkinson
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ConnectionFactory.class)
@ConditionalOnSingleCandidate(ConnectionFactory.class)
class R2dbcInitializationConfiguration {
@Bean
R2dbcScriptDatabaseInitializer r2dbcScriptDatabaseInitializer(ConnectionFactory connectionFactory,
SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = SettingsCreator.createFrom(properties);
return new R2dbcScriptDatabaseInitializer(
determineConnectionFactory(connectionFactory, properties.getUsername(), properties.getPassword()),
settings);
}
private static ConnectionFactory determineConnectionFactory(ConnectionFactory connectionFactory, String username,
String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
ConnectionFactoryBuilder.derivefrom(connectionFactory).username(username).password(password).build();
}
return connectionFactory;
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2012-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
*
* 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.autoconfigure.sql.init;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
/**
* Helpers class for creating {@link DatabaseInitializationSettings} from
* {@link SqlInitializationProperties}.
*
* @author Andy Wilkinson
*/
final class SettingsCreator {
private SettingsCreator() {
}
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(
scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform()));
settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getEncoding());
return settings;
}
private static List<String> scriptLocations(List<String> locations, String fallback, String platform) {
if (locations != null) {
return locations;
}
List<String> fallbackLocations = new ArrayList<>();
fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql");
fallbackLocations.add("optional:classpath*:" + fallback + ".sql");
return fallbackLocations;
}
}

View File

@ -16,27 +16,16 @@
package org.springframework.boot.autoconfigure.sql.init;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for initializing an SQL database.
@ -45,49 +34,11 @@ import org.springframework.util.StringUtils;
* @since 2.5.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
@ConditionalOnProperty(prefix = "spring.sql.init", name = "enabled", matchIfMissing = true)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureAfter({ R2dbcAutoConfiguration.class, DataSourceAutoConfiguration.class })
@EnableConfigurationProperties(SqlInitializationProperties.class)
@Import(DataSourceInitializationDependencyConfigurer.class)
@Import({ R2dbcInitializationConfiguration.class, DataSourceInitializationConfiguration.class })
public class SqlInitializationAutoConfiguration {
@Bean
DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties initializationProperties) {
DatabaseInitializationSettings settings = createSettings(initializationProperties);
return new DataSourceScriptDatabaseInitializer(determineDataSource(dataSource,
initializationProperties.getUsername(), initializationProperties.getPassword()), settings);
}
private static DatabaseInitializationSettings createSettings(SqlInitializationProperties properties) {
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
settings.setSchemaLocations(
scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform()));
settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform()));
settings.setContinueOnError(properties.isContinueOnError());
settings.setSeparator(properties.getSeparator());
settings.setEncoding(properties.getEncoding());
return settings;
}
private static List<String> scriptLocations(List<String> locations, String fallback, String platform) {
if (locations != null) {
return locations;
}
List<String> fallbackLocations = new ArrayList<>();
fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql");
fallbackLocations.add("optional:classpath*:" + fallback + ".sql");
return fallbackLocations;
}
private static DataSource determineDataSource(DataSource dataSource, String username, String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
DataSourceBuilder.derivedFrom(dataSource).username(username).password(password)
.type(SimpleDriverDataSource.class).build();
}
return dataSource;
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2012-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
*
* 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.autoconfigure.sql.init;
import java.nio.charset.Charset;
import java.util.List;
import javax.sql.DataSource;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
import org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializer;
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SqlInitializationAutoConfiguration}.
*
* @author Andy Wilkinson
*/
public class SqlInitializationAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SqlInitializationAutoConfiguration.class)).withPropertyValues(
"spring.datasource.generate-unique-name:true", "spring.r2dbc.generate-unique-name:true");
@Test
void whenNoDataSourceOrConnectionFactoryIsAvailableThenAutoConfigurationBacksOff() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class));
}
@Test
void whenConnectionFactoryIsAvailableThenR2dbcInitializerIsAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(R2dbcScriptDatabaseInitializer.class));
}
@Test
void whenConnectionFactoryIsAvailableAndInitializationIsDisabledThenInitializerIsNotAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withPropertyValues("spring.sql.init.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class));
}
@Test
void whenDataSourceIsAvailableThenDataSourceInitializerIsAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
.run((context) -> assertThat(context).hasSingleBean(DataSourceScriptDatabaseInitializer.class));
}
@Test
void whenDataSourceIsAvailableAndInitializationIsDisabledThenInitializerIsNotAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
.withPropertyValues("spring.sql.init.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class));
}
@Test
void whenDataSourceAndConnectionFactoryAreAvailableThenOnlyR2dbcInitializerIsAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withUserConfiguration(DataSourceAutoConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class)
.hasSingleBean(DataSource.class).hasSingleBean(R2dbcScriptDatabaseInitializer.class)
.doesNotHaveBean(DataSourceScriptDatabaseInitializer.class));
}
@Test
void whenAnInitializerIsDefinedThenInitializerIsNotAutoConfigured() {
this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class))
.withUserConfiguration(DataSourceAutoConfiguration.class, DatabaseInitializerConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(AbstractScriptDatabaseInitializer.class)
.hasBean("customInitializer"));
}
@Configuration(proxyBeanMethods = false)
static class DatabaseInitializerConfiguration {
@Bean
AbstractScriptDatabaseInitializer customInitializer() {
return new AbstractScriptDatabaseInitializer(new DatabaseInitializationSettings()) {
@Override
protected void runScripts(List<Resource> resources, boolean continueOnError, String separator,
Charset encoding) {
// No-op
}
};
}
}
}

View File

@ -1573,7 +1573,6 @@ The following example shows how to define a data source by setting properties:
----
Assuming that your `FancyDataSource` has regular JavaBean properties for the URL, the username, and the pool size, these settings are bound automatically before the `DataSource` is made available to other components.
The regular <<howto-initialize-a-database-using-spring-jdbc,database initialization>> also happens (so the relevant sub-set of `spring.datasource.*` can still be used with your custom configuration).
Spring Boot also provides a utility builder class, called `DataSourceBuilder`, that can be used to create one of the standard data sources (if it is on the classpath).
The builder can detect the one to use based on what's available on the classpath.
@ -2022,55 +2021,26 @@ It is a Hibernate feature (and has nothing to do with Spring).
[[howto-initialize-a-database-using-spring-jdbc]]
[[howto-initialize-a-database-using-basic-scripts]]
=== Initialize a Database using basic SQL scripts
Spring Boot can automatically create the schema (DDL scripts) of your `DataSource` and initialize it (DML scripts).
Spring Boot can automatically create the schema (DDL scripts) of your JDBC `DataSource` or R2DBC `ConnectionFactory` 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 configprop:spring.sql.init.platform[].
This allows you to switch to database-specific scripts if necessary.
For example, you might choose to set it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`, `postgresql`, and so on).
SQL database initialization can be disabled by setting configprop:spring.sql.init.enabled[] to `false`.
By default, Spring Boot enables the fail-fast feature of its script-based database initializer.
This means that, if the scripts cause exceptions, the application fails to start.
You can tune that behavior by setting configprop:spring.sql.init.continue-on-error[].
[NOTE]
====
When only basic SQL scripts are used, Spring Boot automatically initializes the `DataSource`.
This initialization can be disabled by setting the configprop:spring.sql.init.enabled[] property to `false`.
By default, script-based `DataSource` initialization is performed before any JPA `EntityManagerFactory` beans are created.
Script-based `DataSource` initialization is performed, by default, before any JPA `EntityManagerFactory` beans are created.
`schema.sql` can be used to create the schema for JPA-managed entities and `data.sql` can be used to populate it.
We do not recommend using multiple data source initialization technologies.
However, if you want script-based `DataSource` initialization to be able to build upon the schema creation performed by Hibernate, set configprop:spring.jpa.defer-datasource-initialization[] to `true`.
While do not recommend using multiple data source initialization technologies, if you want script-based `DataSource` initialization to be able to build upon the schema creation performed by Hibernate, set configprop:spring.jpa.defer-datasource-initialization[] to `true`.
This will defer data source initialization until after any `EntityManagerFactory` beans have been created and initialized.
`schema.sql` can then be used to make additions to any schema creation performed by Hibernate and `data.sql` can be used to populate it.
If you are using a <<spring-boot-features.adoc#howto-use-a-higher-level-database-migration-tool,Higher-level Database Migration Tool>>, like Flyway or Liquibase, you should use them alone to create and initialize the schema.
Using the basic `schema.sql` and `data.sql` scripts alongside Flyway or Liquibase is not recommended and support will be removed in a future release.
====
By default, Spring Boot enables the fail-fast feature of the Spring JDBC initializer.
This means that, if the scripts cause exceptions, the application fails to start.
You can tune that behavior by setting configprop:spring.sql.init.continue-on-error[].
To take complete control over the script-based initialization of a `DataSource`, define your own `ScriptDataSourceInitializer` bean.
Doing so will cause the auto-configuration of script-based initialization to back off.
If you have multiple `DataSource`s in your application, you can define multiple `ScriptDataSourceInitializer` beans.
[[howto-initialize-a-database-using-r2dbc]]
=== 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.
You can initialize the database on startup using SQL scripts as shown in the following example:
[source,java,indent=0]
----
include::{include-howto}/dataaccess/R2dbcDatabaseInitializationConfiguration.java[tag=*]
----
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.

View File

@ -3936,7 +3936,7 @@ TIP: You do not need to specify a driver class name, since Spring Boot obtains t
NOTE: At least the url should be provided.
Information specified in the URL takes precedence over individual properties, i.e. `name`, `username`, `password` and pooling options.
TIP: The "`How-to`" section includes a <<howto.adoc#howto-initialize-a-database-using-r2dbc, section on how to initialize a database>>.
TIP: The "`How-to`" section includes a <<howto.adoc#howto-initialize-a-database-using-basic-scripts, section on how to initialize a database>>.
To customize the connections created by a `ConnectionFactory`, i.e., set specific parameters that you do not want (or cannot) configure in your central database configuration, you can use a `ConnectionFactoryOptionsBuilderCustomizer` `@Bean`.
The following example shows how to manually override the database port while the rest of the options is taken from the application configuration:

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-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.
@ -16,15 +16,8 @@
package smoketest.data.r2dbc;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
@SpringBootApplication
public class SampleR2dbcApplication {
@ -33,13 +26,4 @@ public class SampleR2dbcApplication {
SpringApplication.run(SampleR2dbcApplication.class, args);
}
@Bean
public ApplicationRunner initializeDatabase(ConnectionFactory connectionFactory, ResourceLoader resourceLoader) {
return (arguments) -> {
Resource[] scripts = new Resource[] { resourceLoader.getResource("classpath:database-init.sql") };
new ResourceDatabasePopulator(scripts).populate(connectionFactory).block();
};
}
}

View File

@ -1,10 +1,2 @@
CREATE TABLE CITY (
id INTEGER IDENTITY 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,6 @@
CREATE TABLE CITY (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(30),
state VARCHAR(30),
country VARCHAR(30)
);