8: ORM
Phlo 附带一个强大的内置 ORM,允许您将数据库表定义为类。模型可以通过 columns 快速定义,或通过声明式 schema 完整定义。记录被视为 instances,支持 props、methods、views、relations 和多种数据库引擎。
8.1: 基础知识
一个 ORM 模型是一个 .phlo 文件,包含:
@ class:模型(和表)的名称@ extends: modelstatic table和columns或schema- 可选:关系(
parent,child,many) - Props、methods 和 views 针对每个 record instance 操作
示例:
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static columns = 'id,name,email,active,created'8.2: 定义模型
7.2.1 使用 columns(快速且轻量)
使用 columns 来创建简单的表格:
@ class: shipment
@ extends: model
view: $this->destination ($this->user)
static table = 'shipments'
static order = 'changed DESC'
static columns = 'id,user,destination,costs,valid,weight,shipped,created,changed'
static objParents = ['user' => 'user']
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static order = 'changed DESC'
static columns = 'id,name,email,level,active,created,changed'
static objChildren = ['shipments' => 'shipment']
7.2.2 使用 schema 和 field(...)(丰富且声明式)
使用 schema,您可以在一个地方定义字段、关系和用户界面:
@ class: shipment
@ extends: model
view: $this->destination ($this->user)
static table = 'shipments'
static schema => arr (
id: field (type: 'token', length: 4, title: 'ID'),
destination: field (type: 'text', required: true, search: true),
user: field (type: 'parent', obj: 'user', required: true),
costs: field (type: 'price', prefix: '€ '),
valid: field (type: 'bool'),
attachments: field (type: 'child', obj: 'attachment', list: true),
)
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static schema => arr (
id: field (type: 'token'),
name: field (type: 'text', search: true, required: true),
email: field (type: 'email', required: true),
shipments: field (type: 'child', obj: 'shipment'),
groups: field (type: 'many', obj: 'group', table: 'user_groups'),
)
schema在与 PhloCMS 结合使用时尤其强大,但也可以单独使用。
8.3: CRUD
获取:
$user = user::record(id: 1)
$list = shipment::records(order: 'created DESC')
创建:
$shipment = shipment::create(destination: 'Paris', user: 1)
编辑和保存:
$shipment->destination = 'Lyon'
$shipment->objSave
删除:
shipment::delete('id=?', $shipment->id)
record(...)→ 单个记录(或 null)records(...)→ 记录数组(类实例)create(...)→ 插入 + 立即获取objSave→ 保存实例(根据 id 插入/更新)delete(...)→ 带有 SQL where 子句的静态删除
8.4: 关系导航
关系可作为属性使用:
| 类型 | 声明 | 用法 |
|---|---|---|
| parent | type: parent |
$shipment->user |
| child | type: child |
$user->shipments |
| many | type: many |
$user->groups |
多对多
type: many 使用一个中间表:
groups: field (
type: 'many',
obj: 'group',
table: 'user_groups',
)
导航:
$user = user::record(id: 1)
foreach ($user->groups as $group)
echo $group->title
关系是为了性能而批量加载的。没有跨数据库的连接;每个类都从其自己的引擎加载。
8.5: 实例动态
每个记录都是您模型类的真实实例。
您可以使用 props、methods 和 views 来添加虚拟字段、计算或表示:
@ class: shipment
@ extends: model
prop summary => $this->destination.' ('.$this->user.')'
method tax => $this->costs * 0.21
view:
<p>$this->summary</p>
<p>$this->tax</p>
用法:
$shipment = shipment::record(id: 'AB12')
echo $shipment->summary
echo $shipment
使用记录作为字符串会调用其视图表示。
Props 和 methods 始终在 record instance 上操作,而不是静态地。
8.6: 过滤和查询
所有查询方法都接受命名参数和类似 SQL 的过滤器:
shipment::records(destination: 'Paris')
shipment::records(where: 'valid=1 AND weight>10')
shipment::pair(columns: 'id,destination')
支持:where、order、group、joins、缓存和模式感知列。
8.7: 缓存和性能
ORM 使用内部缓冲区 (objRecords, objLoaded) 进行关系查找,并通过可选的 APCu 缓存:
static objCache = true
或者是秒数:
static objCache = 600
记录和关系是批量加载的。
在循环中使用 records() 进行批量选择,而不是使用 record()。
8.8: 多个引擎
Phlo 是与数据库无关的:模型通过 static DB 解析其引擎,并且没有隐式默认值。通过从 app.phlo 将引擎注入到模型基类中,为整个应用程序设置一次引擎:
static %model.DB => %MySQL
为单个模型重写它,使用自己的 static DB。
SQLite
@ class: notes
@ extends: model
static DB => %SQLite(data.'notes.db')
static table = 'notes'
static columns = 'id,title,body'
PostgreSQL
@ class: invoices
@ extends: model
static DB => %PostgreSQL
static table = 'invoices'
static columns = 'id,customer_id,total,created'
不同引擎上的表可以在关系中组合;每个类获取自己的数据。
/data/creds.ini
对于 MySQL 和 PostgreSQL 等引擎,请将您的凭据放在:
/data/creds.ini
[mysql]
host = localhost
database = db_name
user = db_user
password = db_password
[postgresql]
host = localhost
database = my_pg_db
user = pg_user
password = pg_pass
Phlo 通过 %creds->... 自动加载这些。
8.9: 选择加入的功能:审计、验证、自定义主键
一个 @ extends: model 让你开箱即用地获得 CRUD + 身份映射 + 关系。每个模型可以通过一个静态标志启用三个额外的选项。
8.9.1 审计日志
static objAudit = true
从那时起,每个 create、objSave(更新)和 delete 都会通过 security/audit 资源记录到审计表中:
| 操作 | 记录内容 |
|---|---|
create(...) |
完整的新值 |
objSave(更新) |
差异:仅更改的字段,from → to |
delete(...) |
完整的旧值,按受影响的记录 |
设置:
- 将
security/audit添加到data/app.json中的资源。 - 导入模式 SQL 一次:
mysql <database> < /opt/phlo/resources/security/audit.sql。
排除敏感字段:
method afterCreate => %audit->log($this, 'create', [], (array)$this, exclude: ['password_hash'])
按环境切换,仅限开发:
static objAudit => debug
或仅发布:
static objAudit => !debug
(debug 是来自 phlo_app(debug: ...) 的运行时常量。)
读取日志。security/audit 资源还提供了一个小型查询 API,模型作为第一个参数:
| 调用 | 返回 |
|---|---|
%audit->history($model, $recordId, $limit = 50) |
每个记录的所有更改,最新的在前 |
%audit->byUser($model, $userId, $fromTs = 0, $limit = 100) |
自 $fromTs 以来用户所做的每个更改 |
%audit->purge($model, $olderThanSeconds = 31536000) |
删除超过保留期限的条目 |
$rows = %audit->history(invoice, $invoiceId)
每个 audit_log 行都包含 ts、user(在更改时的 %session->user,或为 null)、model、record_id、action、ip 和一个 changes JSON blob,其结构取决于操作:create 存储完整的新行,update 仅存储更改的字段,格式为 {"col": {"from": ..., "to": ...}},而 delete 存储完整的旧行。
8.9.2 验证
static objValidate = true
在 create() 之前,Phlo 会对 static schema() 中的每个字段运行匹配的 objValidate($value)。如果出现错误:create() 返回 null,错误信息可以通过 Class::objErrors() 获取:
if (!user::create($args)){
return apply(errors: user::objErrors())
}
schema() 中的字段规则:
static schema => arr (
email: field (type: 'email', required: true),
name: field (type: 'text', length: 100, required: true),
slug: field (type: 'text', pattern: '^[a-z0-9-]+$'),
status: field (type: 'text', enum: ['draft', 'sent', 'paid']),
)
自定义字段验证:在您自己的字段子类中重写 method objValidate($value)。
8.9.3 自定义主键
默认值为 id(自增整数)。可以重写以使用其他主键:
static idColumn = 'sku'
static idType = 'string'
效果:
- 身份映射使用
sku作为其键 Class::record(sku: 'XYZ-123')(而不是id:)- 使用
create():您自己提供主键值(没有自动递增) $record->id不起作用,请使用$record->sku
8.9.4 组合
这三个选项是独立的,可以组合使用:
@ class: giftcard
@ extends: model
static objAudit = true
static objValidate = true
static idColumn = 'sku'
static idType = 'string'
每个功能默认都是关闭的(false,'id','int')。没有选择的模型仍然是一个普通模型。
8.10: 函数概述
| 函数 / 属性 | 类型 | 描述 |
|---|---|---|
record(...) |
static | 获取 1 条记录(或 null) |
records(...) |
static | 获取多条记录(数组) |
create(...) |
static | 插入 + 获取 |
objSave |
instance | 插入或更新 |
delete(where, …) |
static | 删除 |
pair, item, column |
static | 快速查询助手 |
objParents / schema: parent |
declarative | 父关系 |
objChildren / schema: child |
declarative | 子关系 |
schema: many |
declarative | 多对多 |
objCache |
static | 可选的 APCu 缓存 |
static DB |
static | 每个模型的引擎 |
8.11: Best practices
- 使用
columns进行快速、简单的模型。 - 使用
schema进行丰富的定义和CMS集成。 - 定义一个
view以获取字符串表示。 - 使用 props 进行虚拟字段。
- 使用
objSave而不是save。 - 使用
records()进行批量加载。 - 使用
static DB为每个引擎分离模型;使用static %model.DB设置应用程序范围的默认值。 - 将凭据保存在
/data/creds.ini中。 - 保持模型声明式;将逻辑放在 props/methods 中。