Commit c0b267fe authored by Stephane Nicoll's avatar Stephane Nicoll

Polish "Fix detection logic for embedded databases"

See gh-23693
parent ab02084e
...@@ -322,13 +322,13 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB ...@@ -322,13 +322,13 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB
* @since 1.4.0 * @since 1.4.0
*/ */
public String determineUsername() { public String determineUsername() {
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl()) if (StringUtils.hasText(this.username)) {
&& !StringUtils.hasText(this.username)) {
return "sa";
}
else {
return this.username; return this.username;
} }
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
return "sa";
}
return null;
} }
/** /**
...@@ -350,13 +350,13 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB ...@@ -350,13 +350,13 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB
* @since 1.4.0 * @since 1.4.0
*/ */
public String determinePassword() { public String determinePassword() {
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl()) if (StringUtils.hasText(this.password)) {
&& !StringUtils.hasText(this.password)) {
return "";
}
else {
return this.password; return this.password;
} }
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
return "";
}
return null;
} }
public String getJndiName() { public String getJndiName() {
......
...@@ -127,6 +127,7 @@ class FlywayAutoConfigurationTests { ...@@ -127,6 +127,7 @@ class FlywayAutoConfigurationTests {
assertThat(context).hasSingleBean(Flyway.class); assertThat(context).hasSingleBean(Flyway.class);
DataSource dataSource = context.getBean(Flyway.class).getConfiguration().getDataSource(); DataSource dataSource = context.getBean(Flyway.class).getConfiguration().getDataSource();
assertThat(dataSource).isNotNull(); assertThat(dataSource).isNotNull();
assertThat(dataSource).hasFieldOrPropertyWithValue("user", "sa");
assertThat(dataSource).hasFieldOrPropertyWithValue("password", ""); assertThat(dataSource).hasFieldOrPropertyWithValue("password", "");
}); });
} }
......
...@@ -78,32 +78,6 @@ class DataSourcePropertiesTests { ...@@ -78,32 +78,6 @@ class DataSourcePropertiesTests {
assertThat(properties.determineUrl()).isEqualTo("jdbc:mysql://mydb"); assertThat(properties.determineUrl()).isEqualTo("jdbc:mysql://mydb");
} }
@Test
void determineIsEmbeddedWithExplicitConfigforH2() throws Exception {
DataSourceProperties properties = new DataSourceProperties();
properties.setUrl("jdbc:h2:~/test");
properties.setUsername("");
properties.setPassword("");
properties.afterPropertiesSet();
assertThat(properties.getUrl()).isEqualTo("jdbc:h2:~/test");
assertThat(properties.determineUrl()).isEqualTo("jdbc:h2:~/test");
assertThat(properties.determineUsername()).isEqualTo("");
assertThat(properties.determinePassword()).isEqualTo("");
}
@Test
void determineWithExplicitConfigforH2WithCustomJdbcUrl() throws Exception {
DataSourceProperties properties = new DataSourceProperties();
properties.setUrl("jdbc:h2:~/test");
properties.setUsername("as");
properties.setPassword("as");
properties.afterPropertiesSet();
assertThat(properties.getUrl()).isEqualTo("jdbc:h2:~/test");
assertThat(properties.determineUrl()).isEqualTo("jdbc:h2:~/test");
assertThat(properties.determineUsername()).isEqualTo("as");
assertThat(properties.determinePassword()).isEqualTo("as");
}
@Test @Test
void determineUrlWithGenerateUniqueName() throws Exception { void determineUrlWithGenerateUniqueName() throws Exception {
DataSourceProperties properties = new DataSourceProperties(); DataSourceProperties properties = new DataSourceProperties();
...@@ -151,11 +125,21 @@ class DataSourcePropertiesTests { ...@@ -151,11 +125,21 @@ class DataSourcePropertiesTests {
assertThat(properties.determineUsername()).isEqualTo("foo"); assertThat(properties.determineUsername()).isEqualTo("foo");
} }
@Test
void determineUsernameWithNonEmbeddedUrl() throws Exception {
DataSourceProperties properties = new DataSourceProperties();
properties.setUrl("jdbc:h2:~/test");
properties.afterPropertiesSet();
assertThat(properties.getPassword()).isNull();
assertThat(properties.determineUsername()).isNull();
}
@Test @Test
void determinePassword() throws Exception { void determinePassword() throws Exception {
DataSourceProperties properties = new DataSourceProperties(); DataSourceProperties properties = new DataSourceProperties();
properties.afterPropertiesSet(); properties.afterPropertiesSet();
assertThat(properties.getPassword()).isNull(); assertThat(properties.getPassword()).isNull();
assertThat(properties.determinePassword()).isEqualTo("");
} }
@Test @Test
...@@ -167,6 +151,15 @@ class DataSourcePropertiesTests { ...@@ -167,6 +151,15 @@ class DataSourcePropertiesTests {
assertThat(properties.determinePassword()).isEqualTo("bar"); assertThat(properties.determinePassword()).isEqualTo("bar");
} }
@Test
void determinePasswordWithNonEmbeddedUrl() throws Exception {
DataSourceProperties properties = new DataSourceProperties();
properties.setUrl("jdbc:h2:~/test");
properties.afterPropertiesSet();
assertThat(properties.getPassword()).isNull();
assertThat(properties.determinePassword()).isNull();
}
@Test @Test
void determineCredentialsForSchemaScripts() { void determineCredentialsForSchemaScripts() {
DataSourceProperties properties = new DataSourceProperties(); DataSourceProperties properties = new DataSourceProperties();
......
...@@ -246,6 +246,7 @@ class LiquibaseAutoConfigurationTests { ...@@ -246,6 +246,7 @@ class LiquibaseAutoConfigurationTests {
.run(assertLiquibase((liquibase) -> { .run(assertLiquibase((liquibase) -> {
DataSource dataSource = liquibase.getDataSource(); DataSource dataSource = liquibase.getDataSource();
assertThat(((HikariDataSource) dataSource).isClosed()).isTrue(); assertThat(((HikariDataSource) dataSource).isClosed()).isTrue();
assertThat(((HikariDataSource) dataSource).getUsername()).isEqualTo("sa");
assertThat(((HikariDataSource) dataSource).getPassword()).isEqualTo(""); assertThat(((HikariDataSource) dataSource).getPassword()).isEqualTo("");
})); }));
} }
......
...@@ -1982,8 +1982,8 @@ This is controlled through two external properties: ...@@ -1982,8 +1982,8 @@ This is controlled through two external properties:
You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`. You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`.
Spring Boot chooses a default value for you based on whether it thinks your database is embedded. Spring Boot chooses a default value for you based on whether it thinks your database is embedded.
It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases. It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases.
An embedded database is detected by looking at the `Connection` type. An embedded database is detected by looking at the `Connection` type and JDBC url.
`hsqldb`, `h2`, and `derby` are embedded, and others are not. `hsqldb`, `h2`, and `derby` are candidates, and others are not.
Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform. Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform.
You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database. You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database.
......
...@@ -101,7 +101,7 @@ public class TestDatabaseAutoConfiguration { ...@@ -101,7 +101,7 @@ public class TestDatabaseAutoConfiguration {
private BeanDefinition createEmbeddedBeanDefinition(boolean primary) { private BeanDefinition createEmbeddedBeanDefinition(boolean primary) {
BeanDefinition beanDefinition = new RootBeanDefinition(EmbeddedDataSourceFactoryBean.class); BeanDefinition beanDefinition = new RootBeanDefinition(EmbeddedDataSourceFactoryBean.class);
beanDefinition.setPrimary(true); beanDefinition.setPrimary(primary);
return beanDefinition; return beanDefinition;
} }
......
...@@ -17,8 +17,11 @@ ...@@ -17,8 +17,11 @@
package org.springframework.boot.jdbc; package org.springframework.boot.jdbc;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Locale; import java.util.Locale;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.sql.DataSource; import javax.sql.DataSource;
...@@ -44,18 +47,19 @@ public enum EmbeddedDatabaseConnection { ...@@ -44,18 +47,19 @@ public enum EmbeddedDatabaseConnection {
/** /**
* No Connection. * No Connection.
*/ */
NONE(null, null, null), NONE(null, null, null, (url) -> false),
/** /**
* H2 Database Connection. * H2 Database Connection.
*/ */
H2(EmbeddedDatabaseType.H2, DatabaseDriver.H2.getDriverClassName(), H2(EmbeddedDatabaseType.H2, DatabaseDriver.H2.getDriverClassName(),
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"), "jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", (url) -> url.contains(":h2:mem")),
/** /**
* Derby Database Connection. * Derby Database Connection.
*/ */
DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true"), DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true",
(url) -> true),
/** /**
* HSQL Database Connection. * HSQL Database Connection.
...@@ -63,13 +67,13 @@ public enum EmbeddedDatabaseConnection { ...@@ -63,13 +67,13 @@ public enum EmbeddedDatabaseConnection {
*/ */
@Deprecated @Deprecated
HSQL(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver", HSQL(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
"jdbc:hsqldb:mem:%s"), "jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:")),
/** /**
* HSQL Database Connection. * HSQL Database Connection.
*/ */
HSQLDB(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver", HSQLDB(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
"jdbc:hsqldb:mem:%s"); "jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:"));
private final EmbeddedDatabaseType type; private final EmbeddedDatabaseType type;
...@@ -79,15 +83,20 @@ public enum EmbeddedDatabaseConnection { ...@@ -79,15 +83,20 @@ public enum EmbeddedDatabaseConnection {
private final String url; private final String url;
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String url) { private final Predicate<String> embeddedUrl;
this(type, driverClass, null, url);
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String url,
Predicate<String> embeddedUrl) {
this(type, driverClass, null, url, embeddedUrl);
} }
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String fallbackDriverClass, String url) { EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String fallbackDriverClass, String url,
Predicate<String> embeddedUrl) {
this.type = type; this.type = type;
this.driverClass = driverClass; this.driverClass = driverClass;
this.alternativeDriverClass = fallbackDriverClass; this.alternativeDriverClass = fallbackDriverClass;
this.url = url; this.url = url;
this.embeddedUrl = embeddedUrl;
} }
/** /**
...@@ -116,34 +125,41 @@ public enum EmbeddedDatabaseConnection { ...@@ -116,34 +125,41 @@ public enum EmbeddedDatabaseConnection {
return (this.url != null) ? String.format(this.url, databaseName) : null; return (this.url != null) ? String.format(this.url, databaseName) : null;
} }
boolean isEmbeddedUrl(String url) {
return this.embeddedUrl.test(url);
}
boolean isDriverCompatible(String driverClass) {
return (driverClass != null
&& (driverClass.equals(this.driverClass) || driverClass.equals(this.alternativeDriverClass)));
}
/** /**
* Convenience method to determine if a given driver class name represents an embedded * Convenience method to determine if a given driver class name represents an embedded
* database type. * database type.
* @param driverClass the driver class * @param driverClass the driver class
* @return true if the driver class is one of the embedded types * @return true if the driver class is one of the embedded types
* @deprecated since 2.4.0 in favor of {@link #isEmbedded(String, String)}
*/ */
@Deprecated @Deprecated
public static boolean isEmbedded(String driverClass) { public static boolean isEmbedded(String driverClass) {
return driverClass != null && (matches(HSQL, driverClass) || matches(H2, driverClass) return isEmbedded(driverClass, null);
|| matches(DERBY, driverClass) || matches(HSQLDB, driverClass));
} }
/** /**
* Convenience method to determine if a given driver class name and url represents an * Convenience method to determine if a given driver class name and url represent an
* embedded database type.The exception is made for the H2 database for embedded * embedded database type.
* types.
* @param driverClass the driver class * @param driverClass the driver class
* @param url the jdbc url * @param url the jdbc url (can be {@code null)}
* @return true if the driver class is one of the embedded types * @return true if the driver class and url refer to an embedded database
*/ */
public static boolean isEmbedded(String driverClass, String url) { public static boolean isEmbedded(String driverClass, String url) {
return (driverClass != null return driverClass != null && getEmbeddedDatabaseConnection(driverClass).isEmbeddedUrl(url);
&& (matches(HSQL, driverClass) || (matches(H2, driverClass) && url.contains(":h2:mem"))
|| matches(DERBY, driverClass) || matches(HSQLDB, driverClass)));
} }
private static boolean matches(EmbeddedDatabaseConnection candidate, String driverClass) { private static EmbeddedDatabaseConnection getEmbeddedDatabaseConnection(String driverClass) {
return driverClass.equals(candidate.driverClass) || driverClass.equals(candidate.alternativeDriverClass); return Stream.of(H2, HSQLDB, DERBY).filter((connection) -> connection.isDriverCompatible(driverClass))
.findFirst().orElse(NONE);
} }
/** /**
...@@ -184,7 +200,8 @@ public enum EmbeddedDatabaseConnection { ...@@ -184,7 +200,8 @@ public enum EmbeddedDatabaseConnection {
@Override @Override
public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException { public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
String productName = connection.getMetaData().getDatabaseProductName(); DatabaseMetaData metaData = connection.getMetaData();
String productName = metaData.getDatabaseProductName();
if (productName == null) { if (productName == null) {
return false; return false;
} }
...@@ -192,7 +209,7 @@ public enum EmbeddedDatabaseConnection { ...@@ -192,7 +209,7 @@ public enum EmbeddedDatabaseConnection {
EmbeddedDatabaseConnection[] candidates = EmbeddedDatabaseConnection.values(); EmbeddedDatabaseConnection[] candidates = EmbeddedDatabaseConnection.values();
for (EmbeddedDatabaseConnection candidate : candidates) { for (EmbeddedDatabaseConnection candidate : candidates) {
if (candidate != NONE && productName.contains(candidate.name())) { if (candidate != NONE && productName.contains(candidate.name())) {
return true; return candidate.isEmbeddedUrl(metaData.getURL());
} }
} }
return false; return false;
......
...@@ -16,10 +16,24 @@ ...@@ -16,10 +16,24 @@
package org.springframework.boot.jdbc; package org.springframework.boot.jdbc;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link EmbeddedDatabaseConnection}. * Tests for {@link EmbeddedDatabaseConnection}.
...@@ -78,15 +92,65 @@ class EmbeddedDatabaseConnectionTests { ...@@ -78,15 +92,65 @@ class EmbeddedDatabaseConnectionTests {
.withMessageContaining("DatabaseName must not be empty"); .withMessageContaining("DatabaseName must not be empty");
} }
@ParameterizedTest(name = "{1}")
@MethodSource("embeddedDriverAndUrlParameters")
void isEmbeddedWithDriverAndUrl(EmbeddedDatabaseConnection connection, String url, boolean embedded) {
assertThat(EmbeddedDatabaseConnection.isEmbedded(connection.getDriverClassName(), url)).isEqualTo(embedded);
}
static Object[] embeddedDriverAndUrlParameters() {
return new Object[] { new Object[] { EmbeddedDatabaseConnection.H2, "jdbc:h2:~/test", false },
new Object[] { EmbeddedDatabaseConnection.H2, "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", true },
new Object[] { EmbeddedDatabaseConnection.HSQLDB, "jdbc:hsqldb:hsql://localhost", false },
new Object[] { EmbeddedDatabaseConnection.HSQLDB, "jdbc:hsqldb:mem:test", true },
new Object[] { EmbeddedDatabaseConnection.DERBY, "jdbc:derby:memory:test", true } };
}
@Test
void isEmbeddedWithH2DataSource() {
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build());
}
@Test @Test
void isEmbeddedForh2CustomDatabaseName() { void isEmbeddedWithHsqlDataSource() {
assertThat(EmbeddedDatabaseConnection.isEmbedded("org.h2.Driver", "jdbc:h2:~/test")).isFalse(); testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build());
} }
@Test @Test
void isEmbeddedForh2EmbeddedDatabaseName() { void isEmbeddedWithDerbyDataSource() {
assertThat(EmbeddedDatabaseConnection.isEmbedded("org.h2.Driver", testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY).build());
"jdbc:h2:mem:b3c7d078-1362-4be7-a088-e25dcc3aee32;DB_CLOSE_DELAY=-1")).isTrue(); }
void testEmbeddedDatabase(EmbeddedDatabase database) {
try {
assertThat(EmbeddedDatabaseConnection.isEmbedded(database)).isTrue();
}
finally {
database.shutdown();
}
}
@Test
void isEmbeddedWithUnknownDataSource() throws SQLException {
assertThat(EmbeddedDatabaseConnection.isEmbedded(mockDataSource("unknown-db", null))).isFalse();
}
@Test
void isEmbeddedWithH2File() throws SQLException {
assertThat(EmbeddedDatabaseConnection
.isEmbedded(mockDataSource(EmbeddedDatabaseConnection.H2.getDriverClassName(), "jdbc:h2:~/test")))
.isFalse();
}
DataSource mockDataSource(String productName, String connectionUrl) throws SQLException {
DatabaseMetaData metaData = mock(DatabaseMetaData.class);
given(metaData.getDatabaseProductName()).willReturn(productName);
given(metaData.getURL()).willReturn(connectionUrl);
Connection connection = mock(Connection.class);
given(connection.getMetaData()).willReturn(metaData);
DataSource dataSource = mock(DataSource.class);
given(dataSource.getConnection()).willReturn(connection);
return dataSource;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment