关联查询

Viswoole ORM 支持定义模型之间的关联关系,包括一对一、一对多等常见关系类型。关联关系通过预加载可有效解决 N+1 查询问题。

一对一 (HasOne)

一对一关联表示一个模型拥有另一个模型的单条关联记录,例如一个用户对应一份个人资料。

定义关联

php
namespace App\Model;

use Viswoole\Database\Model;
use Viswoole\Database\Model\RelationQuery;

class UsersModel extends Model
{
    protected string $table = 'users';
    protected string $pk = 'id';

    /**
     * 用户个人资料(一对一)
     *
     * @return RelationQuery
     */
    public function profile(): RelationQuery
    {
        // hasOne(关联模型类, 外键字段名)
        return $this->hasOne(ProfileModel::class, 'user_id');
    }
}

class ProfileModel extends Model
{
    protected string $table = 'profiles';
    protected string $pk = 'id';
}

使用关联

php
// 动态属性访问 — 自动加载关联数据
$user = UsersModel::find(1);

echo $user->profile->bio;      // 访问关联模型的字段
echo $user->profile->avatar;   // 个人头像

数据表示例:

text
users 表          profiles 表
┌────┬──────┐    ┌────────┬─────────┬──────────┐
│ id │ name │    │ id     │ user_id │ bio      │
├────┼──────┤    ├────────┼─────────┼──────────┤
│ 1  │ 张三 │───▶│ 1      │ 1       │ PHP开发者│
│ 2  │ 李四 │───▶│ 2      │ 2       │ 设计师   │
└────┴──────┘    └────────┴─────────┴──────────┘

一对多 (HasMany)

一对多关联表示一个模型拥有多条关联记录,例如一个用户可以拥有多个订单。

定义关联

php
class UsersModel extends Model
{
    protected string $table = 'users';
    protected string $pk = 'id';

    /**
     * 用户角色(一对多)
     *
     * @return RelationQuery
     */
    public function roles(): RelationQuery
    {
        // hasMany(关联模型类, 外键字段名)
        return $this->hasMany(UserRoles::class, 'user_id');
    }

    /**
     * 用户订单(一对多)
     *
     * @return RelationQuery
     */
    public function orders(): RelationQuery
    {
        return $this->hasMany(OrderModel::class, 'user_id');
    }
}

class UserRoles extends Model
{
    protected string $table = 'user_roles';
    protected string $pk = 'id';
}

class OrderModel extends Model
{
    protected string $table = 'orders';
    protected string $pk = 'id';
}

使用关联

php
$user = UsersModel::find(1);

// 获取用户的所有角色(集合)
foreach ($user->roles as $role) {
    echo $role->role_name;
}

// 获取用户的所有订单
foreach ($user->orders as $order) {
    echo "订单号: {$order->order_no}, 金额: {$order->amount}";
}

// 统计关联数量
echo "角色数: " . count($user->roles);
echo "订单数: " . count($user->orders);

数据表示例:

text
users 表              user_roles 表
┌────┬──────┐         ┌────┬─────────┬───────────┐
│ id │ name │         │ id │ user_id │ role_name │
├────┼──────┤         ├────┼─────────┼───────────┤
│ 1  │ 张三 │────┬───▶│ 1  │ 1       │ admin     │
│ 2  │ 李四 │     ├──▶│ 2  │ 1       │ editor    │
└────┴──────┘     └───▶│ 3  │ 2       │ viewer    │
                  ┌───▶│ 4  │ 2       │ guest     │
                  │    └────┴─────────┴───────────┘


               orders 表
               ┌────┬─────────┬───────┐
               │ id │ user_id │ amount│
               ├────┼─────────┼───────┤
               │ 1  │ 1       │ 99.00 │
               │ 2  │ 1       │ 199.00│
               │ 3  │ 2       │ 50.00 │
               └────┴─────────┴───────┘

预加载 (Eager Loading)

预加载是解决 N+1 查询问题 的核心手段。当循环遍历模型并访问其关联属性时,如果不使用预加载,会产生 N+1 次 SQL 查询。

问题演示:N+1 查询

php
// ❌ 不使用预加载 — 产生 N+1 次查询
$users = UsersModel::select();           // 第 1 次查询:获取所有用户

