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 编写。

定义实体

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 列 → 时间对象
}
php
enum UserStatus: int
{
    case Active = 1;
    case Disabled = 2;
}

属性命名约定

属性名驼峰、数据库列名蛇形,框架自动互转:属性 userName ↔ 列 user_name。未映射到实体属性的查询列(如 with() 关联结果、pivot 数据)会存入附加数据,可用于展示但不参与写入。

支持的属性类型

属性类型列值(数据库)水合结果
int'1' / 1 / 1.0int(数字字符串严格转整型,超 19 位拒绝防溢出)
float数字或数字字符串float
string标量string
bool0 / 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类名推断数据表名
$suffixEntity推断表名时去除的类名后缀
$pkid主键字段名
$channelNamenull绑定的数据库通道
$hidden[]toArray() 序列化时隐藏的字段

其余模型配置($autoWriteTimestamp、$enableSoftDelete 等)与 ORM 模型完全一致,参见 ORM 模型。

查询数据

静态调用经魔术转发到实体查询器(EntityQuery),条件构建与 ORM 模型完全一致:

php
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() 递归序列化为实体数组:

php
$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)

php
// 方式一:构造传参(驼峰属性名)
$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 变更过的列:

php
$user = UserEntity::find(1);
$user->age = 21;
$user->save();          // 仅 UPDATE age 列,其他列不参与

$user->age = 20;        // 改回原值
$user->save();          // 返回 false:无变更,不执行 SQL

删除实体

php
$user = UserEntity::find(1);
$user->delete();        // 实体启用软删除时为软删除,传 true 强制硬删除

附加数据(关联结果)

with() 预加载的关联数据与 pivot 中间表数据不属于实体字段,水合时存入附加数据:可通过属性方式读取(IDE 推导为 mixed)、会随 toArray() 输出,但不参与 save() 写入,避免把关联结构误写入业务表:

php
$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()。

php
$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)。

下一步

  • 关联关系:with() 预加载在实体上的关联数据形态
  • ORM 模型:软删除、自动时间戳、获取器/修改器详细用法
  • 查询构造器:实体查询器的底层能力