TP6中$pk必须在模型内显式声明为protected $pk = '字段名',仅支持字符串单主键,不生效于配置文件或动态设置,且TP5的$_pk已废弃。

ThinkPHP 6 的 pk 属性怎么设才生效
直接说结论:pk 必须在模型类里显式声明为 protected $pk = 'xxx',且不能靠配置文件或动态设置覆盖。TP6 默认只认 id,哪怕数据库主键是 user_id 或 order_no,不写这行代码,所有基于主键的查询(find()、update()、delete())都会悄悄用错字段。
常见错误现象:调用 UserModel::find(123) 返回空,但查日志发现 SQL 是 WHERE id = 123,而实际主键字段叫 user_id;或者 save() 后 getLastInsID() 拿不到值,因为 TP 默认从 id 字段取自增 ID。
-
$pk必须是字符串,不支持数组(复合主键需另配$pk+$pkRule,但 TP6 原生不支持自动识别) - 如果表有自增主键但名字不是
id,除了设$pk,还得同步设$autoWriteTimestamp = false(避免时间戳字段干扰) - 注意命名风格:数据库字段是
user_id,就写protected $pk = 'user_id',别写成userId或USER_ID,TP 不做大小写转换
自增主键字段名改了,insertGetId() 为啥还是返回 0
因为 ThinkPHP 的 insertGetId() 底层依赖 PDO 的 lastInsertId(),而这个函数只对数据库默认自增列(通常是 id)有效。如果你把自增字段改成 log_id,但没告诉 TP 这个字段才是“真正的自增主键”,它就会去读 id 字段——而该字段可能根本不存在或不是自增的。
解决办法不是改 SQL,而是让模型明确知道哪个字段承载自增逻辑:
立即学习“PHP免费学习笔记(深入)”;
- 必须同时设置
protected $pk = 'log_id'和protected $auto = true - 确保数据库中该字段确实是
AUTO_INCREMENT(MySQL)或SERIAL(PostgreSQL),TP 不校验,只信任你写的配置 - 如果用了分表或中间件重写了
insert()方法,要检查是否绕过了insertGetId()的默认逻辑
示例:
class LogModel extends Model
{
protected $pk = 'log_id';
protected $auto = true;
}
TP5 升级到 TP6 后 _pk 属性失效了
没错,_pk 是 TP5 的写法,TP6 已废弃。很多老项目升级后发现模型里还留着 protected $_pk = 'uid',但完全不起作用——TP6 解析模型属性时直接忽略下划线前缀的变量。
迁移时最容易漏掉这点,尤其当项目里混用 TP5/TP6 文档或插件时:
- 删掉所有
$_pk、$_auto、$_validate这类旧命名,换成 TP6 的$pk、$auto、$validate - TP6 的
$pk是强制模型级配置,不能再像 TP5 那样在Db::table('user')->pk('uid')动态指定 - 如果用了多数据库连接,每个模型的
$pk是独立的,不能靠全局配置统一覆盖
主键不是数字、没有自增,wherePk() 还能用吗
能用,但要注意语义和陷阱。wherePk() 只是语法糖,等价于 where($model->getPk(), $value),它不关心字段类型或是否自增。所以 UUID、字符串订单号、组合编码都能用。
不过容易踩的坑是「隐式类型转换」:
- 如果
$pk = 'order_no'是字符串,但传入wherePk(123),TP 会生成WHERE order_no = 123,数据库可能把字符串字段转成数字比较,导致意外匹配(比如'ORD123'被当成0) - 安全做法是显式传字符串:
wherePk('ORD2024001'),或用where($model->getPk(), 'ORD2024001') - 这种场景下别依赖
find(),它内部仍会尝试调用getLastInsID(),而无自增字段时会报错或返回 null
复杂点在于:TP 对非数字主键的批量操作(如 destroy([1,2,3]))默认按数字处理,传字符串数组必须手动保证类型一致,否则 SQL 条件会出错。


















