ORM 模型
ORM 模型(Object-Relational Mapping)以面向对象方式操作数据表,基类为 Viswoole\Database\Model。模型在查询构造器之上自动处理表名推断、时间戳写入、软删除、获取器/修改器与关联预加载,静态方法调用自动转发到模型查询器。
本文依据框架源码
src/Database/Model.php、src/Database/Model/Query.php与src/Database/Collection/{BaseCollection,DataSet,Collection}.php编写。
定义模型
namespace App\Model;
use Viswoole\Database\Model;
class UserModel extends Model
{
// 对应数据表;省略时按类名推断(见下文推断规则)
protected string $table = 'user';
// 主键字段名
protected string $pk = 'id';
// 数据库通道,null 使用默认通道
protected ?string $channelName = null;
}表名推断规则
未显式定义 $table 时,框架从类名推断:去除 $suffix 后缀(默认 Model),驼峰转蛇形并小写。例如 UserProfileModel → user_profile,UserModel → user。
模型属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
$table | string | 类名推断 | 数据表名 |
$suffix | string | Model | 推断表名时去除的类名后缀 |
$pk | string | id | 主键字段名 |
$channelName | string|null | null | 绑定的数据库通道,见 多通道 |
$hidden | array | [] | toArray() 序列化时隐藏的字段 |
$autoWritePk | bool | false | 写入时自动生成主键,模型需定义 public function autoWritePk() 返回主键值 |
查询数据
模型静态方法自动转发到模型查询器(Viswoole\Database\Model\Query,继承查询构造器),因此查询构造器的全部条件、排序、分页方法均可直接使用:
use App\Model\UserModel;
// 静态调用:UserModel::where(...) → 转发到 Query 实例
$users = UserModel::where('status', 1)
->whereIn('type', [1, 2])
->orderBy('id', 'desc')
->page(1, 20)
->select();
// 实例调用(IDE 提示更友好):推荐 $model->query()->select()
$model = new UserModel();
$user = $model->query()->where('id', 1)->find();推荐调用方式
$model->query() / $model->query-> 与静态调用 UserModel::where(...) 结果一致。实例方式拥有完整的 IDE 智能提示,且省去魔术方法转发开销,推荐在模型方法内部使用 $this->query->。
查询结果
| 方法 | 返回 | 说明 |
|---|---|---|
find($value = null, $allowEmpty = true) | DataSet | 主键或条件查单条;空结果为空 DataSet,用 isEmpty() 判断;$allowEmpty = false 时空结果抛 DataNotFoundException |
first($allowEmpty = true) | DataSet | 条件查单条(自动 LIMIT 1) |
select() / get() | Collection | 多行集合 |
getArray() | array | 多行纯数组 |
count() / min() / max() / avg() / sum() | 标量 | 聚合查询 |
cursor() / chunk(int $size) | Generator | 大结果集游标/分批处理 |
DataSet(单行)与 Collection(多行)支持数组式访问与链式集合操作:
$user = UserModel::find(1);
if (!$user->isEmpty()) {
$user->name = '李四'; // 修改字段(自动追踪变更)
$user->save(); // 仅持久化变更的字段
$user->merge(['email' => 'lisi@example.com'])->save();
$user->delete(); // 删除当前行(启用软删除时为软删除)
}
// Collection:filter / map / sortBy / where / update / delete 等
UserModel::where('status', 0)->select()->update(['status' => -1]);toArray() 会应用模型 $hidden 隐藏字段与获取器转换;对象直接 JSON 序列化(如响应输出)等价于 toArray()。
创建数据
// create(array $data, array $columns = []):写入并返回含主键的 DataSet
// $columns 为允许写入的字段白名单
$user = UserModel::create(
['name' => '张三', 'status' => 1, 'hack' => 'x'],
['name', 'status'] // hack 字段被过滤
);
echo $user->id;
// 也可使用构造器风格的写入
UserModel::insert(['name' => '张三']);
UserModel::where('id', 1)->update(['name' => '李四']);自动时间戳
$autoWriteTimestamp 控制创建/更新时间的自动写入:
| 值 | 行为 |
|---|---|
0 | 关闭(默认) |
1 | 插入时写入创建时间 |
2 | 更新时写入更新时间 |
3 | 插入写创建时间 + 更新写更新时间 |
| 属性 | 默认值 | 说明 |
|---|---|---|
$createTimeFieldName | create_time | 创建时间字段名 |
$updateTimeFieldName | update_time | 更新时间字段名 |
$createTimeFormatType | datetime | 创建时间格式 |
$updateTimeFormatType | datetime | 更新时间格式 |
格式类型支持 datetime(Y-m-d H:i:s)、timestamp(Unix 时间戳)、date(Y-m-d),或任意 date() 可识别的格式串。手动传入的时间字段不会被覆盖。
class UserModel extends Model
{
protected int $autoWriteTimestamp = 3; // 创建与更新时间都自动写入
}软删除
软删除(Soft Delete)不物理删除记录,而是向删除标记字段写入当前时间;查询时自动排除已删除数据。
class UserModel extends Model
{
protected bool $enableSoftDelete = true; // 启用软删除
protected string $softDeleteFieldName = 'delete_time'; // 标记字段名(默认)
protected string $softDeleteFieldType = 'datetime'; // 字段值格式
protected null|string|int $softDeleteFieldDefaultValue = null; // 未删除状态的字段值
}| 操作 | 写法 | 说明 |
|---|---|---|
| 软删除 | $user->delete() | 写入 delete_time 当前时间 |
| 硬删除 | $user->delete(true) | 物理 DELETE,$real = true 时忽略软删除 |
| 批量删除 | UserModel::whereIn('id', [1,2])->delete() | 同样遵循软删除 |
| 恢复 | UserModel::restore(1) 或 UserModel::where('id', 1)->restore() | 将 delete_time 写回默认值;须传主键或带 where 条件,未启用软删除返回 0 |
| 含已删数据 | UserModel::withTrashed(true)->select() | 查询包含已软删除记录;withTrashed(false) 恢复过滤 |
软删除过滤原理
启用软删除后,SELECT 自动追加 delete_time IS NULL(或 delete_time = 默认值)条件;withTrashed(true) 可临时取消该过滤。
没有 onlyTrashed()
框架未提供 onlyTrashed()。仅查已删除数据可用 UserModel::withTrashed(true)->whereNotNull('delete_time')->select()。
获取器与修改器
获取器(Accessor)在读取序列化时转换字段值,修改器(Mutator)在写入数据库前转换字段值。命名规则:蛇形字段名转驼峰后加前缀,如 user_name 对应 getUserNameAttr / setUserNameAttr。
class UserModel extends Model
{
/**
* 获取器:状态值转文字(仅在 toArray()/JSON 序列化时应用)
*/
public function getStatusAttr(mixed $value): string
{
return match ((int)$value) {
1 => '正常',
0 => '禁用',
default => '未知',
};
}
/**
* 修改器:密码写入前自动哈希(insert / insertGetId / update / create 均生效)
*/
public function setPasswordAttr(mixed $value): string
{
return password_hash((string)$value, PASSWORD_DEFAULT);
}
}
$user = UserModel::find(1);
$user->toArray()['status']; // '正常'(获取器生效)
UserModel::create(['name' => 'a', 'password' => 'secret']);
// 库中 password 存储的是哈希值(修改器生效)使用要点:
- 必须声明为
public:框架从模型外部作用域校验方法可见性,非 public 视为未定义; - 获取器仅在
toArray()序列化时应用,直接访问属性返回数据库原始值; - 修改器只转换字段值、不改变字段名;批量插入逐行应用;
- 框架自动注入的字段(
create_time/update_time/ 自动主键)不经过修改器。
通配获取器/修改器
需要按字段名模式统一转换时,可定义通配方法 getAttr(string $field, mixed $value) / setAttr(string $field, mixed $value),$field 为蛇形原名字段名。查找顺序:具名 get|set{Field}Attr 优先,未命中回退通配方法,两者皆无返回原值。
class UserModel extends Model
{
/**
* 通配获取器:*_id 字段统一字符串化,避免 JS 处理大整数精度丢失
*/
public function getAttr(string $field, mixed $value): mixed
{
if ($value !== null && str_ends_with($field, '_id')) {
return (string)$value;
}
return $value;
}
}完整示例
namespace App\Model;
use Viswoole\Database\Model;
class UserModel extends Model
{
protected string $table = 'user';
protected string $pk = 'id';
protected array $hidden = ['password']; // 序列化隐藏字段
protected int $autoWriteTimestamp = 1; // 插入时写入 create_time
protected bool $enableSoftDelete = true; // 启用软删除
public function setPasswordAttr(mixed $value): string
{
return password_hash((string)$value, PASSWORD_DEFAULT);
}
public function getStatusAttr(mixed $value): string
{
return (int)$value === 1 ? '正常' : '禁用';
}
}