foreach ($users as $user) {
    echo $user->profile->bio;            // 每次循环都额外查询 profile
}
// 假设有 100 个用户 → 总共 101 次查询

解决方案:with 预加载

php
// ✅ 使用预加载 — 仅 2 次查询
$users = UsersModel::with(['profile'])->select();
// 第 1 次:SELECT * FROM users
// 第 2 次:SELECT * FROM profiles WHERE user_id IN (1,2,3,...)

foreach ($users as $user) {
    echo $user->profile->bio;            // 无额外查询
}

预加载多个关联

php
// 同时预加载多个关联关系
$users = UsersModel::with(['profile', 'roles', 'orders'])->select();

foreach ($users as $user) {
    echo $user->profile->bio;      // 已预加载
    foreach ($user->roles as $role) { // 已预加载
        echo $role->role_name;
    }
}

嵌套预加载

注意:当前版本不支持点号语法的嵌套预加载(如 with(['orders.items']))。with() 方法会检查关联方法是否存在,orders.items 会被当作方法名查找从而抛出异常。如需加载多层关联,请分别在各模型中调用 with()

带条件的预加载

对预加载的关联添加约束条件:

php
$users = UsersModel::with(['orders' => function ($query) {
    // 只预加载近 30 天的订单
    $query->where('create_time', '>=', date('Y-m-d', strtotime('-30 days')))
          ->orderBy('create_time', 'desc');
}])->select();

关联查询进阶

关联条件筛选

注意:当前版本未提供 has()withCount() 方法。如需基于关联关系筛选或统计关联数量,可通过子查询或 whereExists 等方式实现。

例如,查询有订单的用户可使用 whereExists

php
// 查询有订单的用户
$users = UsersModel::whereExists(
    'SELECT 1 FROM orders WHERE orders.user_id = users.id'
)->select();

如需统计关联数量,可使用 leftJoin 结合 groupBy 聚合查询:

php
use Viswoole\Database\Facade\Db;

$users = Db::table('users')
    ->columns('users.*', Db::raw('COUNT(orders.id) as orders_count'))
    ->join('orders', 'users.id', 'orders.user_id')
    ->groupBy('users.id')
    ->getArray();

foreach ($users as $user) {
    echo "{$user['name']} 的订单数: {$user['orders_count']}";
}

完整示例

php
namespace App\Model;

use Viswoole\Database\Facade\Db;
use Viswoole\Database\Model;
use Viswoole\Database\Model\RelationQuery;

/**
 * 用户模型 — 含完整关联定义
 */
class UsersModel extends Model
{
    protected string $table = 'users';
    protected string $pk = 'id';
    protected int $autoWriteTimestamp = 1;
    protected bool $enableSoftDelete = true;
    protected array $hidden = ['password'];

    /**
     * 个人资料(一对一)
     *
     * @return RelationQuery
     */
    public function profile(): RelationQuery
    {
        return $this->hasOne(ProfileModel::class, 'user_id');
    }

    /**
     * 用户角色(一对多)
     *
     * @return RelationQuery
     */
    public function roles(): RelationQuery
    {
        return $this->hasMany(UserRoles::class, 'user_id');
    }

    /**
     * 用户订单(一对多)
     *
     * @return RelationQuery
     */
    public function orders(): RelationQuery
    {
        return $this->hasMany(OrderModel::class, 'user_id');
    }
}

// ============================================
// 控制器中使用
// ============================================

// 获取用户详情(含关联数据)
public function show(int $id): array
{
    $user = UsersModel::with(['profile', 'roles'])
        ->find($id);

    // find() 返回 DataSet,空结果为空 DataSet(非 null),需用 isEmpty() 判断
    if ($user->isEmpty()) {
        throw new NotFoundException('用户不存在');
    }

    return $user->toArray();
}

// 用户列表(含订单计数 — 使用 join 聚合替代 withCount)
public function index(): array
{
    return Db::table('users')
        ->columns('users.*', Db::raw('COUNT(orders.id) as orders_count'))
        ->join('orders', 'users.id', 'orders.user_id')
        ->where('users.status', 1)
        ->groupBy('users.id')
        ->orderBy('users.create_time', 'desc')
        ->page(1, 20)
        ->getArray();
}