GH-685 - Polishing.

Disable schema support for MySQL as it doesn't know about schemas per database.

General polishing.
This commit is contained in:
Oliver Drotbohm
2024-07-08 23:12:32 +02:00
parent 75b3ff4cac
commit 07ddeac700
8 changed files with 225 additions and 128 deletions

View File

@@ -46,6 +46,13 @@
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
@@ -59,13 +66,13 @@
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>

View File

@@ -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;
}
}

View File

@@ -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;
};
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -1,3 +1,2 @@
spring.main.banner-mode=OFF
spring.modulith.events.jdbc-schema-initialization.enabled=true
spring.test.database.replace=NONE