From 07ddeac700d10794bb1e298d729d99e5fa67f154 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 8 Jul 2024 23:12:32 +0200 Subject: [PATCH] GH-685 - Polishing. Disable schema support for MySQL as it doesn't know about schemas per database. General polishing. --- .../spring-modulith-events-jdbc/pom.xml | 11 +- .../jdbc/DatabaseSchemaInitializer.java | 43 +++-- .../modulith/events/jdbc/DatabaseType.java | 9 +- .../jdbc/JdbcConfigurationProperties.java | 40 +++- .../jdbc/JdbcEventPublicationRepository.java | 176 +++++++++--------- .../DatabaseSchemaInitializerUnitTests.java | 57 ++++++ ...PublicationRepositoryIntegrationTests.java | 16 +- .../src/test/resources/application.properties | 1 - 8 files changed, 225 insertions(+), 128 deletions(-) create mode 100644 spring-modulith-events/spring-modulith-events-jdbc/src/test/java/org/springframework/modulith/events/jdbc/DatabaseSchemaInitializerUnitTests.java 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