From 00e7110594e39bd81da79934719ad491275ddb80 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Wed, 20 Apr 2016 00:09:01 +0200 Subject: [PATCH] Implement individual attribute persistence in JdbcOperationsSessionRepository --- docs/src/docs/asciidoc/index.adoc | 3 +- .../jdbc/JdbcOperationsSessionRepository.java | 318 ++++++++++++------ .../session/jdbc/schema-db2.sql | 13 +- .../session/jdbc/schema-derby.sql | 13 +- .../session/jdbc/schema-h2.sql | 13 +- .../session/jdbc/schema-hsqldb.sql | 13 +- .../session/jdbc/schema-mysql.sql | 13 +- .../session/jdbc/schema-oracle.sql | 13 +- .../session/jdbc/schema-postgresql.sql | 13 +- .../session/jdbc/schema-sqlite.sql | 13 +- .../session/jdbc/schema-sqlserver.sql | 13 +- .../session/jdbc/schema-sybase.sql | 13 +- .../JdbcOperationsSessionRepositoryTests.java | 41 ++- 13 files changed, 360 insertions(+), 132 deletions(-) diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 960e421..48a43be 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -1037,7 +1037,8 @@ However, you can override the default `ConversionService` by providing a Bean na [[api-jdbcoperationssessionrepository-storage]] ==== Storage Details -By default, this implementation uses `SPRING_SESSION` table to store sessions. Note that the table name can be easily customized as already described. +By default, this implementation uses `SPRING_SESSION` and `SPRING_SESSION_ATTRIBUTES` tables to store sessions. +Note that the table name can be easily customized as already described. In that case the table used to store attributes will be named using the provided table name, suffixed with `_ATTRIBUTES`. Due to the differences between the various database vendors, especially when it comes to storing binary data, make sure to use SQL script specific to your database. Scripts for most major database vendors are packaged as `org/springframework/session/jdbc/schema-\*.sql`, where `*` is the target database type. diff --git a/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java index 720343b..8a6e498 100644 --- a/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/jdbc/JdbcOperationsSessionRepository.java @@ -16,9 +16,11 @@ package org.springframework.session.jdbc; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -36,11 +38,12 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.serializer.support.DeserializingConverter; import org.springframework.core.serializer.support.SerializingConverter; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.lob.DefaultLobHandler; @@ -84,20 +87,35 @@ import org.springframework.util.StringUtils; * * Spring Framework Reference Documentation. *

- * By default, this implementation uses SPRING_SESSION table to store - * sessions. Note that the table name can be customized using the - * {@link #setTableName(String)} method. + * By default, this implementation uses SPRING_SESSION and + * SPRING_SESSION_ATTRIBUTES tables to store sessions. Note that the table + * name can be customized using the {@link #setTableName(String)} method. In that case the + * table used to store attributes will be named using the provided table name, suffixed + * with _ATTRIBUTES. * * Depending on your database, the table definition can be described as below: * *

  * CREATE TABLE SPRING_SESSION (
  *   SESSION_ID CHAR(36),
+ *   CREATION_TIME BIGINT NOT NULL,
  *   LAST_ACCESS_TIME BIGINT NOT NULL,
+ *   MAX_INACTIVE_INTERVAL INT NOT NULL,
  *   PRINCIPAL_NAME VARCHAR(100),
- *   SESSION_BYTES BLOB,
  *   CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
  * );
+ *
+ * CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
+ *
+ * CREATE TABLE SPRING_SESSION_ATTRIBUTES (
+ *  SESSION_ID CHAR(36),
+ *  ATTRIBUTE_NAME VARCHAR(100),
+ *  ATTRIBUTE_BYTES BYTEA,
+ *  CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
+ *  CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
+ * );
+ *
+ * CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);
  * 
