一、模型关联:数据关系的艺术

在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; // 格式化后的发布时间
}

在实际项目中,还有一些最佳实践值得注意:

  1. 关联查询优化:

    • 使用with预加载避免N+1问题
    • 只查询需要的字段,避免select *
    • 对频繁查询的关联建立索引
  2. 查询范围的使用建议:

    • 全局范围用于强制业务规则(如多租户隔离)
    • 局部范围封装常用查询条件
    • 动态范围提高代码复用性
  3. 自动完成的注意事项:

    • 修改器只影响模型操作,不影响Db门面
    • 获取器会增加少量性能开销
    • 敏感数据处理应该在多个层面进行
  4. 性能优化技巧:

    • 使用cache()方法缓存查询结果
    • 大数据量查询使用chunk分批处理
    • 合理使用索引提高查询效率

ThinkPHP6的模型功能强大而灵活,但也要避免过度设计。记住:简单优于复杂,明确优于隐晦。合理运用模型关联、查询范围和自动完成,可以让你的代码更加优雅、可维护性更高。