Skip to content

Commit 15b89e0

Browse files
authored
Added CustomSessionInsertQueryConfiguration class to fix duplicate key errors on session creation/update (box#711)
1 parent 3442e35 commit 15b89e0

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.box.l10n.mojito.service.security.user.session;
2+
3+
import com.box.l10n.mojito.service.DBUtils;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Conditional;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
13+
import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration;
14+
import org.springframework.util.StringUtils;
15+
16+
17+
/**
18+
* Workaround for issue <a href="https://github.com/spring-projects/spring-session/issues/1213">Concurrent requests adding the same session attribute result in duplicate key constraint violation</a>
19+
* to change the Create Session Attribute query to use an MySQL UPDATE command to update the ATTRIBUTE_BYTES
20+
* of the existing row if a duplicate key is encountered.
21+
*
22+
* <b>NOTE:</b>This custom insert query should only be used with a MySQL database.
23+
*/
24+
@Configuration
25+
@ConditionalOnProperty(value = "l10n.spring.session.use-custom-mysql-create-session-attribute-query", havingValue = "true")
26+
public class CustomCreateSessionAttributeInsertQueryConfiguration extends JdbcHttpSessionConfiguration {
27+
28+
static Logger logger = LoggerFactory.getLogger(CustomCreateSessionAttributeInsertQueryConfiguration.class);
29+
30+
private static final String CREATE_SESSION_ATTRIBUTE_QUERY_ON_DUPLICATE_KEY_UPDATE =
31+
"INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
32+
+ "SELECT PRIMARY_ID, ?, ? "
33+
+ "FROM %TABLE_NAME% "
34+
+ "WHERE SESSION_ID = ? ON DUPLICATE KEY UPDATE ATTRIBUTE_BYTES=VALUES(ATTRIBUTE_BYTES)";
35+
36+
@Autowired
37+
DBUtils dbUtils;
38+
39+
@Value("${spring.session.jdbc.table-name}")
40+
private String customTableName;
41+
42+
@Bean
43+
@Override
44+
public JdbcIndexedSessionRepository sessionRepository() {
45+
logger.debug("Setting Spring Session custom session attribute query.");
46+
JdbcIndexedSessionRepository sessionRepository = super.sessionRepository();
47+
sessionRepository.setTableName(getTableName());
48+
updateCreateSessionAttributeQuery(sessionRepository);
49+
return sessionRepository;
50+
}
51+
52+
private void updateCreateSessionAttributeQuery(JdbcIndexedSessionRepository sessionRepository) {
53+
logger.debug("Updating the Create Session Attribute query");
54+
if (dbUtils.isMysql()) {
55+
sessionRepository.setCreateSessionAttributeQuery(getCustomCreateSessionAttributeQuery());
56+
} else {
57+
logger.warn("The database is not MySQL, skipping query update.");
58+
}
59+
}
60+
61+
private String getCustomCreateSessionAttributeQuery() {
62+
return StringUtils.replace(
63+
CREATE_SESSION_ATTRIBUTE_QUERY_ON_DUPLICATE_KEY_UPDATE,
64+
"%TABLE_NAME%",
65+
getTableName());
66+
}
67+
68+
private String getTableName() {
69+
return customTableName != null ? customTableName : JdbcIndexedSessionRepository.DEFAULT_TABLE_NAME;
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.box.l10n.mojito.service.security.user.session;
2+
3+
import com.box.l10n.mojito.service.DBUtils;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
import org.springframework.boot.test.mock.mockito.MockBean;
10+
import org.springframework.boot.test.mock.mockito.SpyBean;
11+
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
12+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
13+
import org.springframework.transaction.PlatformTransactionManager;
14+
import org.springframework.util.ReflectionUtils;
15+
16+
import javax.sql.DataSource;
17+
import java.lang.reflect.Field;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNotNull;
21+
import static org.mockito.Mockito.times;
22+
import static org.mockito.Mockito.verify;
23+
24+
@RunWith(SpringJUnit4ClassRunner.class)
25+
@SpringBootTest(classes = {
26+
CustomCreateSessionAttributeInsertQueryConfiguration.class
27+
}, properties = {
28+
"l10n.spring.session.use-custom-mysql-create-session-attribute-query=true",
29+
"spring.session.jdbc.table-name=test_table",
30+
"spring.datasource.url=jdbc:mysql:testDB"
31+
})
32+
public class CustomCreateSessionAttributeInsertQueryConfigurationTest {
33+
34+
@MockBean
35+
DataSource dataSourceMock;
36+
37+
@MockBean
38+
PlatformTransactionManager platformTransactionManagerMock;
39+
40+
@SpyBean
41+
DBUtils dbUtils;
42+
43+
@Autowired
44+
JdbcIndexedSessionRepository jdbcIndexedSessionRepository;
45+
46+
String requiredString = "INSERT INTO test_table_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) "
47+
+ "SELECT PRIMARY_ID, ?, ? "
48+
+ "FROM test_table "
49+
+ "WHERE SESSION_ID = ? ON DUPLICATE KEY UPDATE ATTRIBUTE_BYTES=VALUES(ATTRIBUTE_BYTES)";
50+
51+
@Test
52+
public void testCustomSessionAttributeQueryIsSetOnSessionRepository() throws IllegalAccessException {
53+
assertNotNull(jdbcIndexedSessionRepository);
54+
Field createSessionAttributeQueryField = ReflectionUtils.findField(jdbcIndexedSessionRepository.getClass(), "createSessionAttributeQuery", String.class);
55+
createSessionAttributeQueryField.setAccessible(true);
56+
String actualQueryString = (String) createSessionAttributeQueryField.get(jdbcIndexedSessionRepository);
57+
verify(dbUtils, times(1)).isMysql();
58+
assertEquals(requiredString, actualQueryString);
59+
}
60+
}

0 commit comments

Comments
 (0)