diff --git a/spring-modulith-events/spring-modulith-events-jdbc/pom.xml b/spring-modulith-events/spring-modulith-events-jdbc/pom.xml
index 9b2f9834..e0812176 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/pom.xml
+++ b/spring-modulith-events/spring-modulith-events-jdbc/pom.xml
@@ -46,6 +46,13 @@
spring-boot-autoconfigure
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
@@ -59,13 +66,13 @@
spring-boot-starter-jdbc
test
-
+
org.springframework
spring-web
test
-
+
jakarta.servlet
jakarta.servlet-api
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java
index 522c87be..d3316306 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializer.java
@@ -21,9 +21,9 @@ import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ResourceLoader;
-import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
-import org.springframework.util.ObjectUtils;
+import org.springframework.util.Assert;
/**
* Initializes the DB schema used to store events
@@ -38,36 +38,44 @@ class DatabaseSchemaInitializer implements InitializingBean {
private final DataSource dataSource;
private final ResourceLoader resourceLoader;
private final DatabaseType databaseType;
- private final JdbcTemplate jdbcTemplate;
+ private final JdbcOperations jdbcOperations;
private final JdbcConfigurationProperties properties;
DatabaseSchemaInitializer(DataSource dataSource, ResourceLoader resourceLoader, DatabaseType databaseType,
- JdbcTemplate jdbcTemplate, JdbcConfigurationProperties properties) {
+ JdbcOperations jdbcOperations, JdbcConfigurationProperties properties) {
+
+ Assert.isTrue(properties.getSchemaInitialization().isEnabled(),
+ "Schema initialization disabled! Initializer should not have been registered!");
this.dataSource = dataSource;
this.resourceLoader = resourceLoader;
this.databaseType = databaseType;
- this.jdbcTemplate = jdbcTemplate;
+ this.jdbcOperations = jdbcOperations;
this.properties = properties;
+
+ properties.verify(databaseType);
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
+ */
@Override
public void afterPropertiesSet() throws Exception {
- String initialSchema;
try (Connection connection = dataSource.getConnection()) {
- initialSchema = connection.getSchema();
+ var initialSchema = connection.getSchema();
+ var schemaName = properties.getSchema();
- String schemaName = properties.getSchema();
- if (!ObjectUtils.isEmpty(schemaName)) { // A schema name has been specified.
+ if (schemaName != null && !schemaName.isEmpty()) { // A schema name has been specified.
- if (eventPublicationTableExists(jdbcTemplate, schemaName)) {
+ if (eventPublicationTableExists(schemaName)) {
return;
}
- jdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);
- jdbcTemplate.execute(databaseType.sqlStatementSetSchema(schemaName));
+ jdbcOperations.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);
+ jdbcOperations.execute(databaseType.getSetSchemaSql(schemaName));
}
var locator = new DatabaseSchemaLocator(resourceLoader);
@@ -75,18 +83,21 @@ class DatabaseSchemaInitializer implements InitializingBean {
// Return to the initial schema.
if (initialSchema != null) {
- jdbcTemplate.execute(databaseType.sqlStatementSetSchema(initialSchema));
+ jdbcOperations.execute(databaseType.getSetSchemaSql(initialSchema));
}
}
}
- private boolean eventPublicationTableExists(JdbcTemplate jdbcTemplate, String schema) {
- String query = """
+ private boolean eventPublicationTableExists(String schema) {
+
+ var query = """
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = ? AND table_name = 'EVENT_PUBLICATION'
""";
- Integer count = jdbcTemplate.queryForObject(query, Integer.class, schema);
+
+ var count = jdbcOperations.queryForObject(query, Integer.class, schema);
+
return count != null && count > 0;
}
}
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java
index 80e0c4f6..35f572cf 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/DatabaseType.java
@@ -47,6 +47,8 @@ enum DatabaseType {
POSTGRES("postgresql", "PostgreSQL");
+ static final String SCHEMA_NOT_SUPPORTED = "Setting the schema name not supported on MySQL!";
+
static DatabaseType from(String productName) {
return Arrays.stream(DatabaseType.values())
@@ -77,9 +79,12 @@ enum DatabaseType {
return "/schema-" + value + ".sql";
}
- String sqlStatementSetSchema(String schema) {
+ String getSetSchemaSql(String schema) {
+
return switch (this) {
- case MYSQL, H2, HSQLDB -> "SET SCHEMA " + schema;
+
+ case MYSQL -> throw new IllegalArgumentException(SCHEMA_NOT_SUPPORTED);
+ case H2, HSQLDB -> "SET SCHEMA " + schema;
case POSTGRES -> "SET search_path TO " + schema;
};
}
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcConfigurationProperties.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcConfigurationProperties.java
index e76911b9..d268b3e0 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcConfigurationProperties.java
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcConfigurationProperties.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 the original author or authors.
+ * Copyright 2024 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.
@@ -24,31 +24,32 @@ import org.springframework.lang.Nullable;
* Configuration properties for JDBC.
*
* @author Raed Ben Hamouda
+ * @author Oliver Drotbohm
*/
@ConfigurationProperties(prefix = "spring.modulith.events.jdbc")
class JdbcConfigurationProperties {
- private final boolean schemaInitializationEnabled;
+ private final SchemaInitialization schemaInitialization;
private final String schema;
/**
* Creates a new {@link JdbcConfigurationProperties} instance.
*
- * @param schemaInitializationEnabled whether to initialize the JDBC event publication schema. Defaults to {@literal false}.
+ * @param schemaInitialization whether to initialize the JDBC event publication schema. Defaults to {@literal false}.
* @param schema the schema name of event publication table, can be {@literal null}.
*/
@ConstructorBinding
- JdbcConfigurationProperties(@DefaultValue("false") boolean schemaInitializationEnabled, @Nullable String schema) {
+ JdbcConfigurationProperties(SchemaInitialization schemaInitialization, @Nullable String schema) {
- this.schemaInitializationEnabled = schemaInitializationEnabled;
+ this.schemaInitialization = schemaInitialization;
this.schema = schema;
}
/**
* Whether to initialize the JDBC event publication schema.
*/
- public boolean isSchemaInitializationEnabled() {
- return schemaInitializationEnabled;
+ public SchemaInitialization getSchemaInitialization() {
+ return schemaInitialization;
}
/**
@@ -56,7 +57,32 @@ class JdbcConfigurationProperties {
*
* @return can be {@literal null}.
*/
+ @Nullable
public String getSchema() {
return schema;
}
+
+ void verify(DatabaseType databaseType) {
+
+ if (schema != null && databaseType.equals(DatabaseType.MYSQL)) {
+ throw new IllegalStateException(DatabaseType.SCHEMA_NOT_SUPPORTED);
+ }
+ }
+
+ static class SchemaInitialization {
+
+ private final boolean enabled;
+
+ /**
+ * @param enabled
+ */
+ @ConstructorBinding
+ SchemaInitialization(@DefaultValue("false") boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ boolean isEnabled() {
+ return enabled;
+ }
+ }
}
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java
index 2ff7d84d..1b58dac6 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/main/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepository.java
@@ -54,6 +54,73 @@ class JdbcEventPublicationRepository implements EventPublicationRepository, Bean
private static final Logger LOGGER = LoggerFactory.getLogger(JdbcEventPublicationRepository.class);
+ private static final String SQL_STATEMENT_INSERT = """
+ INSERT INTO %s (ID, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT)
+ VALUES (?, ?, ?, ?, ?)
+ """;
+
+ private static final String SQL_STATEMENT_FIND_COMPLETED = """
+ SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
+ FROM %s
+ WHERE COMPLETION_DATE IS NOT NULL
+ ORDER BY PUBLICATION_DATE ASC
+ """;
+
+ private static final String SQL_STATEMENT_FIND_UNCOMPLETED = """
+ SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
+ FROM %s
+ WHERE COMPLETION_DATE IS NULL
+ ORDER BY PUBLICATION_DATE ASC
+ """;
+
+ private static final String SQL_STATEMENT_FIND_UNCOMPLETED_BEFORE = """
+ SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
+ FROM %s
+ WHERE
+ COMPLETION_DATE IS NULL
+ AND PUBLICATION_DATE < ?
+ ORDER BY PUBLICATION_DATE ASC
+ """;
+
+ private static final String SQL_STATEMENT_UPDATE_BY_EVENT_AND_LISTENER_ID = """
+ UPDATE %s
+ SET COMPLETION_DATE = ?
+ WHERE
+ LISTENER_ID = ?
+ AND SERIALIZED_EVENT = ?
+ """;
+
+ private static final String SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID = """
+ SELECT *
+ FROM %s
+ WHERE
+ SERIALIZED_EVENT = ?
+ AND LISTENER_ID = ?
+ AND COMPLETION_DATE IS NULL
+ ORDER BY PUBLICATION_DATE
+ """;
+
+ private static final String SQL_STATEMENT_DELETE = """
+ DELETE
+ FROM %s
+ WHERE
+ ID IN
+ """;
+
+ private static final String SQL_STATEMENT_DELETE_UNCOMPLETED = """
+ DELETE
+ FROM %s
+ WHERE
+ COMPLETION_DATE IS NOT NULL
+ """;
+
+ private static final String SQL_STATEMENT_DELETE_UNCOMPLETED_BEFORE = """
+ DELETE
+ FROM %s
+ WHERE
+ COMPLETION_DATE < ?
+ """;
+
private static final int DELETE_BATCH_SIZE = 100;
private final JdbcOperations operations;
@@ -61,15 +128,15 @@ class JdbcEventPublicationRepository implements EventPublicationRepository, Bean
private final DatabaseType databaseType;
private ClassLoader classLoader;
- private String sqlStatementInsert;
- private String sqlStatementFindCompleted;
- private String sqlStatementFindUncompleted;
- private String sqlStatementFindUncompletedBefore;
- private String sqlStatementUpdateByEventAndListenerId;
- private String sqlStatementFindByEventAndListenerId;
- private String sqlStatementDelete;
- private String sqlStatementDeleteUncompleted;
- private String sqlStatementDeleteUncompletedBefore;
+ private final String sqlStatementInsert,
+ sqlStatementFindCompleted,
+ sqlStatementFindUncompleted,
+ sqlStatementFindUncompletedBefore,
+ sqlStatementUpdateByEventAndListenerId,
+ sqlStatementFindByEventAndListenerId,
+ sqlStatementDelete,
+ sqlStatementDeleteUncompleted,
+ sqlStatementDeleteUncompletedBefore;
/**
* Creates a new {@link JdbcEventPublicationRepository} for the given {@link JdbcOperations}, {@link EventSerializer},
@@ -92,7 +159,18 @@ class JdbcEventPublicationRepository implements EventPublicationRepository, Bean
this.serializer = serializer;
this.databaseType = databaseType;
- prepareSqlStatements(properties.getSchema());
+ var schema = properties.getSchema();
+ var table = ObjectUtils.isEmpty(schema) ? "EVENT_PUBLICATION" : schema + ".EVENT_PUBLICATION";
+
+ this.sqlStatementInsert = SQL_STATEMENT_INSERT.formatted(table);
+ this.sqlStatementFindCompleted = SQL_STATEMENT_FIND_COMPLETED.formatted(table);
+ this.sqlStatementFindUncompleted = SQL_STATEMENT_FIND_UNCOMPLETED.formatted(table);
+ this.sqlStatementFindUncompletedBefore = SQL_STATEMENT_FIND_UNCOMPLETED_BEFORE.formatted(table);
+ this.sqlStatementUpdateByEventAndListenerId = SQL_STATEMENT_UPDATE_BY_EVENT_AND_LISTENER_ID.formatted(table);
+ this.sqlStatementFindByEventAndListenerId = SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID.formatted(table);
+ this.sqlStatementDelete = SQL_STATEMENT_DELETE.formatted(table);
+ this.sqlStatementDeleteUncompleted = SQL_STATEMENT_DELETE_UNCOMPLETED.formatted(table);
+ this.sqlStatementDeleteUncompletedBefore = SQL_STATEMENT_DELETE_UNCOMPLETED_BEFORE.formatted(table);
}
/*
@@ -222,84 +300,6 @@ class JdbcEventPublicationRepository implements EventPublicationRepository, Bean
operations.update(sqlStatementDeleteUncompletedBefore, Timestamp.from(instant));
}
- /**
- * Prepares SQL statements based on the provided schema name.
- *
- * @param schema can be {@literal null}.
- */
- private void prepareSqlStatements(String schema) {
-
- String tableName = ObjectUtils.isEmpty(schema) ? "EVENT_PUBLICATION" : schema + ".EVENT_PUBLICATION";
-
-
- sqlStatementInsert = """
- INSERT INTO %s (ID, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT)
- VALUES (?, ?, ?, ?, ?)
- """.formatted(tableName);
-
- sqlStatementFindCompleted = """
- SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
- FROM %s
- WHERE COMPLETION_DATE IS NOT NULL
- ORDER BY PUBLICATION_DATE ASC
- """.formatted(tableName);
-
- sqlStatementFindUncompleted = """
- SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
- FROM %s
- WHERE COMPLETION_DATE IS NULL
- ORDER BY PUBLICATION_DATE ASC
- """.formatted(tableName);
-
- sqlStatementFindUncompletedBefore = """
- SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
- FROM %s
- WHERE
- COMPLETION_DATE IS NULL
- AND PUBLICATION_DATE < ?
- ORDER BY PUBLICATION_DATE ASC
- """.formatted(tableName);
-
- sqlStatementUpdateByEventAndListenerId = """
- UPDATE %s
- SET COMPLETION_DATE = ?
- WHERE
- LISTENER_ID = ?
- AND SERIALIZED_EVENT = ?
- """.formatted(tableName);
-
- sqlStatementFindByEventAndListenerId = """
- SELECT *
- FROM %s
- WHERE
- SERIALIZED_EVENT = ?
- AND LISTENER_ID = ?
- AND COMPLETION_DATE IS NULL
- ORDER BY PUBLICATION_DATE
- """.formatted(tableName);
-
- sqlStatementDelete = """
- DELETE
- FROM %s
- WHERE
- ID IN
- """.formatted(tableName);
-
- sqlStatementDeleteUncompleted = """
- DELETE
- FROM %s
- WHERE
- COMPLETION_DATE IS NOT NULL
- """.formatted(tableName);
-
- sqlStatementDeleteUncompletedBefore = """
- DELETE
- FROM %s
- WHERE
- COMPLETION_DATE < ?
- """.formatted(tableName);
- }
-
private String serializeEvent(Object event) {
return serializer.serialize(event).toString();
}
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerUnitTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerUnitTests.java
new file mode 100644
index 00000000..e0a95cf6
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerUnitTests.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 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.modulith.events.jdbc;
+
+import static org.assertj.core.api.Assertions.*;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.modulith.events.jdbc.JdbcConfigurationProperties.SchemaInitialization;
+
+/**
+ * Unit tests for {@link DatabaseSchemaInitializer}.
+ *
+ * @author Oliver Drotbohm
+ */
+@ExtendWith(MockitoExtension.class)
+class DatabaseSchemaInitializerUnitTests {
+
+ @Mock DataSource dataSource;
+ @Mock ResourceLoader resourceLoader;
+ @Mock JdbcTemplate jdbcTemplate;
+
+ // GH-685
+ @ParameterizedTest
+ @ValueSource(strings = { "", "test" })
+ void rejectsExplicitSchemaNameForMySql(String schema) {
+ assertThatIllegalStateException().isThrownBy(() -> createInitializer(withSchema(schema)));
+ }
+
+ private DatabaseSchemaInitializer createInitializer(JdbcConfigurationProperties properties) {
+ return new DatabaseSchemaInitializer(dataSource, resourceLoader, DatabaseType.MYSQL, jdbcTemplate, properties);
+ }
+
+ private static JdbcConfigurationProperties withSchema(String schema) {
+ return new JdbcConfigurationProperties(new SchemaInitialization(true), schema);
+ }
+}
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java
index 810ace6b..e441c6b6 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/JdbcEventPublicationRepositoryIntegrationTests.java
@@ -334,8 +334,8 @@ class JdbcEventPublicationRepositoryIntegrationTests {
}
@Nested
- @JdbcTest(properties = {"spring.modulith.events.jdbc.schema-initialization.enabled=true",
- "spring.modulith.events.jdbc.schema=test"})
+ @JdbcTest(properties = { "spring.modulith.events.jdbc.schema-initialization.enabled=true",
+ "spring.modulith.events.jdbc.schema=test" })
static class WithDefinedSchemaName extends TestBase {
@Override
@@ -345,8 +345,8 @@ class JdbcEventPublicationRepositoryIntegrationTests {
}
@Nested
- @JdbcTest(properties = {"spring.modulith.events.jdbc.schema-initialization.enabled=true",
- "spring.modulith.events.jdbc.schema="})
+ @JdbcTest(properties = { "spring.modulith.events.jdbc.schema-initialization.enabled=true",
+ "spring.modulith.events.jdbc.schema=" })
static class WithEmptySchemaName extends TestBase {
@Override
@@ -405,14 +405,6 @@ class JdbcEventPublicationRepositoryIntegrationTests {
@ActiveProfiles("mysql")
class MysqlWithNoDefinedSchemaName extends WithNoDefinedSchemaName {}
- @Nested
- @ActiveProfiles("mysql")
- class MysqlWithDefinedSchemaName extends WithDefinedSchemaName {}
-
- @Nested
- @ActiveProfiles("mysql")
- class MysqlWithEmptySchemaName extends WithEmptySchemaName {}
-
@Value
private static final class TestEvent {
String eventId;
diff --git a/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application.properties b/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application.properties
index eb5f2088..56a60ee9 100644
--- a/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application.properties
+++ b/spring-modulith-events/spring-modulith-events-jdbc/src/test/resources/application.properties
@@ -1,3 +1,2 @@
spring.main.banner-mode=OFF
-spring.modulith.events.jdbc-schema-initialization.enabled=true
spring.test.database.replace=NONE