laravel 数据库 2 -- 关联关系
20 March 2017
原文: laravel china -- laravel 5.4 -- 模型关联

一对一

users 表

id
name   用户名称
age    用户年龄

userinfos 表

id
location 用户住址
uid      对应的用户 id

主动关联

class User extends Model {
    public function userinfo() {
        return $this->hasOne('App\Userinfo');
        return $this->hasOne('App\Userinfo', 'uid');
        return $this->hasOne('App\Userinfo', 'uid', 'id');
        // 第二个参数 uid 是 在 userinfos 表中对应的字段 (外键), 默认 user_id
        // 第三个参数 id  是 在 users 表中对应的主键, 默认 id
    }
}
$userinfo = $user->find(1)->userinfo;

会执行下面的 sql 语句

select * from `users` where `users`.`id` = '1' limit 1
select * from `userinfos` where `userinfos`.`uid` = '1' and `userinfos`.`uid` is not null limit 1

相对关联

class Userinfo extends Model {
    public function user() {
        return $this->belongsTo('App\User');
        return $this->belongsTo('App\User', 'uid');
        return $this->belongsTo('App\User', 'uid', 'id');
        // 第二个参数 uid 是 在 userinfos 表中对应的外键, 默认 user_id
        // 第三个参数 id  是 在 users 表中对应的主键, 默认 id

    }
}

一对多

users 表

id
name   用户名称
age    用户年龄

task 表

id
name   任务名称
uid    对应的用户 id
class User extends Model {
    public function Task() {
        return $this->hasMany('App\Task');
        return $this->hasMany('App\Task', 'uid');
        return $this->hasMany('App\Task', 'uid', 'id');
        // 第二个参数 uid 是 在 tasks 表中对应的字段 (外键), 默认 user_id
        // 第三个参数 id  是 在 users 表中对应的主键, 默认 id
    }
}

定义相对的关联

class Task extends Model {
    public function user() {
        return $this->belongsTo('App\User');
        return $this->belongsTo('App\User', 'uid');
        return $this->belongsTo('App\User', 'uid', 'id');
        // 第二个参数 uid 是 在 tasks 表中对应的外键, 默认 user_id
        // 第三个参数 id  是 在 users 表中对应的主键, 默认 id
    }
}

多对多

例如, 一个用户 user 可能用有很多身份 role, 而一种身份可能很多用户都有
多对多关联需要用到三个数据库表: users, roles, 和 role_user
role_user 命名是以相关联的两个模型数据库表, 依照字母顺序命名

users 表

id
name   用户名称
age    用户年龄

roles 表

id
name   角色名称

role_user 表

rid    对应的角色 id
uid    对应的用户 id
class User extends Model {
    public function roles() {
        return $this->belongsToMany('App\Role');
        return $this->belongsToMany('App\Role', 'role_user', 'uid', 'rid');
        // 第二个参数 中间表的名字
        // 第三个参数 users 表的 id 在 role_user 中名字, 默认是是 user_id
        // 第四个参数 roles 表的 id 在 role_user 中名字, 默认是是 role_id
    }
}

在 Role 模型定义相对的关联:

class Role extends Model {
    public function users() {
        return $this->belongsToMany('App\User', 'role_user', 'rid', 'uid');
    }
}

远层一对多关联

例如,一个 Country 模型可能通过 Users 关联到很多 Posts 模型。数据库表间的关系可能看起来如下:

countries

id
name

users

id
country_id
name

posts

id
user_id
title
class Country extends Model {
    public function posts() {
        return $this->hasManyThrough('App\Post', 'App\User');
        // 一个 Country 有多个 User, 一个 User 有多个 Post
    }
}

...

// 通过 $country->posts 来获取 posts

如果想要手动指定关联的字段名称,可以传入第三和第四个参数到方法里:

class Country extends Model {
    public function posts() {
        return $this->hasManyThrough('App\Post', 'App\User', 'country_id', 'user_id', 'id');
    }
}

多态关联

多态关联允许一个模型在单个关联中从属一个以上其它模型。举个例子,想象一下使用你应用的用户可以「评论」文章和视频。使用多态关联关系,您可以使用一个 comments 数据表就可以同时满足两个使用场景。首先,让我们观察一下用来创建这关联的数据表结构:

posts

id
title
body

videos

id
title
url

comments

id
content
commentable_id
commentable_type

两个重要的需要注意的字段是 comments 表上的 commentable_id 和 commentable_type。commentable_id 列对应 Post 或 Video 的 ID 值,而 commentable_type 列对应所属模型的类名。当访问 commentable 关联时,ORM 根据 commentable_type 字段来判断所属模型的类型并返回相应模型实例。

