diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/AbstractJdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/AbstractJdbcIndexedSessionRepositoryITests.java index 9dc583db7..15e9b16a3 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/AbstractJdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/AbstractJdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -29,8 +29,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -38,6 +45,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; +import org.springframework.session.config.SessionRepositoryCustomizer; import org.springframework.session.jdbc.JdbcIndexedSessionRepository.JdbcSession; import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession; import org.springframework.test.util.ReflectionTestUtils; @@ -45,6 +53,8 @@ import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Base class for {@link JdbcIndexedSessionRepository} integration tests. @@ -57,15 +67,27 @@ abstract class AbstractJdbcIndexedSessionRepositoryITests { private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private DataSource dataSource; + @Autowired private JdbcIndexedSessionRepository repository; + private JdbcOperations jdbcOperations; + + private LobHandler lobHandler; + private SecurityContext context; private SecurityContext changedContext; @BeforeEach void setUp() { + this.jdbcOperations = new JdbcTemplate(this.dataSource); + this.lobHandler = new DefaultLobHandler(); this.context = SecurityContextHolder.createEmptyContext(); this.context.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(), "na", AuthorityUtils.createAuthorityList("ROLE_USER"))); @@ -759,6 +781,32 @@ void saveWithLargeAttribute() { assertThat((byte[]) session.getAttribute(attributeName)).hasSize(arraySize); } + @Test // gh-1213 + void saveNewSessionAttributeConcurrently() { + JdbcSession session = this.repository.createSession(); + this.repository.save(session); + String attributeName = "attribute1"; + String attributeValue = "value1"; + try (LobCreator lobCreator = this.lobHandler.getLobCreator()) { + this.jdbcOperations.update("INSERT INTO SPRING_SESSION_ATTRIBUTES VALUES (?, ?, ?)", (ps) -> { + ps.setString(1, (String) ReflectionTestUtils.getField(session, "primaryKey")); + ps.setString(2, attributeName); + lobCreator.setBlobAsBytes(ps, 3, "value2".getBytes()); + }); + } + session.setAttribute(attributeName, attributeValue); + if (this.applicationContext.getBeansOfType(SessionRepositoryCustomizer.class).isEmpty()) { + // without DB specific upsert configured we're seeing duplicate key error + assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(() -> this.repository.save(session)); + } + else { + // with DB specific upsert configured we're fine + assertThatCode(() -> this.repository.save(session)).doesNotThrowAnyException(); + assertThat((String) this.repository.findById(session.getId()).getAttribute(attributeName)) + .isEqualTo(attributeValue); + } + } + private String getSecurityName() { return this.context.getAuthentication().getName(); } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/DatabaseContainers.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/DatabaseContainers.java index 71a9a25c1..0d97f3aa3 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/DatabaseContainers.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/DatabaseContainers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -17,7 +17,6 @@ package org.springframework.session.jdbc; import java.time.Duration; -import java.time.temporal.ChronoUnit; import org.testcontainers.containers.Db2Container; import org.testcontainers.containers.JdbcDatabaseContainer; @@ -38,169 +37,42 @@ final class DatabaseContainers { private DatabaseContainers() { } - static Db211Container db211() { - return new Db211Container(); + static Db2Container db2() { + return new Db2Container("ibmcom/db2:11.5.4.0"); } - static MariaDBContainer mariaDb5() { - return new MariaDb5Container(); + static MariaDBContainer mariaDb() { + return new MariaDBContainer<>("mariadb:10.5.7"); } - static MariaDBContainer mariaDb10() { - return new MariaDb10Container(); + static MySQLContainer mySql() { + return new MySQLContainer<>("mysql:8.0.22"); } - static MySQLContainer mySql5() { - return new MySql5Container(); - } - - static MySQLContainer mySql8() { - return new MySql8Container(); - } - - static OracleXeContainer oracleXe() { - return new OracleXeContainer(); - } - - static PostgreSQLContainer postgreSql9() { - return new PostgreSql9Container(); - } - - static PostgreSQLContainer postgreSql10() { - return new PostgreSql10Container(); - } - - static PostgreSQLContainer postgreSql11() { - return new PostgreSql11Container(); - } - - static MSSQLServerContainer sqlServer2017() { - return new SqlServer2017Container(); - } - - private static class Db211Container extends Db2Container { - - Db211Container() { - super("ibmcom/db2:11.5.0.0a"); - } - - } - - private static class MariaDb5Container extends MariaDBContainer { - - MariaDb5Container() { - super("mariadb:5.5.64"); - } - - @Override - protected void configure() { - super.configure(); - setCommand("mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", - "--innodb_large_prefix", "--innodb_file_format=barracuda", "--innodb-file-per-table"); - } - - } - - private static class MariaDb10Container extends MariaDBContainer { - - MariaDb10Container() { - super("mariadb:10.4.8"); - } - - @Override - protected void configure() { - super.configure(); - setCommand("mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); - } - - } - - private static class MySql5Container extends MySQLContainer { - - MySql5Container() { - super("mysql:5.7.27"); - } - - @Override - protected void configure() { - super.configure(); - setCommand("mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); - } - - @Override - public String getDriverClassName() { - return "com.mysql.cj.jdbc.Driver"; - } - - } - - private static class MySql8Container extends MySQLContainer { - - MySql8Container() { - super("mysql:8.0.17"); - } - - @Override - protected void configure() { - super.configure(); - setCommand("mysqld", "--default-authentication-plugin=mysql_native_password"); - } - - @Override - public String getDriverClassName() { - return "com.mysql.cj.jdbc.Driver"; - } - - } + static OracleContainer oracle() { + return new OracleContainer() { - private static class OracleXeContainer extends OracleContainer { + @Override + protected void configure() { + this.waitStrategy = new LogMessageWaitStrategy().withRegEx(".*DATABASE IS READY TO USE!.*\\s") + .withStartupTimeout(Duration.ofMinutes(10)); + addEnv("ORACLE_PWD", getPassword()); + } - @Override - protected void configure() { - super.configure(); - this.waitStrategy = new LogMessageWaitStrategy().withRegEx(".*DATABASE IS READY TO USE!.*\\s") - .withStartupTimeout(Duration.of(10, ChronoUnit.MINUTES)); - setShmSize(1024L * 1024L * 1024L); - addEnv("ORACLE_PWD", getPassword()); - } - - @Override - protected void waitUntilContainerStarted() { - getWaitStrategy().waitUntilReady(this); - } + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } + }; } - private static class PostgreSql9Container extends PostgreSQLContainer { - - PostgreSql9Container() { - super("postgres:9.6.15"); - } - - } - - private static class PostgreSql10Container extends PostgreSQLContainer { - - PostgreSql10Container() { - super("postgres:10.10"); - } - + static PostgreSQLContainer postgreSql() { + return new PostgreSQLContainer<>("postgres:13.0"); } - private static class PostgreSql11Container extends PostgreSQLContainer { - - PostgreSql11Container() { - super("postgres:11.5"); - } - - } - - private static class SqlServer2017Container extends MSSQLServerContainer { - - SqlServer2017Container() { - super("mcr.microsoft.com/mssql/server:2017-CU16"); - } - + static MSSQLServerContainer sqlServer() { + return new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-CU8-ubuntu-16.04"); } } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryCustomizerITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryCustomizerITests.java new file mode 100644 index 000000000..b7611131f --- /dev/null +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryCustomizerITests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for {@link JdbcIndexedSessionRepository} using IBM DB2 database with + * {@link Db2JdbcIndexedSessionRepositoryCustomizer}. + * + * @author Vedran Pavic + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class Db2JdbcIndexedSessionRepositoryCustomizerITests extends Db2JdbcIndexedSessionRepositoryITests { + + @Configuration + static class CustomizerConfig extends Config { + + @Bean + Db2JdbcIndexedSessionRepositoryCustomizer db2JdbcIndexedSessionRepositoryCustomizer() { + return new Db2JdbcIndexedSessionRepositoryCustomizer(); + } + + } + +} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db211JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryITests.java similarity index 86% rename from spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db211JdbcIndexedSessionRepositoryITests.java rename to spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryITests.java index 8b5c8571d..8a9e77dc9 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db211JdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -27,21 +27,21 @@ import org.springframework.test.context.web.WebAppConfiguration; /** - * Integration tests for {@link JdbcIndexedSessionRepository} using IBM DB2 11.x database. + * Integration tests for {@link JdbcIndexedSessionRepository} using IBM DB2 database. * * @author Vedran Pavic */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration -class Db211JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { +class Db2JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { @Configuration static class Config extends BaseContainerConfig { @Bean Db2Container databaseContainer() { - Db2Container databaseContainer = DatabaseContainers.db211(); + Db2Container databaseContainer = DatabaseContainers.db2(); databaseContainer.start(); return databaseContainer; } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDb10JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDb10JdbcIndexedSessionRepositoryITests.java deleted file mode 100644 index 948194ce9..000000000 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDb10JdbcIndexedSessionRepositoryITests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014-2019 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.session.jdbc; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.testcontainers.containers.MariaDBContainer; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; - -/** - * Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 10.x database. - * - * @author Vedran Pavic - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -@ContextConfiguration -class MariaDb10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { - - @Configuration - static class Config extends BaseContainerConfig { - - @Bean - MariaDBContainer databaseContainer() { - MariaDBContainer databaseContainer = DatabaseContainers.mariaDb10(); - databaseContainer.start(); - return databaseContainer; - } - - @Bean - ResourceDatabasePopulator databasePopulator() { - return DatabasePopulators.mySql(); - } - - } - -} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDbJdbcIndexedSessionRepositoryCustomizerITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDbJdbcIndexedSessionRepositoryCustomizerITests.java new file mode 100644 index 000000000..5e66ba4a7 --- /dev/null +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDbJdbcIndexedSessionRepositoryCustomizerITests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB database with + * {@link MySqlJdbcIndexedSessionRepositoryCustomizer}. + * + * @author Vedran Pavic + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class MariaDbJdbcIndexedSessionRepositoryCustomizerITests extends MariaDbJdbcIndexedSessionRepositoryITests { + + @Configuration + static class CustomizerConfig extends Config { + + @Bean + MySqlJdbcIndexedSessionRepositoryCustomizer mySqlJdbcIndexedSessionRepositoryCustomizer() { + return new MySqlJdbcIndexedSessionRepositoryCustomizer(); + } + + } + +} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDb5JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDbJdbcIndexedSessionRepositoryITests.java similarity index 83% rename from spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDb5JdbcIndexedSessionRepositoryITests.java rename to spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDbJdbcIndexedSessionRepositoryITests.java index 688dcecda..81d874e86 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDb5JdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MariaDbJdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -27,21 +27,21 @@ import org.springframework.test.context.web.WebAppConfiguration; /** - * Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB 5.x database. + * Integration tests for {@link JdbcIndexedSessionRepository} using MariaDB database. * * @author Vedran Pavic */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration -class MariaDb5JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { +class MariaDbJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { @Configuration static class Config extends BaseContainerConfig { @Bean - MariaDBContainer databaseContainer() { - MariaDBContainer databaseContainer = DatabaseContainers.mariaDb5(); + MariaDBContainer databaseContainer() { + MariaDBContainer databaseContainer = DatabaseContainers.mariaDb(); databaseContainer.start(); return databaseContainer; } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryCustomizerITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryCustomizerITests.java new file mode 100644 index 000000000..8013a8bd4 --- /dev/null +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryCustomizerITests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for {@link JdbcIndexedSessionRepository} using MySQL database with + * {@link MySqlJdbcIndexedSessionRepositoryCustomizer}. + * + * @author Vedran Pavic + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class MySqlJdbcIndexedSessionRepositoryCustomizerITests extends MySqlJdbcIndexedSessionRepositoryITests { + + @Configuration + static class CustomizerConfig extends Config { + + @Bean + MySqlJdbcIndexedSessionRepositoryCustomizer mySqlJdbcIndexedSessionRepositoryCustomizer() { + return new MySqlJdbcIndexedSessionRepositoryCustomizer(); + } + + } + +} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySql8JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryITests.java similarity index 83% rename from spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySql8JdbcIndexedSessionRepositoryITests.java rename to spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryITests.java index 84841822d..6a4f79ed1 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySql8JdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -27,21 +27,21 @@ import org.springframework.test.context.web.WebAppConfiguration; /** - * Integration tests for {@link JdbcIndexedSessionRepository} using MySQL 8.x database. + * Integration tests for {@link JdbcIndexedSessionRepository} using MySQL database. * * @author Vedran Pavic */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration -class MySql8JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { +class MySqlJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { @Configuration static class Config extends BaseContainerConfig { @Bean - MySQLContainer databaseContainer() { - MySQLContainer databaseContainer = DatabaseContainers.mySql8(); + MySQLContainer databaseContainer() { + MySQLContainer databaseContainer = DatabaseContainers.mySql(); databaseContainer.start(); return databaseContainer; } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryCustomizerITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryCustomizerITests.java new file mode 100644 index 000000000..b44bd54e5 --- /dev/null +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryCustomizerITests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for {@link JdbcIndexedSessionRepository} using Oracle database with + * {@link OracleJdbcIndexedSessionRepositoryCustomizer}. + * + * @author Vedran Pavic + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class OracleJdbcIndexedSessionRepositoryCustomizerITests extends OracleJdbcIndexedSessionRepositoryITests { + + @Configuration + static class CustomizerConfig extends Config { + + @Bean + OracleJdbcIndexedSessionRepositoryCustomizer oracleJdbcIndexedSessionRepositoryCustomizer() { + return new OracleJdbcIndexedSessionRepositoryCustomizer(); + } + + } + +} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryITests.java index 0ad4299f5..7abe83a95 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -54,7 +54,7 @@ static class Config extends BaseContainerConfig { @Bean OracleContainer databaseContainer() { - OracleContainer databaseContainer = DatabaseContainers.oracleXe(); + OracleContainer databaseContainer = DatabaseContainers.oracle(); databaseContainer.start(); return databaseContainer; } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql10JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql10JdbcIndexedSessionRepositoryITests.java deleted file mode 100644 index 33dbd0937..000000000 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql10JdbcIndexedSessionRepositoryITests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2014-2019 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.session.jdbc; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.testcontainers.containers.PostgreSQLContainer; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; - -/** - * Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 10.x - * database. - * - * @author Vedran Pavic - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -@ContextConfiguration -class PostgreSql10JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { - - @Configuration - static class Config extends BaseContainerConfig { - - @Bean - PostgreSQLContainer databaseContainer() { - PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql10(); - databaseContainer.start(); - return databaseContainer; - } - - @Bean - ResourceDatabasePopulator databasePopulator() { - return DatabasePopulators.postgreSql(); - } - - } - -} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql11JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql11JdbcIndexedSessionRepositoryITests.java deleted file mode 100644 index a51f700e5..000000000 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql11JdbcIndexedSessionRepositoryITests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2014-2019 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.session.jdbc; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.testcontainers.containers.PostgreSQLContainer; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; - -/** - * Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 11.x - * database. - * - * @author Vedran Pavic - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -@ContextConfiguration -class PostgreSql11JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { - - @Configuration - static class Config extends BaseContainerConfig { - - @Bean - PostgreSQLContainer databaseContainer() { - PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql11(); - databaseContainer.start(); - return databaseContainer; - } - - @Bean - ResourceDatabasePopulator databasePopulator() { - return DatabasePopulators.postgreSql(); - } - - } - -} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryCustomizerITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryCustomizerITests.java new file mode 100644 index 000000000..68504c01d --- /dev/null +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryCustomizerITests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL database + * with {@link PostgreSqlJdbcIndexedSessionRepositoryCustomizer}. + * + * @author Vedran Pavic + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class PostgreSqlJdbcIndexedSessionRepositoryCustomizerITests extends PostgreSqlJdbcIndexedSessionRepositoryITests { + + @Configuration + static class CustomizerConfig extends Config { + + @Bean + PostgreSqlJdbcIndexedSessionRepositoryCustomizer postgreSqlJdbcIndexedSessionRepositoryCustomizer() { + return new PostgreSqlJdbcIndexedSessionRepositoryCustomizer(); + } + + } + +} diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql9JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryITests.java similarity index 82% rename from spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql9JdbcIndexedSessionRepositoryITests.java rename to spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryITests.java index 480efcfb8..df40af84e 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSql9JdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -27,22 +27,21 @@ import org.springframework.test.context.web.WebAppConfiguration; /** - * Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 9.x - * database. + * Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL database. * * @author Vedran Pavic */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration -class PostgreSql9JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { +class PostgreSqlJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { @Configuration static class Config extends BaseContainerConfig { @Bean - PostgreSQLContainer databaseContainer() { - PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql9(); + PostgreSQLContainer databaseContainer() { + PostgreSQLContainer databaseContainer = DatabaseContainers.postgreSql(); databaseContainer.start(); return databaseContainer; } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySql5JdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryCustomizerITests.java similarity index 65% rename from spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySql5JdbcIndexedSessionRepositoryITests.java rename to spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryCustomizerITests.java index 6c573613c..568d09ba2 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/MySql5JdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryCustomizerITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -17,38 +17,30 @@ package org.springframework.session.jdbc; import org.junit.jupiter.api.extension.ExtendWith; -import org.testcontainers.containers.MySQLContainer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; /** - * Integration tests for {@link JdbcIndexedSessionRepository} using MySQL 5.x database. + * Integration tests for {@link JdbcIndexedSessionRepository} using SQL Server database + * with {@link SqlServerJdbcIndexedSessionRepositoryCustomizer}. * * @author Vedran Pavic */ @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration -class MySql5JdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcIndexedSessionRepositoryITests { +class SqlServerJdbcIndexedSessionRepositoryCustomizerITests extends SqlServerJdbcIndexedSessionRepositoryITests { @Configuration - static class Config extends BaseContainerConfig { + static class CustomizerConfig extends Config { @Bean - MySQLContainer databaseContainer() { - MySQLContainer databaseContainer = DatabaseContainers.mySql5(); - databaseContainer.start(); - return databaseContainer; - } - - @Bean - ResourceDatabasePopulator databasePopulator() { - return DatabasePopulators.mySql(); + SqlServerJdbcIndexedSessionRepositoryCustomizer sqlServerJdbcIndexedSessionRepositoryCustomizer() { + return new SqlServerJdbcIndexedSessionRepositoryCustomizer(); } } diff --git a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryITests.java b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryITests.java index 752196fc6..e8cdf23e7 100644 --- a/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryITests.java +++ b/spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -41,8 +41,8 @@ class SqlServerJdbcIndexedSessionRepositoryITests extends AbstractContainerJdbcI static class Config extends BaseContainerConfig { @Bean - MSSQLServerContainer databaseContainer() { - MSSQLServerContainer databaseContainer = DatabaseContainers.sqlServer2017(); + MSSQLServerContainer databaseContainer() { + MSSQLServerContainer databaseContainer = DatabaseContainers.sqlServer(); databaseContainer.start(); return databaseContainer; } diff --git a/spring-session-jdbc/src/integration-test/resources/container-license-acceptance.txt b/spring-session-jdbc/src/integration-test/resources/container-license-acceptance.txt index 4b8ae90ef..d7c7055d4 100644 --- a/spring-session-jdbc/src/integration-test/resources/container-license-acceptance.txt +++ b/spring-session-jdbc/src/integration-test/resources/container-license-acceptance.txt @@ -1,2 +1,2 @@ -ibmcom/db2:11.5.0.0a -mcr.microsoft.com/mssql/server:2017-CU16 +ibmcom/db2:11.5.4.0 +mcr.microsoft.com/mssql/server:2019-CU8-ubuntu-16.04 diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryCustomizer.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryCustomizer.java new file mode 100644 index 000000000..d2df717a9 --- /dev/null +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/Db2JdbcIndexedSessionRepositoryCustomizer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.springframework.session.config.SessionRepositoryCustomizer; + +/** + * A {@link SessionRepositoryCustomizer} implementation that applies IBM DB2 specific + * optimized SQL statements to {@link JdbcIndexedSessionRepository}. + * + * @author Vedran Pavic + * @since 2.5.0 + */ +public class Db2JdbcIndexedSessionRepositoryCustomizer + implements SessionRepositoryCustomizer { + + // @formatter:off + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "" + + "MERGE INTO %TABLE_NAME%_ATTRIBUTES SA " + + "USING ( " + + " VALUES (?, ?, ?) " + + ") A (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + "ON (SA.SESSION_PRIMARY_ID = A.SESSION_PRIMARY_ID and SA.ATTRIBUTE_NAME = A.ATTRIBUTE_NAME) " + + "WHEN MATCHED THEN " + + " UPDATE SET ATTRIBUTE_BYTES = A.ATTRIBUTE_BYTES " + + "WHEN NOT MATCHED THEN " + + " INSERT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + " VALUES (A.SESSION_PRIMARY_ID, A.ATTRIBUTE_NAME, A.ATTRIBUTE_BYTES)"; + // @formatter:on + + @Override + public void customize(JdbcIndexedSessionRepository sessionRepository) { + sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY); + } + +} diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java index 604e44f52..8f05c6aa3 100644 --- a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -40,6 +40,8 @@ import org.springframework.core.serializer.support.DeserializingConverter; import org.springframework.core.serializer.support.SerializingConverter; import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.ResultSetExtractor; @@ -139,55 +141,64 @@ public class JdbcIndexedSessionRepository private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; // @formatter:off - private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) " + private static final String CREATE_SESSION_QUERY = "" + + "INSERT INTO %TABLE_NAME% (PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) " + "VALUES (?, ?, ?, ?, ?, ?, ?)"; // @formatter:on // @formatter:off - private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " - + "SELECT PRIMARY_ID, ?, ? " - + "FROM %TABLE_NAME% " - + "WHERE SESSION_ID = ?"; + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "" + + "INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + "VALUES (?, ?, ?)"; // @formatter:on // @formatter:off - private static final String GET_SESSION_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " + private static final String GET_SESSION_QUERY = "" + + "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " + "FROM %TABLE_NAME% S " - + "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " + + "LEFT JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " + "WHERE S.SESSION_ID = ?"; // @formatter:on // @formatter:off - private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? " + private static final String UPDATE_SESSION_QUERY = "" + + "UPDATE %TABLE_NAME% " + + "SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? " + "WHERE PRIMARY_ID = ?"; // @formatter:on // @formatter:off - private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = "UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? " + private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = "" + + "UPDATE %TABLE_NAME%_ATTRIBUTES " + + "SET ATTRIBUTE_BYTES = ? " + "WHERE SESSION_PRIMARY_ID = ? " + "AND ATTRIBUTE_NAME = ?"; // @formatter:on // @formatter:off - private static final String DELETE_SESSION_ATTRIBUTE_QUERY = "DELETE FROM %TABLE_NAME%_ATTRIBUTES " + private static final String DELETE_SESSION_ATTRIBUTE_QUERY = "" + + "DELETE FROM %TABLE_NAME%_ATTRIBUTES " + "WHERE SESSION_PRIMARY_ID = ? " + "AND ATTRIBUTE_NAME = ?"; // @formatter:on // @formatter:off - private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% " + private static final String DELETE_SESSION_QUERY = "" + + "DELETE FROM %TABLE_NAME% " + "WHERE SESSION_ID = ?"; // @formatter:on // @formatter:off - private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " + private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "" + + "SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES " + "FROM %TABLE_NAME% S " - + "LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " + + "LEFT JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID " + "WHERE S.PRINCIPAL_NAME = ?"; // @formatter:on // @formatter:off - private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY = "DELETE FROM %TABLE_NAME% " + private static final String DELETE_SESSIONS_BY_EXPIRY_TIME_QUERY = "" + + "DELETE FROM %TABLE_NAME% " + "WHERE EXPIRY_TIME < ?"; // @formatter:on @@ -268,7 +279,7 @@ public void setTableName(String tableName) { */ public void setCreateSessionQuery(String createSessionQuery) { Assert.hasText(createSessionQuery, "Query must not be empty"); - this.createSessionQuery = createSessionQuery; + this.createSessionQuery = getQuery(createSessionQuery); } /** @@ -277,7 +288,7 @@ public void setCreateSessionQuery(String createSessionQuery) { */ public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) { Assert.hasText(createSessionAttributeQuery, "Query must not be empty"); - this.createSessionAttributeQuery = createSessionAttributeQuery; + this.createSessionAttributeQuery = getQuery(createSessionAttributeQuery); } /** @@ -286,7 +297,7 @@ public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) { */ public void setGetSessionQuery(String getSessionQuery) { Assert.hasText(getSessionQuery, "Query must not be empty"); - this.getSessionQuery = getSessionQuery; + this.getSessionQuery = getQuery(getSessionQuery); } /** @@ -295,7 +306,7 @@ public void setGetSessionQuery(String getSessionQuery) { */ public void setUpdateSessionQuery(String updateSessionQuery) { Assert.hasText(updateSessionQuery, "Query must not be empty"); - this.updateSessionQuery = updateSessionQuery; + this.updateSessionQuery = getQuery(updateSessionQuery); } /** @@ -304,7 +315,7 @@ public void setUpdateSessionQuery(String updateSessionQuery) { */ public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) { Assert.hasText(updateSessionAttributeQuery, "Query must not be empty"); - this.updateSessionAttributeQuery = updateSessionAttributeQuery; + this.updateSessionAttributeQuery = getQuery(updateSessionAttributeQuery); } /** @@ -313,7 +324,7 @@ public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) { */ public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) { Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty"); - this.deleteSessionAttributeQuery = deleteSessionAttributeQuery; + this.deleteSessionAttributeQuery = getQuery(deleteSessionAttributeQuery); } /** @@ -322,7 +333,7 @@ public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) { */ public void setDeleteSessionQuery(String deleteSessionQuery) { Assert.hasText(deleteSessionQuery, "Query must not be empty"); - this.deleteSessionQuery = deleteSessionQuery; + this.deleteSessionQuery = getQuery(deleteSessionQuery); } /** @@ -331,7 +342,7 @@ public void setDeleteSessionQuery(String deleteSessionQuery) { */ public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) { Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty"); - this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery; + this.listSessionsByPrincipalNameQuery = getQuery(listSessionsByPrincipalNameQuery); } /** @@ -340,7 +351,7 @@ public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNa */ public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) { Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty"); - this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery; + this.deleteSessionsByExpiryTimeQuery = getQuery(deleteSessionsByExpiryTimeQuery); } /** @@ -463,30 +474,49 @@ private void insertSessionAttributes(JdbcSession session, List attribute Assert.notEmpty(attributeNames, "attributeNames must not be null or empty"); try (LobCreator lobCreator = this.lobHandler.getLobCreator()) { if (attributeNames.size() > 1) { - this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, new BatchPreparedStatementSetter() { - - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - String attributeName = attributeNames.get(i); - ps.setString(1, attributeName); - lobCreator.setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName))); - ps.setString(3, session.getId()); - } - - @Override - public int getBatchSize() { - return attributeNames.size(); - } + try { + this.jdbcOperations.batchUpdate(this.createSessionAttributeQuery, + new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + String attributeName = attributeNames.get(i); + ps.setString(1, session.primaryKey); + ps.setString(2, attributeName); + lobCreator.setBlobAsBytes(ps, 3, serialize(session.getAttribute(attributeName))); + } + + @Override + public int getBatchSize() { + return attributeNames.size(); + } - }); + }); + } + catch (DuplicateKeyException ex) { + throw ex; + } + catch (DataIntegrityViolationException ex) { + // parent record not found - we are ignoring this error because we + // assume that a concurrent request has removed the session + } } else { - this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> { - String attributeName = attributeNames.get(0); - ps.setString(1, attributeName); - lobCreator.setBlobAsBytes(ps, 2, serialize(session.getAttribute(attributeName))); - ps.setString(3, session.getId()); - }); + try { + this.jdbcOperations.update(this.createSessionAttributeQuery, (ps) -> { + String attributeName = attributeNames.get(0); + ps.setString(1, session.primaryKey); + ps.setString(2, attributeName); + lobCreator.setBlobAsBytes(ps, 3, serialize(session.getAttribute(attributeName))); + }); + } + catch (DuplicateKeyException ex) { + throw ex; + } + catch (DataIntegrityViolationException ex) { + // parent record not found - we are ignoring this error because we + // assume that a concurrent request has removed the session + } } } } diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryCustomizer.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryCustomizer.java new file mode 100644 index 000000000..0745c3327 --- /dev/null +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/MySqlJdbcIndexedSessionRepositoryCustomizer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.springframework.session.config.SessionRepositoryCustomizer; + +/** + * A {@link SessionRepositoryCustomizer} implementation that applies MySQL specific + * optimized SQL statements to {@link JdbcIndexedSessionRepository}. + * + * @author Vedran Pavic + * @since 2.5.0 + */ +public class MySqlJdbcIndexedSessionRepositoryCustomizer + implements SessionRepositoryCustomizer { + + // @formatter:off + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "" + + "INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + "VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE ATTRIBUTE_BYTES = VALUES(ATTRIBUTE_BYTES)"; + // @formatter:on + + @Override + public void customize(JdbcIndexedSessionRepository sessionRepository) { + sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY); + } + +} diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryCustomizer.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryCustomizer.java new file mode 100644 index 000000000..2ea3fd66c --- /dev/null +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/OracleJdbcIndexedSessionRepositoryCustomizer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.springframework.session.config.SessionRepositoryCustomizer; + +/** + * A {@link SessionRepositoryCustomizer} implementation that applies Oracle specific + * optimized SQL statements to {@link JdbcIndexedSessionRepository}. + * + * @author Vedran Pavic + * @since 2.5.0 + */ +public class OracleJdbcIndexedSessionRepositoryCustomizer + implements SessionRepositoryCustomizer { + + // @formatter:off + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "" + + "MERGE INTO %TABLE_NAME%_ATTRIBUTES SA " + + "USING ( " + + " SELECT ? AS SESSION_PRIMARY_ID, ? AS ATTRIBUTE_NAME, ? AS ATTRIBUTE_BYTES " + + " FROM DUAL " + + ") A " + + "ON (SA.SESSION_PRIMARY_ID = A.SESSION_PRIMARY_ID and SA.ATTRIBUTE_NAME = A.ATTRIBUTE_NAME) " + + "WHEN MATCHED THEN " + + " UPDATE SET ATTRIBUTE_BYTES = A.ATTRIBUTE_BYTES " + + "WHEN NOT MATCHED THEN " + + " INSERT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + " VALUES (A.SESSION_PRIMARY_ID, A.ATTRIBUTE_NAME, A.ATTRIBUTE_BYTES)"; + // @formatter:on + + @Override + public void customize(JdbcIndexedSessionRepository sessionRepository) { + sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY); + } + +} diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryCustomizer.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryCustomizer.java new file mode 100644 index 000000000..9b805e7be --- /dev/null +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/PostgreSqlJdbcIndexedSessionRepositoryCustomizer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.springframework.session.config.SessionRepositoryCustomizer; + +/** + * A {@link SessionRepositoryCustomizer} implementation that applies PostgreSQL specific + * optimized SQL statements to {@link JdbcIndexedSessionRepository}. + * + * @author Vedran Pavic + * @since 2.5.0 + */ +public class PostgreSqlJdbcIndexedSessionRepositoryCustomizer + implements SessionRepositoryCustomizer { + + // @formatter:off + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "" + + "INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + "VALUES (?, ?, ?) " + + "ON CONFLICT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME) " + + "DO UPDATE SET ATTRIBUTE_BYTES = EXCLUDED.ATTRIBUTE_BYTES"; + // @formatter:on + + @Override + public void customize(JdbcIndexedSessionRepository sessionRepository) { + sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY); + } + +} diff --git a/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryCustomizer.java b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryCustomizer.java new file mode 100644 index 000000000..26f34d397 --- /dev/null +++ b/spring-session-jdbc/src/main/java/org/springframework/session/jdbc/SqlServerJdbcIndexedSessionRepositoryCustomizer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2020 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.session.jdbc; + +import org.springframework.session.config.SessionRepositoryCustomizer; + +/** + * A {@link SessionRepositoryCustomizer} implementation that applies SQL Server specific + * optimized SQL statements to {@link JdbcIndexedSessionRepository}. + * + * @author Vedran Pavic + * @since 2.5.0 + */ +public class SqlServerJdbcIndexedSessionRepositoryCustomizer + implements SessionRepositoryCustomizer { + + // @formatter:off + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = "" + + "MERGE INTO %TABLE_NAME%_ATTRIBUTES SA " + + "USING ( " + + " VALUES (?, ?, ?) " + + ") A (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + "ON (SA.SESSION_PRIMARY_ID = A.SESSION_PRIMARY_ID and SA.ATTRIBUTE_NAME = A.ATTRIBUTE_NAME) " + + "WHEN MATCHED THEN " + + " UPDATE SET ATTRIBUTE_BYTES = A.ATTRIBUTE_BYTES " + + "WHEN NOT MATCHED THEN " + + " INSERT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + " VALUES (A.SESSION_PRIMARY_ID, A.ATTRIBUTE_NAME, A.ATTRIBUTE_BYTES);"; + // @formatter:on + + @Override + public void customize(JdbcIndexedSessionRepository sessionRepository) { + sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY); + } + +} diff --git a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java index d1e81c76f..63e2bb89b 100644 --- a/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java +++ b/spring-session-jdbc/src/test/java/org/springframework/session/jdbc/JdbcIndexedSessionRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2020 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. @@ -290,9 +290,9 @@ void saveNewWithSingleAttribute() { this.repository.save(session); assertThat(session.isNew()).isFalse(); - verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION("), + verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION ("), isA(PreparedStatementSetter.class)); - verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("), + verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("), isA(PreparedStatementSetter.class)); verifyNoMoreInteractions(this.jdbcOperations); } @@ -306,9 +306,9 @@ void saveNewWithMultipleAttributes() { this.repository.save(session); assertThat(session.isNew()).isFalse(); - verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION("), + verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION ("), isA(PreparedStatementSetter.class)); - verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("), + verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("), isA(BatchPreparedStatementSetter.class)); verifyNoMoreInteractions(this.jdbcOperations); } @@ -321,7 +321,7 @@ void saveUpdatedAddSingleAttribute() { this.repository.save(session); assertThat(session.isNew()).isFalse(); - verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("), + verify(this.jdbcOperations, times(1)).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("), isA(PreparedStatementSetter.class)); verifyNoMoreInteractions(this.jdbcOperations); } @@ -335,7 +335,7 @@ void saveUpdatedAddMultipleAttributes() { this.repository.save(session); assertThat(session.isNew()).isFalse(); - verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("), + verify(this.jdbcOperations, times(1)).batchUpdate(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("), isA(BatchPreparedStatementSetter.class)); verifyNoMoreInteractions(this.jdbcOperations); } @@ -424,7 +424,7 @@ void saveUpdatedAddAndModifyAttribute() { this.repository.save(session); assertThat(session.isNew()).isFalse(); - verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("), + verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("), isA(PreparedStatementSetter.class)); verifyNoMoreInteractions(this.jdbcOperations); } @@ -679,7 +679,7 @@ void flushModeImmediateSetAttribute() { JdbcSession session = this.repository.new JdbcSession(new MapSession(), "primaryKey", false); String attrName = "someAttribute"; session.setAttribute(attrName, "someValue"); - verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES("), + verify(this.jdbcOperations).update(startsWith("INSERT INTO SPRING_SESSION_ATTRIBUTES ("), isA(PreparedStatementSetter.class)); verifyNoMoreInteractions(this.jdbcOperations); }