From c0fbe0ae5adcdfc0c99fbba4118493e3f998486d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 21 Mar 2015 00:21:59 +0100 Subject: [PATCH] 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 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 , 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() - Issue: SPR-8849 --- .../EmbeddedDatabaseBeanDefinitionParser.java | 16 ++++- .../datasource/embedded/EmbeddedDatabase.java | 6 +- .../embedded/EmbeddedDatabaseBuilder.java | 22 +++++- .../embedded/EmbeddedDatabaseFactory.java | 67 +++++++++++++------ .../jdbc/config/spring-jdbc-4.2.xsd | 11 +++ .../config/JdbcNamespaceIntegrationTests.java | 21 ++++-- .../EmbeddedDatabaseBuilderTests.java | 54 ++++++++++++++- .../config/jdbc-config-db-name-generated.xml | 12 ++++ .../embedded/db-schema-without-dropping.sql | 1 + 9 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/embedded/db-schema-without-dropping.sql diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java index 5093a52732..5f7abbe080 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java +++ b/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"; + /** + * Constant for the "generate-name" attribute. + */ + static final String GENERATE_NAME_ATTRIBUTE = "generate-name"; + @Override protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(EmbeddedDatabaseFactoryBean.class); + setGenerateUniqueDatabaseNameFlag(element, builder); setDatabaseName(element, builder); setDatabaseType(element, builder); DatabasePopulatorConfigUtils.setDatabasePopulator(element, builder); @@ -64,6 +70,13 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser 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) { // 1) Check for an explicit database name String name = element.getAttribute(DB_NAME_ATTRIBUTE); @@ -76,8 +89,7 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser if (StringUtils.hasText(name)) { builder.addPropertyValue("databaseName", name); } - - // 3) Let EmbeddedDatabaseFactory set the default "testdb" name + // else, let EmbeddedDatabaseFactory use the default "testdb" name } private void setDatabaseType(Element element, BeanDefinitionBuilder builder) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java index 6c5ad88f82..e0c340feda 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import javax.sql.DataSource; * *

