一、模型关联:数据关系的艺术
在ThinkPHP6中,模型关联就像是在帮数据"相亲",让不同的数据表找到它们的"另一半"。想象一下,用户表和文章表原本是陌生人,通过模型关联,它们就能愉快地"牵手"了。
一对一关联就像身份证和人的关系,一个人只能有一张身份证。我们来看看如何实现:
// 用户模型
class User extends Model
{
// 关联用户资料表(一对一)
public function profile()
{
// hasOne参数:关联模型类名,外键,主键
return $this->hasOne(Profile::class, 'user_id', 'id');
}
}
// 资料模型
class Profile extends Model
{
// 反向关联用户表
public function user()
{
// belongsTo参数:关联模型类名,外键,当前模型主键
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
// 使用示例
$user = User::find(1);
// 获取关联资料
echo $user->profile->age; // 输出用户的年龄
一对多关联则像是一个用户拥有多篇文章的关系:
// 用户模型
class User extends Model
{
// 关联文章表(一对多)
public function articles()
{
// hasMany参数:关联模型类名,外键,主键
return $this->hasMany(Article::class, 'user_id', 'id');
}
}
// 文章模型
class Article extends Model
{
// 反向关联用户表
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
// 使用示例
$user = User::find(1);
// 获取用户所有文章
foreach ($user->articles as $article) {
echo $article->title;
}
多对多关联稍微复杂些,就像用户和角色的关系,一个用户可以拥有多个角色,一个角色也可以属于多个用户:
// 用户模型
class User extends Model
{
// 关联角色表(多对多)
public function roles()
{
// belongsToMany参数:关联模型类名,中间表名,外键,关联键
return $this->belongsToMany(Role::class, 'user_role', 'user_id', 'role_id');
}
}
// 角色模型
class Role extends Model
{
// 反向关联用户表
public function users()
{
return $this->belongsToMany(User::class, 'user_role', 'role_id', 'user_id');
}
}
// 使用示例
$user = User::find(1);
// 获取用户所有角色
foreach ($user->roles as $role) {
echo $role->name;
}
模型关联在实际开发中非常实用,比如电商系统中的用户与订单、商品与分类等场景。但要注意,关联查询虽然方便,过度使用会导致性能问题,特别是N+1查询问题。ThinkPHP6提供了with预加载来解决这个问题:
// 一次性预加载关联数据,避免N+1查询
$users = User::with(['articles', 'profile'])->select();
foreach ($users as $user) {
// 这里不会产生额外的查询
echo $user->profile->age;
foreach ($user->articles as $article) {
echo $article->title;
}
}
二、查询范围:给SQL戴上"滤镜"
查询范围就像是给数据库查询加了个"滤镜",让我们可以轻松地复用常用的查询条件。ThinkPHP6中的查询范围分为全局范围、局部范围和动态范围三种。
全局范围就像是一个"默认滤镜",会自动应用到所有查询:
class Article extends Model
{
// 定义全局范围
protected $globalScope = ['status'];
// 状态正常的文章范围
public function scopeStatus($query)
{
$query->where('status', 1);
}
}
// 所有查询都会自动加上status=1的条件
$articles = Article::select(); // 等同于 Article::where('status', 1)->select()
局部范围则像是手动选择的"滤镜",需要时才应用:
class Article extends Model
{
// 热门文章范围
public function scopePopular($query)
{
$query->where('view_count', '>', 100)
->order('view_count', 'desc');
}
// 最新文章范围
public function scopeRecent($query)
{
$query->order('create_time', 'desc');
}
}
// 使用局部范围
$popularArticles = Article::popular()->select();
$recentArticles = Article::recent()->limit(10)->select();
动态范围更加灵活,可以接收参数:
class Article extends Model
{
// 动态范围:分类筛选
public function scopeCategory($query, $categoryId)
{
$query->where('category_id', $categoryId);
}
}
// 使用动态范围
$techArticles = Article::category(1)->select(); // 获取分类ID为1的文章
$lifeArticles = Article::category(2)->select(); // 获取分类ID为2的文章
查询范围在实际开发中非常有用,比如:
- 只查询已发布的文章
- 自动过滤已删除的数据
- 按照特定排序规则获取数据
但要注意,全局范围会影响所有查询,有时候我们需要临时移除它:
// 临时移除全局范围
$allArticles = Article::withoutGlobalScope('status')->select();
三、自动完成:数据的"美容院"
自动完成功能就像是数据的"美容院",在数据写入数据库前或读取出来后,自动进行"美容"处理。ThinkPHP6提供了修改器、获取器和自动时间戳等功能来实现这一点。
修改器在数据写入前进行"美容":
class User extends Model
{
// 密码修改器
public function setPasswordAttr($value)
{
return md5($value); // 存入数据库前自动加密
}
// 用户名修改器
public function setNameAttr($value)
{
return htmlspecialchars($value); // 防止XSS攻击
}
}
// 使用示例
$user = new User;
$user->name = '<script>alert(1)</script>'; // 会自动转义
$user->password = '123456'; // 会自动加密为md5
$user->save();
获取器则在数据读取后进行"美容":
class User extends Model
{
// 状态获取器
public function getStatusAttr($value)
{
$status = [0 => '禁用', 1 => '正常'];
return $status[$value]; // 将数字状态转为文字描述
}
// 生日获取器
public function getBirthdayAttr($value)
{
return date('Y年m月d日', strtotime($value)); // 格式化日期
}
}
// 使用示例
$user = User::find(1);
echo $user->status; // 输出"正常"而不是1
echo $user->birthday; // 输出"2023年01月01日"而不是"2023-01-01"
自动时间戳是ThinkPHP6非常实用的功能:
class Article extends Model
{
// 开启自动时间戳
protected $autoWriteTimestamp = true;
// 定义时间戳字段名
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
}
// 使用示例
$article = new Article;
$article->title = 'ThinkPHP6教程';
$article->save(); // 会自动设置create_time和update_time
自动完成功能在实际开发中应用广泛:
- 数据加密/解密
- 字段格式化
- 自动维护创建/更新时间
- 数据脱敏处理
但要注意,修改器和获取器会影响性能,特别是在处理大量数据时。另外,自动时间戳只对模型操作有效,直接使用Db门面不会触发。
四、综合应用与最佳实践
让我们通过一个博客系统的例子,把前面学到的知识综合运用起来:
// 文章模型
class Article extends Model
{
// 自动时间戳
protected $autoWriteTimestamp = true;
// 全局范围:只查询已发布文章
protected $globalScope = ['published'];
public function scopePublished($query)
{
$query->where('status', 1);
}
// 关联分类
public function category()
{
return $this->belongsTo(Category::class, 'category_id', 'id');
}
// 关联标签(多对多)
public function tags()
{
return $this->belongsToMany(Tag::class, 'article_tag', 'article_id', 'tag_id');
}
// 内容修改器(保存时处理)
public function setContentAttr($value)
{
// 过滤恶意代码
$value = htmlspecialchars($value);
// 将换行转为<br>
return nl2br($value);
}
// 发布时间获取器
public function getPublishTimeAttr($value)
{
return date('Y-m-d H:i', strtotime($value));
}
// 局部范围:热门文章
public function scopeHot($query)
{
$query->where('view_count', '>', 100)
->order('view_count', 'desc');
}
// 动态范围:按分类查询
public function scopeOfCategory($query, $categoryId)
{
$query->where('category_id', $categoryId);
}
}
// 使用示例
// 获取热门技术文章(分类ID为1)
$articles = Article::with(['category', 'tags'])
->hot()
->ofCategory(1)
->paginate(10);
foreach ($articles as $article) {
echo $article->title;
echo $article->category->name; // 关联分类名称
foreach ($article->tags as $tag) {
echo $tag->name; // 关联标签名称
}
echo $article->publish_time; // 格式化后的发布时间
}
在实际项目中,还有一些最佳实践值得注意:
关联查询优化:
- 使用with预加载避免N+1问题
- 只查询需要的字段,避免select *
- 对频繁查询的关联建立索引
查询范围的使用建议:
- 全局范围用于强制业务规则(如多租户隔离)
- 局部范围封装常用查询条件
- 动态范围提高代码复用性
自动完成的注意事项:
- 修改器只影响模型操作,不影响Db门面
- 获取器会增加少量性能开销
- 敏感数据处理应该在多个层面进行
性能优化技巧:
- 使用cache()方法缓存查询结果
- 大数据量查询使用chunk分批处理
- 合理使用索引提高查询效率
ThinkPHP6的模型功能强大而灵活,但也要避免过度设计。记住:简单优于复杂,明确优于隐晦。合理运用模型关联、查询范围和自动完成,可以让你的代码更加优雅、可维护性更高。
评论