本地 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。生產上更推薦手工遷移:讓應用自動改表風險不小,尤其是有歷史數據和主鍵約束的時候。自動建表適合本地和測試環境,生產用一次性的遷移腳本更可控。
遷移腳本要納入版本管理,跟應用發佈一起走。別在本地手工建好表就算完,測試和生產環境同樣要跑一遍。漏了一處,報錯會在最不該出現的時候冒出來。