An {@code EmbeddedDatabase} is also a {@link DataSource} and adds a * {@link #shutdown} operation so that the embedded database instance can be - * shutdown. + * shut down gracefully. * * @author Keith Donald * @author Sam Brannen @@ -32,7 +32,7 @@ import javax.sql.DataSource; public interface EmbeddedDatabase extends DataSource { /** - * Shutdown this embedded database. + * Shut down this embedded database. */ void shutdown(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java index 8e94ae3b52..13f66dcd5b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java +++ b/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"); * you may not use this file except in compliance with the License. @@ -78,12 +78,32 @@ public class EmbeddedDatabaseBuilder { this.resourceLoader = resourceLoader; } + /** + * Specify whether a unique ID should be generated and used as the database name. + *

If the configuration for this builder is reused across multiple + * application contexts within a single JVM, this flag should be enabled + * (i.e., set to {@code true}) in order to ensure that each application context + * gets its own embedded database. + *

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. *

Defaults to {@link EmbeddedDatabaseFactory#DEFAULT_DATABASE_NAME} if * not called. + *

Will be overridden if the {@code generateUniqueName} flag has been + * set to {@code true}. * @param databaseName the name of the embedded database to build * @return {@code this}, to facilitate method chaining + * @see #generateUniqueName */ public EmbeddedDatabaseBuilder setName(String databaseName) { this.databaseFactory.setDatabaseName(databaseName); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index 9ce946ee76..0b4f43c035 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/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"); * 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.sql.Connection; import java.sql.SQLException; +import java.util.UUID; import java.util.logging.Logger; + import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -30,25 +32,28 @@ import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; import org.springframework.util.Assert; /** - * Factory for creating {@link EmbeddedDatabase} instances. + * Factory for creating an {@link EmbeddedDatabase} instance. * - *

Callers are guaranteed that a returned database has been fully initialized - * and populated. + *

Callers are guaranteed that the returned database has been fully + * initialized and populated. * - *

Can be configured: + *

The factory can be configured as follows: *

    - *
  • Call {@link #setDatabaseName(String)} to change the name of the database. - *
  • Call {@link #setDatabaseType(EmbeddedDatabaseType)} to set the database - * type if you wish to use one of the supported types. - *
  • Call {@link #setDatabaseConfigurer(EmbeddedDatabaseConfigurer)} to - * configure support for your own embedded database type. - *
  • Call {@link #setDatabasePopulator(DatabasePopulator)} to change the - * algorithm used to populate the database. - *
  • Call {@link #setDataSourceFactory(DataSourceFactory)} to change the type - * of {@link DataSource} used to connect to the database. + *
  • Call {@link #generateUniqueDatabaseName} to set a unique, random name + * for the database. + *
  • Call {@link #setDatabaseName} to set an explicit name for the database. + *
  • Call {@link #setDatabaseType} to set the database type if you wish to + * use one of the supported types. + *
  • Call {@link #setDatabaseConfigurer} to configure support for your own + * embedded database type. + *
  • Call {@link #setDatabasePopulator} to change the algorithm used to + * populate the database. + *
  • Call {@link #setDataSourceFactory} to change the type of + * {@link DataSource} used to connect to the database. *
* - *

Call {@link #getDatabase()} to get the {@link EmbeddedDatabase} instance. + *

After configuring the factory, call {@link #getDatabase()} to obtain + * a reference to the {@link EmbeddedDatabase} instance. * * @author Keith Donald * @author Juergen Hoeller @@ -62,9 +67,10 @@ public class EmbeddedDatabaseFactory { */ public static final String DEFAULT_DATABASE_NAME = "testdb"; - private static final Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class); + private boolean generateUniqueDatabaseName = false; + private String databaseName = DEFAULT_DATABASE_NAME; private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory(); @@ -76,10 +82,25 @@ public class EmbeddedDatabaseFactory { 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. + *

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. *

Defaults to {@value #DEFAULT_DATABASE_NAME}. + *

Will be overridden if the {@code generateUniqueDatabaseName} flag + * has been set to {@code true}. * @param databaseName name of the embedded database + * @see #setGenerateUniqueDatabaseName */ public void setDatabaseName(String databaseName) { 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}. */ public EmbeddedDatabase getDatabase() { @@ -136,12 +157,20 @@ public class EmbeddedDatabaseFactory { /** - * Hook to initialize the embedded database. Subclasses may call this method - * to force initialization. + * Hook to initialize the embedded database. + *

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. + *

Subclasses may call this method to force initialization; however, + * this method should only be invoked once. *

After calling this method, {@link #getDataSource()} returns the * {@link DataSource} providing connectivity to the database. */ protected void initDatabase() { + if (this.generateUniqueDatabaseName) { + setDatabaseName(UUID.randomUUID().toString()); + } + // Create the embedded database source first if (logger.isInfoEnabled()) { logger.info("Creating embedded database '" + this.databaseName + "'"); diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd index c307361c68..c5f03511f4 100644 --- a/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd @@ -44,6 +44,17 @@ ]]> + + + + 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. + + + url.endsWith(DEFAULT_DATABASE_NAME)); } @Test 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 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 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 @@ -189,14 +200,14 @@ public class JdbcNamespaceIntegrationTests { } } - private void assertCorrectSetupForSingleDataSource(String file, String dbName) { + private void assertCorrectSetupForSingleDataSource(String file, Predicate urlPredicate) { ConfigurableApplicationContext context = context(file); try { DataSource dataSource = context.getBean(DataSource.class); assertNumRowsInTestTable(new JdbcTemplate(dataSource), 1); assertTrue(dataSource instanceof AbstractDriverBasedDataSource); AbstractDriverBasedDataSource adbDataSource = (AbstractDriverBasedDataSource) dataSource; - assertThat(adbDataSource.getUrl(), containsString(dbName)); + assertTrue(urlPredicate.test(adbDataSource.getUrl())); } finally { context.close(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java index 3d4300648e..3c0dea20d6 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -17,10 +17,10 @@ package org.springframework.jdbc.datasource.embedded; import org.junit.Test; - import org.springframework.core.io.ClassRelativeResourceLoader; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.init.CannotReadScriptException; +import org.springframework.jdbc.datasource.init.ScriptStatementFailedException; import static org.junit.Assert.*; 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) { 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) { - JdbcTemplate template = new JdbcTemplate(db); - assertEquals("Keith", template.queryForObject("select NAME from T_TEST", String.class)); + assertDatabaseCreated(db); db.shutdown(); } diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml new file mode 100644 index 0000000000..7a146db879 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml @@ -0,0 +1,12 @@ + + + + +