JDBC

Spring Session JDBC をアプリケーションに追加する

Spring Session JDBC を使用するには、アプリケーションに org.springframework.session:spring-session-jdbc 依存関係を追加する必要があります。

implementation 'org.springframework.session:spring-session-jdbc'
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>

Spring Boot を使用している場合は、Spring Session JDBC の有効化が処理されます。詳細については、そのドキュメントを参照してください。それ以外の場合は、@EnableJdbcHttpSession を構成クラスに追加する必要があります。

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {
    //...
}

以上で、アプリケーションは Spring Session JDBC を使用するように構成されます。

セッションストレージの詳細を理解する

デフォルトでは、実装では SPRING_SESSION テーブルと SPRING_SESSION_ATTRIBUTES テーブルを使用してセッションを保存します。テーブル名をカスタマイズする場合、属性の格納に使用されるテーブルの名前は、指定されたテーブル名に接尾辞 _ATTRIBUTES を付けて付けられることに注意してください。さらにカスタマイズが必要な場合は、リポジトリで使用される SQL クエリをカスタマイズできます。

さまざまなデータベースベンダー間の違いのため、特にバイナリデータの保存に関しては、データベースに固有の SQL スクリプトを使用してください。ほとんどの主要なデータベースベンダーのスクリプトは、org/springframework/session/jdbc/schema-*.sql としてパッケージ化されています。ここで、* はターゲットデータベース型です。

例: PostgreSQL では、次のスキーマスクリプトを使用できます。

CREATE TABLE SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES BYTEA NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);

テーブル名のカスタマイズ

データベーステーブル名をカスタマイズするには、@EnableJdbcHttpSession アノテーションの tableName 属性を使用できます。

  • Java

@Configuration
@EnableJdbcHttpSession(tableName = "MY_TABLE_NAME")
public class SessionConfig {
    //...
}

もう 1 つの方法は、SessionRepositoryCustomizer<JdbcIndexedSessionRepository> の実装を Bean として公開し、実装内でテーブルを直接変更することです。

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean
    public TableNameCustomizer tableNameCustomizer() {
        return new TableNameCustomizer();
    }

}

public class TableNameCustomizer
        implements SessionRepositoryCustomizer<JdbcIndexedSessionRepository> {

    @Override
    public void customize(JdbcIndexedSessionRepository sessionRepository) {
        sessionRepository.setTableName("MY_TABLE_NAME");
    }

}

SQL クエリのカスタマイズ

Spring Session JDBC によって実行される SQL クエリをカスタマイズできると便利な場合があります。データベース内のセッションまたはその属性に同時に変更が加えられる可能性があるシナリオがあります。たとえば、リクエストですでに存在する属性を挿入する必要があり、重複キー例外が発生する可能性があります。そのため、そのようなシナリオを処理する RDBMS 固有のクエリを適用できます。Spring Session JDBC がデータベースに対して実行する SQL クエリをカスタマイズするには、JdbcIndexedSessionRepository の set*Query メソッドを使用できます。

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean
    public QueryCustomizer tableNameCustomizer() {
        return new QueryCustomizer();
    }

}

public class QueryCustomizer
        implements SessionRepositoryCustomizer<JdbcIndexedSessionRepository> {

    private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
            INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) (1)
            VALUES (?, ?, ?)
            ON CONFLICT (SESSION_PRIMARY_ID, ATTRIBUTE_NAME)
            DO NOTHING
            """;

    private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
		UPDATE %TABLE_NAME%_ATTRIBUTES
		SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
		WHERE SESSION_PRIMARY_ID = ?
		AND ATTRIBUTE_NAME = ?
		""";

    @Override
    public void customize(JdbcIndexedSessionRepository sessionRepository) {
        sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
        sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
    }

}
1 クエリ内の %TABLE_NAME% プレースホルダーは、JdbcIndexedSessionRepository で使用されている構成されたテーブル名に置き換えられます。

Spring Session JDBC には、最も一般的な RDBMS 用に最適化された SQL クエリを構成する SessionRepositoryCustomizer<JdbcIndexedSessionRepository> の実装がいくつか付属しています。

セッション属性を JSON として保存する

デフォルトでは、Spring Session JDBC はセッション属性値をバイト配列として保存します。この配列は属性値の JDK 直列化の結果です。

場合によっては、セッション属性を JSON などのさまざまな形式で保存すると便利です。これにより、RDBMS でネイティブサポートが提供され、SQL クエリでの関数と演算子の互換性が向上する可能性があります。

