Spring AI 教程

数据库存储(JdbcChatMemoryRepository)

提示:如果不能访问 OpenAI,请点击 AiCode API 注册账号,通过代理访问。  

在 Spring AI 1.0.0 中,JdbcChatMemoryRepository 是 ChatMemoryRepository 接口的一个实现类,它利用 JDBC(Java Database Connectivity)技术,将聊天记忆数据存储到关系型数据库中。

JdbcChatMemoryRepository 的主要功能是提供一种持久化的方式来管理聊天会话的相关信息,包括会话 ID、会话中的消息等。通过与关系型数据库交互,它可以在应用程序重启后依然保留聊天记忆数据,适用于对数据持久性有要求的场景。

注意,JdbcChatMemoryRepository 并不在 spring-ai-model-1.0.0.jar 包中,而是位于 spring-ai-model-chat-memory-repository-jdbc-1.0.0.jar 中,如下图:

数据库存储(JdbcChatMemoryRepository)

根据上图,目前提供四种数据库方言,分别是 hsqldb、mariadb(mysql 也可用)、postgresql 和 sqlserver。

  

原理分析

JdbcChatMemoryRepository 内部采用 JdbcTemplate 去与数据库交互,源码如下:

package org.springframework.ai.chat.memory.repository.jdbc;

//...

/**
 * ChatMemoryRepository 接口的 JDBC 实现类,用于通过关系型数据库持久化存储聊天会话消息。
 * 支持事务管理和多数据库方言适配,确保数据的持久性和操作的原子性。
 */
public final class JdbcChatMemoryRepository implements ChatMemoryRepository {

    /**
     * JDBC 操作模板,封装数据库连接和 SQL 执行逻辑
     */
    private final JdbcTemplate jdbcTemplate;

    /**
     * 事务模板,用于管理数据库操作的事务(如保存消息时的删除旧数据+插入新数据的原子性)
     */
    private final TransactionTemplate transactionTemplate;

    /**
     * 数据库方言,封装不同数据库的 SQL 语句差异(如 MySQL、PostgreSQL 等)
     */
    private final JdbcChatMemoryRepositoryDialect dialect;

    /**
     * 日志记录器,用于记录操作日志
     */
    private static final Logger logger = LoggerFactory.getLogger(JdbcChatMemoryRepository.class);

    /**
     * 构造方法,初始化 JDBC 模板、数据库方言和事务管理器
     *
     * @param jdbcTemplate JDBC 操作模板,不可为 null
     * @param dialect 数据库方言,不可为 null
     * @param txManager 事务管理器,可为 null(默认使用数据源事务管理器)
     */
    private JdbcChatMemoryRepository(JdbcTemplate jdbcTemplate, 
        JdbcChatMemoryRepositoryDialect dialect, PlatformTransactionManager txManager) {
        Assert.notNull(jdbcTemplate, "jdbcTemplate cannot be null");
        Assert.notNull(dialect, "dialect cannot be null");
        this.jdbcTemplate = jdbcTemplate;
        this.dialect = dialect;
        // 初始化事务模板,若未指定事务管理器则使用默认的数据源事务管理器
        this.transactionTemplate = new TransactionTemplate(
            txManager != null ? txManager : new DataSourceTransactionManager(jdbcTemplate.getDataSource())
        );
    }

    /**
     * 查询所有存在的会话 ID 列表
     * @return 所有会话 ID 的列表,无会话时返回空列表
     */
    public List<String> findConversationIds() {
        // 通过方言获取查询会话 ID 的 SQL,执行查询并返回结果
        return this.jdbcTemplate.queryForList(this.dialect.getSelectConversationIdsSql(), String.class);
    }

