Skip to content

Commit cb8b8a8

Browse files
committed
[WIP] Provide database specific JdbcIndexedSessionRepository customizers
This commit provides JdbcIndexedSessionRepository customizers for the following databases: - PostgreSQL - Oracle - MySQL (TODO) - SQL Server (TODO) These customizers are intended to address the concurrency issues occurring on insert of new session attribute by applying database specific SQL upsert/merge statement instead of a generic insert. Closes: spring-projects#1213
1 parent b722b12 commit cb8b8a8

6 files changed

+224
-11
lines changed

spring-session-jdbc/src/integration-test/java/org/springframework/session/jdbc/AbstractJdbcIndexedSessionRepositoryITests.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,10 +27,16 @@
2727

2828
import org.junit.jupiter.api.BeforeEach;
2929
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
3031

3132
import org.springframework.beans.factory.annotation.Autowired;
3233
import org.springframework.context.annotation.Bean;
34+
import org.springframework.jdbc.core.JdbcOperations;
35+
import org.springframework.jdbc.core.JdbcTemplate;
3336
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
37+
import org.springframework.jdbc.support.lob.DefaultLobHandler;
38+
import org.springframework.jdbc.support.lob.LobCreator;
39+
import org.springframework.jdbc.support.lob.LobHandler;
3440
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3541
import org.springframework.security.core.Authentication;
3642
import org.springframework.security.core.authority.AuthorityUtils;
@@ -57,15 +63,24 @@ abstract class AbstractJdbcIndexedSessionRepositoryITests {
5763

5864
private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
5965

66+
@Autowired
67+
private DataSource dataSource;
68+
6069
@Autowired
6170
private JdbcIndexedSessionRepository repository;
6271

72+
private JdbcOperations jdbcOperations;
73+
74+
private LobHandler lobHandler;
75+
6376
private SecurityContext context;
6477

6578
private SecurityContext changedContext;
6679

6780
@BeforeEach
6881
void setUp() {
82+
this.jdbcOperations = new JdbcTemplate(this.dataSource);
83+
this.lobHandler = new DefaultLobHandler();
6984
this.context = SecurityContextHolder.createEmptyContext();
7085
this.context.setAuthentication(new UsernamePasswordAuthenticationToken("username-" + UUID.randomUUID(), "na",
7186
AuthorityUtils.createAuthorityList("ROLE_USER")));
@@ -759,6 +774,23 @@ void saveWithLargeAttribute() {
759774
assertThat((byte[]) session.getAttribute(attributeName)).hasSize(arraySize);
760775
}
761776

777+
@Test // gh-1213
778+
@EnabledIfSystemProperty(named = "spring.session.jdbc.customQueries", matches = "true")
779+
void saveNewSessionAttributeConcurrently() {
780+
JdbcSession session = this.repository.createSession();
781+
this.repository.save(session);
782+
String attributeName = "attribute1";
783+
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
784+
this.jdbcOperations.update("insert into spring_session_attributes values (?, ?, ?)", (ps) -> {
785+
ps.setString(1, (String) ReflectionTestUtils.getField(session, "primaryKey"));
786+
ps.setString(2, "attribute1");
787+
lobCreator.setBlobAsBytes(ps, 3, "value2".getBytes());
788+
});
789+
}
790+
session.setAttribute(attributeName, "value1");
791+
this.repository.save(session);
792+
}
793+
762794
private String getSecurityName() {
763795
return this.context.getAuthentication().getName();
764796
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.jdbc;
18+
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
22+
/**
23+
* Integration tests for {@link JdbcIndexedSessionRepository} using Oracle database with
24+
* {@link OracleJdbcIndexedSessionRepositoryCustomizer}.
25+
*
26+
* @author Vedran Pavic
27+
*/
28+
public class OracleJdbcIndexedSessionRepositoryWithCustomizerITests extends OracleJdbcIndexedSessionRepositoryITests {
29+
30+
@Configuration
31+
static class ConfigWithCustomizer extends Config {
32+
33+
@Bean
34+
OracleJdbcIndexedSessionRepositoryCustomizer oracleJdbcIndexedSessionRepositoryCustomizer() {
35+
System.setProperty("spring.session.jdbc.customQueries", "true");
36+
return new OracleJdbcIndexedSessionRepositoryCustomizer();
37+
}
38+
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.jdbc;
18+
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
22+
/**
23+
* Integration tests for {@link JdbcIndexedSessionRepository} using PostgreSQL 11.x
24+
* database with {@link PostgreSqlJdbcIndexedSessionRepositoryCustomizer}.
25+
*
26+
* @author Vedran Pavic
27+
*/
28+
public class PostgreSql11CustomizerJdbcIndexedSessionRepositoryITests
29+
extends PostgreSql11JdbcIndexedSessionRepositoryITests {
30+
31+
@Configuration
32+
static class ConfigWithCustomizer extends Config {
33+
34+
@Bean
35+
PostgreSqlJdbcIndexedSessionRepositoryCustomizer postgreSqlJdbcIndexedSessionRepositoryCustomizer() {
36+
System.setProperty("spring.session.jdbc.customQueries", "true");
37+
return new PostgreSqlJdbcIndexedSessionRepositoryCustomizer();
38+
}
39+
40+
}
41+
42+
}

spring-session-jdbc/src/main/java/org/springframework/session/jdbc/JdbcIndexedSessionRepository.java

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -268,7 +268,7 @@ public void setTableName(String tableName) {
268268
*/
269269
public void setCreateSessionQuery(String createSessionQuery) {
270270
Assert.hasText(createSessionQuery, "Query must not be empty");
271-
this.createSessionQuery = createSessionQuery;
271+
this.createSessionQuery = getQuery(createSessionQuery);
272272
}
273273

274274
/**
@@ -277,7 +277,7 @@ public void setCreateSessionQuery(String createSessionQuery) {
277277
*/
278278
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
279279
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
280-
this.createSessionAttributeQuery = createSessionAttributeQuery;
280+
this.createSessionAttributeQuery = getQuery(createSessionAttributeQuery);
281281
}
282282

283283
/**
@@ -286,7 +286,7 @@ public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
286286
*/
287287
public void setGetSessionQuery(String getSessionQuery) {
288288
Assert.hasText(getSessionQuery, "Query must not be empty");
289-
this.getSessionQuery = getSessionQuery;
289+
this.getSessionQuery = getQuery(getSessionQuery);
290290
}
291291

292292
/**
@@ -295,7 +295,7 @@ public void setGetSessionQuery(String getSessionQuery) {
295295
*/
296296
public void setUpdateSessionQuery(String updateSessionQuery) {
297297
Assert.hasText(updateSessionQuery, "Query must not be empty");
298-
this.updateSessionQuery = updateSessionQuery;
298+
this.updateSessionQuery = getQuery(updateSessionQuery);
299299
}
300300

301301
/**
@@ -304,7 +304,7 @@ public void setUpdateSessionQuery(String updateSessionQuery) {
304304
*/
305305
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
306306
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
307-
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
307+
this.updateSessionAttributeQuery = getQuery(updateSessionAttributeQuery);
308308
}
309309

310310
/**
@@ -313,7 +313,7 @@ public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
313313
*/
314314
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
315315
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
316-
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
316+
this.deleteSessionAttributeQuery = getQuery(deleteSessionAttributeQuery);
317317
}
318318

319319
/**
@@ -322,7 +322,7 @@ public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
322322
*/
323323
public void setDeleteSessionQuery(String deleteSessionQuery) {
324324
Assert.hasText(deleteSessionQuery, "Query must not be empty");
325-
this.deleteSessionQuery = deleteSessionQuery;
325+
this.deleteSessionQuery = getQuery(deleteSessionQuery);
326326
}
327327

328328
/**
@@ -331,7 +331,7 @@ public void setDeleteSessionQuery(String deleteSessionQuery) {
331331
*/
332332
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
333333
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
334-
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
334+
this.listSessionsByPrincipalNameQuery = getQuery(listSessionsByPrincipalNameQuery);
335335
}
336336

337337
/**
@@ -340,7 +340,7 @@ public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNa
340340
*/
341341
public void setDeleteSessionsByExpiryTimeQuery(String deleteSessionsByExpiryTimeQuery) {
342342
Assert.hasText(deleteSessionsByExpiryTimeQuery, "Query must not be empty");
343-
this.deleteSessionsByExpiryTimeQuery = deleteSessionsByExpiryTimeQuery;
343+
this.deleteSessionsByExpiryTimeQuery = getQuery(deleteSessionsByExpiryTimeQuery);
344344
}
345345

346346
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.jdbc;
18+
19+
import org.springframework.session.config.SessionRepositoryCustomizer;
20+
21+
/**
22+
* A {@link SessionRepositoryCustomizer} implementation that applies Oracle specific
23+
* optimized SQL statements to {@link JdbcIndexedSessionRepository}.
24+
*
25+
* @author Vedran Pavic
26+
* @since 2.5.0
27+
*/
28+
public class OracleJdbcIndexedSessionRepositoryCustomizer
29+
implements SessionRepositoryCustomizer<JdbcIndexedSessionRepository> {
30+
31+
// @formatter:off
32+
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = ""
33+
+ "MERGE INTO %TABLE_NAME%_ATTRIBUTES SA "
34+
+ "USING ( "
35+
+ " SELECT PRIMARY_ID AS SESSION_PRIMARY_ID, ? AS ATTRIBUTE_NAME, ? AS ATTRIBUTE_BYTES "
36+
+ " FROM %TABLE_NAME% "
37+
+ " WHERE SESSION_ID = ? "
38+
+ ") S "
39+
+ "ON (SA.SESSION_PRIMARY_ID = S.SESSION_PRIMARY_ID and SA.ATTRIBUTE_NAME = S.ATTRIBUTE_NAME) "
40+
+ "WHEN MATCHED THEN "
41+
+ " UPDATE SET SA.ATTRIBUTE_BYTES = S.ATTRIBUTE_BYTES "
42+
+ "WHEN NOT MATCHED THEN "
43+
+ " INSERT (SA.SESSION_PRIMARY_ID, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES) "
44+
+ " VALUES (S.SESSION_PRIMARY_ID, S.ATTRIBUTE_NAME, S.ATTRIBUTE_BYTES)";
45+
// @formatter:on
46+
47+
@Override
48+
public void customize(JdbcIndexedSessionRepository sessionRepository) {
49+
sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
50+
}
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.jdbc;
18+
19+
import org.springframework.session.config.SessionRepositoryCustomizer;
20+
21+
/**
22+
* A {@link SessionRepositoryCustomizer} implementation that applies PostgreSQL specific
23+
* optimized SQL statements to {@link JdbcIndexedSessionRepository}.
24+
*
25+
* @author Vedran Pavic
26+
* @since 2.5.0
27+
*/
28+
public class PostgreSqlJdbcIndexedSessionRepositoryCustomizer
29+
implements SessionRepositoryCustomizer<JdbcIndexedSessionRepository> {
30+
31+
// @formatter:off
32+
private static final String CREATE_SESSION_ATTRIBUTE_QUERY = ""
33+
+ "INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
34+
+ " SELECT PRIMARY_ID, ?, ? "
35+
+ " FROM %TABLE_NAME% "
36+
+ " WHERE SESSION_ID = ? "
37+
+ "ON CONFLICT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME) "
38+
+ "DO UPDATE SET ATTRIBUTE_BYTES = EXCLUDED.ATTRIBUTE_BYTES";
39+
// @formatter:on
40+
41+
@Override
42+
public void customize(JdbcIndexedSessionRepository sessionRepository) {
43+
sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
44+
}
45+
46+
}

0 commit comments

Comments
 (0)