この例では、RDBMS として PostgreSQL (英語) を使用し、JDK 直列化ではなく JSON を使用してセッション属性値を直列化します。まず、attribute_values 列に jsonb 型を持つ SPRING_SESSION_ATTRIBUTES テーブルを作成しましょう。

  • SQL

CREATE TABLE SPRING_SESSION
(
    -- ...
);

-- indexes...

CREATE TABLE SPRING_SESSION_ATTRIBUTES
(
    -- ...
    ATTRIBUTE_BYTES    JSONB        NOT NULL,
    -- ...
);

属性値の直列化方法をカスタマイズするには、まず、Object から byte[] へ、またはその逆の変換を担当するカスタム ConversionService を Spring Session JDBC に提供する必要があります。これを行うには、springSessionConversionService という名前の、型 ConversionService の Bean を作成します。

  • Java

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;

@Configuration
@EnableJdbcHttpSession
public class SessionConfig implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Bean("springSessionConversionService")
    public GenericConversionService springSessionConversionService(ObjectMapper objectMapper) { (1)
        ObjectMapper copy = objectMapper.copy(); (2)
        // Register Spring Security Jackson Modules
        copy.registerModules(SecurityJackson2Modules.getModules(this.classLoader)); (3)
        // Activate default typing explicitly if not using Spring Security
        // copy.activateDefaultTyping(copy.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        GenericConversionService converter = new GenericConversionService();
        converter.addConverter(Object.class, byte[].class, new SerializingConverter(new JsonSerializer(copy))); (4)
        converter.addConverter(byte[].class, Object.class, new DeserializingConverter(new JsonDeserializer(copy))); (4)
        return converter;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    static class JsonSerializer implements Serializer<Object> {

        private final ObjectMapper objectMapper;

        JsonSerializer(ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
        }

        @Override
        public void serialize(Object object, OutputStream outputStream) throws IOException {
            this.objectMapper.writeValue(outputStream, object);
        }

    }

    static class JsonDeserializer implements Deserializer<Object> {

        private final ObjectMapper objectMapper;

        JsonDeserializer(ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
        }

        @Override
        public Object deserialize(InputStream inputStream) throws IOException {
            return this.objectMapper.readValue(inputStream, Object.class);
        }

    }

}
1 アプリケーションでデフォルトで使用される ObjectMapper を挿入します。必要に応じて、新しいものを作成することもできます。
2 その ObjectMapper のコピーを作成して、そのコピーにのみ変更を適用します。
3Spring Security を使用しているため、Spring Security のオブジェクトを適切に直列化 / 逆直列化する方法を Jackson に指示する Jackson モジュールを登録する必要があります。セッション内に保持されている他のオブジェクトに対しても同じことを行う必要がある場合があります。
4 作成した JsonSerializer/JsonDeserializer を ConversionService に追加します。

Spring Session JDBC が属性値を byte[] に変換する方法を構成したため、セッション属性を挿入および更新するクエリをカスタマイズする必要があります。カスタマイズが必要なのは、Spring Session JDBC が SQL 文でコンテンツをバイトとして設定するためですが、bytea は jsonb と互換性がないため、bytea 値をテキストにエンコードしてから jsonb に変換する必要があります。

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
            INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
            VALUES (?, ?, encode(?, 'escape')::jsonb) (1)
            """;

    private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
            UPDATE %TABLE_NAME%_ATTRIBUTES
            SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
            WHERE SESSION_PRIMARY_ID = ?
            AND ATTRIBUTE_NAME = ?
            """;

    @Bean
    SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
        return (sessionRepository) -> {
            sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
            sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
        };
    }

}
1PostgreSQL エンコード (英語) 関数を使用して bytea から text に変換します

これで、データベースに JSON として保存されたセッション属性を確認できるようになります。実装全体を確認してテストを実行できるサンプルが用意されています [GitHub] (英語)

UserDetails の実装が Spring Security の org.springframework.security.core.userdetails.User クラスを継承する場合、カスタムデシリアライザーを登録することが重要です。そうしないと、Jackson は既存の org.springframework.security.jackson2.UserDeserializer を使用し、期待どおりの UserDetails 実装にはなりません。詳細については、gh-3009 [GitHub] (英語) を参照してください。

代替 DataSource の指定

デフォルトでは、Spring Session JDBC は、アプリケーションで使用可能なプライマリ DataSource Bean を使用します。ただし、アプリケーションに複数の DataSourceBean が含まれるシナリオがいくつかあります。そのようなシナリオでは、Bean を @SpringSessionDataSource で修飾することで、どの DataSource を使用するかを Spring Session JDBC に指示できます。

  • Java

