本地 H2 跑得通,换成 MySQL 或 PostgreSQL 就报表不存在。根因是
JdbcChatMemoryRepository的 schema 自动初始化只认嵌入式数据库,外部库不会自动建表。另外 Spring AI 2.0.0 起表结构多了sequence_id列,老表不加列会报Unknown column。
Issue & Env(案发现场与环境)
- 业务场景:用
JdbcChatMemoryRepository存多轮对话记忆,本地开发连 H2,测试或生产连 MySQL、PostgreSQL。 - 现象一:换库后启动或首次对话就报表不存在。
- 现象二:升级 Spring AI 2.0 后,老表报列不存在。
- 环境:Spring AI 1.1.x / 2.0.0,Spring Boot 3.x,H2 / MySQL / PostgreSQL。
同一份代码、同一份配置,只换了 JDBC 连接串,结果就不一样。问题不在业务代码,在表的初始化策略。
两现象经常被当成两个独立问题,其实是同一套记忆表在不同阶段暴露出来的。表都没建,先撞表不存在;表建了但结构旧,就撞列不存在。
StackTrace(堆栈日志)
MySQL 上表没建,SQL 直接失败:
org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [SELECT conversation_id, content, type, timestamp FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY timestamp]
Caused by: java.sql.SQLSyntaxErrorException: Table 'demo.SPRING_AI_CHAT_MEMORY' doesn't exist
升级 Spring AI 2.0 之后,老表缺列:
Caused by: java.sql.SQLSyntaxErrorException: Unknown column 'sequence_id' in 'field list'
这两类报错指向同一个根:表状态和框架预期对不上。前一个是表根本没有,后一个是表在但结构旧。
Reproduce(复现代码)
依赖里带上 OpenAI 与两个数据库驱动:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
Bean 定义只声明记忆仓库,不手工建表:
@Bean
ChatMemory chatMemory(JdbcTemplate jdbcTemplate) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new JdbcChatMemoryRepository(jdbcTemplate))
.maxMessages(20)
.build();
}
这一段代码,连 H2 能自动建表,连 MySQL 就报表不存在。
Root Cause(根因分析)
自动建表只认嵌入式库
JdbcChatMemoryRepository 的 schema 初始化,默认只对嵌入式数据库执行,比如 H2、HSQL、Derby。MySQL、PostgreSQL 这些外部库不会自动建表。
所以本地 H2 一切正常,一换库就崩。这不是配置写错,是初始化策略的默认行为。默认值就是给「本地跑得起来」服务的,不是给生产用的。
为什么 H2 有表 MySQL 没有
根源在初始化条件。框架启动时判断当前数据库是不是嵌入式类型,是才执行建表脚本。H2 属于嵌入式,MySQL、PostgreSQL 不属于,脚本就被跳过。
同一份代码,在两种库上走出了两条分支。表现是「换库就报错」,实际是初始化根本没在外部库上发生过。
2.0 起表结构多了 sequence_id 列
Spring AI 2.0.0 起,记忆表新增了 sequence_id BIGINT 列,用来做会话内消息排序。旧实现靠 timestamp 排序,同一毫秒内写入的多条消息顺序会乱掉,快问快答时尤其明显。
老表不加这一列,框架查询里却带了 sequence_id,于是报 Unknown column。表在、列不在,报错就从「表不存在」换成了「列不存在」。
截至撰写时未经核实:sequence_id 具体在 2.0.0 的哪个小版本引入,未逐版核对,升级前建议对照升级说明确认一遍。
The Fix(解决方案)
手工建表(推荐)
外部库上手工建表,别依赖应用自动建。建表语句:
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,
sequence_id BIGINT NOT NULL,
PRIMARY KEY (conversation_id, sequence_id)
);
字段说明:
conversation_id:会话 ID,应用侧显式传入。content:消息正文。type:消息类型,如 USER、ASSISTANT、SYSTEM。timestamp:写入时间。sequence_id:会话内排序序号,2.0 起用于稳定排序。
类型名按数据库微调:PostgreSQL 可用 BIGSERIAL 或配序列,MySQL 可用 BIGINT AUTO_INCREMENT,按主键策略选一种。
老表升级加列
已有 1.x 表结构,补一列即可:
ALTER TABLE SPRING_AI_CHAT_MEMORY
ADD COLUMN sequence_id BIGINT NOT NULL DEFAULT 0;
加完建议回填一次历史数据的序号,再考虑加唯一约束。否则 sequence_id 全是 0,排序又会退化成原来那样。
让应用自动初始化
内置的初始化也可以对非嵌入式库放开:
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
该属性的取值是 embedded(默认)与 always。生产上更推荐手工迁移:让应用自动改表风险不小,尤其是有历史数据和主键约束的时候。自动建表适合本地和测试环境,生产用一次性的迁移脚本更可控。
迁移脚本要纳入版本管理,跟应用发布一起走。别在本地手工建好表就算完,测试和生产环境同样要跑一遍。漏了一处,报错会在最不该出现的时候冒出来。
