Browse Source

Support unique names for embedded databases

Development teams often encounter errors with embedded databases if
their test suite inadvertently attempts to recreate additional
instances of the same database. This can happen quite easily if an XML
configuration file or @Configuration class is responsible for creating
an embedded database and the corresponding configuration is then reused
across multiple testing scenarios within the same test suite (i.e.,
within the same JVM process) -- for example, integration tests against
embedded databases whose ApplicationContext configuration only differs
with regard to which bean definition profiles are active.

The root cause of such errors is the fact that Spring's
EmbeddedDatabaseFactory (used internally by both the
<jdbc:embedded-database> XML namespace element and the
EmbeddedDatabaseBuilder for Java Config) will set the name of the
embedded database to "testdb" if not otherwise specified. For the case
of <jdbc:embedded-database>, the embedded database is typically
assigned a name equal to the bean's id. Thus, subsequent attempts to
create an embedded database will not result in a new database. Instead,
the same JDBC connection URL will be reused, and attempts to create a
new embedded database will actually point to an existing embedded
database created from the same configuration.

This commit addresses this common issue by introducing support for
generating unique names for embedded databases. This support can be
enabled via:

 - EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()

 - EmbeddedDatabaseBuilder.generateUniqueName()

 - <jdbc:embedded-database generate-name="true" ... >

Issue: SPR-8849
pull/759/head
Sam Brannen 10 years ago
parent
commit
c0fbe0ae5a
  1. 16
      spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java
  2. 4
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java
  3. 22
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java
  4. 67
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java
  5. 11
      spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd
  6. 21
      spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java
  7. 54
      spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java
  8. 12
      spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml
  9. 1
      spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/embedded/db-schema-without-dropping.sql

16
spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java

