GH-3 - Implement JDBC based repository

Signed-off-by: Björn Kieling <bkieling@vmware.com>
This commit is contained in:
Dmitry Belyaev
2022-07-19 17:10:48 +02:00
committed by Bjoern Kieling
parent 913ec7bb4e
commit 6a4260471e
17 changed files with 1107 additions and 0 deletions

View File

@@ -16,6 +16,7 @@
<modules> <modules>
<module>spring-modulith-events-core</module> <module>spring-modulith-events-core</module>
<module>spring-modulith-events-jpa</module> <module>spring-modulith-events-jpa</module>
<module>spring-modulith-events-jdbc</module>
<module>spring-modulith-events-jackson</module> <module>spring-modulith-events-jackson</module>
<module>spring-modulith-events-tests</module> <module>spring-modulith-events-tests</module>
<module>spring-modulith-events-starter</module> <module>spring-modulith-events-starter</module>

View File

@@ -0,0 +1,93 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-modulith-events</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Spring Modulith - Events - JDBC-based registry</name>
<artifactId>spring-modulith-events-jdbc</artifactId>
<properties>
<java.version>17</java.version>
<module.name>org.springframework.modulith.events.jdbc</module.name>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-modulith-events-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
</dependency>
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
-->
<!-- Testing -->
<!-- <dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<scope>test</scope>
</dependency>-->
<!--
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
<scope>test</scope>
</dependency>
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2022 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 org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.util.FileCopyUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
/**
* Initializes the DB schema used to store events
*
* @author Dmitry Belyaev, Björn Kieling
*/
public class DatabaseSchemaInitializer implements ResourceLoaderAware, InitializingBean {
private final JdbcTemplate jdbcTemplate;
private ResourceLoader resourceLoader;
@Value("${spring.modulith.events.schema-initialization.enabled:false}")
private boolean initEnabled;
public DatabaseSchemaInitializer(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void afterPropertiesSet() throws MetaDataAccessException {
if (!initEnabled) {
return;
}
DatabaseType databaseType = DatabaseType.fromMetaData(jdbcTemplate.getDataSource());
String databaseName = databaseType.name().toLowerCase();
var schemaDdlResource = resourceLoader.getResource("/schema-" + databaseName + ".sql");
var schemaDdl = asString(schemaDdlResource);
jdbcTemplate.execute(schemaDdl);
}
private String asString(Resource resource) {
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2006-2022 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 org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.util.HashMap;
import java.util.Map;
/**
* Enum representing a database type, such as DB2 or oracle. The type also contains a
* product name, which is expected to be the same as the product name provided by the
* database driver's metadata.
*
* @author Lucas Ward
*/
public enum DatabaseType {
DERBY("Apache Derby"), DB2("DB2"), DB2VSE("DB2VSE"), DB2ZOS("DB2ZOS"), DB2AS400("DB2AS400"),
HSQL("HSQL Database Engine"), SQLSERVER("Microsoft SQL Server"), MYSQL("MySQL"), ORACLE("Oracle"),
POSTGRES("PostgreSQL"), SYBASE("Sybase"), H2("H2"), SQLITE("SQLite"), HANA("HDB");
private static final Map<String, DatabaseType> nameMap;
static {
nameMap = new HashMap<>();
for (DatabaseType type : values()) {
nameMap.put(type.getProductName(), type);
}
}
// A description is necessary due to the nature of database descriptions
// in metadata.
private final String productName;
private DatabaseType(String productName) {
this.productName = productName;
}
public String getProductName() {
return productName;
}
/**
* Static method to obtain a DatabaseType from the provided product name.
* @param productName {@link String} containing the product name.
* @return the {@link DatabaseType} for given product name.
* @throws IllegalArgumentException if none is found.
*/
public static DatabaseType fromProductName(String productName) {
if (productName.equals("MariaDB"))
productName = "MySQL";
if (!nameMap.containsKey(productName)) {
throw new IllegalArgumentException("DatabaseType not found for product name: [" + productName + "]");
}
else {
return nameMap.get(productName);
}
}
/**
* Convenience method that pulls a database product name from the DataSource's
* metadata.
* @param dataSource {@link DataSource} to the database to be used.
* @return {@link DatabaseType} for the {@link DataSource} specified.
* @throws MetaDataAccessException thrown if error occured during Metadata lookup.
*/
public static DatabaseType fromMetaData(DataSource dataSource) throws MetaDataAccessException {
String databaseProductName = JdbcUtils.extractDatabaseMetaData(dataSource,
DatabaseMetaData::getDatabaseProductName);
if (StringUtils.hasText(databaseProductName) && databaseProductName.startsWith("DB2")) {
String databaseProductVersion = JdbcUtils.extractDatabaseMetaData(dataSource,
DatabaseMetaData::getDatabaseProductVersion);
if (databaseProductVersion.startsWith("ARI")) {
databaseProductName = "DB2VSE";
}
else if (databaseProductVersion.startsWith("DSN")) {
databaseProductName = "DB2ZOS";
}
else if (databaseProductName.contains("AS")
&& (databaseProductVersion.startsWith("QSQ") || databaseProductVersion
.substring(databaseProductVersion.indexOf('V')).matches("V\\dR\\d[mM]\\d"))) {
databaseProductName = "DB2AS400";
}
else {
databaseProductName = JdbcUtils.commonDatabaseName(databaseProductName);
}
}
else {
databaseProductName = JdbcUtils.commonDatabaseName(databaseProductName);
}
return fromProductName(databaseProductName);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2022 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 org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.events.config.EventPublicationConfigurationExtension;
/**
* @author Dmitry Belyaev, Björn Kieling
*/
@Configuration(proxyBeanMethods = false)
class JdbcEventPublicationAutoConfiguration implements EventPublicationConfigurationExtension {
@Bean
public JdbcEventPublicationRepository jpaEventPublicationRepository(
JdbcTemplate jdbcTemplate, EventSerializer serializer) {
// TODO Why do we want to instantiate the serializer here and what
// happens if no serializer is available or is not compatible to
// JDBC serialization?
return new JdbcEventPublicationRepository(jdbcTemplate, serializer);
}
@Bean
public DatabaseSchemaInitializer databaseSchemaInitializer(JdbcTemplate jdbcTemplate) {
return new DatabaseSchemaInitializer(jdbcTemplate);
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2022 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 java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.modulith.events.CompletableEventPublication;
import org.springframework.modulith.events.EventPublication;
import org.springframework.modulith.events.EventPublicationRepository;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.events.PublicationTargetIdentifier;
import org.springframework.transaction.annotation.Transactional;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Repository to store {@link EventPublication}s.
*
* @author Dmitry Belyaev, Björn Kieling
*/
@Slf4j
@RequiredArgsConstructor
public class JdbcEventPublicationRepository implements EventPublicationRepository {
private static final String SQL_STATEMENT_INSERT = """
INSERT INTO EVENT_PUBLICATION (ID, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT)
VALUES (?, ?, ?, ?, ?)
""";
private static final String SQL_STATEMENT_FIND_UNCOMPLETED = """
SELECT ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE, SERIALIZED_EVENT
FROM EVENT_PUBLICATION
WHERE COMPLETION_DATE IS NULL
""";
private static final String SQL_STATEMENT_UPDATE = """
UPDATE EVENT_PUBLICATION
SET COMPLETION_DATE = ?
WHERE ID = ?
""";
private static final String SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID = """
SELECT * FROM EVENT_PUBLICATION
WHERE SERIALIZED_EVENT = ? AND LISTENER_ID = ?
ORDER BY PUBLICATION_DATE
""";
private final JdbcTemplate jdbcTemplate;
private final EventSerializer serializer;
@Override
@Transactional
public EventPublication create(EventPublication publication) {
String serializedEvent = serializeEvent(publication.getEvent());
jdbcTemplate.update(SQL_STATEMENT_INSERT, //
UUID.randomUUID(), //
publication.getEvent().getClass().getName(), //
publication.getTargetIdentifier().getValue(), //
publication.getPublicationDate(), //
serializedEvent);
return publication;
}
@Override
@Transactional
public EventPublication updateCompletionDate(CompletableEventPublication publication) {
String serializedEvent = serializeEvent(publication.getEvent());
List<UUID> results = jdbcTemplate.query(SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID,
(rs, rowNum) -> rs.getObject("ID", UUID.class), serializedEvent, publication.getTargetIdentifier().getValue());
if (!results.isEmpty()) {
jdbcTemplate.update(SQL_STATEMENT_UPDATE, publication.getCompletionDate().orElse(null), results.get(0));
}
return publication;
}
@Override
@Transactional(readOnly = true)
public List<EventPublication> findByCompletionDateIsNull() {
return jdbcTemplate.query(SQL_STATEMENT_FIND_UNCOMPLETED, this::mapResultSetToEventPublications);
}
@Override
@Transactional(readOnly = true)
public Optional<EventPublication> findByEventAndTargetIdentifier(Object event,
PublicationTargetIdentifier targetIdentifier) {
String serializedEvent = serializeEvent(event);
List<EventPublication> results = jdbcTemplate.query(SQL_STATEMENT_FIND_BY_EVENT_AND_LISTENER_ID,
this::mapResultSetToEventPublications, serializedEvent, targetIdentifier.getValue());
if (results.isEmpty()) {
return Optional.empty();
} else {
// if there are several events with exactly the same payload we return the oldest one first
return Optional.of(results.get(0));
}
}
private String serializeEvent(Object event) {
return serializer.serialize(event).toString();
}
private List<EventPublication> mapResultSetToEventPublications(ResultSet rs) throws SQLException {
var result = new ArrayList<EventPublication>();
while (rs.next()) {
entityToDomain(rs).ifPresent(result::add);
}
return result;
}
private Optional<EventPublication> entityToDomain(ResultSet rs) throws SQLException {
var id = rs.getObject("ID", UUID.class);
var eventClassName = rs.getString("EVENT_TYPE");
Class<?> eventClass;
try {
eventClass = Class.forName(eventClassName);
} catch (ClassNotFoundException e) {
LOG.warn("Event '{}' of unknown type '{}' found", id, eventClassName);
return Optional.empty();
}
return Optional.of(JdbcEventPublication.builder()
.completionDate(Optional.ofNullable(rs.getTimestamp("COMPLETION_DATE")).map(Timestamp::toInstant).orElse(null))
.eventType(eventClass) //
.listenerId(rs.getString("LISTENER_ID")) //
.publicationDate(rs.getTimestamp("PUBLICATION_DATE").toInstant()) //
.serializedEvent(rs.getString("SERIALIZED_EVENT")) //
.serializer(serializer) //
.build());
}
@EqualsAndHashCode
@Builder
private static class JdbcEventPublication implements CompletableEventPublication {
private final UUID id;
private final Instant publicationDate;
private final String listenerId;
private final String serializedEvent;
private final Class<?> eventType;
private final EventSerializer serializer;
private Instant completionDate;
@Override
public Object getEvent() {
return serializer.deserialize(serializedEvent, eventType);
}
@Override
public PublicationTargetIdentifier getTargetIdentifier() {
return PublicationTargetIdentifier.of(listenerId);
}
@Override
public Instant getPublicationDate() {
return publicationDate;
}
@Override
public Optional<Instant> getCompletionDate() {
return Optional.ofNullable(completionDate);
}
@Override
public boolean isPublicationCompleted() {
return completionDate != null;
}
@Override
public CompletableEventPublication markCompleted() {
this.completionDate = Instant.now();
return this;
}
}
}

View File

@@ -0,0 +1 @@
org.springframework.modulith.events.jdbc.JdbcEventPublicationAutoConfiguration

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS EVENT_PUBLICATION
(
ID UUID NOT NULL,
COMPLETION_DATE TIMESTAMP(9) WITH TIME ZONE,
EVENT_TYPE VARCHAR(512) NOT NULL,
LISTENER_ID VARCHAR(512) NOT NULL,
PUBLICATION_DATE TIMESTAMP(9) WITH TIME ZONE NOT NULL,
SERIALIZED_EVENT VARCHAR(4000) NOT NULL,
PRIMARY KEY (ID)
)

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS EVENT_PUBLICATION
(
ID UUID NOT NULL,
COMPLETION_DATE TIMESTAMP(9),
EVENT_TYPE VARCHAR(512) NOT NULL,
LISTENER_ID VARCHAR(512) NOT NULL,
PUBLICATION_DATE TIMESTAMP(9) NOT NULL,
SERIALIZED_EVENT VARCHAR(4000) NOT NULL,
PRIMARY KEY (ID)
)

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS EVENT_PUBLICATION
(
ID UUID NOT NULL,
COMPLETION_DATE TIMESTAMP(9) WITH TIME ZONE,
EVENT_TYPE VARCHAR(512) NOT NULL,
LISTENER_ID VARCHAR(512) NOT NULL,
PUBLICATION_DATE TIMESTAMP(9) WITH TIME ZONE NOT NULL,
SERIALIZED_EVENT VARCHAR(4000) NOT NULL,
PRIMARY KEY (ID)
)

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2022 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 org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationContext;
import org.springframework.modulith.events.EventPublicationRegistry;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.testapp.TestApplication;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dmitry Belyaev, Björn Kieling
*/
@SpringBootTest(
classes = TestApplication.class,
properties = "spring.modulith.events.schema-initialization.enabled=true"
)
public class JdbcEventPublicationAutoConfigurationIntegrationTests {
@Autowired
private ApplicationContext context;
@MockBean
private EventSerializer serializer;
@Test
void bootstrapsApplicationComponents() {
assertThat(context.getBean(EventPublicationRegistry.class)).isNotNull();
assertThat(context.getBean(JdbcEventPublicationRepository.class)).isNotNull();
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2022 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.testapp;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.modulith.events.jdbc.DatabaseSchemaInitializer;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
/**
* @author Dmitry Belyaev, Björn Kieling
*/
public class DatabaseSchemaInitializerTest {
@Nested
@DataJdbcTest(properties = {
"spring.modulith.events.schema-initialization.enabled=true"
})
@Import(DatabaseSchemaInitializer.class)
class InitializationEnabled {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void shouldCreateDatabaseSchemaOnStartUp() {
Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class);
assertThat(count).isEqualTo(0);
}
}
@Nested
@DataJdbcTest(properties = {
"spring.modulith.events.schema-initialization.enabled=false"
})
@Import(DatabaseSchemaInitializer.class)
class InitializationDisabled {
@SpyBean
private JdbcTemplate jdbcTemplate;
@Test
public void shouldNotCreateDatabaseSchemaOnStartUp() {
Mockito.verify(jdbcTemplate, Mockito.never()).execute(anyString());
}
}
@Nested
@DataJdbcTest
@Import(DatabaseSchemaInitializer.class)
class InitializationDisabledByDefault {
@SpyBean
private JdbcTemplate jdbcTemplate;
@Test
public void shouldNotCreateDatabaseSchemaOnStartUp() {
Mockito.verify(jdbcTemplate, Mockito.never()).execute(anyString());
}
}
@Nested
@DataJdbcTest(properties = {
"spring.modulith.events.schema-initialization.enabled=true"
})
@ActiveProfiles("hsql")
@Import(DatabaseSchemaInitializer.class)
class InitializationUseHsql {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void shouldCreateDatabaseSchemaOnStartUp() {
Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class);
assertThat(count).isEqualTo(0);
}
}
@Nested
@DataJdbcTest(properties = {
"spring.modulith.events.schema-initialization.enabled=true"
})
@ActiveProfiles("h2")
@Import(DatabaseSchemaInitializer.class)
class InitializationUseH2 {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void shouldCreateDatabaseSchemaOnStartUp() {
Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class);
assertThat(count).isEqualTo(0);
}
}
@Nested
@Disabled
@DataJdbcTest(properties = {
"spring.modulith.events.schema-initialization.enabled=true"
})
@ActiveProfiles("postgres")
@Import(DatabaseSchemaInitializer.class)
class InitializationUsePostgres {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void shouldCreateDatabaseSchemaOnStartUp() {
Long count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM EVENT_PUBLICATION", Long.class);
assertThat(count).isEqualTo(0);
}
}
}

View File

@@ -0,0 +1,319 @@
/*
* Copyright 2022 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.testapp;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.modulith.events.CompletableEventPublication;
import org.springframework.modulith.events.EventPublication;
import org.springframework.modulith.events.EventSerializer;
import org.springframework.modulith.events.PublicationTargetIdentifier;
import org.springframework.modulith.events.jdbc.DatabaseSchemaInitializer;
import org.springframework.modulith.events.jdbc.JdbcEventPublicationRepository;
import org.springframework.test.context.ActiveProfiles;
/**
* @author Dmitry Belyaev, Björn Kieling
*/
class JdbcEventPublicationRepositoryIntegrationTests {
private static final PublicationTargetIdentifier TARGET_IDENTIFIER = PublicationTargetIdentifier.of("listener");
private JdbcEventPublicationRepository repository;
private final EventSerializer eventSerializer = mock(EventSerializer.class);
private abstract class TestBase {
@Autowired
protected JdbcTemplate jdbcTemplate;
@BeforeEach
public void setUp() {
repository = new JdbcEventPublicationRepository(jdbcTemplate, eventSerializer);
}
}
@Nested
@DataJdbcTest
@ActiveProfiles("hsql")
@Import(DatabaseSchemaInitializer.class)
class HSQL extends TestBase {
@Nested
class CreateAndUpdate {
@Test
void shouldPersistAndUpdateEventPublication() {
shouldPersistAndUpdateEventPublicationTest();
}
@Test
void shouldUpdateSingleEventPublication() {
shouldUpdateSingleEventPublicationTest();
}
}
@Nested
class FindByCompletionDateIsNull {
@Test
void shouldSilentlyIgnoreNotSerializableEvents() {
shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate);
}
}
@Nested
class FindBySerializedEventAndListenerId {
@Test
void shouldTolerateEmptyResult() {
shouldTolerateEmptyResultTest();
}
@Test
void shouldReturnTheOldestEvent() throws InterruptedException {
shouldReturnTheOldestEventTest();
}
@Test
void shouldSilentlyIgnoreNotSerializableEvents() {
shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate);
}
}
}
@Nested
@DataJdbcTest
@ActiveProfiles("h2")
@Import(DatabaseSchemaInitializer.class)
class H2 extends TestBase {
@Nested
class CreateAndUpdate {
@Test
void shouldPersistAndUpdateEventPublication() {
shouldPersistAndUpdateEventPublicationTest();
}
@Test
void shouldUpdateSingleEventPublication() {
shouldUpdateSingleEventPublicationTest();
}
}
@Nested
class FindByCompletionDateIsNull {
@Test
void shouldSilentlyIgnoreNotSerializableEvents() {
shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate);
}
}
@Nested
class FindBySerializedEventAndListenerId {
@Test
void shouldTolerateEmptyResult() {
shouldTolerateEmptyResultTest();
}
@Test
void shouldReturnTheOldestEvent() throws InterruptedException {
shouldReturnTheOldestEventTest();
}
@Test
void shouldSilentlyIgnoreNotSerializableEvents() {
shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate);
}
}
}
@Nested
@Disabled
@DataJdbcTest
@ActiveProfiles("postgres")
@Import(DatabaseSchemaInitializer.class)
class Postgres extends TestBase {
@Nested
class CreateAndUpdate {
@Test
void shouldPersistAndUpdateEventPublication() {
shouldPersistAndUpdateEventPublicationTest();
}
@Test
void shouldUpdateSingleEventPublication() {
shouldUpdateSingleEventPublicationTest();
}
}
@Nested
class FindByCompletionDateIsNull {
@Test
void shouldSilentlyIgnoreNotSerializableEvents() {
shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate);
}
}
@Nested
class FindBySerializedEventAndListenerId {
@Test
void shouldTolerateEmptyResult() {
shouldTolerateEmptyResultTest();
}
@Test
void shouldReturnTheOldestEvent() throws InterruptedException {
shouldReturnTheOldestEventTest();
}
@Test
void shouldSilentlyIgnoreNotSerializableEvents() {
shouldSilentlyIgnoreNotSerializableEventsTest(jdbcTemplate);
}
}
}
private void shouldPersistAndUpdateEventPublicationTest() {
TestEvent testEvent = new TestEvent("id");
String serializedEvent = "{\"eventId\":\"id\"}";
when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent);
when(eventSerializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
CompletableEventPublication publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
// Store publication
repository.create(publication);
List<EventPublication> eventPublications = repository.findByCompletionDateIsNull();
assertThat(eventPublications).hasSize(1);
assertThat(eventPublications.get(0).getEvent()).isEqualTo(publication.getEvent());
assertThat(eventPublications.get(0).getTargetIdentifier()).isEqualTo(publication.getTargetIdentifier());
assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER))
.isPresent();
// Complete publication
repository.updateCompletionDate(publication.markCompleted());
assertThat(repository.findByCompletionDateIsNull()).isEmpty();
}
private void shouldUpdateSingleEventPublicationTest() {
TestEvent testEvent1 = new TestEvent("id1");
TestEvent testEvent2 = new TestEvent("id2");
String serializedEvent1 = "{\"eventId\":\"id1\"}";
String serializedEvent2 = "{\"eventId\":\"id2\"}";
when(eventSerializer.serialize(testEvent1)).thenReturn(serializedEvent1);
when(eventSerializer.deserialize(serializedEvent1, TestEvent.class)).thenReturn(testEvent1);
when(eventSerializer.serialize(testEvent2)).thenReturn(serializedEvent2);
when(eventSerializer.deserialize(serializedEvent2, TestEvent.class)).thenReturn(testEvent2);
CompletableEventPublication publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
CompletableEventPublication publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
// Store publication
repository.create(publication1);
repository.create(publication2);
// Complete publication
repository.updateCompletionDate(publication2.markCompleted());
List<EventPublication> withCompletionDateNull = repository.findByCompletionDateIsNull();
assertThat(withCompletionDateNull).hasSize(1);
assertThat(withCompletionDateNull.get(0).getEvent()).isEqualTo(testEvent1);
}
private void shouldTolerateEmptyResultTest() {
TestEvent testEvent = new TestEvent("id");
String serializedEvent = "{\"eventId\":\"id\"}";
when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent);
Optional<EventPublication> actual =
repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
assertThat(actual).isEmpty();
}
private void shouldReturnTheOldestEventTest() throws InterruptedException {
TestEvent testEvent = new TestEvent("id");
String serializedEvent = "{\"eventId\":\"id\"}";
when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent);
when(eventSerializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
CompletableEventPublication publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
Thread.sleep(10);
CompletableEventPublication publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
repository.create(publicationNew);
repository.create(publicationOld);
Optional<EventPublication> actual =
repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
assertThat(actual).isNotEmpty();
assertThat(actual.get().getPublicationDate()).isEqualTo(publicationOld.getPublicationDate());
}
private void shouldSilentlyIgnoreNotSerializableEventsTest(JdbcTemplate jdbcTemplate) {
TestEvent testEvent = new TestEvent("id");
String serializedEvent = "{\"eventId\":\"id\"}";
when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent);
when(eventSerializer.deserialize(serializedEvent, TestEvent.class)).thenReturn(testEvent);
CompletableEventPublication publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
// Store publication
repository.create(publication);
jdbcTemplate.update("UPDATE EVENT_PUBLICATION SET EVENT_TYPE='abc'");
Optional<EventPublication> actual =
repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
assertThat(actual).isEmpty();
}
private static final class TestEvent {
private final String eventId;
private TestEvent(String eventId) {
this.eventId = eventId;
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2022 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.testapp;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Dmitry Belyaev, Björn Kieling
*/
@SpringBootApplication
public class TestApplication {
}

View File

@@ -0,0 +1,4 @@
spring.datasource.driverClassName=org.h2.Driver
spring.test.database.replace=NONE
spring.modulith.events.schema-initialization.enabled=true

View File

@@ -0,0 +1,5 @@
spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver
spring.datasource.url=jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1
spring.test.database.replace=NONE
spring.modulith.events.schema-initialization.enabled=true

View File

@@ -0,0 +1,7 @@
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=username
spring.datasource.password=password
spring.test.database.replace=NONE
spring.modulith.events.schema-initialization.enabled=true