关联查询
Viswoole ORM 支持定义模型之间的关联关系,包括一对一(HasOne)、一对多(HasMany)、多对多(BelongsToMany)等常见关系类型。关联数据必须通过 with() 预加载后才能访问。
一对一 (HasOne)
一对一关联表示一个模型拥有另一个模型的单条关联记录,例如一个用户对应一份个人资料。
定义关联
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';
}使用关联
注意:框架不提供懒加载(延迟加载)。关联数据必须通过
with()预加载后才能通过属性访问,未预加载的关联属性返回null。
// 必须先通过 with() 预加载关联数据
$user = UsersModel::with(['profile'])->find(1);
echo $user->profile->bio; // 访问关联模型的字段
echo $user->profile->avatar; // 个人头像数据表示例:
users 表 profiles 表
┌────┬──────┐ ┌────────┬─────────┬──────────┐
│ id │ name │ │ id │ user_id │ bio │
├────┼──────┤ ├────────┼─────────┼──────────┤
│ 1 │ 张三 │───▶│ 1 │ 1 │ PHP开发者│
│ 2 │ 李四 │───▶│ 2 │ 2 │ 设计师 │
└────┴──────┘ └────────┴─────────┴──────────┘一对多 (HasMany)
一对多关联表示一个模型拥有多条关联记录,例如一个用户可以拥有多个订单。
定义关联
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';
}使用关联
// 必须先通过 with() 预加载关联数据
$user = UsersModel::with(['roles', 'orders'])->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);数据表示例:
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 │
└────┴─────────┴───────┘多对多 (BelongsToMany)
多对多关联通过**中间表(pivot)**关联两个模型,例如用户与角色通过 role_user 中间表关联。belongsToMany() 方法签名:
protected function belongsToMany(
Model|string $relationModel, // 关联模型类名或实例
Model|string|null $pivot = null, // 中间表模型类名、实例或表名
?string $foreignPivotKey = null, // 中间表中指向当前模型的外键名
?string $relatedPivotKey = null, // 中间表中指向关联模型的外键名
?string $localKey = null, // 当前模型的关联键名(默认 $pk)
?string $relatedKey = null // 关联模型的关联键名(默认关联模型 $pk)
): BelongsToMany定义关联
namespace App\Model;
use Viswoole\Database\Model;
use Viswoole\Database\Model\BelongsToMany;
class UsersModel extends Model
{
protected string $table = 'users';
protected string $pk = 'id';
/**
* 用户角色(多对多)
*
* @return BelongsToMany
*/
public function roles(): BelongsToMany
{
// 方式一:全默认推断
// 中间表名: users_roles,外键: users_id / roles_id
// return $this->belongsToMany(RoleModel::class);
// 方式二:指定中间表名与外键(推荐,表名明确)
return $this->belongsToMany(RoleModel::class, 'role_user', 'user_id', 'role_id');
// 方式三:中间表传模型实例(需要自定义中间表模型时使用)
// return $this->belongsToMany(RoleModel::class, RoleUserModel::class, 'user_id', 'role_id');
}
}默认推断规则(与 hasOne/hasMany 一致):
- 中间表名:
{当前表名}_{关联表名}(如users_roles) - 外键名:
{表名}_{主键名}(如users_id、roles_id) - 中间表通常无业务语义,框架会动态创建匿名中间表模型并继承当前模型的数据库通道配置
使用关联
与 hasOne/hasMany 一样,多对多关联也必须通过 with() 预加载。结果为 Collection 集合,每条关联数据通过 pivot 键附带对应的中间表行数据(可读取绑定时间等扩展字段):
// 必须先通过 with() 预加载关联数据
$user = UsersModel::with(['roles'])->find(1);
foreach ($user->roles as $role) {
echo $role->name; // 角色名称
echo $role->pivot['bind_time']; // 中间表中的绑定时间等扩展字段
}数据表示例:
users 表 role_user 中间表 roles 表
┌────┬──────┐ ┌────────┬─────────┬────────────┐ ┌────┬────────┐
│ id │ name │ │ user_id│ role_id │ bind_time │ │ id │ name │
├────┼──────┤ ├────────┼─────────┼────────────┤ ├────┼────────┤
│ 1 │ 张三 │───┐ │ 1 │ 10 │ 2026-01-01 │──▶│ 10 │ admin │
│ 2 │ 李四 │───┤ │ 1 │ 11 │ 2026-01-02 │──▶│ 11 │ editor │
└────┴──────┘ │ │ 2 │ 10 │ 2026-01-05 │ └────┴────────┘
└───▶ │ 2 │ 12 │ 2026-02-01 │
└────────┴─────────┴────────────┘wherePivot — 中间表条件过滤
wherePivot() 用于在查询绑定关系时过滤中间表数据(区别于 with() 闭包中的条件,后者过滤的是关联模型数据,如角色被禁用):
class UsersModel extends Model
{
/**
* 仅取 2026-01-05 之后建立的绑定
*
* @return BelongsToMany
*/
public function recentRoles(): BelongsToMany
{
return $this->belongsToMany(RoleModel::class, 'role_user', 'user_id', 'role_id')
->wherePivot('bind_time', '>=', '2026-01-05');
}
/**
* 仅取指定状态的绑定(两参简写:数组自动转 IN 条件)
*
* @return BelongsToMany
*/
public function enabledRoles(): BelongsToMany
{
return $this->belongsToMany(RoleModel::class, 'role_user', 'user_id', 'role_id')
->wherePivot('status', [1, 2]);
}
}attach — 新增绑定
attach() 为当前主数据新增绑定关系(幂等操作:已存在的绑定自动跳过)。可通过第三个参数附加中间表扩展字段:
// 关联方法需在模型实例上调用
$user = new UsersModel();
// 为 1 号用户绑定角色 10、11,并写入绑定时间
$user->roles()->attach(1, [10, 11], ['bind_time' => date('Y-m-d H:i:s')]);
// 单个关联键可直接传标量;$parent 也可传主表行数据集(如 find() 查询结果)
$row = UsersModel::find(1);
$user->roles()->attach($row, 10);detach — 解除绑定
$user = new UsersModel();
// 解除指定绑定(标量或数组均可)
$user->roles()->detach(1, 10);
$user->roles()->detach(1, [10, 11]);
// 解除该用户的全部绑定
$user->roles()->detach(1);sync — 同步绑定(多退少补)
sync() 以目标关联键集合为准对齐绑定:新增缺失的绑定、移除多余的绑定,交集保持不变。返回新增与移除的关联键列表:
$user = new UsersModel();
// 将 1 号用户的角色对齐为 10、20:解绑 11,新增 20
$result = $user->roles()->sync(1, [10, 20], ['bind_time' => '2026-03-01']);
// 结果
// ['attached' => [20], 'detached' => [11]]
// 目标为空数组:清空该用户的全部绑定(等价 detach($parent))
$result = $user->roles()->sync(1, []);
// ['attached' => [], 'detached' => [10, 11]]注意:多对多关联不支持
create()与delete()方法(调用会抛出异常)。新增绑定请使用attach(),解除绑定请使用detach()。
预加载 (Eager Loading)
预加载用于一次性取出模型的关联数据。框架不做懒加载,未预加载的关联属性返回 null,因此必须先通过 with() 预加载关联数据后才能访问。
不使用预加载的情况
// ❌ 不使用预加载 — 无法访问关联数据(框架不做懒加载)
$users = UsersModel::select(); // 第 1 次查询:获取所有用户
foreach ($users as $user) {
echo $user->profile->bio; // 未预加载时 $user->profile 为 null,取不到关联数据
}解决方案:with 预加载
// ✅ 使用预加载 — 仅 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; // 无额外查询
}预加载多个关联
// 同时预加载多个关联关系
$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()。
带条件的预加载
对预加载的关联添加约束条件:
$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:
// 查询有订单的用户
$users = UsersModel::whereExists(
'SELECT 1 FROM orders WHERE orders.user_id = users.id'
)->select();如需统计关联数量,可使用 leftJoin 结合 groupBy 聚合查询:
use Viswoole\Database\Facade\Db;
$users = Db::table('users')
->columns('users.*', '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']}";
}完整示例
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.*', '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();
}