MySQL 的 TEXT、JSON 默认值:括号、求值时机与版本边界
TEXT DEFAULT '' 建表失败,换成 TEXT DEFAULT ('') 却能成功。这对括号并非装饰:MySQL 8.4 要求 TEXT、BLOB、JSON 等类型的非空默认值以表达式形式书写,即便里面只是一个常量。
如果把失败归结为“大字段永远不能有默认值”,会把旧版本限制当成现在的规则;如果只说“加括号即可”,又会漏掉表达式默认值的求值时机和迁移边界。
先确认实际执行的是哪一代语法
MySQL 从 8.0.13 起支持这里讨论的表达式默认值。更早版本不能把下面的 DDL 原样接过去;本文的可操作示例按 MySQL 8.4、InnoDB 编写。MariaDB 的同名语法和功能演进也不能直接当作 MySQL 版本证据。
SELECT VERSION(), @@version_comment, @@SESSION.sql_mode;
做版本检查时,直接在执行 DDL 的连接上运行。迁移工具配置的 JDBC 地址、代理显示的版本与最终执行实例未必是同一层信息。若提示不支持语法,至少先保留完整服务器版本、存储引擎和失败语句,不要先让脚本自动改成可空字段继续执行。
在隔离库执行这个有意失败的定义:
-- 预期失败:TEXT 的非空默认值没有写成表达式。
CREATE TABLE default_text_bad (
id BIGINT NOT NULL,
body TEXT NOT NULL DEFAULT '',
PRIMARY KEY (id)
) ENGINE=InnoDB;
然后创建正式实验表。JSON 默认值用构造函数生成对象,避免把 SQL 字符串引号与 JSON 引号混在一起。
CREATE TABLE job_profile (
id BIGINT NOT NULL,
memo TEXT NOT NULL DEFAULT (''),
options JSON NOT NULL DEFAULT (
JSON_OBJECT('retries', 3, 'labels', JSON_ARRAY())
),
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
expires_at DATETIME(6) NOT NULL DEFAULT (
created_at + INTERVAL 7 DAY
),
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO job_profile (id) VALUES (101);
SELECT id, OCTET_LENGTH(memo) AS memo_bytes,
JSON_TYPE(options) AS options_type,
options, created_at, expires_at
FROM job_profile
WHERE id = 101;
该行的 memo 是空字符串,options 是带 retries 和空标签数组的 JSON 对象,expires_at 根据本行 created_at 加七天得到。默认值解决的是缺省输入,不负责替应用合并一份只传了一半字段的 JSON 配置。若显式写入 {},数据库不会自动把 retries 填回去。
省略列、写 DEFAULT、传 NULL 是三种请求
同一个接口字段没有传值,驱动层可能采取不同策略:省略列、显式发送默认关键字,或者绑定 SQL NULL。最后一种不会自动触发默认值。
INSERT INTO job_profile (id, memo, options)
VALUES (102, DEFAULT, DEFAULT);
-- 在严格模式下单独执行;预期违反 NOT NULL。
INSERT INTO job_profile (id, memo, options)
VALUES (103, NULL, NULL);
INSERT INTO job_profile (id, memo, options)
VALUES (104, '', JSON_OBJECT());
SELECT id, memo, options
FROM job_profile
WHERE id IN (102, 104)
ORDER BY id;
102 使用默认对象,104 明确写入空对象。它们都不是 SQL NULL。ORM 如果把所有缺失字段统一绑定 NULL,再好的默认值定义也接不到这个分支。排查时看实际生成的 SQL 和绑定参数,光看实体类上的默认初始化不够。
另一个容易误用的写法是 DEFAULT(column_name)。对表达式默认值列,不能把它当作一个随时可调用的“取出默认表达式”函数。写入时使用单独的 DEFAULT 关键字,与在查询里调用 DEFAULT(options) 不是同一种语法能力。
-- 单独执行;表达式默认值不支持这样提取。
SELECT DEFAULT(options) FROM job_profile LIMIT 1;
默认表达式不是持续维护的公式
expires_at 的默认表达式在需要默认值时求值。之后修改 created_at,不会自动让截止时间跟着移动。
UPDATE job_profile
SET created_at = created_at + INTERVAL 1 DAY
WHERE id = 101;
SELECT created_at, expires_at,
TIMESTAMPDIFF(DAY, created_at, expires_at) AS days_between
FROM job_profile
WHERE id = 101;
原来七天的差值会缩短为六天。这是一次性初始化的结果。若业务要求截止时间永久等于创建时间加七天,应使用适合的派生表达或在同一次业务更新里显式维护,而不是把 DEFAULT 当成更新触发器。
反过来,已经确定的履约截止时间常常应该保留为快照。以后修改创建时间的展示字段,不应偷偷更改已承诺的期限。数据库功能选择之前先问这列是“初始建议值”还是“始终成立的关系”,两种语义对应不同实现。
若确实想让某一行重新按默认规则赋值,可以明确操作:
UPDATE job_profile
SET expires_at = DEFAULT
WHERE id = 101;
SELECT created_at, expires_at
FROM job_profile
WHERE id = 101;
这次更新有业务含义,不宜埋进一个把所有空字段都重置为默认值的通用更新器。
列依赖会进入 DDL 的维护成本
表达式默认值允许引用其他列,但不是任意脚本环境:不能查询另一张表,也不能读取用户变量或调用自定义存储函数。想把租户的当前配置作为默认值,就应由应用查询后明确写入,或设计有完整事务含义的业务过程。
列顺序也可能成为依赖约束。这里把 created_at 放在 expires_at 前面;对生成列或带表达式默认值的列的引用,需要遵守它们在定义中的先后关系。迁移工具如果“为了美观”重排列顺序,可能让原本合法的依赖失效。评审 DDL 不能只比较每一列独立的类型与默认文本。
此外,UUID()、RAND() 这类非确定函数与常量默认值不同。使用非确定默认表达式的语句会涉及基于语句复制的安全性,不能认为默认逻辑藏在表定义里就与复制格式无关。本文的默认对象和相对截止时间不需要随机函数;没有必要为了省应用一行代码,把可复现输入改成难跟踪的随机行为。
改默认值不会替已有行补历史数据
给一张旧表增加或调整默认规则时,要分清表结构变化与已有值修复。改变已有列的默认值,约束的是后续使用默认分支的写入,已有明确存储的空对象不会自动变成新配置。
ALTER TABLE job_profile
ALTER COLUMN options SET DEFAULT (
JSON_OBJECT('retries', 5, 'labels', JSON_ARRAY())
);
INSERT INTO job_profile (id) VALUES (105);
SELECT id, options
FROM job_profile
WHERE id IN (102, 104, 105)
ORDER BY id;
102 保留原来的重试次数,104 仍是空对象,105 才使用新默认值。若产品要求历史任务也改成五次,需要一条单独、有筛选条件的数据迁移,并决定是否覆盖用户已经主动配置的值。把 ALTER DEFAULT 当成批量修复,验收时就会漏掉最重要的那批旧行。