@ -48,10 +48,16 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser
*/ */
static final String DB_NAME_ATTRIBUTE = "database-name"; static final String DB_NAME_ATTRIBUTE = "database-name";
/**
* Constant for the "generate-name" attribute.
*/
static final String GENERATE_NAME_ATTRIBUTE = "generate-name";
@Override @Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(EmbeddedDatabaseFactoryBean.class); BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(EmbeddedDatabaseFactoryBean.class);
setGenerateUniqueDatabaseNameFlag(element, builder);
setDatabaseName(element, builder); setDatabaseName(element, builder);
setDatabaseType(element, builder); setDatabaseType(element, builder);
DatabasePopulatorConfigUtils.setDatabasePopulator(element, builder); DatabasePopulatorConfigUtils.setDatabasePopulator(element, builder);
@ -64,6 +70,13 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser
return true; return true;
} }
private void setGenerateUniqueDatabaseNameFlag(Element element, BeanDefinitionBuilder builder) {
String generateName = element.getAttribute(GENERATE_NAME_ATTRIBUTE);
if (StringUtils.hasText(generateName)) {
builder.addPropertyValue("generateUniqueDatabaseName", generateName);
}
}
private void setDatabaseName(Element element, BeanDefinitionBuilder builder) { private void setDatabaseName(Element element, BeanDefinitionBuilder builder) {
// 1) Check for an explicit database name // 1) Check for an explicit database name
String name = element.getAttribute(DB_NAME_ATTRIBUTE); String name = element.getAttribute(DB_NAME_ATTRIBUTE);
@ -76,8 +89,7 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser
if (StringUtils.hasText(name)) { if (StringUtils.hasText(name)) {
builder.addPropertyValue("databaseName", name); builder.addPropertyValue("databaseName", name);
} }
// else, let EmbeddedDatabaseFactory use the default "testdb" name
// 3) Let EmbeddedDatabaseFactory set the default "testdb" name
} }
private void setDatabaseType(Element element, BeanDefinitionBuilder builder) { private void setDatabaseType(Element element, BeanDefinitionBuilder builder) {

4
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,7 +23,7 @@ import javax.sql.DataSource;
* *
* <p>An {@code EmbeddedDatabase} is also a {@link DataSource} and adds a * <p>An {@code EmbeddedDatabase} is also a {@link DataSource} and adds a
* {@link #shutdown} operation so that the embedded database instance can be * {@link #shutdown} operation so that the embedded database instance can be
* shutdown. * shut down gracefully.
* *
* @author Keith Donald * @author Keith Donald
* @author Sam Brannen * @author Sam Brannen

22
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -78,12 +78,32 @@ public class EmbeddedDatabaseBuilder {
this.resourceLoader = resourceLoader; this.resourceLoader = resourceLoader;
} }
/**
* Specify whether a unique ID should be generated and used as the database name.
* <p>If the configuration for this builder is reused across multiple
* application contexts within a single JVM, this flag should be <em>enabled</em>
* (i.e., set to {@code true}) in order to ensure that each application context
* gets its own embedded database.
* <p>Enabling this flag overrides any explicit name set via {@link #setName}.
* @param flag {@code true} if a unique database name should be generated
* @return {@code this}, to facilitate method chaining
* @see #setName
* @since 4.2
*/
public EmbeddedDatabaseBuilder generateUniqueName(boolean flag) {
this.databaseFactory.setGenerateUniqueDatabaseName(flag);
return this;
}
/** /**
* Set the name of the embedded database. * Set the name of the embedded database.
* <p>Defaults to {@link EmbeddedDatabaseFactory#DEFAULT_DATABASE_NAME} if * <p>Defaults to {@link EmbeddedDatabaseFactory#DEFAULT_DATABASE_NAME} if
* not called. * not called.
* <p>Will be overridden if the {@code generateUniqueName} flag has been
* set to {@code true}.
* @param databaseName the name of the embedded database to build * @param databaseName the name of the embedded database to build
* @return {@code this}, to facilitate method chaining * @return {@code this}, to facilitate method chaining
* @see #generateUniqueName
*/ */
public EmbeddedDatabaseBuilder setName(String databaseName) { public EmbeddedDatabaseBuilder setName(String databaseName) {
this.databaseFactory.setDatabaseName(databaseName); this.databaseFactory.setDatabaseName(databaseName);

67
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +19,9 @@ package org.springframework.jdbc.datasource.embedded;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.UUID;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -30,25 +32,28 @@ import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* Factory for creating {@link EmbeddedDatabase} instances. * Factory for creating an {@link EmbeddedDatabase} instance.
* *
* <p>Callers are guaranteed that a returned database has been fully initialized * <p>Callers are guaranteed that the returned database has been fully
* and populated. * initialized and populated.
* *
* <p>Can be configured: * <p>The factory can be configured as follows:
* <ul> * <ul>
* <li>Call {@link #setDatabaseName(String)} to change the name of the database. * <li>Call {@link #generateUniqueDatabaseName} to set a unique, random name
* <li>Call {@link #setDatabaseType(EmbeddedDatabaseType)} to set the database * for the database.
* type if you wish to use one of the supported types. * <li>Call {@link #setDatabaseName} to set an explicit name for the database.
* <li>Call {@link #setDatabaseConfigurer(EmbeddedDatabaseConfigurer)} to * <li>Call {@link #setDatabaseType} to set the database type if you wish to
* configure support for your own embedded database type. * use one of the supported types.
* <li>Call {@link #setDatabasePopulator(DatabasePopulator)} to change the * <li>Call {@link #setDatabaseConfigurer} to configure support for your own
* algorithm used to populate the database. * embedded database type.
* <li>Call {@link #setDataSourceFactory(DataSourceFactory)} to change the type * <li>Call {@link #setDatabasePopulator} to change the algorithm used to
* of {@link DataSource} used to connect to the database. * populate the database.
* <li>Call {@link #setDataSourceFactory} to change the type of
* {@link DataSource} used to connect to the database.
* </ul> * </ul>
* *
* <p>Call {@link #getDatabase()} to get the {@link EmbeddedDatabase} instance. * <p>After configuring the factory, call {@link #getDatabase()} to obtain
* a reference to the {@link EmbeddedDatabase} instance.
* *
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
@ -62,9 +67,10 @@ public class EmbeddedDatabaseFactory {
*/ */
public static final String DEFAULT_DATABASE_NAME = "testdb"; public static final String DEFAULT_DATABASE_NAME = "testdb";
private static final Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class); private static final Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class);
private boolean generateUniqueDatabaseName = false;
private String databaseName = DEFAULT_DATABASE_NAME; private String databaseName = DEFAULT_DATABASE_NAME;
private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory(); private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory();
@ -76,10 +82,25 @@ public class EmbeddedDatabaseFactory {
private DataSource dataSource; private DataSource dataSource;
/**
* Set the {@code generateUniqueDatabaseName} flag to enable or disable
* generation of a pseudo-random unique ID to be used as the database name.
* <p>Setting this flag to {@code true} overrides any explicit name set
* via {@link #setDatabaseName}.
* @see #setDatabaseName
* @since 4.2
*/
public void setGenerateUniqueDatabaseName(boolean generateUniqueDatabaseName) {
this.generateUniqueDatabaseName = generateUniqueDatabaseName;
}
/** /**
* Set the name of the database. * Set the name of the database.
* <p>Defaults to {@value #DEFAULT_DATABASE_NAME}. * <p>Defaults to {@value #DEFAULT_DATABASE_NAME}.
* <p>Will be overridden if the {@code generateUniqueDatabaseName} flag
* has been set to {@code true}.
* @param databaseName name of the embedded database * @param databaseName name of the embedded database
* @see #setGenerateUniqueDatabaseName
*/ */
public void setDatabaseName(String databaseName) { public void setDatabaseName(String databaseName) {
Assert.hasText(databaseName, "Database name is required"); Assert.hasText(databaseName, "Database name is required");
@ -124,7 +145,7 @@ public class EmbeddedDatabaseFactory {
} }
/** /**
* Factory method that returns the {@link EmbeddedDatabase embedded database} * Factory method that returns the {@linkplain EmbeddedDatabase embedded database}
* instance, which is also a {@link DataSource}. * instance, which is also a {@link DataSource}.
*/ */
public EmbeddedDatabase getDatabase() { public EmbeddedDatabase getDatabase() {
@ -136,12 +157,20 @@ public class EmbeddedDatabaseFactory {
/** /**
* Hook to initialize the embedded database. Subclasses may call this method * Hook to initialize the embedded database.
* to force initialization. * <p>If the {@code generateUniqueDatabaseName} flag has been set to {@code true},
* the current value of the {@linkplain #setDatabaseName database name} will
* be overridden with an auto-generated name.
* <p>Subclasses may call this method to force initialization; however,
* this method should only be invoked once.
* <p>After calling this method, {@link #getDataSource()} returns the * <p>After calling this method, {@link #getDataSource()} returns the
* {@link DataSource} providing connectivity to the database. * {@link DataSource} providing connectivity to the database.
*/ */
protected void initDatabase() { protected void initDatabase() {
if (this.generateUniqueDatabaseName) {
setDatabaseName(UUID.randomUUID().toString());
}
// Create the embedded database source first // Create the embedded database source first
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("Creating embedded database '" + this.databaseName + "'"); logger.info("Creating embedded database '" + this.databaseName + "'");

11
spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd

@ -44,6 +44,17 @@
]]></xsd:documentation> ]]></xsd:documentation>
</xsd:annotation> </xsd:annotation>
</xsd:attribute> </xsd:attribute>
<xsd:attribute name="generate-name" type="xsd:string" use="optional" default="false">
<xsd:annotation>
<xsd:documentation>
If set to "true", a pseudo-random unique name will be generated for the embedded
database, overriding any implicit name provided via the 'id' attribute or any
explicit name provided via the 'database-name' attribute.
Note that this is not the bean name but rather the name of the embedded database
as used in the JDBC connection URL for the database.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="type" type="databaseType" default="HSQL"> <xsd:attribute name="type" type="databaseType" default="HSQL">
<xsd:annotation> <xsd:annotation>
<xsd:documentation><![CDATA[ <xsd:documentation><![CDATA[

21
spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java

@ -16,6 +16,8 @@
package org.springframework.jdbc.config; package org.springframework.jdbc.config;
import java.util.function.Predicate;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.junit.Rule; import org.junit.Rule;
@ -74,17 +76,26 @@ public class JdbcNamespaceIntegrationTests {
@Test @Test
public void createWithAnonymousDataSourceAndDefaultDatabaseName() throws Exception { public void createWithAnonymousDataSourceAndDefaultDatabaseName() throws Exception {
assertCorrectSetupForSingleDataSource("jdbc-config-db-name-default-and-anonymous-datasource.xml", assertCorrectSetupForSingleDataSource("jdbc-config-db-name-default-and-anonymous-datasource.xml",
DEFAULT_DATABASE_NAME); (url) -> url.endsWith(DEFAULT_DATABASE_NAME));
} }
@Test @Test
public void createWithImplicitDatabaseName() throws Exception { public void createWithImplicitDatabaseName() throws Exception {
assertCorrectSetupForSingleDataSource("jdbc-config-db-name-implicit.xml", "dataSource"); assertCorrectSetupForSingleDataSource("jdbc-config-db-name-implicit.xml", (url) -> url.endsWith("dataSource"));
} }
@Test @Test
public void createWithExplicitDatabaseName() throws Exception { public void createWithExplicitDatabaseName() throws Exception {
assertCorrectSetupForSingleDataSource("jdbc-config-db-name-explicit.xml", "customDbName"); assertCorrectSetupForSingleDataSource("jdbc-config-db-name-explicit.xml", (url) -> url.endsWith("customDbName"));
}
@Test
public void createWithGeneratedDatabaseName() throws Exception {
Predicate<String> urlPredicate = (url) -> url.startsWith("jdbc:hsqldb:mem:");
urlPredicate.and((url) -> !url.endsWith("dataSource"));
urlPredicate.and((url) -> !url.endsWith("shouldBeOverriddenByGeneratedName"));
assertCorrectSetupForSingleDataSource("jdbc-config-db-name-generated.xml", urlPredicate);
} }
@Test @Test
@ -189,14 +200,14 @@ public class JdbcNamespaceIntegrationTests {
} }
} }
private void assertCorrectSetupForSingleDataSource(String file, String dbName) { private void assertCorrectSetupForSingleDataSource(String file, Predicate<String> urlPredicate) {
ConfigurableApplicationContext context = context(file); ConfigurableApplicationContext context = context(file);
try { try {
DataSource dataSource = context.getBean(DataSource.class); DataSource dataSource = context.getBean(DataSource.class);
assertNumRowsInTestTable(new JdbcTemplate(dataSource), 1); assertNumRowsInTestTable(new JdbcTemplate(dataSource), 1);
assertTrue(dataSource instanceof AbstractDriverBasedDataSource); assertTrue(dataSource instanceof AbstractDriverBasedDataSource);
AbstractDriverBasedDataSource adbDataSource = (AbstractDriverBasedDataSource) dataSource; AbstractDriverBasedDataSource adbDataSource = (AbstractDriverBasedDataSource) dataSource;
assertThat(adbDataSource.getUrl(), containsString(dbName)); assertTrue(urlPredicate.test(adbDataSource.getUrl()));
} }
finally { finally {
context.close(); context.close();

54
spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java

@ -17,10 +17,10 @@
package org.springframework.jdbc.datasource.embedded; package org.springframework.jdbc.datasource.embedded;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.io.ClassRelativeResourceLoader; import org.springframework.core.io.ClassRelativeResourceLoader;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.CannotReadScriptException; import org.springframework.jdbc.datasource.init.CannotReadScriptException;
import org.springframework.jdbc.datasource.init.ScriptStatementFailedException;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.*; import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.*;
@ -160,14 +160,62 @@ public class EmbeddedDatabaseBuilderTests {
}); });
} }
@Test
public void createSameSchemaTwiceWithoutUniqueDbNames() throws Exception {
EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))//
.addScripts("db-schema-without-dropping.sql").build();
try {
new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))//
.addScripts("db-schema-without-dropping.sql").build();
fail("Should have thrown a ScriptStatementFailedException");
}
catch (ScriptStatementFailedException e) {
// expected
}
finally {
db1.shutdown();
}
}
@Test
public void createSameSchemaTwiceWithGeneratedUniqueDbNames() throws Exception {
EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))//
.addScripts("db-schema-without-dropping.sql", "db-test-data.sql")//
.generateUniqueName(true)//
.build();
JdbcTemplate template1 = new JdbcTemplate(db1);
assertNumRowsInTestTable(template1, 1);
template1.update("insert into T_TEST (NAME) values ('Sam')");
assertNumRowsInTestTable(template1, 2);
EmbeddedDatabase db2 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))//
.addScripts("db-schema-without-dropping.sql", "db-test-data.sql")//
.generateUniqueName(true)//
.build();
assertDatabaseCreated(db2);
db1.shutdown();
db2.shutdown();
}
private void doTwice(Runnable test) { private void doTwice(Runnable test) {
test.run(); test.run();
test.run(); test.run();
} }
private void assertNumRowsInTestTable(JdbcTemplate template, int count) {
assertEquals(count, template.queryForObject("select count(*) from T_TEST", Integer.class).intValue());
}
private void assertDatabaseCreated(EmbeddedDatabase db) {
assertNumRowsInTestTable(new JdbcTemplate(db), 1);
}
private void assertDatabaseCreatedAndShutdown(EmbeddedDatabase db) { private void assertDatabaseCreatedAndShutdown(EmbeddedDatabase db) {
JdbcTemplate template = new JdbcTemplate(db); assertDatabaseCreated(db);
assertEquals("Keith", template.queryForObject("select NAME from T_TEST", String.class));
db.shutdown(); db.shutdown();
} }

12
spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns="http://www.springframework.org/schema/jdbc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd">
<embedded-database id="dataSource" database-name="shouldBeOverriddenByGeneratedName" generate-name="true">
<script location="classpath:org/springframework/jdbc/config/db-schema.sql" />
<script location="classpath:org/springframework/jdbc/config/db-test-data.sql" />
</embedded-database>
</beans:beans>

1
spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/embedded/db-schema-without-dropping.sql

@ -0,0 +1 @@
create table T_TEST (NAME varchar(50) not null);
Loading…
Cancel
Save