Quote CONDITION whenever necessary for JDBC

SO: https://stackoverflow.com/questions/70286480/how-can-spring-integration-5-5-x-use-mysql-as-message-store

Turns out the condition word is reserved in many SQL DB vendors, e.g.:
https://dev.mysql.com/doc/refman/8.0/en/keywords.html

* Fix SQL scripts for quoting `CONDITION` column name for
those vendors which have it as a reserved word
* Fix `JdbcMessageStore` to have a `CONDITION` quoted to `""` by default and
replaced to "`" for MySQL
* Add `MySqlContainerTest` to test against MySQL Docker container
* Looks like quoted identifiers work well even if they are not reserved words in the SQL vendor
This commit is contained in:
Artem Bilan
2021-12-10 14:21:02 -05:00
committed by Gary Russell
parent 3c32daa25a
commit b424cbe171
12 changed files with 180 additions and 85 deletions

View File

@@ -86,7 +86,7 @@ ext {
micrometerVersion = '1.7.6'
mockitoVersion = '3.12.4'
mongoDriverVersion = '4.3.2'
mysqlVersion = '8.0.26'
mysqlVersion = '8.0.27'
pahoMqttClientVersion = '1.2.5'
postgresVersion = '42.2.23'
r2dbch2Version='0.8.4.RELEASE'
@@ -105,6 +105,7 @@ ext {
springSecurityVersion = project.hasProperty('springSecurityVersion') ? project.springSecurityVersion : '5.5.3'
springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.13'
springWsVersion = '3.1.1'
testcontainersVersion = '1.16.2'
tomcatVersion = '9.0.52'
xmlUnitVersion = '2.8.2'
xstreamVersion = '1.4.17'
@@ -255,7 +256,7 @@ configure(javaProjects) { subproject ->
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.testcontainers:junit-jupiter:1.16.0'
testImplementation "org.testcontainers:junit-jupiter:$testcontainersVersion"
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
@@ -607,6 +608,7 @@ project('spring-integration-jdbc') {
testImplementation "org.postgresql:postgresql:$postgresVersion"
testImplementation "mysql:mysql-connector-java:$mysqlVersion"
testImplementation "org.apache.commons:commons-dbcp2:$commonsDbcp2Version"
testImplementation "org.testcontainers:mysql:$testcontainersVersion"
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind'
}

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.jdbc.store;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
@@ -47,6 +48,9 @@ import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.support.JdbcAccessor;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.jmx.export.annotation.ManagedAttribute;
@@ -87,7 +91,7 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
"(GROUP_KEY, REGION, COMPLETE, LAST_RELEASED_SEQUENCE, CREATED_DATE, UPDATED_DATE)"
+ " values (?, ?, 0, 0, ?, ?)"),
UPDATE_MESSAGE_GROUP("UPDATE %PREFIX%MESSAGE_GROUP set UPDATED_DATE=?, CONDITION=? " +
UPDATE_MESSAGE_GROUP("UPDATE %PREFIX%MESSAGE_GROUP set UPDATED_DATE=?, \"CONDITION\"=? " +
"where GROUP_KEY=? and REGION=?"),
REMOVE_MESSAGE_FROM_GROUP("DELETE from %PREFIX%GROUP_TO_MESSAGE where GROUP_KEY=? and MESSAGE_ID=? and " +
@@ -117,7 +121,7 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
"and %PREFIX%GROUP_TO_MESSAGE.GROUP_KEY = ? " +
"and m.REGION = ?)"),
GET_GROUP_INFO("SELECT COMPLETE, LAST_RELEASED_SEQUENCE, CREATED_DATE, UPDATED_DATE, CONDITION" +
GET_GROUP_INFO("SELECT COMPLETE, LAST_RELEASED_SEQUENCE, CREATED_DATE, UPDATED_DATE, \"CONDITION\"" +
" from %PREFIX%MESSAGE_GROUP where GROUP_KEY=? and REGION=?"),
GET_MESSAGE("SELECT MESSAGE_ID, CREATED_DATE, MESSAGE_BYTES from %PREFIX%MESSAGE where MESSAGE_ID=? and " +
@@ -165,6 +169,8 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
private final JdbcOperations jdbcTemplate;
private final String vendorName;
private final Map<Query, String> queryCache = new HashMap<>();
private String region = "DEFAULT";
@@ -195,6 +201,14 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
this.jdbcTemplate = jdbcOperations;
this.deserializer = new AllowListDeserializingConverter();
this.serializer = new SerializingConverter();
try {
this.vendorName =
JdbcUtils.extractDatabaseMetaData(((JdbcAccessor) jdbcOperations).getDataSource(), // NOSONAR
DatabaseMetaData::getDatabaseProductName);
}
catch (MetaDataAccessException ex) {
throw new IllegalStateException("Cannot extract database vendor name", ex);
}
}
/**
@@ -549,7 +563,7 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
public Iterator<MessageGroup> iterator() {
final Iterator<String> iterator = this.jdbcTemplate.query(getQuery(Query.LIST_GROUP_KEYS),
new SingleColumnRowMapper<String>(), this.region)
new SingleColumnRowMapper<String>(), this.region)
.iterator();
return new Iterator<MessageGroup>() {
@@ -580,14 +594,16 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa
* @return a transformed query with replacements
*/
protected String getQuery(Query base) {
String query = this.queryCache.get(base);
return this.queryCache.computeIfAbsent(base,
query -> {
String parsedSql = StringUtils.replace(query.getSql(), "%PREFIX%", this.tablePrefix);
if ((Query.GET_GROUP_INFO.equals(base) || Query.UPDATE_MESSAGE_GROUP.equals(base))
&& this.vendorName.equals("MySQL")) {
if (query == null) {
query = StringUtils.replace(base.getSql(), "%PREFIX%", this.tablePrefix);
this.queryCache.put(base, query);
}
return query;
parsedSql = parsedSql.replaceFirst("\"(CONDITION)\"", "`$1`");
}
return parsedSql;
});
}
/**

View File

@@ -18,7 +18,7 @@ CREATE TABLE INT_GROUP_TO_MESSAGE (
CREATE TABLE INT_MESSAGE_GROUP (
GROUP_KEY CHAR(36) NOT NULL,
REGION VARCHAR(100) NOT NULL,
CONDITION VARCHAR(255),
"CONDITION" VARCHAR(255),
COMPLETE BIGINT,
LAST_RELEASED_SEQUENCE BIGINT,
CREATED_DATE TIMESTAMP NOT NULL,

View File

@@ -18,7 +18,7 @@ CREATE TABLE INT_GROUP_TO_MESSAGE (
CREATE TABLE INT_MESSAGE_GROUP (
GROUP_KEY CHAR(36) NOT NULL,
REGION VARCHAR(100) NOT NULL,
CONDITION VARCHAR(255),
"CONDITION" VARCHAR(255),
COMPLETE BIGINT,
LAST_RELEASED_SEQUENCE BIGINT,
CREATED_DATE TIMESTAMP NOT NULL,

View File

@@ -18,7 +18,7 @@ CREATE TABLE INT_GROUP_TO_MESSAGE (
CREATE TABLE INT_MESSAGE_GROUP (
GROUP_KEY CHAR(36) NOT NULL,
REGION VARCHAR(100) NOT NULL,
CONDITION VARCHAR(255),
`CONDITION` VARCHAR(255),
COMPLETE BIGINT,
LAST_RELEASED_SEQUENCE BIGINT,
CREATED_DATE DATETIME(6) NOT NULL,

View File

@@ -18,7 +18,7 @@ CREATE TABLE INT_GROUP_TO_MESSAGE (
CREATE TABLE INT_MESSAGE_GROUP (
GROUP_KEY CHAR(36) NOT NULL,
REGION VARCHAR(100) NOT NULL,
CONDITION VARCHAR(255),
"CONDITION" VARCHAR(255),
COMPLETE BIGINT,
LAST_RELEASED_SEQUENCE BIGINT,
CREATED_DATE TIMESTAMP NOT NULL,

View File

@@ -18,7 +18,7 @@ CREATE TABLE INT_GROUP_TO_MESSAGE (
CREATE TABLE INT_MESSAGE_GROUP (
GROUP_KEY CHAR(36) NOT NULL,
REGION VARCHAR(100) NOT NULL,
CONDITION VARCHAR(255),
"CONDITION" VARCHAR(255),
COMPLETE BIGINT,
LAST_RELEASED_SEQUENCE BIGINT,
CREATED_DATE DATETIME NOT NULL,

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2021 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.integration.jdbc.mysql;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.integration.test.util.TestUtils;
/**
* The base contract for JUnit tests based on the container for MqSQL.
*
* @author Artem Bilan
*
* @since 5.5.7
*/
@Testcontainers(disabledWithoutDocker = true)
public interface MySqlContainerTest {
@Container
MySQLContainer<?> MY_SQL_CONTAINER =
new MySQLContainer<>(TestUtils.dockerRegistryFromEnv() + "mysql:latest")
.withReuse(true);
static String getDriverClassName() {
return MY_SQL_CONTAINER.getDriverClassName();
}
static String getJdbcUrl() {
return MY_SQL_CONTAINER.getJdbcUrl();
}
static String getUsername() {
return MY_SQL_CONTAINER.getUsername();
}
static String getPassword() {
return MY_SQL_CONTAINER.getPassword();
}
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/int30"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="maxActive" value="10"/>
<property name="defaultAutoCommit" value="false"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>

View File

@@ -25,59 +25,48 @@ import java.util.UUID;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.jdbc.store.JdbcMessageStore;
import org.springframework.integration.jdbc.store.JdbcMessageStoreTests;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.predicate.MessagePredicate;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Based on the test for Derby:
*
* {@link JdbcMessageStoreTests}
*
* This tests requires at least MySql 5.6.4 as it uses the fractional second support
* in that version. For more information, please see:
*
* https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html
*
* Also, please make sure you are using the respective DDL scripts:
*
* schema-mysql-5_6_4.sql
*
* @author Gunnar Hillert
* @author Artem Bilan
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
@Ignore
public class MySqlJdbcMessageStoreTests {
@SpringJUnitConfig
@DirtiesContext
public class MySqlJdbcMessageStoreTests implements MySqlContainerTest {
private static final Log LOG = LogFactory.getLog(MySqlJdbcMessageStoreTests.class);
@@ -89,13 +78,13 @@ public class MySqlJdbcMessageStoreTests {
@Autowired
private PlatformTransactionManager transactionManager;
@Before
@BeforeEach
public void init() {
messageStore = new JdbcMessageStore(dataSource);
messageStore.setRegion("JdbcMessageStoreTests");
}
@After
@AfterEach
public void afterTest() {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
new TransactionTemplate(this.transactionManager).execute(status -> {
@@ -203,7 +192,7 @@ public class MySqlJdbcMessageStoreTests {
Message<String> message = MessageBuilder.withPayload("foo").build();
message = messageStore.addMessage(message);
Message<String> result = messageStore.addMessage(message);
assertThat(result).isSameAs(message);
assertThat(result).isEqualTo(message);
}
@Test
@@ -336,6 +325,7 @@ public class MySqlJdbcMessageStoreTests {
@Test
@Transactional
@Disabled("Time sensitive")
public void testExpireMessageGroupOnCreateOnly() throws Exception {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
@@ -355,6 +345,7 @@ public class MySqlJdbcMessageStoreTests {
@Test
@Transactional
@Disabled("Time sensitive")
public void testExpireMessageGroupOnIdleOnly() throws Exception {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
@@ -496,4 +487,48 @@ public class MySqlJdbcMessageStoreTests {
.isEqualTo(2);
}
@Test
public void testMessageGroupCondition() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
this.messageStore.addMessagesToGroup(groupId, message);
this.messageStore.setGroupCondition(groupId, "testCondition");
assertThat(this.messageStore.getMessageGroup(groupId).getCondition()).isEqualTo("testCondition");
}
@Configuration
public static class Config {
@Value("org/springframework/integration/jdbc/schema-mysql.sql")
Resource createSchemaScript;
@Value("org/springframework/integration/jdbc/schema-drop-mysql.sql")
Resource dropSchemaScript;
@Bean
DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(MySqlContainerTest.getDriverClassName());
dataSource.setUrl(MySqlContainerTest.getJdbcUrl());
dataSource.setUsername(MySqlContainerTest.getUsername());
dataSource.setPassword(MySqlContainerTest.getPassword());
return dataSource;
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
DataSourceInitializer dataSourceInitializer() {
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(dataSource());
dataSourceInitializer.setDatabasePopulator(new ResourceDatabasePopulator(this.createSchemaScript));
dataSourceInitializer.setDatabaseCleaner(new ResourceDatabasePopulator(this.dropSchemaScript));
return dataSourceInitializer;
}
}
}

View File

@@ -5,9 +5,9 @@
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="org/springframework/integration/jdbc/schema-drop-h2.sql"/>
<jdbc:script location="org/springframework/integration/jdbc/schema-h2.sql" />
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="org/springframework/integration/jdbc/schema-drop-hsqldb.sql"/>
<jdbc:script location="org/springframework/integration/jdbc/schema-hsqldb.sql" />
</jdbc:embedded-database>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

View File

@@ -30,9 +30,8 @@ import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
@@ -47,8 +46,7 @@ import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
/**
@@ -60,8 +58,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Gary Russell
* @author Will Schipp
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext // close at the end after class
@Transactional
public class JdbcMessageStoreTests {
@@ -71,7 +68,7 @@ public class JdbcMessageStoreTests {
private JdbcMessageStore messageStore;
@Before
@BeforeEach
public void init() {
messageStore = new JdbcMessageStore(dataSource);
}
@@ -219,12 +216,12 @@ public class JdbcMessageStoreTests {
public void testAddAndRemoveMessagesFromMessageGroup() throws Exception {
String groupId = "X";
this.messageStore.setRemoveBatchSize(10);
List<Message<?>> messages = new ArrayList<Message<?>>();
List<Message<?>> messages = new ArrayList<>();
for (int i = 0; i < 25; i++) {
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messages.add(message);
}
this.messageStore.addMessagesToGroup(groupId, messages.toArray(new Message<?>[messages.size()]));
this.messageStore.addMessagesToGroup(groupId, messages.toArray(new Message<?>[0]));
MessageGroup group = this.messageStore.getMessageGroup(groupId);
assertThat(group.size()).isEqualTo(25);
this.messageStore.removeMessagesFromGroup(groupId, messages);
@@ -261,7 +258,7 @@ public class JdbcMessageStoreTests {
}
@Test
public void testUpdateLastReleasedSequence() throws Exception {
public void testUpdateLastReleasedSequence() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build();
messageStore.addMessagesToGroup(groupId, message);
@@ -271,7 +268,7 @@ public class JdbcMessageStoreTests {
}
@Test
public void testMessageGroupCount() throws Exception {
public void testMessageGroupCount() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
messageStore.addMessagesToGroup(groupId, message);
@@ -279,7 +276,7 @@ public class JdbcMessageStoreTests {
}
@Test
public void testMessageGroupSizes() throws Exception {
public void testMessageGroupSizes() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
messageStore.addMessagesToGroup(groupId, message);
@@ -408,7 +405,7 @@ public class JdbcMessageStoreTests {
}
@Test
public void testSameMessageToMultipleGroups() throws Exception {
public void testSameMessageToMultipleGroups() {
final String group1Id = "group1";
final String group2Id = "group2";
@@ -522,4 +519,13 @@ public class JdbcMessageStoreTests {
assertThat(messageGroup.isComplete()).isTrue();
}
@Test
public void testMessageGroupCondition() {
String groupId = "X";
Message<String> message = MessageBuilder.withPayload("foo").build();
this.messageStore.addMessagesToGroup(groupId, message);
this.messageStore.setGroupCondition(groupId, "testCondition");
assertThat(this.messageStore.getMessageGroup(groupId).getCondition()).isEqualTo("testCondition");
}
}