import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean
    public DataSource dataSourceOne() {
        // create and configure datasource
        return dataSourceOne;
    }

    @Bean
    @SpringSessionDataSource (1)
    public DataSource dataSourceTwo() {
        // create and configure datasource
        return dataSourceTwo;
    }

}
1dataSourceTwo Bean に @SpringSessionDataSource のアノテーションを付けて、その Bean を DataSource として使用する必要があることを Spring Session JDBC に伝えます。

Spring Session JDBC によるトランザクションの使用方法のカスタマイズ

すべての JDBC 操作はトランザクション方式で実行されます。既存のトランザクションへの干渉による予期しない動作 (たとえば、読み取り専用トランザクションにすでに参加しているスレッドでの保存操作の実行など) を回避するために、トランザクションは伝播を REQUIRES_NEW に設定して実行されます。Spring Session JDBC がトランザクションを使用する方法をカスタマイズするには、springSessionTransactionOperations という名前の TransactionOperations Bean を提供します。例: トランザクション全体を無効にしたい場合は、次のように実行できます。

  • Java

import org.springframework.transaction.support.TransactionOperations;

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean("springSessionTransactionOperations")
    public TransactionOperations springSessionTransactionOperations() {
        return TransactionOperations.withoutTransaction();
    }

}

より詳細な制御が必要な場合は、構成された TransactionTemplate によって使用される TransactionManager を提供することもできます。デフォルトでは、Spring Session はアプリケーションコンテキストからプライマリ TransactionManager Bean を解決しようとします。一部のシナリオでは、たとえば、複数の DataSource がある場合、複数の TransactionManager が存在する可能性が非常に高いため、@SpringSessionTransactionManager で修飾することで、どの TransactionManager Bean を Spring Session JDBC で使用するかを判断できます。

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean
    @SpringSessionTransactionManager
    public TransactionManager transactionManager1() {
        return new MyTransactionManager();
    }

    @Bean
    public TransactionManager transactionManager2() {
        return otherTransactionManager;
    }

}

期限切れセッションのクリーンアップジョブのカスタマイズ

期限切れのセッションによるデータベースのオーバーロードを避けるために、Spring Session JDBC は期限切れのセッション (およびその属性) を削除するクリーンアップジョブを 1 分ごとに実行します。クリーンアップジョブをカスタマイズする理由はいくつかありますが、次のセクションで最も一般的な理由を見てみましょう。ただし、デフォルトジョブのカスタマイズは制限されており、これは意図的なものであり、Spring Session は堅牢なバッチ処理を提供することを意図したものではありません。これより優れた処理を行うフレームワークやライブラリが多数あるためです。さらにカスタマイズ機能が必要な場合は、デフォルトのジョブを無効にして独自のジョブを提供することを検討してください。代わりに、バッチ処理アプリケーションに堅牢なソリューションを提供する Spring Batch を使用することもできます。

期限切れのセッションをクリーンアップする頻度のカスタマイズ

@EnableJdbcHttpSession の cleanupCron 属性を使用して、クリーンアップジョブを実行する頻度を定義する cron 式をカスタマイズできます。

  • Java

@Configuration
@EnableJdbcHttpSession(cleanupCron = "0 0 * * * *") // top of every hour of every day
public class SessionConfig {

}

または、Spring Boot を使用している場合は、spring.session.jdbc.cleanup-cron プロパティを設定します。

spring.session.jdbc.cleanup-cron="0 0 * * * *"

ジョブの無効化

ジョブを無効にするには、Scheduled.CRON_DISABLED を @EnableJdbcHttpSession の cleanupCron 属性に渡す必要があります。

  • Java

@Configuration
@EnableJdbcHttpSession(cleanupCron = Scheduled.CRON_DISABLED)
public class SessionConfig {

}

有効期限による削除クエリのカスタマイズ

JdbcIndexedSessionRepository.setDeleteSessionsByExpiryTimeQuery から SessionRepositoryCustomizer<JdbcIndexedSessionRepository> Bean を使用して、期限切れのセッションを削除するクエリをカスタマイズできます。

  • Java

@Configuration
@EnableJdbcHttpSession
public class SessionConfig {

    @Bean
    public SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
        return (sessionRepository) -> sessionRepository.setDeleteSessionsByExpiryTimeQuery("""
            DELETE FROM %TABLE_NAME%
            WHERE EXPIRY_TIME < ?
            AND OTHER_COLUMN = 'value'
            """);
    }

}