- 快速开始
- 创建第一个模型
- 基本使用
- 模型定义
- 基本模型类
- 表名配置
- 主键配置
- 完整模型示例
- 查询数据
- 查找单条记录
- 查找多条记录
- 查询构建器方法
- 聚合查询
- 查询单个值
- 判断记录是否存在
- 分页查询
- 创建数据
- create() - 批量创建
- new + save() - 手动创建
- fill() - 批量赋值
- 创建时的注意事项
- 更新数据
- save() - 保存模型
- update() - 批量更新
- 检查变更
- 增量/减量更新
- 删除数据
- delete() - 删除模型
- 软删除
- 恢复软删除
- 查询软删除的记录
- 模型关联
- 一对一(HasOne)
- 一对多(HasMany)
- 多对一(BelongsTo)
- 预加载关联(Eager Loading)
- 关联查询方法
- 字段类型转换
- 定义类型转换
- 支持的转换类型
- 使用示例
- 访问器和修改器
- 访问器(Accessors)
- 修改器(Mutators)
- 访问器和类型转换结合
- 批量赋值保护
- fillable(白名单)
- guarded(黑名单)
- 完全禁止批量赋值
- fillable 和 guarded 的优先级
- 推荐配置
- 软删除
- 启用软删除
- 软删除操作
- 查询软删除的记录
- 恢复软删除
- 软删除的最佳实践
- 时间戳
- 自动维护时间戳
- 时间戳行为
- 禁用时间戳
- 自定义时间戳字段名
- 模型事件
- beforeSave() - 保存前
- afterSave() - 保存后
- beforeDelete() - 删除前
- afterDelete() - 删除后
- 事件使用示例
- 查询作用域
- 定义作用域
- 使用作用域
- 作用域最佳实践
- 属性操作
- 获取属性
- 设置属性
- 检查属性
- 获取原始值
- 检查变更
- 获取原始值
- 同步原始值
- 序列化
- toArray() - 转换为数组
- toJson() - 转换为 JSON
- 序列化关联
- 数据库连接
- 使用默认连接
- 指定连接
- 动态设置连接
- 最佳实践
- 1. 模型组织
- 2. 命名规范
- 3. 批量赋值保护
- 4. 类型转换
- 5. 避免 N+1 查询
- 6. 使用查询作用域
- 7. 合理使用软删除
- 8. 模型事件
- 9. 访问器和修改器
- 10. 查询优化
- API 参考
- 静态方法
- 实例方法
- 关联方法
- 常见问题
- Q: 如何获取原始属性值?
- Q: 如何检查模型是否已保存?
- Q: 如何获取变更的字段?
- Q: 模型可以跨数据库连接吗?
- Q: 如何禁用时间戳?
- Q: 访问器和修改器的命名规则是什么?
- Q: 如何预加载多个关联?
- Q: 关联查询如何添加条件?
- 高级功能
- 模型刷新
- 模型复制
- 批量更新
- 批量删除
- 模型比较
- 模型集合操作
- 常见问题
- Q: 如何获取原始属性值?
- Q: 如何检查模型是否已保存?
- Q: 如何获取变更的字段?
- Q: 模型可以跨数据库连接吗?
- Q: 如何禁用时间戳?
- Q: 访问器和修改器的命名规则是什么?
- Q: 如何预加载多个关联?
- Q: 关联查询如何添加条件?
- Q: 如何实现模型作用域?
- Q: 模型事件如何工作?
- 相关文档
ORM 使用指南
最后更新: 2026-01-27 11:02:47ORM 使用指南
Unicode Framework 提供了功能完整的 ActiveRecord 风格 ORM,支持模型定义、查询、关联、事件、作用域等高级特性。本文档是 ORM 的完整使用指南。
快速开始
创建第一个模型
<?php
namespace App\Index\Model;
use Unicode\Framework\Database\Model;
class User extends Model
{
// 模型会自动映射到 users 表
}
基本使用
// 查找用户
$user = User::find(1);
// 创建用户
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
// 更新用户
$user->name = 'Jane Doe';
$user->save();
// 删除用户
$user->delete();
模型定义
基本模型类
<?php
namespace App\Index\Model;
use Unicode\Framework\Database\Model;
class User extends Model
{
// 所有配置都是可选的,框架会使用合理的默认值
}
表名配置
class User extends Model
{
// 方式 1:显式指定表名
protected static ?string $table = 'users';
// 方式 2:不指定,框架自动推断
// 类名 User -> 表名 users
// 类名 UserProfile -> 表名 user_profiles
}
表名推断规则:
- 将类名从驼峰转换为下划线:
UserProfile→user_profile - 自动复数化:
user_profile→user_profiles
主键配置
class User extends Model
{
// 指定主键字段名(默认为 'id')
protected static string $primaryKey = 'id';
// 使用其他主键
protected static string $primaryKey = 'user_id';
}
完整模型示例
<?php
namespace App\Index\Model;
use Unicode\Framework\Database\Model;
use Unicode\Framework\Database\Relations\HasOne;
use Unicode\Framework\Database\Relations\HasMany;
use Unicode\Framework\Database\Relations\BelongsTo;
class User extends Model
{
// 表名(可选)
protected static ?string $table = 'users';
// 主键(可选,默认为 'id')
protected static string $primaryKey = 'id';
// 时间戳配置
protected static bool $timestamps = true;
protected static string $createdAt = 'created_at';
protected static string $updatedAt = 'updated_at';
// 批量赋值保护
protected static array $fillable = [
'name',
'email',
'password',
];
protected static array $guarded = [];
// 软删除
protected static bool $softDelete = false;
protected static string $deleteTime = 'deleted_at';
// 字段类型转换
protected static array $casts = [
'is_active' => 'boolean',
'settings' => 'json',
'created_at' => 'datetime',
'score' => 'integer',
];
// 关联关系
public function profile(): HasOne
{
return $this->hasOne(Profile::class, 'user_id');
}
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'user_id');
}
}
查询数据
查找单条记录
#### find() - 根据主键查找
// 根据主键查找
$user = User::find(1);
// 如果不存在,返回 null
if ($user === null) {
// 用户不存在
}
// 查找多个主键
$users = [];
foreach ([1, 2, 3] as $id) {
$user = User::find($id);
if ($user !== null) {
$users[] = $user;
}
}
#### first() - 查找第一条记录
// 查找第一条记录
$user = User::first();
// 条件查找第一条
$user = User::where('email', 'user@example.com')->first();
// 排序后查找第一条
$user = User::orderBy('created_at', 'desc')->first();
#### 条件查找
// 单条件
$user = User::where('email', 'user@example.com')->first();
// 多条件
$user = User::where('email', 'user@example.com')
->where('status', 'active')
->first();
// 复杂条件
$user = User::where('age', '>', 18)
->where('status', 'active')
->orWhere('is_vip', true)
->first();
查找多条记录
#### all() - 查找所有记录
// 查找所有记录
$users = User::all();
// ⚠️ 注意:如果数据量大,建议使用分页
// 推荐:使用查询构建器限制数量
$users = User::query()->limit(100)->get();
#### get() - 条件查询多条
// 基本查询
$users = User::where('status', 'active')->get();
// 复杂查询
$users = User::query()
->where('status', 'active')
->where('age', '>=', 18)
->orderBy('created_at', 'desc')
->limit(100)
->get();
// 查询指定字段
$users = User::query()
->select(['id', 'name', 'email'])
->get();
查询构建器方法
模型可以使用所有查询构建器的方法:
// WHERE 条件
$users = User::where('status', 'active')->get();
$users = User::where('age', '>', 18)->get();
$users = User::whereIn('id', [1, 2, 3])->get();
$users = User::whereNull('deleted_at')->get();
// 排序
$users = User::orderBy('created_at', 'desc')->get();
$users = User::orderBy('name', 'asc')->orderBy('id', 'desc')->get();
// 限制和偏移
$users = User::limit(10)->get();
$users = User::offset(10)->limit(10)->get();
// 分组
$stats = User::query()
->select('status', db()->raw('COUNT(*) as count'))
->groupBy('status')
->get();
聚合查询
// 计数
$count = User::count();
$activeCount = User::where('status', 'active')->count();
// 最大值
$maxAge = User::max('age');
// 最小值
$minAge = User::min('age');
// 平均值
$avgAge = User::avg('age');
// 求和
$totalScore = User::sum('score');
查询单个值
// 获取单个字段值
$name = User::where('id', 1)->value('name');
// 获取单个字段值(带默认值)
$name = User::where('id', 999)->value('name', 'Unknown');
判断记录是否存在
// 检查记录是否存在
$exists = User::where('email', 'user@example.com')->exists();
// 在条件判断中使用
if (User::where('email', $email)->exists()) {
// 邮箱已存在
}
分页查询
// 使用 limit 和 offset
$page = 1;
$perPage = 20;
$users = User::query()
->offset(($page - 1) * $perPage)
->limit($perPage)
->get();
// 获取总数
$total = User::count();
创建数据
create() - 批量创建
// 基本创建
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => password_hash('password', PASSWORD_DEFAULT),
]);
// 创建后自动获取 ID
echo $user->id; // 自动填充主键
new + save() - 手动创建
// 方式 1:使用 new 和属性赋值
$user = new User();
$user->name = 'John Doe';
$user->email = 'john@example.com';
$user->save();
// 方式 2:使用 new 和 fill
$user = new User();
$user->fill([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$user->save();
fill() - 批量赋值
$user = new User();
$user->fill([
'name' => 'John Doe',
'email' => 'john@example.com',
// 只有 fillable 中的字段会被赋值
]);
$user->save();
创建时的注意事项
// ✅ 推荐:使用 fillable 保护敏感字段
class User extends Model
{
protected static array $fillable = [
'name',
'email',
// 不包含 password、is_admin 等敏感字段
];
}
// 创建时,敏感字段会被忽略
$user = User::create([
'name' => 'John',
'email' => 'john@example.com',
'is_admin' => true, // 会被忽略(不在 fillable 中)
]);
更新数据
save() - 保存模型
// 查找后更新
$user = User::find(1);
$user->name = 'Jane Doe';
$user->email = 'jane@example.com';
$user->save();
// 只更新变更的字段
$user = User::find(1);
$user->name = 'New Name'; // 只有 name 变更
$user->save(); // 只更新 name 字段
update() - 批量更新
// 方式 1:使用 update 方法
$user = User::find(1);
$user->update([
'name' => 'Jane Doe',
'email' => 'jane@example.com',
]);
// 方式 2:使用查询构建器批量更新
User::where('status', 'pending')
->update(['status' => 'active']);
检查变更
$user = User::find(1);
// 检查是否有变更
if ($user->isDirty()) {
$dirty = $user->getDirty(); // 获取变更的字段
echo "Changed fields: " . implode(', ', array_keys($dirty));
$user->save();
}
// 检查特定字段是否变更
if ($user->isDirty('name')) {
echo "Name has been changed";
}
// 获取原始值
$originalName = $user->getOriginal('name');
增量/减量更新
// 使用查询构建器
User::where('id', 1)->increment('score', 10);
User::where('id', 1)->decrement('score', 5);
// 带其他字段更新
User::where('id', 1)->increment('score', 10, [
'updated_at' => date('Y-m-d H:i:s'),
]);
删除数据
delete() - 删除模型
// 方式 1:删除模型实例
$user = User::find(1);
$user->delete();
// 方式 2:批量删除
User::where('status', 'inactive')->delete();
// 方式 3:根据主键删除
User::destroy(1);
User::destroy([1, 2, 3]);
软删除
如果启用了软删除,delete() 会标记删除而不是真正删除:
class User extends Model
{
protected static bool $softDelete = true;
protected static string $deleteTime = 'deleted_at';
}
// 软删除
$user = User::find(1);
$user->delete(); // 设置 deleted_at 字段,不真正删除
// 强制删除(真正删除)
$user->delete(true);
// 或
$user->forceDelete();
恢复软删除
// 恢复软删除的记录
$user = User::onlyTrashed()->where('id', 1)->first();
if ($user !== null) {
$user->restore(); // 清除 deleted_at
}
查询软删除的记录
// 排除软删除(默认)
$users = User::all(); // 不包含已删除的记录
// 包含软删除
$users = User::withTrashed()->get();
// 只查询软删除
$users = User::onlyTrashed()->get();
模型关联
一对一(HasOne)
#### 定义关联
class User extends Model
{
public function profile(): HasOne
{
return $this->hasOne(Profile::class, 'user_id');
}
// 指定外键和本地键
public function profile(): HasOne
{
return $this->hasOne(Profile::class, 'user_id', 'id');
}
}
#### 使用关联
// 获取关联
$user = User::find(1);
$profile = $user->profile; // 自动查询关联的 Profile
// 检查关联是否存在
if ($user->profile !== null) {
echo $user->profile->bio;
}
#### 创建关联记录
$user = User::find(1);
// 方式 1:通过关联创建
$profile = $user->profile()->create([
'bio' => 'User bio',
'avatar' => 'avatar.jpg',
]);
// 方式 2:手动创建并关联
$profile = new Profile(['bio' => 'User bio']);
$user->profile()->save($profile);
一对多(HasMany)
#### 定义关联
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'user_id');
}
// 指定外键和本地键
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'user_id', 'id');
}
}
#### 使用关联
// 获取关联集合
$user = User::find(1);
$posts = $user->posts; // 返回 Post 模型数组
// 遍历关联
foreach ($user->posts as $post) {
echo $post->title;
}
// 条件查询关联
$publishedPosts = $user->posts()
->where('status', 'published')
->get();
#### 创建关联记录
$user = User::find(1);
// 创建关联记录
$post = $user->posts()->create([
'title' => 'New Post',
'content' => 'Post content',
]);
// 批量创建
$user->posts()->createMany([
['title' => 'Post 1', 'content' => 'Content 1'],
['title' => 'Post 2', 'content' => 'Content 2'],
]);
多对一(BelongsTo)
#### 定义关联
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
// 指定外键和关联键
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id', 'id');
}
}
#### 使用关联
// 获取关联
$post = Post::find(1);
$user = $post->user; // 自动查询关联的 User
$category = $post->category;
// 关联可能为 null
if ($post->category !== null) {
echo $post->category->name;
}
#### 关联查询
// 通过关联查询
$posts = Post::whereHas('user', function($query) {
$query->where('status', 'active');
})->get();
// 关联计数
$userCount = Post::withCount('user')->get();
预加载关联(Eager Loading)
预加载可以避免 N+1 查询问题:
// ❌ 错误:N+1 查询
$users = User::all();
foreach ($users as $user) {
foreach ($user->posts as $post) {
// 每次循环都查询数据库
}
}
// ✅ 正确:预加载关联
$users = User::with('posts')->get();
foreach ($users as $user) {
foreach ($user->posts as $post) {
// 已预加载,不查询数据库
}
}
// 预加载多个关联
$users = User::with(['posts', 'profile'])->get();
// 条件预加载
$users = User::with(['posts' => function($query) {
$query->where('status', 'published');
}])->get();
// 嵌套预加载
$users = User::with(['posts.category'])->get();
关联查询方法
// 检查关联是否存在
$hasPosts = $user->posts()->exists();
// 关联计数
$postCount = $user->posts()->count();
// 关联查询
$recentPosts = $user->posts()
->where('created_at', '>', date('Y-m-d', strtotime('-7 days')))
->orderBy('created_at', 'desc')
->get();
字段类型转换
定义类型转换
class User extends Model
{
protected static array $casts = [
'is_active' => 'boolean',
'is_vip' => 'boolean',
'score' => 'integer',
'price' => 'float',
'settings' => 'json',
'metadata' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}
支持的转换类型
| 类型 | 说明 | 示例 |
| ------------------- | -------------- | --------------------------------- |
| boolean / bool | 布尔值 | true / false |
| integer / int | 整数 | 123 |
| float / double | 浮点数 | 123.45 |
| string | 字符串 | "text" |
| json | JSON 数组/对象 | ["a", "b"] / {"key": "value"} |
| array | 数组 | ["a", "b"] |
| object | 对象 | {"key": "value"} |
| datetime / date | 日期时间 | "2024-01-01 12:00:00" |
| timestamp | Unix 时间戳 | 1704067200 |
使用示例
class User extends Model
{
protected static array $casts = [
'settings' => 'json',
'is_vip' => 'boolean',
'score' => 'integer',
];
}
$user = User::find(1);
// 自动类型转换
$settings = $user->settings; // 自动转换为数组
$isVip = $user->is_vip; // 自动转换为布尔值
$score = $user->score; // 自动转换为整数
// 设置时自动转换
$user->settings = ['theme' => 'dark', 'lang' => 'zh'];
$user->save(); // 自动转换为 JSON 字符串存储
// JSON 字段
$user->settings = [
'theme' => 'dark',
'notifications' => ['email' => true, 'sms' => false],
];
$user->save();
访问器和修改器
访问器(Accessors)
访问器允许您在获取属性时进行转换:
class User extends Model
{
// 访问器命名:get{FieldName}Attribute
public function getFullNameAttribute(): string
{
return $this->first_name . ' ' . $this->last_name;
}
public function getAvatarUrlAttribute(): string
{
if ($this->avatar) {
return '/storage/avatars/' . $this->avatar;
}
return '/images/default-avatar.png';
}
public function getIsAdultAttribute(): bool
{
return $this->age >= 18;
}
}
// 使用访问器
$user = User::find(1);
echo $user->full_name; // 自动调用 getFullNameAttribute
echo $user->avatar_url; // 自动调用 getAvatarUrlAttribute
echo $user->is_adult ? 'Adult' : 'Minor';
修改器(Mutators)
修改器允许您在设置属性时进行转换:
class User extends Model
{
// 修改器命名:set{FieldName}Attribute
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
}
public function setEmailAttribute(string $value): void
{
$this->attributes['email'] = strtolower(trim($value));
}
public function setNameAttribute(string $value): void
{
$this->attributes['name'] = ucwords(strtolower($value));
}
}
// 使用修改器
$user = new User();
$user->password = 'plaintext'; // 自动加密
$user->email = ' JOHN@EXAMPLE.COM '; // 自动转换为 'john@example.com'
$user->name = 'john doe'; // 自动转换为 'John Doe'
$user->save();
访问器和类型转换结合
class User extends Model
{
protected static array $casts = [
'settings' => 'json',
];
// 访问器可以进一步处理类型转换后的值
public function getThemeAttribute(): string
{
$settings = $this->settings ?? [];
return $settings['theme'] ?? 'light';
}
// 修改器可以处理设置前的值
public function setSettingsAttribute(array $value): void
{
// 在存储前进行验证或处理
$this->attributes['settings'] = json_encode($value, JSON_UNESCAPED_UNICODE);
}
}
批量赋值保护
fillable(白名单)
使用 fillable 定义允许批量赋值的字段:
class User extends Model
{
protected static array $fillable = [
'name',
'email',
'password',
];
}
// 只有 fillable 中的字段可以被批量赋值
$user = User::create([
'name' => 'John',
'email' => 'john@example.com',
'is_admin' => true, // 这个字段会被忽略(不在 fillable 中)
]);
guarded(黑名单)
使用 guarded 定义禁止批量赋值的字段:
class User extends Model
{
protected static array $fillable = [];
protected static array $guarded = ['id', 'is_admin', 'created_at'];
}
// guarded 中的字段不能被批量赋值
$user = User::create([
'name' => 'John',
'email' => 'john@example.com',
'is_admin' => true, // 这个字段会被忽略(在 guarded 中)
]);
完全禁止批量赋值
class User extends Model
{
protected static array $fillable = [];
protected static array $guarded = ['*']; // 禁止所有字段
}
// 必须手动设置每个字段
$user = new User();
$user->name = 'John';
$user->email = 'john@example.com';
$user->save();
fillable 和 guarded 的优先级
fillable 不为空,只允许 fillable 中的字段fillable 为空且 guarded 不为空且不包含 '*',排除 guarded 中的字段guarded 包含 '*',禁止所有批量赋值推荐配置
// ✅ 推荐:使用 fillable(白名单更安全)
class User extends Model
{
protected static array $fillable = [
'name',
'email',
'password',
// 明确列出允许的字段
];
}
// ❌ 不推荐:使用 guarded(容易遗漏)
class User extends Model
{
protected static array $guarded = ['id', 'is_admin'];
// 如果新增敏感字段,可能忘记添加到 guarded
}
软删除
启用软删除
class User extends Model
{
protected static bool $softDelete = true;
protected static string $deleteTime = 'deleted_at';
}
软删除操作
// 软删除(设置 deleted_at)
$user = User::find(1);
$user->delete(); // 设置 deleted_at,不真正删除
// 强制删除(真正删除)
$user->delete(true);
// 或
$user->forceDelete();
查询软删除的记录
// 排除软删除(默认行为)
$users = User::all(); // 不包含已删除的记录
$user = User::find(1); // 如果已软删除,返回 null
// 包含软删除
$users = User::withTrashed()->get();
$user = User::withTrashed()->find(1); // 即使已软删除也能找到
// 只查询软删除
$users = User::onlyTrashed()->get();
$user = User::onlyTrashed()->where('id', 1)->first();
恢复软删除
// 恢复单个记录
$user = User::onlyTrashed()->where('id', 1)->first();
if ($user !== null) {
$user->restore(); // 清除 deleted_at
}
// 批量恢复
User::onlyTrashed()
->where('deleted_at', '>', date('Y-m-d', strtotime('-30 days')))
->get()
->each(function($user) {
$user->restore();
});
软删除的最佳实践
// ✅ 推荐:对需要保留历史的数据使用软删除
class Order extends Model
{
protected static bool $softDelete = true;
protected static string $deleteTime = 'deleted_at';
}
// ✅ 推荐:对临时数据不使用软删除
class Cache extends Model
{
protected static bool $softDelete = false;
}
// ✅ 推荐:定期清理过期的软删除记录
class CleanupTask
{
public function cleanupOldSoftDeletes(): void
{
$cutoffDate = date('Y-m-d', strtotime('-90 days'));
User::onlyTrashed()
->where('deleted_at', '<', $cutoffDate)
->get()
->each(function($user) {
$user->forceDelete(); // 真正删除
});
}
}
时间戳
自动维护时间戳
class User extends Model
{
// 启用时间戳(默认)
protected static bool $timestamps = true;
protected static string $createdAt = 'created_at';
protected static string $updatedAt = 'updated_at';
}
时间戳行为
// 创建时自动设置 created_at 和 updated_at
$user = User::create(['name' => 'John']);
// created_at 和 updated_at 自动设置为当前时间
// 更新时自动更新 updated_at
$user->name = 'Jane';
$user->save(); // updated_at 自动更新为当前时间
// 手动设置时间戳
$user = new User();
$user->name = 'John';
$user->created_at = '2024-01-01 00:00:00';
$user->updated_at = '2024-01-01 00:00:00';
$user->save();
禁用时间戳
// 全局禁用
class User extends Model
{
protected static bool $timestamps = false;
}
// 单次操作禁用
$user = User::find(1);
$user->timestamps = false; // 注意:这不是标准方法,需要检查实现
$user->save();
自定义时间戳字段名
class User extends Model
{
protected static string $createdAt = 'create_time';
protected static string $updatedAt = 'update_time';
}
模型事件
模型提供了生命周期事件钩子:
beforeSave() - 保存前
class User extends Model
{
protected function beforeSave(): bool
{
// 保存前的逻辑
if (empty($this->name)) {
return false; // 返回 false 取消保存
}
// 自动设置某些字段
if (!$this->exists) {
$this->activation_token = bin2hex(random_bytes(32));
}
return true; // 返回 true 继续保存
}
}
afterSave() - 保存后
class User extends Model
{
protected function afterSave(): void
{
// 保存后的逻辑
if (!$this->exists) {
// 新创建的用户,发送欢迎邮件
$this->sendWelcomeEmail();
} else {
// 更新的用户,记录变更日志
$this->logChanges();
}
}
private function sendWelcomeEmail(): void
{
// 发送邮件逻辑
}
private function logChanges(): void
{
// 记录变更日志
}
}
beforeDelete() - 删除前
class User extends Model
{
protected function beforeDelete(): bool
{
// 删除前的检查
if ($this->hasActiveOrders()) {
return false; // 有活跃订单,不允许删除
}
// 清理关联数据
$this->posts()->delete();
$this->profile()->delete();
return true;
}
private function hasActiveOrders(): bool
{
return db('orders')
->where('user_id', $this->id)
->where('status', 'active')
->exists();
}
}
afterDelete() - 删除后
class User extends Model
{
protected function afterDelete(): void
{
// 删除后的清理工作
// 删除用户上传的文件
$this->deleteUserFiles();
// 记录删除日志
Log::info('User deleted', ['user_id' => $this->id]);
}
private function deleteUserFiles(): void
{
// 删除文件逻辑
}
}
事件使用示例
class Order extends Model
{
protected function beforeSave(): bool
{
// 自动计算总价
if ($this->isDirty('items')) {
$this->total = $this->calculateTotal();
}
// 验证库存
if (!$this->validateStock()) {
return false;
}
return true;
}
protected function afterSave(): void
{
// 更新库存
if ($this->wasRecentlyCreated) {
$this->updateStock();
}
// 发送通知
$this->sendNotification();
}
protected function beforeDelete(): bool
{
// 检查是否可以删除
if ($this->status === 'shipped') {
return false; // 已发货的订单不能删除
}
return true;
}
protected function afterDelete(): void
{
// 恢复库存
$this->restoreStock();
// 记录删除日志
Log::info('Order deleted', ['order_id' => $this->id]);
}
}
查询作用域
查询作用域允许您封装常用的查询逻辑:
定义作用域
class User extends Model
{
// 作用域方法命名:scope{ScopeName}
public static function scopeActive($query)
{
return $query->where('status', 'active');
}
public static function scopeOlderThan($query, int $age)
{
return $query->where('age', '>', $age);
}
public static function scopeVerified($query)
{
return $query->whereNotNull('email_verified_at');
}
public static function scopeInRole($query, string $role)
{
return $query->where('role', $role);
}
}
使用作用域
// 单个作用域
$activeUsers = User::active()->get();
// 多个作用域链式调用
$adults = User::active()->olderThan(18)->get();
// 带参数的作用域
$seniors = User::olderThan(65)->get();
$admins = User::inRole('admin')->get();
// 组合使用
$verifiedAdults = User::verified()->olderThan(18)->get();
作用域最佳实践
class User extends Model
{
// ✅ 推荐:按业务逻辑组织作用域
public static function scopeActive($query)
{
return $query->where('status', 'active');
}
public static function scopeInactive($query)
{
return $query->where('status', 'inactive');
}
public static function scopeVerified($query)
{
return $query->whereNotNull('email_verified_at');
}
public static function scopeUnverified($query)
{
return $query->whereNull('email_verified_at');
}
// 复杂作用域
public static function scopeEligibleForPromotion($query)
{
return $query->where('status', 'active')
->where('age', '>=', 18)
->whereNotNull('email_verified_at')
->where('score', '>', 100);
}
}
// 使用
$eligibleUsers = User::eligibleForPromotion()->get();
属性操作
获取属性
$user = User::find(1);
// 方式 1:使用属性访问(推荐)
$name = $user->name;
// 方式 2:使用 getAttribute 方法
$name = $user->getAttribute('name');
$name = $user->getAttribute('name', 'Default Name'); // 带默认值
// 获取所有属性
$attributes = $user->getAttributes();
设置属性
$user = User::find(1);
// 方式 1:使用属性赋值(推荐)
$user->name = 'New Name';
// 方式 2:使用 setAttribute 方法
$user->setAttribute('name', 'New Name');
检查属性
$user = User::find(1);
// 检查属性是否存在
if (isset($user->name)) {
echo $user->name;
}
// 检查属性是否为空
if (empty($user->avatar)) {
$user->avatar = 'default.jpg';
}
获取原始值
$user = User::find(1);
$originalName = $user->name; // 'John'
$user->name = 'Jane';
$originalName = $user->getOriginal('name'); // 'John'(原始值)
// 获取所有原始值
$original = $user->getOriginal();
检查变更
$user = User::find(1);
// 检查是否有变更
if ($user->isDirty()) {
$dirty = $user->getDirty(); // 获取变更的字段
// ['name' => 'New Name']
}
// 检查特定字段是否变更
if ($user->isDirty('name')) {
echo "Name has been changed";
}
// 检查字段是否未变更
if (!$user->isDirty('email')) {
echo "Email has not been changed";
}
注意:isDirty() 方法在当前的 Model 实现中通过 getDirty() 方法实现,可以通过检查 getDirty() 是否为空来判断:
// 检查是否有变更
$hasChanges = !empty($user->getDirty());
// 检查特定字段是否变更
$nameChanged = isset($user->getDirty()['name']);
获取原始值
$user = User::find(1);
$originalName = $user->name; // 'John'
$user->name = 'Jane';
$originalName = $user->getOriginal('name'); // 'John'(原始值)
// 获取所有原始值
$original = $user->getOriginal();
注意:当前 Model 实现中,getOriginal() 方法需要直接访问 $original 属性或通过 getAttributes() 对比实现。建议在模型中添加辅助方法:
// 在模型中使用
$user = User::find(1);
$original = $user->original; // 直接访问 protected 属性(不推荐)
// 推荐:在模型中添加方法
public function getOriginal(?string $key = null): mixed
{
if ($key === null) {
return $this->original;
}
return $this->original[$key] ?? null;
}
同步原始值
$user = User::find(1);
$user->name = 'New Name';
// 同步原始值(取消变更)- 保存后自动同步
$user->save(); // 保存后 original 会自动更新为当前 attributes
// 手动同步(如果需要)
// 注意:当前实现中,保存后会自动同步,无需手动调用
序列化
toArray() - 转换为数组
$user = User::find(1);
// 转换为数组
$array = $user->toArray();
// [
// 'id' => 1,
// 'name' => 'John Doe',
// 'email' => 'john@example.com',
// ...
// ]
// 在响应中使用
return Response::json($user->toArray());
toJson() - 转换为 JSON
$user = User::find(1);
// 转换为 JSON 字符串
$json = $user->toJson();
// '{"id":1,"name":"John Doe","email":"john@example.com",...}'
// 在响应中使用
return Response::json($user->toArray());
// 或直接返回模型(框架会自动转换)
return Response::json($user);
序列化关联
$user = User::with('posts')->find(1);
// 包含关联的数组
$array = $user->toArray();
// [
// 'id' => 1,
// 'name' => 'John Doe',
// 'posts' => [
// ['id' => 1, 'title' => 'Post 1'],
// ['id' => 2, 'title' => 'Post 2'],
// ],
// ]
数据库连接
使用默认连接
class User extends Model
{
// 使用默认数据库连接
}
指定连接
class User extends Model
{
protected static function getConnection(): ConnectionInterface
{
return DatabaseManager::getInstance()->connection('mysql2');
}
}
动态设置连接
// 为模型设置连接
User::setConnection(DatabaseManager::getInstance()->connection('mysql2'));
// 使用指定连接查询
$user = User::on('mysql2')->find(1);
最佳实践
1. 模型组织
// ✅ 推荐:按功能模块组织模型
app/Index/Model/
├── User.php
├── Profile.php
├── Post.php
├── Category.php
└── Comment.php
2. 命名规范
// ✅ 推荐:模型类名使用单数,表名使用复数
class User extends Model { } // 表名:users
class Post extends Model { } // 表名:posts
class UserProfile extends Model { } // 表名:user_profiles
3. 批量赋值保护
// ✅ 推荐:使用 fillable 保护敏感字段
class User extends Model
{
protected static array $fillable = [
'name',
'email',
// 不包含 password、is_admin、token 等敏感字段
];
}
4. 类型转换
// ✅ 推荐:为所有需要转换的字段定义类型
class User extends Model
{
protected static array $casts = [
'is_active' => 'boolean',
'settings' => 'json',
'score' => 'integer',
'created_at' => 'datetime',
];
}
5. 避免 N+1 查询
// ❌ 错误:N+1 查询
$users = User::all();
foreach ($users as $user) {
$posts = $user->posts; // 每次循环都查询数据库
}
// ✅ 正确:预加载关联
$users = User::with('posts')->get();
foreach ($users as $user) {
$posts = $user->posts; // 已预加载,不查询数据库
}
6. 使用查询作用域
// ✅ 推荐:封装常用查询逻辑
class User extends Model
{
public static function scopeActive($query)
{
return $query->where('status', 'active');
}
}
// 使用
$activeUsers = User::active()->get();
7. 合理使用软删除
// ✅ 推荐:对需要保留历史的数据使用软删除
class Order extends Model
{
protected static bool $softDelete = true;
}
// ✅ 推荐:对临时数据不使用软删除
class Session extends Model
{
protected static bool $softDelete = false;
}
8. 模型事件
// ✅ 推荐:在模型事件中处理业务逻辑
class Order extends Model
{
protected function beforeSave(): bool
{
// 自动计算、验证等
return true;
}
protected function afterSave(): void
{
// 发送通知、更新缓存等
}
}
9. 访问器和修改器
// ✅ 推荐:使用访问器处理计算属性
class User extends Model
{
public function getFullNameAttribute(): string
{
return $this->first_name . ' ' . $this->last_name;
}
}
// ✅ 推荐:使用修改器处理输入
class User extends Model
{
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
}
}
10. 查询优化
// ✅ 推荐:只查询需要的字段
$users = User::query()
->select(['id', 'name', 'email'])
->get();
// ✅ 推荐:使用索引字段查询
$user = User::where('email', 'user@example.com')->first();
// ✅ 推荐:使用分页
$users = User::query()
->offset(0)
->limit(20)
->get();
API 参考
静态方法
#### find(int|string $id): ?static
根据主键查找模型。
$user = User::find(1);
#### all(): array
查找所有记录。
$users = User::all();
#### create(array $attributes): static
创建新记录。
$user = User::create(['name' => 'John']);
#### query(): QueryBuilder
获取查询构建器。
$users = User::query()->where('status', 'active')->get();
#### withTrashed(): QueryBuilder
包含软删除记录的查询。
$users = User::withTrashed()->get();
#### onlyTrashed(): QueryBuilder
只查询软删除的记录。
$users = User::onlyTrashed()->get();
#### getTableName(): string
获取表名。
$tableName = User::getTableName(); // 'users'
#### getPrimaryKey(): string
获取主键名。
$primaryKey = User::getPrimaryKey(); // 'id'
实例方法
#### save(): bool
保存模型。
$user->save();
#### update(array $attributes): bool
更新模型属性。
$user->update(['name' => 'Jane']);
#### delete(bool $force = false): bool
删除模型。
$user->delete();
$user->delete(true); // 强制删除
#### forceDelete(): bool
强制删除(即使启用软删除也真正删除)。
$user->forceDelete();
#### restore(): bool
恢复软删除的记录。
$user->restore();
#### fill(array $attributes): self
批量赋值。
$user->fill(['name' => 'John', 'email' => 'john@example.com']);
#### getAttribute(string $key, mixed $default = null): mixed
获取属性值。
$name = $user->getAttribute('name');
$name = $user->getAttribute('name', 'Default');
#### setAttribute(string $key, mixed $value): void
设置属性值。
$user->setAttribute('name', 'John');
#### getAttributes(): array
获取所有属性。
$attributes = $user->getAttributes();
#### getDirty(): array
获取变更的字段。
$dirty = $user->getDirty();
#### getOriginal(?string $key = null): mixed
获取原始值。
$original = $user->getOriginal('name');
$allOriginal = $user->getOriginal();
#### isDirty(?string $key = null): bool
检查是否有变更。
$isDirty = $user->isDirty();
$nameDirty = $user->isDirty('name');
#### toArray(): array
转换为数组。
$array = $user->toArray();
#### toJson(): string
转换为 JSON。
$json = $user->toJson();
关联方法
#### hasOne(string $related, ?string $foreignKey = null, string $localKey = 'id'): HasOne
定义一对一关联。
public function profile(): HasOne
{
return $this->hasOne(Profile::class, 'user_id');
}
#### hasMany(string $related, ?string $foreignKey = null, string $localKey = 'id'): HasMany
定义一对多关联。
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'user_id');
}
#### belongsTo(string $related, ?string $foreignKey = null, string $ownerKey = 'id'): BelongsTo
定义多对一关联。
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
常见问题
Q: 如何获取原始属性值?
A: 使用 getOriginal() 方法:
$user = User::find(1);
$originalName = $user->getOriginal('name');
Q: 如何检查模型是否已保存?
A: 使用 exists 属性:
$user = new User();
$user->exists; // false
$user->save();
$user->exists; // true
Q: 如何获取变更的字段?
A: 使用 getDirty() 方法:
$user = User::find(1);
$user->name = 'New Name';
$dirty = $user->getDirty(); // ['name' => 'New Name']
Q: 模型可以跨数据库连接吗?
A: 可以,在模型中重写 getConnection() 方法或使用 setConnection() 方法。
Q: 如何禁用时间戳?
A: 设置 $timestamps = false:
class User extends Model
{
protected static bool $timestamps = false;
}
Q: 访问器和修改器的命名规则是什么?
A:
get{FieldName}Attributeset{FieldName}Attribute字段名使用驼峰命名,例如 full_name 对应 getFullNameAttribute。
Q: 如何预加载多个关联?
A: 使用 with() 方法:
$users = User::with(['posts', 'profile', 'comments'])->get();
Q: 关联查询如何添加条件?
A: 使用闭包:
$users = User::with(['posts' => function($query) {
$query->where('status', 'published');
}])->get();
高级功能
模型刷新
从数据库重新加载模型数据:
$user = User::find(1);
$user->name = 'New Name';
// 从数据库重新加载(丢弃本地修改)
$user = User::find(1); // 重新查询
// 或者使用查询构建器重新查询
$freshUser = User::query()
->where('id', $user->id)
->first();
模型复制
创建模型的副本(用于创建相似记录):
$user = User::find(1);
// 复制模型(排除主键和时间戳)
$newUser = new User();
$newUser->fill($user->getAttributes());
unset($newUser->id); // 移除主键
unset($newUser->created_at);
unset($newUser->updated_at);
$newUser->save();
批量更新
使用查询构建器批量更新:
// 批量更新
User::where('status', 'pending')
->update(['status' => 'active']);
// 条件批量更新
User::where('age', '<', 18)
->update(['is_minor' => true]);
批量删除
// 批量删除
User::where('status', 'inactive')->delete();
// 根据主键批量删除
// 注意:当前实现中没有 destroy() 方法,可以使用查询构建器
User::whereIn('id', [1, 2, 3])->delete();
模型比较
比较两个模型实例:
$user1 = User::find(1);
$user2 = User::find(2);
// 比较主键
if ($user1->id === $user2->id) {
echo "Same user";
}
// 比较属性
if ($user1->email === $user2->email) {
echo "Same email";
}
模型集合操作
// 获取所有模型
$users = User::all();
// 遍历集合
foreach ($users as $user) {
echo $user->name;
}
// 过滤集合
$activeUsers = array_filter($users, function($user) {
return $user->status === 'active';
});
// 映射集合
$names = array_map(function($user) {
return $user->name;
}, $users);
常见问题
Q: 如何获取原始属性值?
A: 当前 Model 实现中,原始值存储在 $original 属性中。可以在模型中添加辅助方法:
class User extends Model
{
public function getOriginal(?string $key = null): mixed
{
if ($key === null) {
return $this->original;
}
return $this->original[$key] ?? null;
}
}
Q: 如何检查模型是否已保存?
A: 使用 exists 属性:
$user = new User();
$user->exists; // false
$user->save();
$user->exists; // true
Q: 如何获取变更的字段?
A: 使用 getDirty() 方法:
$user = User::find(1);
$user->name = 'New Name';
$dirty = $user->getDirty(); // ['name' => 'New Name']
// 检查是否有变更
$hasChanges = !empty($user->getDirty());
// 检查特定字段是否变更
$nameChanged = isset($user->getDirty()['name']);
Q: 模型可以跨数据库连接吗?
A: 可以,在模型中重写 getConnection() 方法或使用 setConnection() 方法:
class User extends Model
{
protected static function getConnection(): ConnectionInterface
{
return DatabaseManager::getInstance()->connection('mysql2');
}
}
// 或者动态设置
User::setConnection(DatabaseManager::getInstance()->connection('mysql2'));
Q: 如何禁用时间戳?
A: 设置 $timestamps = false:
class User extends Model
{
protected static bool $timestamps = false;
}
Q: 访问器和修改器的命名规则是什么?
A:
get{FieldName}Attributeset{FieldName}Attribute字段名使用驼峰命名,例如 full_name 对应 getFullNameAttribute。
Q: 如何预加载多个关联?
A: 当前实现中,关联是��加载的。可以通过查询构建器预加载:
// 方式 1:使用 JOIN
$users = User::query()
->leftJoin('profiles', 'users.id', '=', 'profiles.user_id')
->select('users.', 'profiles.')
->get();
// 方式 2:手动预加载(在循环外查询)
$userIds = array_column($users, 'id');
$profiles = Profile::whereIn('user_id', $userIds)->get();
$profilesByUserId = [];
foreach ($profiles as $profile) {
$profilesByUserId[$profile->user_id] = $profile;
}
Q: 关联查询如何添加条件?
A: 在关联方法中返回查询构建器,然后添加条件:
class User extends Model
{
public function activePosts(): HasMany
{
return $this->hasMany(Post::class, 'user_id')
->where('status', 'published');
}
}
// 使用
$user = User::find(1);
$activePosts = $user->activePosts; // 只返回已发布的文章
Q: 如何实现模型作用域?
A: 定义静态方法,方法名以 scope 开头:
class User extends Model
{
public static function scopeActive($query)
{
return $query->where('status', 'active');
}
}
// 使用
$activeUsers = User::active()->get();
Q: 模型事件如何工作?
A: 重写模型事件方法:
class User extends Model
{
protected function beforeSave(): bool
{
// 保存前的逻辑
return true; // 返回 false 取消保存
}
protected function afterSave(): void
{
// 保存后的逻辑
}
protected function beforeDelete(): bool
{
// 删除前的逻辑
return true; // 返回 false 取消删除
}
protected function afterDelete(): void
{
// 删除后的逻辑
}
}