    /**
     * 根据会话 ID 查询对应的消息列表
     * @param conversationId 会话唯一标识,不可为 null 或空字符串
     * @return 该会话的消息列表,会话不存在时返回空列表
     * @throws IllegalArgumentException 若 conversationId 为 null 或空
     */
    public List<Message> findByConversationId(String conversationId) {
        Assert.hasText(conversationId, "conversationId cannot be null or empty");
        // 通过方言获取查询消息的 SQL,使用 MessageRowMapper 映射结果集,并传入会话 ID 作为参数
        return this.jdbcTemplate.query(
            this.dialect.getSelectMessagesSql(),
            new MessageRowMapper(),
            new Object[]{conversationId}
        );
    }

    /**
     * 保存指定会话的消息列表(先删除旧消息,再批量插入新消息,操作在事务中执行)
     *
     * @param conversationId 会话唯一标识,不可为 null 或空字符串
     * @param messages 待保存的消息列表,不可为 null 且不能包含 null 元素
     * @throws IllegalArgumentException 若参数不满足合法性要求
     */
    public void saveAll(String conversationId, List<Message> messages) {
        Assert.hasText(conversationId, "conversationId cannot be null or empty");
        Assert.notNull(messages, "messages cannot be null");
        Assert.noNullElements(messages, "messages cannot contain null elements");

        // 在事务中执行:先删除该会话的旧消息,再批量插入新消息
        this.transactionTemplate.execute((status) -> {
            this.deleteByConversationId(conversationId);
            this.jdbcTemplate.batchUpdate(
                this.dialect.getInsertMessageSql(),
                new AddBatchPreparedStatement(conversationId, messages)
            );
            return null;
        });
    }

    /**
     * 根据会话 ID 删除对应的所有消息
     *
     * @param conversationId 会话唯一标识,不可为 null 或空字符串
     * @throws IllegalArgumentException 若 conversationId 为 null 或空
     */
    public void deleteByConversationId(String conversationId) {
        Assert.hasText(conversationId, "conversationId cannot be null or empty");
        // 通过方言获取删除消息的 SQL,执行删除操作
        this.jdbcTemplate.update(this.dialect.getDeleteMessagesSql(), new Object[]{conversationId});
    }

    //...(省略内部类如 MessageRowMapper、AddBatchPreparedStatement 的实现)
}

简单示例

下面将以本地 MySQL 数据库为例。

添加依赖

首先,在项目中添加以下依赖:

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-autoconfigure-model-chat-memory-repository-jdbc</artifactId>
  <version>1.0.0</version>
</dependency>
<!-- Spring Data JDBC -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!-- mysql 驱动 -->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
</dependency>

安装&配置数据库

如果你还没有数据库,先在本地安装 mysql 数据库,然后创建名为 chat_db 的数据库。

完成 mysql 安装后,在 Spring Boot 配置中配置数据库,如下:

spring:
  application:
    name: springai_demo1
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/chat_db
    username: root
    password: aaaaaa

注意,还有一步不要忘了?就是创建数据库表,数据库表的结构可以参考依赖 jar 包中的 schema-*.sql 文件,如下:

数据库存储(JdbcChatMemoryRepository)

schema-mariadb.sql 文件的内容如下:

CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
  conversation_id VARCHAR(36) NOT NULL,
  content TEXT NOT NULL,
  type VARCHAR(10) NOT NULL,
  `timestamp` TIMESTAMP NOT NULL,
  CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);

CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`);

直接在 MySQL 上执行有错误,修改后的版本:

CREATE TABLE `SPRING_AI_CHAT_MEMORY` (
  `conversation_id` varchar(36) NOT NULL,
  `content` text NOT NULL,
  `type` varchar(10) NOT NULL,
  `timestamp` timestamp NOT NULL,
  KEY `SPRING_AI_CHAT_MEMORY_timestamp_IDX` (`timestamp`) USING BTREE,
  CONSTRAINT `TYPE_CHECK` CHECK ((`type` in (_utf8mb4'USER',_utf8mb4'ASSISTANT',_utf8mb4'SYSTEM',_utf8mb4'TOOL')))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

注意:可以通过 spring.ai.chat.memory.repository.jdbc.initialize-schema 参数进行配置,该配置决定了应用启动时,Spring AI 是否会自动处理(创建)聊天记忆相关的数据库表结构。具体行为取决于配置的值,常见取值包括:

  • always:每次应用启动时,都会执行建表语句(若表不存在则创建,若已存在可能会忽略或根据方言处理)。

  • embedded:仅在使用嵌入式数据库(如 H2、HSQLDB)时自动建表,适用于开发或测试场景。

  • never:不自动初始化表结构,需手动创建表(需严格遵循 JdbcChatMemoryRepository 预期的表结构定义)。

由于我们这里使用的是 mysql 数据库,配置为 always 将会抛出错误:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdbcChatMemoryScriptDatabaseInitializer' defined in class path resource [org/springframework/ai/model/chat/memory/repository/jdbc/autoconfigure/JdbcChatMemoryRepositoryAutoConfiguration.class]: No schema scripts found at location 'classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql'

...

原因是 schema-mysql.sql 脚本不存在。那就自己手动创建就可以了。

编写代码

Spring AI 为 JdbcChatMemoryRepository 提供自动配置,可直接在应用中使用。

@Autowired
private JdbcChatMemoryRepository chatMemoryRepository;

ChatMemory chatMemory = MessageWindowChatMemory.builder()
    .chatMemoryRepository(chatMemoryRepository)
    .maxMessages(10)
    .build();

如需手动创建 JdbcChatMemoryRepository,可通过注入 JdbcTemplate 实例及 JdbcChatMemoryRepositoryDialect 实现:

ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder()
    .jdbcTemplate(jdbcTemplate)
    .dialect(new PostgresChatMemoryDialect())
    .build();

ChatMemory chatMemory = MessageWindowChatMemory.builder()
    .chatMemoryRepository(chatMemoryRepository)
    .maxMessages(10)
    .build();

完整示例代码如下:

package com.hxstrive.springai.springai_openai.chatMemoryRepository.chatMemoryRepository2;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class SpringAiDemoApplication implements CommandLineRunner {

	@Autowired
	private ChatModel chatModel;

	@Autowired
	private JdbcChatMemoryRepository chatMemoryRepository;

	private ChatMemory chatMemory;

	public static void main(String[] args) {
		SpringApplication.run(SpringAiDemoApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		// 创建 ChatMemory 实现
		chatMemory = MessageWindowChatMemory.builder()
				.chatMemoryRepository(chatMemoryRepository) // 使用JdbcChatMemoryRepository作为存储仓库
				.maxMessages(20) // 最多保存20条消息
				.build();


		StringBuilder builder = new StringBuilder();

		// 唯一会话ID,同一个会话上下文中保持一致
		String sessionId = "USER-251017100313";

		String question = "四川大学是985吗?";
		String ai = chat(sessionId, question);
		builder.append(String.format("用户问题:%s\n", question));
		builder.append(String.format("机器回复:%s\n", ai));

		question = "是211吗?";
		ai = chat(sessionId, question);
		builder.append(String.format("用户问题:%s\n", question));
		builder.append(String.format("机器回复:%s\n", ai));

		System.out.println(builder.toString());
	}

	private String chat(String sessionId, String question) {
		ChatClient chatClient = ChatClient.builder(chatModel)
				.defaultSystem("你是AI助手小粒,所有回复均使用中文。")
				.defaultAdvisors(
						new SimpleLoggerAdvisor() // 输出聊天日志
				).build();

		// 1. 将用户输入添加到记忆
		UserMessage userMessage = new UserMessage(question);
		chatMemory.add(sessionId, userMessage);

		// 2. 获取记忆中的所有消息(上下文),传递给 AI 模型
		ChatClient.CallResponseSpec responseSpec = chatClient.prompt().messages(chatMemory.get(sessionId)).call();

		// 3. 将 AI 回复添加到记忆,更新上下文
		AssistantMessage assistantMessage = responseSpec.chatResponse().getResult().getOutput();
		chatMemory.add(sessionId, assistantMessage);

		// 4. 返回 AI 回复内容
		return assistantMessage.getText();
	}

}

运行结果如下:
数据库存储(JdbcChatMemoryRepository)

支持的数据库与方言抽象层

Spring AI 通过方言抽象层支持多种关系型数据库,以下为开箱即用支持的数据库:

  • PostgreSQL

  • MySQL / MariaDB

  • SQL Server

  • HSQLDB

使用 JdbcChatMemoryRepositoryDialect.from(DataSource) 时可基于 JDBC URL 自动识别正确方言。源码如下:

static JdbcChatMemoryRepositoryDialect from(DataSource dataSource) {
    try {
        String url = dataSource.getConnection().getMetaData().getURL().toLowerCase();
        if (url.contains("postgresql")) {
            return new PostgresChatMemoryRepositoryDialect();
        }

        if (url.contains("mysql")) {
            return new MysqlChatMemoryRepositoryDialect();
        }

        if (url.contains("mariadb")) {
            return new MysqlChatMemoryRepositoryDialect();
        }

        if (url.contains("sqlserver")) {
            return new SqlServerChatMemoryRepositoryDialect();
        }

        if (url.contains("hsqldb")) {
            return new HsqldbChatMemoryRepositoryDialect();
        }
    } catch (Exception var2) {
    }

    return new PostgresChatMemoryRepositoryDialect(); // 默认采用 PostgresSQL 数据库
}

然后,通过实现 JdbcChatMemoryRepositoryDialect 接口可扩展其他数据库支持。

配置属性

  • spring.ai.chat.memory.repository.jdbc.initialize-schema  控制初始化 Schema 的时机。可选值:embedded(默认)、always(总是创建)、never(从不创建)。默认为 embedded

  • spring.ai.chat.memory.repository.jdbc.schema  用于初始化的 Schema 脚本位置。支持 classpath: URL 及平台占位符。默认为 classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql

  • spring.ai.chat.memory.repository.jdbc.platform  若初始化脚本中使用 @@platform@@ 占位符,则指定其对应的平台标识。默认为 auto-detected

Schema 初始化

自动配置将在启动时使用特定于供应商的 SQL 脚本创建 SPRING_AI_CHAT_MEMORY 表。默认情况下,仅针对嵌入式数据库(H2、HSQL、Derby 等)执行 Schema 初始化。

可通过 spring.ai.chat.memory.repository.jdbc.initialize-schema 属性控制 Schema 初始化行为:

spring.ai.chat.memory.repository.jdbc.initialize-schema=embedded # Only for embedded DBs (default)
spring.ai.chat.memory.repository.jdbc.initialize-schema=always   # Always initialize
spring.ai.chat.memory.repository.jdbc.initialize-schema=never    # Never initialize (useful with Flyway/Liquibase)

要覆盖默认的 Schema 脚本位置,请使用:

spring.ai.chat.memory.repository.jdbc.schema=classpath:/custom/path/schema-mysql.sql

扩展方言

要新增数据库支持,需实现 JdbcChatMemoryRepositoryDialect 接口并提供消息查询、插入及删除的 SQL 语句。随后可将自定义方言传入 Repository Builder。如下:

ChatMemoryRepository chatMemoryRepository = JdbcChatMemoryRepository.builder()
    .jdbcTemplate(jdbcTemplate)
    .dialect(new MyCustomDbDialect())
    .build();

下面是 MySQL 方言官方实现:

package org.springframework.ai.chat.memory.repository.jdbc;

public class MysqlChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {
    public MysqlChatMemoryRepositoryDialect() {
    }

    public String getSelectMessagesSql() {
        return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY `timestamp`";
    }

    public String getInsertMessageSql() {
        return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, `timestamp`) VALUES (?, ?, ?, ?)";
    }

    public String getSelectConversationIdsSql() {
        return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY";
    }

    public String getDeleteMessagesSql() {
        return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?";
    }
}

上述源码中就四条 SQL 语句,你可以仿照实现自己的数据库方言(如:SQLite、Oracle 等等)。更多信息请访问官方文档。

  

提示:如果不能访问 OpenAI,请点击 AiCode API 注册账号,通过代理访问。  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号