* * Due to the differences between the various database vendors, especially when it comes @@ -114,19 +132,49 @@ public class JdbcOperationsSessionRepository implements private static final String DEFAULT_TABLE_NAME = "SPRING_SESSION"; - private static final String CREATE_SESSION_QUERY = "INSERT INTO %TABLE_NAME%(SESSION_ID, LAST_ACCESS_TIME, PRINCIPAL_NAME, SESSION_BYTES) VALUES (?, ?, ?, ?)"; + private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; - private static final String GET_SESSION_QUERY = "SELECT LAST_ACCESS_TIME, SESSION_BYTES FROM %TABLE_NAME% WHERE SESSION_ID = ?"; + private static final String CREATE_SESSION_QUERY = + "INSERT INTO %TABLE_NAME%(SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, PRINCIPAL_NAME) " + + "VALUES (?, ?, ?, ?, ?)"; - private static final String UPDATE_SESSION_QUERY = "UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ?, PRINCIPAL_NAME = ?, SESSION_BYTES = ? WHERE SESSION_ID = ?"; + private static final String CREATE_SESSION_ATTRIBUTE_QUERY = + "INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) " + + "VALUES (?, ?, ?)"; - private static final String UPDATE_SESSION_LAST_ACCESS_TIME_QUERY = "UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ? WHERE SESSION_ID = ?"; + private static final String GET_SESSION_QUERY = + "SELECT 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.SESSION_ID = SA.SESSION_ID " + + "WHERE S.SESSION_ID = ?"; - private static final String DELETE_SESSION_QUERY = "DELETE FROM %TABLE_NAME% WHERE SESSION_ID = ?"; + private static final String UPDATE_SESSION_QUERY = + "UPDATE %TABLE_NAME% SET LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, PRINCIPAL_NAME = ? " + + "WHERE SESSION_ID = ?"; - private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT LAST_ACCESS_TIME, SESSION_BYTES FROM %TABLE_NAME% WHERE PRINCIPAL_NAME = ?"; + private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = + "UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? " + + "WHERE SESSION_ID = ? " + + "AND ATTRIBUTE_NAME = ?"; - private static final String DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY = "DELETE FROM %TABLE_NAME% WHERE LAST_ACCESS_TIME < ?"; + private static final String DELETE_SESSION_ATTRIBUTE_QUERY = + "DELETE FROM %TABLE_NAME%_ATTRIBUTES " + + "WHERE SESSION_ID = ? " + + "AND ATTRIBUTE_NAME = ?"; + + private static final String DELETE_SESSION_QUERY = + "DELETE FROM %TABLE_NAME% " + + "WHERE SESSION_ID = ?"; + + private static final String LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY = + "SELECT 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.SESSION_ID = SA.SESSION_ID " + + "WHERE S.PRINCIPAL_NAME = ?"; + + private static final String DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY = + "DELETE FROM %TABLE_NAME% " + + "WHERE LAST_ACCESS_TIME < ?"; private static final Log logger = LogFactory .getLog(JdbcOperationsSessionRepository.class); @@ -231,9 +279,27 @@ public class JdbcOperationsSessionRepository implements public void setValues(PreparedStatement ps) throws SQLException { ps.setString(1, session.getId()); - ps.setLong(2, session.getLastAccessedTime()); - ps.setString(3, session.getPrincipalName()); - serialize(ps, 4, session.delegate); + ps.setLong(2, session.getCreationTime()); + ps.setLong(3, session.getLastAccessedTime()); + ps.setInt(4, session.getMaxInactiveIntervalInSeconds()); + ps.setString(5, session.getPrincipalName()); + } + + }); + final List attributeNames = new ArrayList(session.getAttributeNames()); + JdbcOperationsSessionRepository.this.jdbcOperations.batchUpdate( + getQuery(CREATE_SESSION_ATTRIBUTE_QUERY), + new BatchPreparedStatementSetter() { + + public void setValues(PreparedStatement ps, int i) throws SQLException { + String attributeName = attributeNames.get(i); + ps.setString(1, session.getId()); + ps.setString(2, attributeName); + serialize(ps, 3, session.getAttribute(attributeName)); + } + + public int getBatchSize() { + return attributeNames.size(); } }); @@ -242,10 +308,10 @@ public class JdbcOperationsSessionRepository implements }); } else { - if (session.isAttributesChanged()) { - this.transactionOperations.execute(new TransactionCallbackWithoutResult() { + this.transactionOperations.execute(new TransactionCallbackWithoutResult() { - protected void doInTransactionWithoutResult(TransactionStatus status) { + protected void doInTransactionWithoutResult(TransactionStatus status) { + if (session.isChanged()) { JdbcOperationsSessionRepository.this.jdbcOperations.update( getQuery(UPDATE_SESSION_QUERY), new PreparedStatementSetter() { @@ -253,55 +319,84 @@ public class JdbcOperationsSessionRepository implements public void setValues(PreparedStatement ps) throws SQLException { ps.setLong(1, session.getLastAccessedTime()); - ps.setString(2, session.getPrincipalName()); - serialize(ps, 3, session.delegate); + ps.setInt(2, session.getMaxInactiveIntervalInSeconds()); + ps.setString(3, session.getPrincipalName()); ps.setString(4, session.getId()); } }); } + Map delta = session.getDelta(); + if (!delta.isEmpty()) { + for (final Map.Entry entry : delta.entrySet()) { + if (entry.getValue() == null) { + JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(DELETE_SESSION_ATTRIBUTE_QUERY), + new PreparedStatementSetter() { - }); - } - else if (session.isLastAccessTimeChanged()) { - this.transactionOperations.execute(new TransactionCallbackWithoutResult() { + public void setValues(PreparedStatement ps) throws SQLException { + ps.setString(1, session.getId()); + ps.setString(2, entry.getKey()); + } - protected void doInTransactionWithoutResult(TransactionStatus status) { - JdbcOperationsSessionRepository.this.jdbcOperations.update( - getQuery(UPDATE_SESSION_LAST_ACCESS_TIME_QUERY), - new PreparedStatementSetter() { + }); + } + else { + int updatedCount = JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY), + new PreparedStatementSetter() { - public void setValues(PreparedStatement ps) - throws SQLException { - ps.setLong(1, session.getLastAccessedTime()); - ps.setString(2, session.getId()); - } + public void setValues(PreparedStatement ps) throws SQLException { + serialize(ps, 1, entry.getValue()); + ps.setString(2, session.getId()); + ps.setString(3, entry.getKey()); + } - }); + }); + if (updatedCount == 0) { + JdbcOperationsSessionRepository.this.jdbcOperations.update( + getQuery(CREATE_SESSION_ATTRIBUTE_QUERY), + new PreparedStatementSetter() { + + public void setValues(PreparedStatement ps) throws SQLException { + ps.setString(1, session.getId()); + ps.setString(2, entry.getKey()); + serialize(ps, 3, entry.getValue()); + } + + }); + } + } + } } + } - }); - } - else { - return; - } + }); } session.clearChangeFlags(); } public JdbcSession getSession(final String id) { - ExpiringSession session = this.transactionOperations.execute(new TransactionCallback() { + final ExpiringSession session = this.transactionOperations.execute(new TransactionCallback() { public ExpiringSession doInTransaction(TransactionStatus status) { - try { - return JdbcOperationsSessionRepository.this.jdbcOperations.queryForObject( - getQuery(GET_SESSION_QUERY), - new Object[] { id }, - JdbcOperationsSessionRepository.this.mapper); - } - catch (EmptyResultDataAccessException ignored) { + List sessions = JdbcOperationsSessionRepository.this.jdbcOperations.query( + new PreparedStatementCreator() { + + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement ps = con.prepareStatement(getQuery(GET_SESSION_QUERY), + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + ps.setString(1, id); + return ps; + } + + }, + JdbcOperationsSessionRepository.this.mapper + ); + if (sessions.isEmpty()) { return null; } + return sessions.get(0); } }); @@ -338,9 +433,19 @@ public class JdbcOperationsSessionRepository implements public List doInTransaction(TransactionStatus status) { return JdbcOperationsSessionRepository.this.jdbcOperations.query( - getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY), - new Object[] { indexValue }, - JdbcOperationsSessionRepository.this.mapper); + new PreparedStatementCreator() { + + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement ps = con.prepareStatement( + getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY), + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + ps.setString(1, indexValue); + return ps; + } + + }, + JdbcOperationsSessionRepository.this.mapper + ); } }); @@ -402,9 +507,9 @@ public class JdbcOperationsSessionRepository implements private static GenericConversionService createDefaultConversionService() { GenericConversionService converter = new GenericConversionService(); - converter.addConverter(ExpiringSession.class, byte[].class, + converter.addConverter(Object.class, byte[].class, new SerializingConverter()); - converter.addConverter(byte[].class, ExpiringSession.class, + converter.addConverter(byte[].class, Object.class, new DeserializingConverter()); return converter; } @@ -413,20 +518,20 @@ public class JdbcOperationsSessionRepository implements return StringUtils.replace(base, "%TABLE_NAME%", this.tableName); } - private void serialize(PreparedStatement ps, int paramIndex, ExpiringSession session) + private void serialize(PreparedStatement ps, int paramIndex, Object attributeValue) throws SQLException { this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex, - (byte[]) this.conversionService.convert(session, - TypeDescriptor.valueOf(ExpiringSession.class), + (byte[]) this.conversionService.convert(attributeValue, + TypeDescriptor.valueOf(Object.class), TypeDescriptor.valueOf(byte[].class))); } - private ExpiringSession deserialize(ResultSet rs, String columnName) + private Object deserialize(ResultSet rs, String columnName) throws SQLException { - return (ExpiringSession) this.conversionService.convert( + return this.conversionService.convert( this.lobHandler.getBlobAsBytes(rs, columnName), TypeDescriptor.valueOf(byte[].class), - TypeDescriptor.valueOf(ExpiringSession.class)); + TypeDescriptor.valueOf(Object.class)); } /** @@ -440,9 +545,9 @@ public class JdbcOperationsSessionRepository implements private boolean isNew; - private boolean lastAccessTimeChanged; + private boolean changed; - private boolean attributesChanged; + private Map delta = new HashMap(); JdbcSession() { this.delegate = new MapSession(); @@ -454,54 +559,28 @@ public class JdbcOperationsSessionRepository implements this.delegate = delegate; } - public boolean isNew() { + boolean isNew() { return this.isNew; } - public boolean isLastAccessTimeChanged() { - return this.lastAccessTimeChanged; + boolean isChanged() { + return this.changed; } - public boolean isAttributesChanged() { - return this.attributesChanged; + Map getDelta() { + return this.delta; } - public void clearChangeFlags() { + void clearChangeFlags() { this.isNew = false; - this.lastAccessTimeChanged = false; - this.attributesChanged = false; + this.changed = false; + this.delta.clear(); } - public String getPrincipalName() { + String getPrincipalName() { return PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this); } - public long getCreationTime() { - return this.delegate.getCreationTime(); - } - - public void setLastAccessedTime(long lastAccessedTime) { - this.delegate.setLastAccessedTime(lastAccessedTime); - this.lastAccessTimeChanged = true; - } - - public long getLastAccessedTime() { - return this.delegate.getLastAccessedTime(); - } - - public void setMaxInactiveIntervalInSeconds(int interval) { - this.delegate.setMaxInactiveIntervalInSeconds(interval); - this.attributesChanged = true; - } - - public int getMaxInactiveIntervalInSeconds() { - return this.delegate.getMaxInactiveIntervalInSeconds(); - } - - public boolean isExpired() { - return this.delegate.isExpired(); - } - public String getId() { return this.delegate.getId(); } @@ -516,12 +595,42 @@ public class JdbcOperationsSessionRepository implements public void setAttribute(String attributeName, Object attributeValue) { this.delegate.setAttribute(attributeName, attributeValue); - this.attributesChanged = true; + this.delta.put(attributeName, attributeValue); + if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) || + SPRING_SECURITY_CONTEXT.equals(attributeName)) { + this.changed = true; + } } public void removeAttribute(String attributeName) { this.delegate.removeAttribute(attributeName); - this.attributesChanged = true; + this.delta.put(attributeName, null); + } + + public long getCreationTime() { + return this.delegate.getCreationTime(); + } + + public void setLastAccessedTime(long lastAccessedTime) { + this.delegate.setLastAccessedTime(lastAccessedTime); + this.changed = true; + } + + public long getLastAccessedTime() { + return this.delegate.getLastAccessedTime(); + } + + public void setMaxInactiveIntervalInSeconds(int interval) { + this.delegate.setMaxInactiveIntervalInSeconds(interval); + this.changed = true; + } + + public int getMaxInactiveIntervalInSeconds() { + return this.delegate.getMaxInactiveIntervalInSeconds(); + } + + public boolean isExpired() { + return this.delegate.isExpired(); } } @@ -533,8 +642,6 @@ public class JdbcOperationsSessionRepository implements */ static class PrincipalNameResolver { - private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; - private SpelExpressionParser parser = new SpelExpressionParser(); public String resolvePrincipal(Session session) { @@ -556,8 +663,19 @@ public class JdbcOperationsSessionRepository implements private class ExpiringSessionMapper implements RowMapper { public ExpiringSession mapRow(ResultSet rs, int rowNum) throws SQLException { - ExpiringSession session = deserialize(rs, "SESSION_BYTES"); + MapSession session = new MapSession(rs.getString("SESSION_ID")); + session.setCreationTime(rs.getLong("CREATION_TIME")); session.setLastAccessedTime(rs.getLong("LAST_ACCESS_TIME")); + session.setMaxInactiveIntervalInSeconds(rs.getInt("MAX_INACTIVE_INTERVAL")); + String attributeName = rs.getString("ATTRIBUTE_NAME"); + if (attributeName != null) { + session.setAttribute(attributeName, deserialize(rs, "ATTRIBUTE_BYTES")); + while (rs.next() && session.getId().equals(rs.getString("SESSION_ID"))) { + session.setAttribute(rs.getString("ATTRIBUTE_NAME"), + deserialize(rs, "ATTRIBUTE_BYTES")); + } + rs.previous(); + } return session; } diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-db2.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-db2.sql index df2e15f..fb0e841 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-db2.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-db2.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES BLOB, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES BLOB, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-derby.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-derby.sql index df2e15f..fb0e841 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-derby.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-derby.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES BLOB, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES BLOB, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-h2.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-h2.sql index 0d1fd13..84d9bc9 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-h2.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-h2.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES LONGVARBINARY, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES LONGVARBINARY, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-hsqldb.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-hsqldb.sql index 0d1fd13..84d9bc9 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-hsqldb.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-hsqldb.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES LONGVARBINARY, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES LONGVARBINARY, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql index 1b041ea..9a6ceaf 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-mysql.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES BLOB, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ) ENGINE=InnoDB; CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES BLOB, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +) ENGINE=InnoDB; + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql index abb50b1..2493a9d 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME NUMBER(19,0) NOT NULL, LAST_ACCESS_TIME NUMBER(19,0) NOT NULL, + MAX_INACTIVE_INTERVAL NUMBER(10,0) NOT NULL, PRINCIPAL_NAME VARCHAR2(100 CHAR), - SESSION_BYTES BLOB, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR2(100 CHAR), + ATTRIBUTE_BYTES BLOB, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-postgresql.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-postgresql.sql index e863704..7713613 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-postgresql.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-postgresql.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES BYTEA, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES BYTEA, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlite.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlite.sql index 1388988..55eddd5 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlite.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlite.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHARACTER(36), + CREATION_TIME INTEGER NOT NULL, LAST_ACCESS_TIME INTEGER NOT NULL, + MAX_INACTIVE_INTERVAL INTEGER NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES BLOB, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES BLOB, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlserver.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlserver.sql index c81c482..666727b 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlserver.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sqlserver.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES IMAGE, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ); CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES IMAGE, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sybase.sql b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sybase.sql index e795d96..635a315 100644 --- a/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sybase.sql +++ b/spring-session/src/main/resources/org/springframework/session/jdbc/schema-sybase.sql @@ -1,9 +1,20 @@ CREATE TABLE SPRING_SESSION ( SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, PRINCIPAL_NAME VARCHAR(100), - SESSION_BYTES IMAGE, CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID) ) LOCK DATAROWS; CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME); + +CREATE TABLE SPRING_SESSION_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES IMAGE, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE +) LOCK DATAROWS; + +CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID); diff --git a/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java b/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java index 077fdb8..6039e90 100644 --- a/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java +++ b/spring-session/src/test/java/org/springframework/session/jdbc/JdbcOperationsSessionRepositoryTests.java @@ -16,6 +16,7 @@ package org.springframework.session.jdbc; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -31,6 +32,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -44,7 +46,6 @@ import org.springframework.transaction.TransactionDefinition; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.AdditionalMatchers.and; -import static org.mockito.AdditionalMatchers.not; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.contains; @@ -200,7 +201,7 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(session.isNew()).isFalse(); assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update( - and(startsWith("UPDATE"), contains("SESSION_BYTES")), + and(startsWith("UPDATE"), contains("ATTRIBUTE_BYTES")), isA(PreparedStatementSetter.class)); } @@ -215,7 +216,7 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(session.isNew()).isFalse(); assertPropagationRequiresNew(); verify(this.jdbcOperations, times(1)).update( - and(startsWith("UPDATE"), not(contains("SESSION_BYTES"))), + and(startsWith("UPDATE"), contains("LAST_ACCESS_TIME")), isA(PreparedStatementSetter.class)); } @@ -239,8 +240,8 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(session).isNull(); assertPropagationRequiresNew(); - verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"), - eq(new Object[] { sessionId }), isA(RowMapper.class)); + verify(this.jdbcOperations, times(1)).query( + isA(PreparedStatementCreator.class), isA(RowMapper.class)); } @Test @@ -248,17 +249,16 @@ public class JdbcOperationsSessionRepositoryTests { MapSession expired = new MapSession(); expired.setLastAccessedTime(System.currentTimeMillis() - (MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS * 1000 + 1000)); - given(this.jdbcOperations.queryForObject(startsWith("SELECT"), - eq(new Object[] { expired.getId() }), isA(RowMapper.class))) - .willReturn(expired); + given(this.jdbcOperations.query(isA(PreparedStatementCreator.class), + isA(RowMapper.class))).willReturn(Collections.singletonList(expired)); JdbcOperationsSessionRepository.JdbcSession session = this.repository .getSession(expired.getId()); assertThat(session).isNull(); assertPropagationRequiresNew(); - verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"), - eq(new Object[] { expired.getId() }), isA(RowMapper.class)); + verify(this.jdbcOperations, times(1)).query( + isA(PreparedStatementCreator.class), isA(RowMapper.class)); verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(expired.getId())); } @@ -267,9 +267,8 @@ public class JdbcOperationsSessionRepositoryTests { public void getSessionFound() { MapSession saved = new MapSession(); saved.setAttribute("savedName", "savedValue"); - given(this.jdbcOperations.queryForObject(startsWith("SELECT"), - eq(new Object[] { saved.getId() }), isA(RowMapper.class))) - .willReturn(saved); + given(this.jdbcOperations.query(isA(PreparedStatementCreator.class), + isA(RowMapper.class))).willReturn(Collections.singletonList(saved)); JdbcOperationsSessionRepository.JdbcSession session = this.repository .getSession(saved.getId()); @@ -278,8 +277,8 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(session.isNew()).isFalse(); assertThat(session.getAttribute("savedName")).isEqualTo("savedValue"); assertPropagationRequiresNew(); - verify(this.jdbcOperations, times(1)).queryForObject(startsWith("SELECT"), - eq(new Object[] { saved.getId() }), isA(RowMapper.class)); + verify(this.jdbcOperations, times(1)).query( + isA(PreparedStatementCreator.class), isA(RowMapper.class)); } @Test @@ -314,8 +313,8 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(sessions).isEmpty(); assertPropagationRequiresNew(); - verify(this.jdbcOperations, times(1)).query(startsWith("SELECT"), - eq(new Object[] { principal }), isA(RowMapper.class)); + verify(this.jdbcOperations, times(1)).query( + isA(PreparedStatementCreator.class), isA(RowMapper.class)); } @Test @@ -330,8 +329,8 @@ public class JdbcOperationsSessionRepositoryTests { MapSession saved2 = new MapSession(); saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication); saved.add(saved2); - given(this.jdbcOperations.query(startsWith("SELECT"), - eq(new Object[] { principal }), isA(RowMapper.class))).willReturn(saved); + given(this.jdbcOperations.query(isA(PreparedStatementCreator.class), + isA(RowMapper.class))).willReturn(saved); Map sessions = this.repository .findByIndexNameAndIndexValue( @@ -340,8 +339,8 @@ public class JdbcOperationsSessionRepositoryTests { assertThat(sessions).hasSize(2); assertPropagationRequiresNew(); - verify(this.jdbcOperations, times(1)).query(startsWith("SELECT"), - eq(new Object[] { principal }), isA(RowMapper.class)); + verify(this.jdbcOperations, times(1)).query( + isA(PreparedStatementCreator.class), isA(RowMapper.class)); } @Test