class Comment extends Model {       // 获取所有拥有的 commentable 模型。
    public function commentable() {
        return $this->morphTo();
    }
}

class Post extends Model {          // 获取所有文章的评论。
    public function comments() {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model {         // 获取所有视频的评论。
    public function comments() {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

多态的多对多关联

例如, Blog 的 Post 和 Video 模型可以共用多态的 Tag 关联模型

posts

id
content

videos

id
name

tags

id
name

taggables

tag_id
taggable_id
taggable_type
➜ php artisan make:model Posts --migration
➜ php artisan make:model Tag --migration
➜ php artisan make:model Video --migration
➜ php artisan make:migration create_taggables --create=taggables

Post 和 Video 模型都可以经由 tags 方法建立 morphToMany 关联:

class Post extends Model {
    public function tags() {
        return $this->morphToMany('App\Tag', 'taggable');
        // return $this->morphToMany(Tag::class, 'taggable', 'taggables', 'taggable_id', 'tag_id');
    }
}

class Video extends Model {
    public function tags() {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

在 Tag 模型里针对每一种关联建立一个方法:

class Tag extends Model {
    public function posts() {
        return $this->morphedByMany('App\Post', 'taggable');
    }
    public function videos() {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

关联查询

您可能想要取得所有「至少有一篇评论」的 Blog 文章。可以使用 has 方法:

$posts = Post::has('comments')->get();    // 仅返回 Post 表的数据

也可以指定运算符和数量

$posts = Post::has('comments', '>=', 3)->get();

如果想要更进阶的用法,可以使用 whereHasorWhereHas 方法, 在 has 查询里设置 where 条件:

$posts = Post::whereHas('comments', function($query) {
    $query->where('content', 'like', 'foo%');
})->get();

也可以使用 "点号" 的形式来获取嵌套的 has 声明:

$posts = Post::has('comments.votes')->get();

假设你想要获取所有没有评论的博客文章,可以传递关联关系名称到 doesntHave 方法来实现:

$posts = App\Post::doesntHave('comments')->get();

添加自定义约束条件到关联关系约束,例如检查评论内容:

$posts = Post::whereDoesntHave('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

不加载关联关系的情况下统计关联结果数目,可以使用 withCount 方法,该方法会放置一个 {relation}_count 字段到结果模型。例如:

$posts = App\Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

像添加约束条件到查询一样来添加多个关联关系的 "计数":

$posts = Post::withCount(['votes', 'comments' => function ($query) {
    $query->where('content', 'like', 'foo%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

可以为关联关系计数结果设置别名,从而允许在一个关联关系上进行多维度计数:

$posts = Post::withCount([
    'comments',
    'comments AS pending_comments' => function ($query) {
        $query->where('approved', false);
    }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

预载入

预载入是用来减少 N + 1 查询问题. 例如, 一个 Book 模型数据会关联到一个 Author.
关联会像下面这样定义:

class Book extends Model {
    public function author() {
        return $this->belongsTo('App\Author');
    }
}

现在考虑下面的代码:

foreach (Book::all() as $book) {
    echo $book->author->name;
}

上面的循环会执行一次查询取回所有数据库表上的书籍,然而每本书籍都会执行一次查询取得作者。
所以若我们有 25 本书,就会进行 26 次查询 (1 次 book 查询 + 25 次 author 查询)

很幸运地,我们可以使用预载入大量减少查询次数, 使用 with 方法指定想要预载入的关联对象:

foreach (Book::with('author')->get() as $book) {
    echo $book->author->name;
}
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)

载入多种关联:

$books = Book::with('author', 'publisher')->get();

预载入巢状关联:

$books = Book::with('author.contacts')->get();

上面的例子中,author 关联会被预载入,author 的 contacts (表) 关联也会被预载入

预载入条件限制

有时您可能想要预载入关联, 同时也想要指定载入时的查询限制. 下面有一个例子:

$users = User::with(['posts' => function($query) {
    $query->where('title', 'like', '%first%');

}])->get();

上面的例子里,我们预载入了 user 的 posts 关联,并限制条件为 post 的 title 字段需包含 "first"

select * from users;
select * from posts where id in (1, 2, 3, ...) and title like '%first%';

当然,预载入的闭合函数里不一定只能加上条件限制,也可以加上排序:

$users = User::with(['posts' => function($query) {
    $query->orderBy('created_at', 'desc');
}])->get();
select * from users;
select * from posts where id in (1, 2, 3, ...) order by created_at desc;

延迟预载入

可以直接从模型的 collection 预载入关联对象,这对于需要根据情况决定是否载入关联对象时,或是跟缓存一起使用时很有用

Book::with('posts')->all();

Book::with(['posts' => function($query) {
    $query->orderBy('published_date', 'asc');
}]);



$books = Book::all();
$books->load('author', 'publisher');

$books->load(['author' => function($query) {
    $query->orderBy('published_date', 'asc');
}]);

插入关联模型

一对一, 一对多

comments:

id
message
post_id

posts:

id
content

save

$post = App\Post::find(1);

$comment = new App\Comment(['message' => 'A new comment.']);
$post->comments()->save($comment);


$post = App\Post::find(1);

$post->comments()->saveMany([
    new App\Comment(['message' => 'A new comment.']),
    new App\Comment(['message' => 'Another comment.']),
]);

create

$post = App\Post::find(1);
$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);


$post = App\Post::find(1);
$comment = $post->comments()->createMany([
    ['message' => 'A new comment.'],
    ['message' => 'A new comment.'],
    ['message' => 'A new comment.']
]);

belongsTo

userinfo

id
username
uid

user

id
class Userinfo {
    public function user() {
        return $this->belongsTo('App\User', 'uid', 'id');
    }
}

$userinfo = App\Userinfo::find(1);
$user     = App\User::find(2);
$userinfo->user()->associate($user);   // 更新 userinfo 的 uid
$userinfo->save();


$userinfo->user()->disassociate();     // 删除 userinfo 的 uid
$userinfo->save();

多对多

user

id
user_name

role

id
role_name

role_user (中间表)

rid
uid
active  (1/0 是否有效)
class Role {
    public function users() {
        return $this->belongsToMany('App\User', 'role_user', 'rid', 'uid');
    }
}

class User {
    public function roles() {
        return $this->belongsToMany('App\Role', 'role_user', 'uid', 'rid');
    }
}
$role = new App\Role([
    'role_name' => 'role1'
]);
$user = App\User::find(1);
$user->roles()->save($role, ['active', 1]);


$user = App\User::find(1);
$user->roles()->create([
    'role_name' => 'role1'
], ['active', 1]);


$user = App\User::find(1);
$user->roles()->saveMany([
    new App\Role(['role_name' => 'role1']),
    new App\Role(['role_name' => 'role2']),
    new App\Role(['role_name' => 'role3'])
], ['active', 1]);


$user = App\User::find(1);
$user->roles()->createMany([
    ['role_name' => 'role1'],
    ['role_name' => 'role2'],
    ['role_name' => 'role3']
], ['active', 1]);

多对多 2

attach / detach 是针对多对多关系的中间表来操作的

$user = App\User::find(1);
$user->roles()->attach(10);
$user->roles()->attach(11, ['expires' => $expires]);

# 中间表
# user_id   role_id  expire
# 1         10       xxx
# 1         11       xxx


// 从指定用户中移除角色..
$user->roles()->detach(10);

// 从指定用户移除所有角色...
$user->roles()->detach();

// 数组形式的 id
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([1 => ['expires' => $expires], 2, 3]);


$user->roles()->toggle(10);   // attach/detach

同步
你还可以使用 sync 方法构建多对多关联。sync 方法接收数组形式的 ID 并将其放置到中间表。任何不在该数组中的 ID 对应记录将会从中间表中移除。因此,该操作完成后,只有在数组中的 ID 对应记录还存在于中间表:

$user->roles()->sync([1, 2, 3]);

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

如果你不想要删除存在的 ID,可以使用 syncWithoutDetaching 方法:

$user->roles()->syncWithoutDetaching([1, 2, 3]);

多对多关联还提供了一个 toggle 方法用于切换给定 ID 的附加状态,如果给定 ID 当前被附加,则取消附加,类似的,如果当前没有附加,则附加:

$user->roles()->toggle([1, 2, 3]);

处理多对多关联时,save 方法接收额外中间表属性数组作为第二个参数:

App\User::find(1)->roles()->save($role, ['expires' => $expires]);更新中间表记录

如果你需要更新中间表中已存在的行,可以使用 updateExistingPivot 方法。该方法接收中间记录外键和属性数组进行更新:

$user = App\User::find(1);
$user->roles()->updateExistingPivot($roleId, $attributes);

自动更新上层时间戳

class Comment extends Model {

    protected $touches = ['post'];

    public function post() {
        return $this->belongsTo('App\Post');
    }
}

// 现在, 当你更新 Comment 时, 所属模型 Post 将也会更新其 updated_at 值