关联查询
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();
}