Entity 实体模型
Entity 实体模型是 ORM 模型的类型化增强:用 PHP 类型化属性声明表字段,查询结果自动水合为实体实例,属性读写全程携带精确类型——$user->userName 在 IDE 中直接推导为 string,而非 ORM 模型动态属性返回的 mixed。基类为 Viswoole\Database\Entity,完整继承 ORM 模型的软删除、自动时间戳、获取器/修改器与关联预加载能力。
本文依据框架源码
src/Database/Entity.php与src/Database/Entity/{EntityQuery,EntityCollection,EntityHydrator,PropertyMeta}.php编写。
定义实体
namespace App\Entity;
use Viswoole\Database\Entity;
class UserEntity extends Entity
{
// 对应数据表;省略时按类名推断(后缀默认 Entity)
protected string $table = 'users';
protected string $pk = 'id';
// —— 以下每个 public 属性对应一个表列 ——
public int $id; // 非空列:int
public string $userName; // 列 user_name(驼峰属性 ↔ 蛇形列名自动互转)
public int $age;
public UserStatus $status; // 回退枚举列
public ?string $remark = null; // 可空列:?string
public ?DateTimeImmutable $createTime = null; // datetime 列 → 时间对象
}enum UserStatus: int
{
case Active = 1;
case Disabled = 2;
}属性命名约定
属性名驼峰、数据库列名蛇形,框架自动互转:属性 userName ↔ 列 user_name。未映射到实体属性的查询列(如 with() 关联结果、pivot 数据)会存入附加数据,可用于展示但不参与写入。
支持的属性类型
| 属性类型 | 列值(数据库) | 水合结果 |
|---|---|---|
int | '1' / 1 / 1.0 | int(数字字符串严格转整型,超 19 位拒绝防溢出) |
float | 数字或数字字符串 | float |
string | 标量 | string |
bool | 0 / 1(含 '0' / '1') | bool(不做 'true' 等真值推断) |
array / mixed | 任意 | 原样透传(适用于 JSON 列) |
DateTimeInterface | '2026-01-01 08:30:00' 或 Unix 时间戳 | DateTimeImmutable(声明具体子类则为该类) |
回退枚举 BackedEnum | 与回退类型一致的 int/string | 枚举实例 |
类型转换规则:
- 可空属性(
?int等)遇NULL列值时属性为null;非可空属性遇NULL直接抛异常,防止脏数据静默通过; - 类型不匹配(如
int属性遇到'abc')抛InvalidArgumentException,错误信息包含实体类、属性名与列名; - 不支持
readonly属性:水合与save()需要对属性赋值,声明了readonly会在首次使用时抛出明确异常。
实体属性
| 属性 | 默认值 | 说明 |
|---|---|---|
$table | 类名推断 | 数据表名 |
$suffix | Entity | 推断表名时去除的类名后缀 |
$pk | id | 主键字段名 |
$channelName | null | 绑定的数据库通道 |
$hidden | [] | toArray() 序列化时隐藏的字段 |
其余模型配置($autoWriteTimestamp、$enableSoftDelete 等)与 ORM 模型完全一致,参见 ORM 模型。
查询数据
静态调用经魔术转发到实体查询器(EntityQuery),条件构建与 ORM 模型完全一致:
use App\Entity\UserEntity;
// 单条查询:返回 ?UserEntity —— 未命中返回 null
$user = UserEntity::where('status', 1)->find(1);
if ($user === null) {
// 未查到记录
}
// nullsafe 读取
$name = UserEntity::find(1)?->userName;
// 多行查询:返回 EntityCollection<UserEntity>
$users = UserEntity::where('age', '>', 18)
->orderBy('id', 'desc')
->select();
// 不允许空结果:空时抛 DataNotFoundException
$user = UserEntity::find(999, allowEmpty: false);
// 大结果集:游标逐条产出实体
foreach (UserEntity::where('status', 1)->cursor() as $user) {
// $user 为 UserEntity 实例
}与 ORM 模型判空方式一致
实体模型与 ORM 模型的 find() 未命中均返回 null,统一用 === null 或 ?-> 判断。
查询结果
| 方法 | 返回 | 说明 |
|---|---|---|
find($value = null, $allowEmpty = true) | \App\\Entity\\UserEntity|null | 主键或条件查单条;空结果为 null;$allowEmpty = false 时空结果抛 DataNotFoundException |
first($allowEmpty = true) | UserEntity|null | 条件查单条(自动 LIMIT 1) |
select() / get() | EntityCollection | 实体集合,元素为实体实例 |
count() / min() / max() / avg() / sum() | 标量 | 聚合查询(与模型一致) |
cursor() | Generator<UserEntity> | 游标逐条产出实体 |
chunk(int $size) | Generator<EntityCollection> | 分批产出实体集合 |
EntityCollection 提供 first() / last() / filter() / map() / each() 等集合操作,toArray() 递归序列化为实体数组:
$users = UserEntity::where('status', 1)->select();
$adults = $users->filter(fn(UserEntity $u) => $u->age >= 18);
$names = $users->map(fn(UserEntity $u) => $u->userName); // 回调必须返回实体
$users->each(fn(UserEntity $u) => print($u->userName));
// 批量更新/删除(按主键 IN 匹配),成功后同步内存实体
$users->update(['status' => 2]);
$users->delete();写入数据
新建实体(INSERT)
// 方式一:构造传参(驼峰属性名)
$user = new UserEntity([
'userName' => '张三',
'age' => 20,
'status' => UserStatus::Active,
]);
$user->save(); // 整体 INSERT,自增主键回填到 $user->id
// 方式二:逐属性赋值
$user = new UserEntity();
$user->userName = '李四';
$user->age = 22;
$user->save();
// 主键已显式赋值(如雪花 ID)时直接写入该主键,不走自增回填
$user = new UserEntity(['id' => 1941234567890123456, 'userName' => '王五']);
$user->save();
// 便捷创建:写入并返回含主键的实体
$user = UserEntity::create(['userName' => '赵六', 'status' => 1]);空白实体不允许保存
无任何属性赋值(或仅有声明默认值的可空列)的实体调用 save() 会抛 InvalidArgumentException。否则会在 MySQL 下静默执行 INSERT INTO t () VALUES () 插入一条全默认值记录。
修改实体(增量 UPDATE)
从数据库查询得到的实体记录了水合时的原始值快照,save() 只 UPDATE 变更过的列:
$user = UserEntity::find(1);
$user->age = 21;
$user->save(); // 仅 UPDATE age 列,其他列不参与
$user->age = 20; // 改回原值
$user->save(); // 返回 false:无变更,不执行 SQL删除实体
$user = UserEntity::find(1);
$user->delete(); // 实体启用软删除时为软删除,传 true 强制硬删除附加数据(关联结果)
with() 预加载的关联数据与 pivot 中间表数据不属于实体字段,水合时存入附加数据:可通过属性方式读取(IDE 推导为 mixed)、会随 toArray() 输出,但不参与 save() 写入,避免把关联结构误写入业务表:
$user = UserEntity::with('articles')->find(1);
foreach ($user->articles as $article) {
echo $article->title;
}
$user->userName = '新名字';
$user->save(); // 仅 UPDATE user_name,articles 不参与写入序列化
toArray() 输出蛇形列名键数组,与 ORM 模型约定一致:枚举转回退值、日期对象格式化为 Y-m-d H:i:s、应用 get{Field}Attr 获取器与 $hidden 隐藏字段;对象直接 JSON 序列化(如响应输出)等价于 toArray()。
$user = UserEntity::find(1);
$user->toArray();
// [
// 'id' => 1,
// 'user_name' => 'alice',
// 'status' => 1, // 枚举 → 回退值
// 'create_time' => '2026-01-01 08:30:00', // 日期对象 → 字符串
// ]Entity 与 Model 如何选择
| 场景 | 建议 |
|---|---|
| 新项目 / 新模块,追求类型安全与 IDE 体验 | Entity:字段即类型化属性,重构改名有静态分析兜底 |
| 动态字段较多的表、依赖通配获取器按模式批量转换 | Model:动态属性更灵活 |
| 两者可共存 | 同一张表可以同时定义 UserModel 与 UserEntity,按场景选用,互不影响 |
能力完全对等
Entity 继承自 Model,软删除、自动时间戳、获取器/修改器、with() 关联预加载、关联写入(attach() / detach() / sync())等全部 ORM 能力用法一致——差异只在数据的承载方式(类型化属性 vs 动态属性)与空结果语义(null vs 空 DataSet)。
