###多对多关系
多对多关系和之前的关系完全不一样,因为多对多关系可能出现很多冗余数据,用之前自带的表存不下了。
我们定义两个模型:Article 和 Tag,分别表示文章和标签,他们是多对多的关系。表结构应该是这样的:
article: id ... ...
tag: id ... ...
article_tag: article_id tag_id
在 Model 中使用:
<?php
class Tag extends Eloquent {
protected $table = 'tags';
public function belongsToManyArticle()
{
return $this->belongsToMany('Article', 'article_tag', 'tag_id', 'article_id');
}
}
需要注意的是,第三个参数是本类的 id,第四个参数是第一个参数那个类的 id。
使用跟 hasMany 一样:
$tagsWithArticles = Tag::take(10)->get()->belongsToManyArticle()->get();
这里会得到一个非常复杂的对象,可以自行 var_dump()
。跟大家说一个诀窍,var_dump()
以后,用 Chrome 右键 “查看源代码”,就可以看到非常整齐的对象/数组展开了。
Laravel Model 的 fillable (白名单)与 guarded (黑名单)
protected $fillable = ['name'];
protected $guarded = ['price'];
定义了 name 字段可以写入/修改,而 price 字段不可以。
需要注意的是,fillable 与 guarded 只限制了 create 方法,而不会限制 save。
我觉得 Laravel 这样设计的原因是,create 通常是使用 request 中的所有请求参数来创建对象,而 save 则是一个字段一个字段的手动赋值,所以 create 有可能会恶意加入不应该插入的字段,例如 is_admin。基于此,create 还是需要有 fillable, guarded 来过滤一层比较安全。
1、创建模型
1.1 使用save方法创建模型
调用Eloquent模型类的save
方法即可创建模型并插入数据到数据库:
$post = new Post; $post->title = 'test 4'; $post->content = 'test content'; $post->user_id = 1; $post->cat_id = 1; if($post->save()){ echo '添加文章成功!'; }else{ echo '添加文章失败!'; }
1.2 使用create方法插入数据
除此之外还可以使用create
方法插入数据,由于该方法中用到了批量赋值(Mass Assignment),所以我们需要在模型类中设置$fillable
属性或者$guarded
属性,以表明哪些属性可以通过该方法设置,哪些不可以。
开始之前,我们先解释下什么是批量赋值,以及为什么要使用批量赋值。
批量赋值的英文名称是Mass Assignment,所谓的批量赋值是指当我们将一个数组发送到模型类用于创建新的模型实例的时候(通常是表单请求数据),我们可以简单通过如下方式实现:
$post = Post::create(Input::all());
而不是像使用save
方法那样一个一个的设置属性值,如果模型属性很多的话,使用save
简直是噩梦有木有。
但事物总是相对的,使用批量赋值是很方便,但同时也带来了安全隐患,很多时候模型类的某些属性值不是我们所期望通过批量赋值修改的,比如用户模型有个user_type
属性,如果用户通过请求数据将其类型修改为管理员类型,这显然是不允许的,正是基于这一考虑,Eloquent模型类为我们提供了$fillable
属性和$guarded
属性,我们可以将其分别看作“白名单”和“黑名单”,定义在$fillable
中的属性可以通过批量赋值进行赋值,而定义在$guarded
中的属性在批量赋值时会被过滤掉。
那么如果我们确实想要修改定义在$guarded
中的属性怎么办?答案是使用save
方法。
此外需要注意的是$fillable
和$guarded
方法同时只能定义一个,原因嘛很简单,非黑即白,定义了一个另外一个也就确定了。
可见批量赋值不仅为我们创建模型提供了便利,还避免了安全隐患,提高了系统的安全性。
下面我们来演示一下批量赋值的使用。首先在Post模型中定义$guarded
属性如下:
protected $guarded = ['views','user_id','updated_at','created_at'];
然后在控制器中实现创建模型实例的逻辑:
$input = [ 'title'=>'test 5', 'content'=>'test content', 'cat_id'=>1, 'views'=>100, 'user_id'=>2 ]; $post = Post::create($input); dd($post);
1.3 其他插入数据的方法
Eloquent模型类还支持其它插入数据的方法——firstOrCreate
和firstOrNew
,两者都是先通过通过传入属性值在数据库中查找匹配记录,如果没有找到则创建一个新的模型实例,不同之处在于后者不会将数据持久化到数据库,需要调用save
方法才行。
2、更新模型
2.1 使用save方法更新模型
save
方法还可以用于更新模型,要更新模型数据,先要获取该模型实例,然后修改模型属性,再调用save
方法保存即可:
$post = Post::find(1); $post->title = 'test 1 title'; if($post->save()){ echo '更新文章成功!'; }else{ echo '更新文章失败!'; }
2.2 使用update方法更新数据
和create
相对应的,Eloquent模型类还支持使用update
方法更新数据,同样要用到批量赋值:
$input = [ 'title'=>'test 6 title', 'content'=>'test content 6', 'cat_id'=>1, 'views'=>200, 'user_id'=>1 ]; $post = Post::find(6); if($post->update($input)){ echo '更新文章成功!'; dd($post); }else{ echo '更新文章失败!'; }
https://segmentfault.com/a/1190000014916636
https://learnku.com/articles/3798/laravel-skills-pivot
https://learnku.com/articles/6356/laravel-eloquent-usage
https://laravelacademy.org/post/8403.html
https://learnku.com/docs/laravel/5.5/queries/1327#parameter-grouping
https://caihongtengxu.github.io/2018/10/30/20181030/
switch ($where['type']) {
//基层医院姓名
case 1:
$condition [] = ['create_doctor_hospital', 'like', $where['name'] . "%"];
break;
//基层医生姓名
case 2:
$condition [] = ['create_doctor_name', 'like', $where['name'] . "%"];
break;
//上级医生姓名
case 3:
$doctor_model=UserModel::select('doctoruid')
->where('doctorname', 'like', $where['name'] . "%")
->get('doctoruid');
//todo:会存在匹配到多个叫刘江淮的医生 结果集要怎么处理
if(empty($doctor_model)){
break;
}else{
foreach ($doctor_model as $doc){
$check_doctor_id[]=$doc->doctoruid;
}
}
$condition[] =['check_doctor_uid','in',$check_doctor_id];
break;
}
}
if (isset($where['start_time']) && isset($where['end_time'])) {
$order_data = $this
->join(HDW_CHECKREPORT_ECG, $this->table . '.', '=', HDW_CHECKREPORT_ECG . '.id')
->select(HDW_CHECKREPORT_ECG . '.check_time', HDW_CHECKREPORT_ECG . '.patient_name', HDW_CHECKREPORT_ECG . '.patient_mobile', HDW_CHECKREPORT_ECG . '.patient_idcard', $this->table . '.orderid', $this->table . '.pay_money')
->whereBetween(HDW_ORDER . '.created_at', [$where['start_time'], $where['end_time'] + 24 * 60 * 60])
->where($condition)
->get();
} else {
//todo:sql待测试
$order_data = $this
->join('doctor_user', $this->table . '.check_doctor_uid', '=', 'doctor_user.doctoruid')
->join(HDW_PRODUCT, $this->table . '.product_id', '=', HDW_PRODUCT . '.productid')
->select($this->table . '.orderid', $this->table . '.patient_name', $this->table . '.product_id', $this->table . '.create_doctor_uid', $this->table . '.pay_money', $this->table . '.create_doctor_hospital', $this->table . '.check_doctor_uid', $this->table . '.create_doctor_name', 'doctor_user.doctorname', HDW_PRODUCT . '.name')
->where($condition)
->get();
}
由于前几个条件都是拼接数组 作为where条件传入的 在搜索上级医生的时候 由于会匹配到多个医生 所以需要使用到wherein 但是不知道怎么去拼接whereIn 或者 作为数组传进去 求sg大神解惑
https://zhuanlan.zhihu.com/p/35807856
20 个 Laravel Eloquent 必备的实用技巧
Eloquent ORM 看起来是一个简单的机制,但是在底层,有很多半隐藏的函数和鲜为人知的方式来实现更多功能。在这篇文章中,我将演示几个小技巧。
1. 递增和递减
要代替以下实现:
$article = Article::find($article_id);
$article->read_count++;
$article->save();
你可以这样做:
$article = Article::find($article_id);
$article->increment('read_count');
以下这些方法也可以实现:
Article::find($article_id)->increment('read_count');
Article::find($article_id)->increment('read_count', 10); // +10
Product::find($produce_id)->decrement('stock'); // -1
2. 先执行 X 方法,X 方法执行不成功则执行 Y 方法
Eloquent 有相当一部分函数可以把两个方法结合在一起使用, 例如 『 请先执行 X 方法, X 方法执行不成功则执行 Y 方法 』。
实例 1 -- findOrFail()
:
要替代以下代码的实现:
$user = User::find($id);
if (!$user) { abort (404); }
你可以这样写:
$user = User::findOrFail($id);
实例 2 -- firstOrCreate()
:
要替代以下代码的实现:
$user = User::where('email', $email)->first();
if (!$user) {
User::create([
'email' => $email
]);
}
这样写就可以了:
$user = User::firstOrCreate(['email' => $email]);
3. 模型的 boot() 方法
在一个 Eloquent 模型中,有个神奇的地方,叫 boot()
,在那里,你可以覆盖默认的行为:
class User extends Model
{
public static function boot()
{
parent::boot();
static::updating(function($model)
{
// 写点日志啥的
// 覆盖一些属性,类似这样 $model->something = transform($something);
});
}
}
在创建模型对象时设置某些字段的值,大概是最受欢迎的例子之一了。 一起来看看在创建模型对象时,你想要生成 UUID 字段 该怎么做。
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->uuid = (string)Uuid::generate();
});
}
4. 带条件与排序的关联关系
定义关联关系的一般方式:
public function users() {
return $this->hasMany('AppUser');
}
你知道吗?也可以在上面的基础上增加 where
或者 orderBy
?
举个例子,如果你想关联某些类型的用户,同时使用 email 字段排序,你可以这样做:
public function approvedUsers() {
return $this->hasMany('AppUser')->where('approved', 1)->orderBy('email');
}
5. 模型特性:时间、追加等
Eloquent模型有些参数,使用类的属性形式。最常用是:
class User extends Model {
protected $table = 'users';
protected $fillable = ['email', 'password']; // 可以被批量赋值字段,如 User::create() 新增时,可使用字段
protected $dates = ['created_at', 'deleted_at']; // 需要被Carbon维护的字段名
protected $appends = ['field1', 'field2']; // json返回时,附加的字段
}
不只这些,还有:
protected $primaryKey = 'uuid'; // 更换主键
public $incrementing = false; // 设置 不自增长
protected $perPage = 25; // 定义分页每页显示数量(默认15)
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at'; //重写 时间字段名
public $timestamps = false; // 设置不需要维护时间字段
还有更多,我只列出了一些有意思的特性,具体参考文档 abstract Model class 了解所有特性.
6. 通过 ID 查询多条记录
所有人都知道 find()
方法,对吧?
$user = User::find(1);
我十分意外竟然很少人知道这个方法可以接受多个 ID 的数组作为参数:
$users = User::find([1,2,3]);
7. WhereX
有一种优雅的方式能将这种代码:
$users = User::where('approved', 1)->get();
转换成这种:
$users = User::whereApproved(1)->get();
对,你没有看错,使用字段名作为后缀添加到 where
后面,它就能通过魔术方法运行了。
另外,在 Eloquent 里也有些和时间相关的预定义方法:
User::whereDate('created_at', date('Y-m-d'));
User::whereDay('created_at', date('d'));
User::whereMonth('created_at', date('m'));
User::whereYear('created_at', date('Y'));
8. 通过关系排序
一个复杂一点的「技巧」。你想对论坛话题按最新发布的帖子来排序?论坛中最新更新的主题在最前面是很常见的需求,对吧?
首先,为主题的最新帖子定义一个单独的关系:
public function latestPost()
{
return $this->hasOne(AppPost::class)->latest();
}
然后,在控制器中,我们可以实现这个「魔法」:
$users = Topic::with('latestPost')->get()->sortByDesc('latestPost.created_at');
9. Eloquent::when() -- 不再使用 if-else
很多人都喜欢使用"if-else"来写查询条件,像这样:
if (request('filter_by') == 'likes') {
$query->where('likes', '>', request('likes_amount', 0));
}
if (request('filter_by') == 'date') {
$query->orderBy('created_at', request('ordering_rule', 'desc'));
}
有一种更好的方法 -- 使用 when()
$query = Author::query();
$query->when(request('filter_by') == 'likes', function ($q) {
return $q->where('likes', '>', request('likes_amount', 0));
});
$query->when(request('filter_by') == 'date', function ($q) {
return $q->orderBy('created_at', request('ordering_rule', 'desc'));
});
它可能看上去不是很优雅,但它强大的功能是传递参数:
$query = User::query();
$query->when(request('role', false), function ($q, $role) {
return $q->where('role_id', $role);
});
$authors = $query->get();
10. 一对多返回默认模型对象
假设现在有种情况是要显示文章的作者,然后模板代码是:
{{ $post->author->name }}
但是如果作者的信息被删除或者因为某些原因没有被设置。代码会返回一个错误,诸如 "property of non-object"。
当然你可以这样处理:
{{ $post->author->name ?? '' }}
你可以通过 Eloquent 关系这样做:
public function author()
{
return $this->belongsTo('AppAuthor')->withDefault();
}
在此示例中,如果文字没有作者的信息, author()
会返回一个空的 AppAuthor
模型对象。
再者,我们也可以给默认的模型对象里面的属性赋默认值。
public function author()
{
return $this->belongsTo('AppAuthor')->withDefault([
'name' => 'Guest Author'
]);
}
11. 通过赋值函数进行排序
想象一下你有这样的代码:
function getFullNameAttribute()
{
return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}
现在,你想要通过 "full_name" 进行排序? 发现是没有效果的:
$clients = Client::orderBy('full_name')->get(); //没有效果
解决办法很简单.我们需要在获取结果后对结果进行排序.
$clients = Client::get()->sortBy('full_name'); // 成功!
注意的是方法名称是不相同的 -- 它不是orderBy,而是sortBy
12. 全局作用域下的默认排序
如果你想要 User::all()
总是按照 name
字段来排序呢? 你可以给它分配一个全局作用域。让我们回到 boot()
这个我们在上文提到过的方法:
protected static function boot()
{
parent::boot();
// 按照 name 正序排序
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('name', 'asc');
});
}
扩展阅读 查询作用域 。
13. 原生查询方法
有时候,我们需要在 Eloquent 语句中添加原生查询。 幸运的是,确实有这样的方法。
// whereRaw
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();
// havingRaw
Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();
// orderByRaw
User::where('created_at', '>', '2016-01-01')
->orderByRaw('(updated_at - created_at) desc')
->get();
14. 复制:复制一行的副本
很简单。说明不是很深入,下面是复制数据库实体(一条数据)的最佳方法:
$task = Tasks::find(1);
$newTask = $task->replicate();
$newTask->save();
15. Chunk() 方法之大块数据
与 Eloquent 不完全相关,它更多的关于 Collection (集合),但是对于处理大数据集合,仍然是很有用的。你可以使用 chunk() 将这些数据分割成小数据块
修改前:
$users = User::all();
foreach ($users as $user) {
// ...
你可以这样做:
User::chunk(100, function ($users) {
foreach ($users as $user) {
// ...
}
});
16. 创建模型时创建额外的东西
我们都知道Artisan命令:
php artisan make:model Company
但是,你知道有三个有用的标记可以为模型生成相关文件吗?
php artisan make:model Company -mcr
- -m 将创建一个迁移文件
- -c 将创建一个控制器
- -r 表示控制器应该是一个资源控制器
17. 调用 save 方法的时候指定 updated_at
你知道 ->save()
方法可以接受参数吗? 我们可以通过传入参数阻止它的默认行为:更新updated_at
的值为当前时间戳。
$product = Product::find($id);
$product->updated_at = '2019-01-01 10:00:00';
$product->save(['timestamps' => false]);
这样,我们成功在 save
时指定了 updated_at
的值。
18. update() 的结果是什么?
你是否想知道这段代码实际上返回什么?
$result = $products->whereNull('category_id')->update(['category_id' => 2]);
我是说,更新操作是在数据库中执行的,但 $result
会包含什么?
答案是受影响的行。 因此如果你想检查多少行受影响, 你不需要额外调用其他任何内容 -- update()
方法会给你返回此数字。
19. 把括号转换成 Eloquent 查询
如果你有个 and
和 or
混合的 SQL 查询,像这样子的:
... WHERE (gender = 'Male' and age >= 18) or (gender = 'Female' and age >= 65)
怎么用 Eloquent 来翻译它呢? 下面是一种错误的方式:
$q->where('gender', 'Male');
$q->orWhere('age', '>=', 18);
$q->where('gender', 'Female');
$q->orWhere('age', '>=', 65);
顺序就没对。正确的打开方式稍微复杂点,使用闭包作为子查询:
$q->where(function ($query) {
$query->where('gender', 'Male')
->where('age', '>=', 18);
})->orWhere(function($query) {
$query->where('gender', 'Female')
->where('age', '>=', 65);
})
20. 复数参数的 orWhere
终于,你可以传递阵列参数给 orWhere()
。平常的方式:
$q->where('a', 1);
$q->orWhere('b', 2);
$q->orWhere('c', 3);
你可以这样做:
$q->where('a', 1);
$q->orWhere(['b' => 2, 'c' => 3]);
刚答了一篇帖子, 算比较常用的技巧,整理一下分享出来。
在使用 Laravel 的关联查询中,我们经常使用 with
方法来避免 N+1
查询,但是 with
会将目标关联的所有字段全部查询出来,对于有强迫症的我们来说,当然是不允许的。
这时候我们可以使用下面的技巧在使用 with 时只查询目标关联的部分字段:
$topics = Topic::limit(2)->with(['user'=>function($query){
$query->select('id','username');
}])->get();
但是每次查询都写得这么繁琐真的好么?不如利用 Laravel 的范围查询将其封装起来:
在 Model 基类中定义一个范围查询(或者使用 Trait)
class BaseModel extends Eloquent{
public function scopeWithOnly($query, $relation, Array $columns)
{
return $query->with([$relation => function ($query) use ($columns){
$query->select(array_merge(['id'], $columns));
}]);
}
}
在我们普通的 Model 类都继承基类:
class Topic extends BaseModel{
public function user()
{
return $this->belongsTo('User');
}
}
然后使用就很方便了:
$topics = Topic::limit(2)->withOnly('user', ['username'])->get();
[ Laravel 从入门到精通 ] 数据库和 Eloquent 进阶 —— 通过查询构建器实现复杂的查询语句
在上一篇教程中,我们通过查询构建器实现了简单的增删改查操作,而日常开发中,往往会涉及到一些更复杂的查询语句,比如连接查询、子查询、排序、分页、聚合查询等等,这一篇教程我们将围绕这些内容展开探讨。
查询小技巧
我们首先来介绍几个 Laravel 自带的语法糖,可以帮助我们快速获取期望的查询结果,提高编码效率。
有时候,我们想要获取的并不是一行或几行记录,而是某个字段的值,你当然你可以查询到一行记录后从结果对象中获取指定字段的值,但是 Laravel 为我们提供了更便捷的语法:
$name = '学院君';
$email = DB::table('users')->where('name', $name)->value('email');
这样,通过 value
方法返回的就是指定字段的值,无需做额外的判断和提取操作。
如果你想要判断某个字段值在数据库中是否存在对应记录,可以通过 exists
方法快速实现:
$exists = DB::table('users')->where('name', $name)->exists();
如果存在,返回 true
,否则返回 false
。该方法还有一个与之相对的方法 doesntExist()
。
你一定有过这样的经历,从数据库获取指定查询结果后,以主键 ID 值为键,以某个字段值为值构建关联数组,以前,你可能不得不遍历查询结果构建数组才能解决这样的问题,在 Laravel 中,我们只需在查询构建器上调用 pluck
方法即可:
$users = DB::table('users')->where('id', '<', 10)->pluck('name', 'id');
该查询返回的结果如下:
注意,我们在传递参数到 pluck
方法的时候,键对应的字段在后面,值对应的字段在前面。
此外,有的时候,我们从数据库返回的结果集比较大,一次性返回进行处理有可能会超出 PHP 内存限制,这时候,我们可以借助 chunk
方法将其分割成多个的组块依次返回进行处理:
$names = [];
DB::table('users')->orderBy('id')->chunk(5, function ($users) use (&$names) {
foreach ($users as $user) {
$names[] = $user->name;
}
});
以上代码的意思是对 users
按照 id
字段升序排序,然后将获取的结果集每次返回5个进行处理,将用户名依次放到 $names
数组中。打印 $names
,结果如下:
聚合函数
在开发后台管理系统时,经常需要对数据进行统计、求和、计算平均值、最小值、最大值等,对应的方法名分别是 count
、sum
、avg
、min
、max
:
$num = DB::table('users')->count(); # 计数 9
$sum = DB::table('users')->sum('id'); # 求和 45
$avg = DB::table('users')->avg('id'); # 平均值 5
$min = DB::table('users')->min('id'); # 最小值 1
$max = DB::table('users')->max('id'); # 最大值 9
高级 Where 查询
前面我们已经用到过通过 where
方法构建查询子句,这里我们将系统介绍 WHERE 查询子句的各种构建。
基本查询
基本查询
最基本的 WHERE 查询子句就是通过 where
方法进行简单查询了:
DB::table('posts')->where('views', 0)->get(); # 此处等号可以省略
DB::table('posts')->where('views', '>', 0)->get();
DB::table('posts')->where('views', '<>', 0)->get();
第一个参数表示字段名,第二个参数表示运算符(支持SQL所有运算符),第三个参数表示比较值。
like查询
有时候我们可能会对字段进行模糊查询,尤其是字符串匹配的时候:
DB::table('posts')->where('title', 'like', 'Laravel学院%')->get();
and查询
如果有多个 WHERE 条件怎么办?在查询构建器中,可以通过方法链轻松搞定:
DB::table('posts')->where('id', '<', 10)->where('views', '>', 0)->get();
上述代码表示获取 where id < 10 and views > 0
的数据库记录,更多的条件用更多的 where
方法即可。此外,我们还可以通过传入数组参数的方式实现上述代码同样的功能:
DB::table('posts')->where([
['id', '<', 10],
['views', '>', 0]
])->get();
or查询
在日常查询中,or 条件的查询也很常见,在查询构建器中,可以通过 orWhere
方法来实现:
DB::table('posts')->where('id', '<', 10)->orWhere('views', '>', 0)->get();
上述代码表示获取 where id < 10 or views > 0
的数据库记录,多个 and 查询可以通过多个 where
方法连接,同理,多个 or
查询也可以通过多个 orWhere
方法连接。
between查询
在一些涉及数字和时间的查询中,BETWEEN 语句可以排上用场,用于获取在指定区间的记录。在查询构建器中,我们可以通过 whereBetween
方法来实现 between 查询:
DB::table('posts')->whereBetween('views', [10, 100])->get();
上述代码表示获取 where view between 10 and 100
的数据库记录。与之相对的还有一个 whereNotBetween
方法,用于获取不在指定区间的数据库记录:
DB::table('posts')->whereNotBetween('views', [10, 100])->get();
对应的 WHERE 条件是 where views not between 10 and 100
。
你可以看出来 between 语句是可以通过 and/or 查询来替代的,只不过使用 between 语句会更简单明了。
in查询
IN 查询也很常见,比如我们需要查询的字段值是某个序列集合的子集的时候。IN 查询可以通过 whereIn 方法来实现:
DB::table('posts')->whereIn('user_id', [1, 3, 5, 7, 9])->get();
对应的 WHERE 子句是 where user_id in (1, 3, 5, 7, 9)
。使用该方法时,需要注意传递给 whereIn
的第二个参数不能是空数组,否则会报错。
同样,与之相对的,还有一个 whereNotIn
方法,表示与 whereIn
相反的查询条件。将上述代码中的 whereIn
方法改为 whereNotIn
,对应的查询子句就是 where user_id not in (1, 3, 5, 7, 9)
。
null查询
NULL 查询就是判断某个字段是否为空的查询,Laravel 查询构建器为我们提供了 whereNull
方法用于实现该查询:
DB::table('users')->whereNull('email_verified_at')->get();
对应的 WHERE 查询子句是 where email_verified_at is null
,同样,该方法也有与之相对的 whereNotNull
方法,例如,要进行 where email_verified_at is not null
查询,可以这么实现:
DB::table('users')->whereNotNull('email_verified_at')->get();
日期查询
关于日常查询,查询构建器为我们提供了丰富的方法,从年月日到具体的时间都有覆盖:
DB::table('posts')->whereYear('created_at', '2018')->get(); # 年
DB::table('posts')->whereMonth('created_at', '11')->get(); # 月
DB::table('posts')->whereDay('created_at', '28')->get(); # 一个月的第几天
DB::table('posts')->whereDate('created_at', '2018-11-28')->get(); # 具体日期
DB::table('posts')->whereTime('created_at', '14:00')->get(); # 时间
上面这几个方法同时还支持 orWhereYear
、orWhereMonth
、orWhereDay
、orWhereDate
、orWhereTime
。
字段相等查询
有的时候,我们并不是在字段和具体值之间进行比较,而是在字段本身之间进行比较,查询构建器提供了 whereColumn
方法来实现这一查询:
DB::table('posts')->whereColumn('updated_at', '>', 'created_at')->get();
对应的 WHERE 查询子句是 where updated_at > created_at
。
JSON查询
从 MySQL 5.7 开始,数据库字段原生支持 JSON 类型,对于 JSON 字段的查询,和普通 where 查询并无区别,只是支持对指定 JSON 属性的查询:
DB::table('users')
->where('options->language', 'en')
->get();
如果属性字段是个数组,还支持通过 whereJsonContains
方法对数组进行包含查询:
DB::table('users')
->whereJsonContains('options->languages', 'en_US')
->get();
DB::table('users')
->whereJsonContains('options->languages', ['en_US', 'zh_CN'])
->get();
高级查询
参数分组
除了以上这些常规的 WHERE 查询之外,查询构建器还支持更加复杂的查询语句,考虑下面这个 SQL 语句:
select * from posts where id <= 10 or (views > 0 and created_at < '2018-11-28 14:00');
貌似我们通过前面学到的方法解决不了这个查询语句的构造,所以我们需要引入更复杂的构建方式,那就是引入匿名函数的方式(和连接查询中构建复杂的连接条件类似):
DB::table('posts')->where('id', '<=', 10)->orWhere(function ($query) {
$query->where('views', '>', 0)
->whereDate('created_at', '<', '2018-11-28')
->whereTime('created_at', '<', '14:00');
})->get();
在这个匿名函数中传入的 $query
变量也是一个查询构建器的实例。这一查询构建方式叫做「参数分组」,在带括号的复杂 WHERE 查询子句中都可以参考这种方式来构建查询语句。
WHERE EXISTS
此外,我们还可以通过查询构建器提供的 whereExists
方法构建 WHERE EXISTS 查询:
DB::table('users')
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('posts')
->whereRaw('posts.user_id = users.id');
})
->get();
对应的 SQL 语句是:
select * from `users` where exists (select 1 from `posts` where posts.user_id = users.id);
用于查询发布过文章的用户。
子查询
有时候,我们会通过子查询关联不同的表进行查询,考虑下面这个 SQL 语句:
select * from posts where user_id in (select id from users where email_verified_at is not null);
对于这条 SQL 语句,我们可以通过查询构建器提供的子查询来实现:
$users = DB::table('users')->whereNotNull('email_verified_at')->select('id');
$posts = DB::table('posts')->whereInSub('user_id', $users)->get();
除了 IN 查询外,普通的 WHERE 查询也可以使用子查询,对应的方法是 whereSub
,但是子查询的效率不如连接查询高,所以我们下面来探讨连接查询在查询构建器中的使用。
连接查询
相关术语
在介绍连接查询之前,你需要对 SQL 的几种连接查询有所了解,SQL 连接查询通常分为以下几种类型:
- 内连接:使用比较运算符进行表间的比较,查询与连接条件匹配的数据,可细分为等值连接和不等连接
- 等值连接(=):如
select * from posts p inner join users u on p.user_id = u.id
- 不等连接(<、>、<>等):如
select * from posts p inner join users u on p.user_id <> u.id
- 等值连接(=):如
- 外链接:
- 左连接:返回左表中的所有行,如果左表中的行在右表中没有匹配行,则返回结果中右表中的对应列返回空值,如
select * from posts p left join users u on p.user_id = u.id
- 右连接:与左连接相反,返回右表中的所有行,如果右表中的行在左表中没有匹配行,则结果中左表中的对应列返回空值,如
select * from posts p right join users u on p.user_id = u.id
- 全连接:返回左表和右表中的所有行。当某行在另一表中没有匹配行,则另一表中的列返回空值,如
select * from posts p full join users u on p.user_id = u.id
- 左连接:返回左表中的所有行,如果左表中的行在右表中没有匹配行,则返回结果中右表中的对应列返回空值,如
- 交叉连接:也称笛卡尔积,不带
where
条件子句,它将会返回被连接的两个表的笛卡尔积,返回结果的行数等于两个表行数的乘积,如果带where
,返回的是匹配的行数。如select * from posts p cross join users u on p.user_id = u.id
看文字太抽象,图示就很明了了:
注:在写 SQL 语句时,OUTER 可以省略。
创建并填充 posts 表
为了方便下面的演示,我们新建一个 posts
数据表,首先创建对应迁移文件:
php artisan make:migration create_posts_table --create=posts
编写新增的迁移文件对应迁移类 CreatePostsTable
的 up
方法如下:
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title')->comment('标题');
$table->text('content')->comment('内容');
$table->integer('user_id')->unsigned()->default(0);
$table->integer('views')->unsigned()->default(0)->comment('浏览数');
$table->index('user_id');
$table->timestamps();
});
}
运行 php artisan migrate
创建 posts
数据表。然后为该数据表创建一个模型类:
php artisan make:model Post
接下来,我们为这个模型类创建一个模型工厂:
php artisan make:factory PostFactory --model=Post
编写模型工厂 database/factories/PostFactory.php
代码如下:
<?php
use FakerGenerator as Faker;
$factory->define(AppPost::class, function (Faker $faker) {
return [
'title' => $faker->title,
'content' => $faker->text,
'user_id' => mt_rand(1, 15),
'views' => $faker->randomDigit
];
});
然后为 posts
表创建填充类:
php artisan make:seeder PostsTableSeeder
在 database/seeds
目录下新生成的填充类 PostsTableSeeder
中,调用模型工厂填充数据表:
<?php
use IlluminateDatabaseSeeder;
class PostsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
factory(AppPost::class, 30)->create();
}
}
这样,我们就可以运行如下 Artisan 命令填充 posts
数据表了:
php artisan db:seed --class=PostsTableSeeder
内连接
首先我们来看内连接在查询构建器中如何实现,以等值连接为例:
$posts = DB::table('posts')
->join('users', 'users.id', '=', 'posts.user_id')
->select('posts.*', 'users.name', 'users.email')
->get();
对照前面的等值连接示例 SQL,很容易理解这段代码,其对应的 SQL 语句是:
select posts.*, users.name, users.email from posts inner join users on users.id = posts.user_id;
在查询构建器中我们通过 join
方法来实现内连接(包含等值连接和不等连接)。上面通过查询构建器查询的结果是:
注:当两张表有字段名相同的字段,并且这两个字段都包含在
select
方法指定的字段中,需要为其中一个字段取别名,否则会产生冲突,例如,假设posts
表中也包含name
字段,那么需要为users.name
取个别名:select('posts.*', 'users.name as username', 'users.email')
。
左连接
左连接也可称作左外连接,在查询构建器中,可以通过 leftJoin
方法实现:
$posts = DB::table('posts')
->leftJoin('users', 'users.id', '=', 'posts.user_id')
->select('posts.*', 'users.name', 'users.email')
->get();
对应的 SQL 语句是:
select posts.*, users.name, users.email from posts left join users on users.id = posts.user_id;
在本例中,由于每个 posts.user_id
都有对应的 users
记录,所以,上述查询结果和等值连接查询结果一致。
右连接
右连接也可称作右外链接,在查询构建器中,可以通过 rightJoin
方法实现:
$posts = DB::table('posts')
->rightJoin('users', 'users.id', '=', 'posts.user_id')
->select('posts.*', 'users.name', 'users.email')
->get();
对应的 SQL 语句如下:
select posts.*, users.name, users.email from posts right join users on users.id = posts.user_id;
在本例中,不是没有用户都有对应的 posts
记录,所以会出现某些 posts
记录为空的结果:
其它连接语句
上面三种是比较常见的连接语句,查询构建器没有提供单独的方法支持全连接,但是有对交叉连接的支持,对应的方法 crossJoin
,使用方法如上面几种查询类似,这里不再单独演示了。
更加复杂的连接条件
有时候,你的连接查询条件可能比较复杂,比如下面这种:
select posts.*, users.name, users.email from posts inner join users on users.id = posts.user_id and users.email_verified_at is not null where posts.views > 0;
这个时候,我们可以通过匿名函数来组装连接查询的条件来构建上面的查询语句:
$posts = DB::table('posts')
->join('users', function ($join) {
$join->on('users.id', '=', 'posts.user_id')
->whereNotNull('users.email_verified_at');
})
->select('posts.*', 'users.name', 'users.email')
->where('posts.views', '>', 0)
->get();
我们可以在匿名函数的 $join
实例上调用所有 Where 查询子句,以组装我们需要的连接查询条件。上述查询会将对应用户邮箱未验证的,文章浏览数为 0
的所以结果过滤掉:
联合查询
查询构建器还支持通过 union
方法合并多个查询结果:
$posts_a = DB::table('posts')->where('views', 0);
$posts_b = DB::table('posts')->where('id', '<=', 10)->union($posts_a)->get();
通过上面这段代码,我们将 views < 0
和 id <= 10
这两个查询结果合并到了一起:
对应的 SQL 语句是:
(select * from `posts` where `id` <= 10) union (select * from `posts` where `views` = 0)
此外,查询构建器也支持 UNION ALL 查询,对应的方法是 unionAll
,该方法与 union
的区别是允许重复记录,将上述代码中的 union
方法改为 unionAll
,会发现查询结果中包含一条重复记录:
排序
对数据库进行查询免不了对查询结果进行排序,查询构建器为此提供了 orderBy
方法,比如我们想要对文章列表按照创建时间进行逆序排序,可以这么做:
$users = DB::table('posts')
->orderBy('created_at', 'desc')
->get();
对应的 SQL 语句如下:
select * from `posts` order by `created_at` desc;
如果是升序排序,可以这么实现:
DB::table('posts')->orderBy('created_at')->get();
默认排序规则就是升序,所以第二个参数 asc
可以省略。
查询构建器还支持通过 inRandomOrder
方法进行随机排序:
DB::table('posts')->inRandomOrder()->get();
注:对于较小的结果集可以使用随机排序,结果集很大的话不要使用,因为性能比较差。
分组
查询构建器还提供了 groupBy
方法用于对结果集进行分组:
$posts = DB::table('posts')
->groupBy('user_id')
->selectRaw('user_id, sum(views) as total_views')
->get();
上述代码对应的 SQL 语句是:
select user_id, sum(views) as total_views from `posts` group by `user_id`;
用于从 user_id
维度统计每个用户发布文章的总浏览数:
如果我们想要进一步对分组结果进行过滤,可以使用 having
方法,比如,要从上述分组结果中过滤出总浏览数大于等于 10
的记录,可以这么做:
$posts = DB::table('posts')
->groupBy('user_id')
->selectRaw('user_id, sum(views) as total_views')
->having('total_views', '>=', 10)
->get();
对应的 SQL 语句是:
select user_id, sum(views) as total_views from `posts` group by `user_id` having `total_views` >= 10;
对应的查询结果是:
分页
日常开发中,另一个常见的查询场景就是分页查询了,在查询构建器中提供了两种方式来进行分页查询。
第一种是通过 skip
方法和 take
方法组合进行分页,skip
方法传入的参数表示从第几条记录开始,take
传入的参数表示一次获取多少条记录:
$posts = DB::table('posts')->orderBy('created_at', 'desc')
->where('views', '>', 0)
->skip(10)->take(5)
->get();
对应的 SQL 语句是:
select * from `posts` where `views` > 0 order by `created_at` desc limit 5 offset 10;
该查询会先按照查询条件和排序条件进行过滤和排序,然后从第10条记录开始获取5条记录返回。
另一种是通过 offset
方法和 limit
方法组合进行分页查询,offset
表示从第几条记录开始,limit
表示一次获取多少条记录,使用方式和 skip
和 take
类似:
$posts = DB::table('posts')->orderBy('created_at', 'desc')
->where('views', '>', 0)
->offset(10)->limit(5)
->get();
对应的 SQL 语句如下:
select * from `posts` where `views` > 0 order by `created_at` desc limit 5 offset 10;
底层最终执行的 SQL 语句完全一样,所以,随便你选择哪种方式都是可以的。
原生查询
如果上面介绍的构建方式还是不能满足你的需求,无法构建出你需要的 SQL 查询语句,那么可以考虑通过查询构建器提供的原生查询方法来构建查询。
查询构建器提供的原生查询支持请参考官方文档,里面说的比较详细,这里就不再赘述了;如果查询构建器提供的原生方法还不能满足你的需求,那只有使用 DB 门面进行彻底的原生查询操作了。
$room = $this->model->where([
't_id' => $data['t_id'],
'r_id' => $data['r_id'],
])->where(function ($query) use ($data) {
$query->where('start_time', '<', $data['start_time'])
->where('end_time', '>', $data['start_time'])
->orWhere(function ($query) use ($data) {
$query->whereBetween('start_time', [$data['start_time'], $data['end_time']]);
});
})->get();
select * from `xx` where (`t_id` = "2" and `r_id` = "1") and (`start_time` < "12344" and `end_time` > "12344" or (`start_time` between "12344" and "123333444"))
模型关联
Eloquent:关联
简介
数据库表通常相互关联。 例如,一篇博客文章可能有许多评论,或者一个订单对应一个下单用户。Eloquent 让这些关联的管理和使用变得简单,并支持多种类型的关联:
定义关联
Eloquent 关联在 Eloquent 模型类中以方法的形式呈现。如同 Eloquent 模型本身,关联也可以作为强大的 查询语句构造器 使用,提供了强大的链式调用和查询功能。例如,我们可以在 posts
关联的链式调用中附加一个约束条件:
$user->posts()->where('active', 1)->get();
不过,在深入使用关联之前,让我们先学习如何定义每种关联类型。
一对一
一对一关联是最基本的关联关系。例如,一个 User
模型可能关联一个 Phone
模型。为了定义这个关联,我们要在 User
模型中写一个 phone
方法,在phone
方法内部调用 hasOne
方法并返回其结果:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* 获得与用户关联的电话记录。
*/
public function phone()
{
return $this->hasOne('AppPhone');
}
}
hasOne
方法的第一个参数是关联模型的类名。关联关系定义好后,我们就可以使用 Eloquent 动态属性获得相关的记录。您可以像在访问模型中定义的属性一样,使用动态属性:
$phone = User::find(1)->phone;
Eloquent 会基于模型名决定外键名称。在当前场景中,Eloquent 假设 Phone
模型有一个 user_id
外键,如果外键名不是这个,可以通过给 hasOne
方法传递第二个参数覆盖默认使用的外键名:
return $this->hasOne('AppPhone', 'foreign_key');
此外,Eloquent 假定外键值是与父级 id
(或自定义 $primaryKey
)列的值相匹配的。 换句话说,Eloquent 将在 Phone
记录的 user_id
列中查找与用户表的 id
列相匹配的值。 如果您希望该关联使用 id
以外的自定义键名,则可以给 hasOne
方法传递第三个参数:
return $this->hasOne('AppPhone', 'foreign_key', 'local_key');
定义反向关联
我们已经能从 User
模型访问到 Phone
模型了。现在,再在 Phone
模型中定义一个关联,此关联能让我们访问到拥有此电话的 User
模型。这时,使用的是与 hasOne
方法对应的 belongsTo
方法:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Phone extends Model
{
/**
* 获得拥有此电话的用户。
*/
public function user()
{
return $this->belongsTo('AppUser');
}
}
在上面的例子中,Eloquent 会尝试匹配 Phone
模型上的 user_id
至 User
模型上的 id
。 它是通过检查关系方法的名称并使用 _id
作为后缀名来确定默认外键名称的。 但是,如果Phone
模型的外键不是user_id
,那么可以将自定义键名作为第二个参数传递给belongsTo
方法:
/**
* 获得拥有此电话的用户。
*/
public function user()
{
return $this->belongsTo('AppUser', 'foreign_key');
}
如果父级模型没有使用 id
作为主键,或者是希望用不同的字段来连接子级模型,则可以通过给 belongsTo
方法传递第三个参数的形式指定父级数据表的自定义键:
/**
* 获得拥有此电话的用户。
*/
public function user()
{
return $this->belongsTo('AppUser', 'foreign_key', 'other_key');
}
默认模型
belongsTo
关联允许定义默认模型,这适应于当关联结果返回的是 null
的情况。这种设计模式通常称为 空对象模式,为您免去了额外的条件判断代码。在下面的例子中,user
关联如果没有找到文章的作者,就会返回一个空的 AppUser
模型。
/**
* 获得此文章的作者。
*/
public function user()
{
return $this->belongsTo('AppUser')->withDefault();
}
您也可以通过传递数组或者使用闭包的形式,填充默认模型的属性:
/**
* 获得此文章的作者。
*/
public function user()
{
return $this->belongsTo('AppUser')->withDefault([
'name' => '游客',
]);
}
/**
* 获得此文章的作者。
*/
public function user()
{
return $this->belongsTo('AppUser')->withDefault(function ($user) {
$user->name = '游客';
});
}
一对多
「一对多」关联用于定义单个模型拥有任意数量的其它关联模型。例如,一篇博客文章可能会有无限多条评论。就像其它的 Eloquent 关联一样,一对多关联的定义也是在 Eloquent 模型中写一个方法:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
/**
* 获得此博客文章的评论。
*/
public function comments()
{
return $this->hasMany('AppComment');
}
}
记住,Eloquent 会自动确定 Comment
模型上正确的外键字段。按照约定,Eloquent 使用父级模型名的「snake case」形式、加上 _id
后缀名作为外键字段。对应到上面的场景,就是 Eloquent 假定 Comment
模型对应到 Post
模型上的那个外键字段是 post_id
。
关联关系定义好后,我们就可以通过访问 comments
属性获得评论集合。记住,因为 Eloquent 提供了「动态属性」,所以我们可以像在访问模型中定义的属性一样,访问关联方法:
$comments = AppPost::find(1)->comments;
foreach ($comments as $comment) {
//
}
当然,由于所有的关联还可以作为查询语句构造器使用,因此你可以使用链式调用的方式、在 comments
方法上添加额外的约束条件:
$comments = AppPost::find(1)->comments()->where('title', 'foo')->first();
形如 hasOne
方法,您也可以在使用 hasMany
方法的时候,通过传递额外参数来覆盖默认使用的外键与本地键。
return $this->hasMany('AppComment', 'foreign_key');
return $this->hasMany('AppComment', 'foreign_key', 'local_key');
一对多(反向)
现在,我们已经能获得一篇文章的所有评论,接着再定义一个通过评论获得所属文章的关联。这个关联是 hasMany
关联的反向关联,在子级模型中使用 belongsTo
方法定义它:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
/**
* 获得此评论所属的文章。
*/
public function post()
{
return $this->belongsTo('AppPost');
}
}
关联关系定义好后,我们就可以在 Comment
模型上使用 post
「动态属性」获得 Post
模型了。
$comment = AppComment::find(1);
echo $comment->post->title;
在上面的例子中,Eloquent 会尝试用 Comment
模型的 post_id
与 Post
模型的 id
进行匹配。默认外键名是 Eloquent 依据关联名、并在关联名后加上 _id
后缀确定的。当然,如果 Comment
模型的外键不是 post_id
,那么可以将自定义键名作为第二个参数传递给belongsTo
方法:
/**
* 获得此评论所属的文章。
*/
public function post()
{
return $this->belongsTo('AppPost', 'foreign_key');
}
如果父级模型没有使用 id
作为主键,或者是希望用不同的字段来连接子级模型,则可以通过给 belongsTo
方法传递第三个参数的形式指定父级数据表的自定义键:
/**
* 获得此评论所属的文章。
*/
public function post()
{
return $this->belongsTo('AppPost', 'foreign_key', 'other_key');
}
多对多
多对多关联比 hasOne
和 hasMany
关联稍微复杂些。这种关联的一个例子就是具有许多角色的用户,而角色也被其他用户共享。例如,许多用户都可以有「管理员」角色。要定义这种关联,需要用到三个数据库表:users
、roles
和 role_user
。role_user
表是以相关联的两个模型数据表、依照字母顺序排列命名的,并且包含 user_id
和 role_id
字段。
多对多关联是通过写一个方法定义的,在方法内部调用 belongsToMany
方法并返回其结果。例如,我们在 User
模型中定义一个 roles
方法:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* 获得此用户的角色。
*/
public function roles()
{
return $this->belongsToMany('AppRole');
}
}
关联关系定义好后,我们就可以通过 roles
动态属性获得用户的角色了:
$user = AppUser::find(1);
foreach ($user->roles as $role) {
//
}
当然,如同所有其它的关联类型,您可以调用 roles
方法,利用链式调用对查询语句添加约束条件:
$roles = AppUser::find(1)->roles()->orderBy('name')->get();
如前所述,为了确定连接表表名,Eloquent 会按照字母顺序合并两个关联模型的名称。 当然,您可以自由地覆盖这个约定,通过给 belongsToMany
方法指定第二个参数实现:
return $this->belongsToMany('AppRole', 'role_user');
除了自定义连接表表名,您也可以通过给 belongsToMany
方法再次传递额外参数来自定义连接表里的键的字段名称。第三个参数是定义此关联的模型在连接表里的键名,第四个参数是另一个模型在连接表里的键名:
return $this->belongsToMany('AppRole', 'role_user', 'user_id', 'role_id');
定义反向关联
定义多对多关联的反向关联,您只要在对方模型里再次调用 belongsToMany
方法就可以了。让我们接着以用户角色为例,在 Role
模型中定义一个 users
方法。
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Role extends Model
{
/**
* 获得此角色下的用户。
*/
public function users()
{
return $this->belongsToMany('AppUser');
}
}
如你所见,除了引入的模型变为 AppUser
外,其它与在 User
模型中定义的完全一样。由于我们重用了 belongsToMany
方法,自定义连接表表名和自定义连接表里的键的字段名称在这里同样适用。
获得中间表字段
您已经学到,多对多关联需要有一个中间表支持,Eloquent 提供了一些有用的方法来和这张表进行交互。例如,假设我们的 User
对象关联了许多的 Role
对象。在获得这些关联对象后,可以使用模型的 pivot
属性访问中间表数据:
$user = AppUser::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
需要注意的是,我们取得的每个 Role
模型对象,都会被自动赋予 pivot
属性,它代表中间表的一个模型对象,能像其它的 Eloquent 模型一样使用。
默认情况下,pivot
对象只包含两个关联模型的键。如果中间表里还有额外字段,则必须在定义关联时明确指出:
return $this->belongsToMany('AppRole')->withPivot('column1', 'column2');
如果您想让中间表自动维护 created_at
和 updated_at
时间戳,那么在定义关联时加上 withTimestamps
方法即可。
return $this->belongsToMany('AppRole')->withTimestamps();
通过中间表过滤关联数据
在定义关联时,您可以使用 wherePivot
和 wherePivotIn
方法过滤 belongsToMany
返回的结果:
return $this->belongsToMany('AppRole')->wherePivot('approved', 1);
return $this->belongsToMany('AppRole')->wherePivotIn('priority', [1, 2]);
定义自定义中间表模型
如果您想定义一个自定义模型来表示关联关系中的中间表,可以在定义关联时调用 using
方法。所有自定义中间表模型都必须扩展自 IlluminateDatabaseEloquentRelationsPivot
类。例如,
我们在写 Role
模型的关联时,使用自定义中间表模型 UserRole
:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Role extends Model
{
/**
* 获得此角色下的用户。
*/
public function users()
{
return $this->belongsToMany('AppUser')->using('AppUserRole');
}
}
当定义 UserRole
模型时,我们要扩展自 Pivot
类:
<?php
namespace App;
use IlluminateDatabaseEloquentRelationsPivot;
class UserRole extends Pivot
{
//
}
远层一对多
「远层一对多」关联提供了方便、简短的方式通过中间的关联来获得远层的关联。例如,一个 Country
模型可以通过中间的 User
模型获得多个 Post
模型。在这个例子中,您可以轻易地收集给定国家的所有博客文章。让我们来看看定义这种关联所需的数据表:
countries
id - integer
name - string
users
id - integer
country_id - integer
name - string
posts
id - integer
user_id - integer
title - string
虽然 posts
表中不包含 country_id
字段,但 hasManyThrough
关联能让我们通过 $country->posts
访问到一个国家下所有的用户文章。为了完成这个查询,Eloquent 会先检查中间表 users
的 country_id
字段,找到所有匹配的用户 ID 后,使用这些 ID,在 posts
表中完成查找。
现在,我们已经知道了定义这种关联所需的数据表结构,接下来,让我们在 Country
模型中定义它:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Country extends Model
{
/**
* 获得某个国家下所有的用户文章。
*/
public function posts()
{
return $this->hasManyThrough('AppPost', 'AppUser');
}
}
hasManyThrough
方法的第一个参数是我们最终希望访问的模型名称,而第二个参数是中间模型的名称。
当执行关联查询时,通常会使用 Eloquent 约定的外键名。如果您想要自定义关联的键,可以通过给 hasManyThrough
方法传递第三个和第四个参数实现,第三个参数表示中间模型的外键名,第四个参数表示最终模型的外键名。第五个参数表示本地键名,而第六个参数表示中间模型的本地键名:
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(
'AppPost',
'AppUser',
'country_id', // 用户表外键...
'user_id', // 文章表外键...
'id', // 国家表本地键...
'id' // 用户表本地键...
);
}
}
多态关联
数据表结构
多态关联允许一个模型在单个关联上属于多个其他模型。例如,想象一下使用您应用的用户可以「评论」文章和视频。使用多态关联,您可以用一个 comments
表同时满足这两个使用场景。让我们来看看构建这种关联所需的数据表结构:
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
comments
表中有两个需要注意的重要字段 commentable_id
和 commentable_type
。commentable_id
用来保存文章或者视频的 ID 值,而 commentable_type
用来保存所属模型的类名。commentable_type
是在我们访问 commentable
关联时, 让 ORM 确定所属的模型是哪个「类型」。
模型结构
接下来,我们来看看创建这种关联所需的模型定义:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
/**
* 获得拥有此评论的模型。
*/
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
/**
* 获得此文章的所有评论。
*/
public function comments()
{
return $this->morphMany('AppComment', 'commentable');
}
}
class Video extends Model
{
/**
* 获得此视频的所有评论。
*/
public function comments()
{
return $this->morphMany('AppComment', 'commentable');
}
}
获取多态关联
一旦您的数据库表准备好、模型定义完成后,就可以通过模型来访问关联了。例如,我们只要简单地使用 comments
动态属性,就可以获得某篇文章下的所有评论:
$post = AppPost::find(1);
foreach ($post->comments as $comment) {
//
}
您也可以在多态模型上,通过访问调用了 morphTo
的关联方法获得多态关联的拥有者。在当前场景中,就是 Comment
模型的 commentable
方法。所以,我们可以使用动态属性来访问这个方法:
$comment = AppComment::find(1);
$commentable = $comment->commentable;
Comment
模型的 commentable
关联会返回 Post
或者 Video
实例,这取决于评论所属的模型类型。
自定义多态关联的类型字段
默认,Laravel 会使用完全限定类名作为关联模型保存在多态模型上的类型字段值。比如,在上面的例子中,Comment
属于 Post
或者 Video
,那么 commentable_type
的默认值对应地就是 AppPost
和 AppVideo
。但是,您可能希望将数据库与程序内部结构解耦。那样的话,你可以定义一个「多态映射表」来指示 Eloquent 使用每个模型自定义类型字段名而不是类名:
use IlluminateDatabaseEloquentRelationsRelation;
Relation::morphMap([
'posts' => 'AppPost',
'videos' => 'AppVideo',
]);
您可以在 AppServiceProvider
中的 boot
函数中使用 Relation::morphMap
方法注册「多态映射表」,或者使用一个独立的服务提供者注册。
多对多多态关联
数据表结构
除了传统的多态关联,您也可以定义「多对多」的多态关联。例如,Post
模型和 Video
模型可以共享一个多态关联至 Tag
模型。 使用多对多多态关联可以让您在文章和视频中共享唯一的标签列表。首先,我们来看看数据表结构:
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
模型结构
接下来,我们准备在模型上定义关联关系。Post
和 Video
两个模型都有一个 tags
方法,方法内部都调用了 Eloquent 类自身的 morphToMany
方法:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
/**
* 获得此文章的所有标签。
*/
public function tags()
{
return $this->morphToMany('AppTag', 'taggable');
}
}
定义反向关联
接下里,在 Tag
模型中,您应该为每个关联模型定义一个方法。在这个例子里,我们要定义一个 posts
方法和一个 videos
方法:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Tag extends Model
{
/**
* 获得此标签下所有的文章。
*/
public function posts()
{
return $this->morphedByMany('AppPost', 'taggable');
}
/**
* 获得此标签下所有的视频。
*/
public function videos()
{
return $this->morphedByMany('AppVideo', 'taggable');
}
}
获取关联
一旦您的数据库表准备好、模型定义完成后,就可以通过模型来访问关联了。例如,我们只要简单地使用 tags
动态属性,就可以获得某篇文章下的所有标签:
$post = AppPost::find(1);
foreach ($post->tags as $tag) {
//
}
您也可以在多态模型上,通过访问调用了 morphedByMany
的关联方法获得多态关联的拥有者。在当前场景中,就是 Tag
模型上的 posts
方法和 videos
方法。所以,我们可以使用动态属性来访问这两个方法:
$tag = AppTag::find(1);
foreach ($tag->videos as $video) {
//
}
查询关联
由于所有类型的关联都通过方法定义,您可以调用这些方法来获取关联实例,而不需要实际运行关联的查询。此外,所有类型的关联都可以作为 查询语句构造器 使用,让你在向数据库执行 SQL 语句前,使用链式调用的方式添加约束条件。
例如,假设一个博客系统,其中 User
模型有许多关联的 Post
模型:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* 获得此用户所有的文章。
*/
public function posts()
{
return $this->hasMany('AppPost');
}
}
您也可以像这样在 posts
关联上添加额外约束条件:
$user = AppUser::find(1);
$user->posts()->where('active', 1)->get();
您可以在关联上使用任何 查询语句构造器 的方法,所以,欢迎查阅查询语句构造器的相关文档以便了解您可以使用哪些方法。
关联方法 Vs. 动态属性
如果您不需要给 Eloquent 关联查询添加额外约束条件,你可以简单的像访问属性一样访问关联。例如,我们刚刚的 User
和 Post
模型例子中,我们可以这样访问所有用户的文章:
$user = AppUser::find(1);
foreach ($user->posts as $post) {
//
}
动态属性是「懒加载」的,意味着它们的关联数据只在实际被访问时才被加载。因此,开发者经常使用 预加载 提前加载他们之后会用到的关联数据。预加载有效减少了 SQL 语句请求数,避免了重复执行一个模型关联加载数据、发送 SQL 请求带来的性能问题。
基于存在的关联查询
当获取模型记录时,您可能希望根据存在的关联对结果进行限制。例如,您想获得至少有一条评论的所有博客文章。为了实现这个功能,您可以给 has
方法传递关联名称:
// 获得所有至少有一条评论的文章...
$posts = AppPost::has('comments')->get();
您也可以指定一个运算符和数目,进一步自定义查询:
// 获得所有有三条或三条以上评论的文章...
$posts = Post::has('comments', '>=', 3)->get();
也可以使用「点」符号构造嵌套的的 has
语句。例如,您可以获得所有至少有一条获赞评论的文章:
// 获得所有至少有一条获赞评论的文章...
$posts = Post::has('comments.votes')->get();
如果您需要更高级的用法,可以使用 whereHas
和 orWhereHas
方法在 has
查询里设置「where」条件。此方法可以让你增加自定义条件至关联约束中,例如对评论内容进行检查:
// 获得所有至少有一条评论内容满足 foo% 条件的文章
$posts = Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
基于不存在的关联查询
当获取模型记录时,您可能希望根据不存在的关联对结果进行限制。例如,您想获得 没有 任何评论的所有博客文章。为了实现这个功能,您可以给 doesntHave
方法传递关联名称:
$posts = AppPost::doesntHave('comments')->get();
如果您需要更高级的用法,可以使用 whereDoesntHave
方法在 doesntHave
查询里设置「where」条件。此方法可以让你增加自定义条件至关联约束中,例如对评论内容进行检查:
$posts = Post::whereDoesntHave('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
关联数据计数
如果您只想统计结果数而不需要加载实际数据,那么可以使用 withCount
方法,此方法会在您的结果集模型中添加一个 {关联名}_count
字段。例如:
$posts = AppPost::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_count' => function ($query) {
$query->where('approved', false);
}
])->get();
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;
预加载
当作为属性访问 Eloquent 关联时,关联数据是「懒加载」的。意味着在你第一次访问该属性时,才会加载关联数据。不过,是当你查询父模型时,Eloquent 可以「预加载」关联数据。预加载避免了 N + 1 查询问题。要说明 N + 1 查询问题,试想一个 Book
模型关联到 Author
模型:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Book extends Model
{
/**
* 获得此书的作者。
*/
public function author()
{
return $this->belongsTo('AppAuthor');
}
}
现在,让我们来获得所有书籍和作者数据:
$books = AppBook::all();
foreach ($books as $book) {
echo $book->author->name;
}
这个循环会运行一次查询取回所有数据表上的书籍数据,然后又运行一次查询获得每本书的作者数据。如果我们有 25 本书,则循环就会执行 26 次查询:1 次是获得所有书籍数据,另外 25 条查询用来获得每本书的作者数据。
谢天谢地,我们使用预加载让整个查询减少到 2 次。这是通过指定关联给 with
方法办到的:
$books = AppBook::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
整个操作,只执行了两条查询:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
预加载多个关联
有时,你需要在一次操作中预加载几个不同的关联。为了实现这个功能,只需在 with
方法上传递额外的参数即可:
$books = AppBook::with(['author', 'publisher'])->get();
嵌套预加载
预加载嵌套关联,可以使用「点」语法。例如,在一个 Eloquent 语句中,预加载所有书籍作者和这些作者的联系信息:
$books = AppBook::with('author.contacts')->get();
为预加载添加约束条件
有时,你可能希望在预加载关联数据的时候,为查询指定额外的约束条件。这有个例子:
$users = AppUser::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
在这个例子中,Eloquent 只会预加载标题里包含 first
文本的文章。您也可以调用其它的 查询语句构造器 进一步自定义预加载约束条件:
$users = AppUser::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
延迟预加载
有时,您可能需要在获得父级模型后才去预加载关联数据。例如,当你需要来动态决定是否加载关联模型时,这可能很有帮助:
$books = AppBook::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
如果您想设置预加载查询的额外约束条件,可以通过给 load
添加数组键的形式达到目的,数组值是接收查询实例的闭包:
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
插入 & 更新关联模型
save
方法
Eloquent 提供了便捷的方法来将新的模型增加至关联中。例如,也许你需要为一个 Post
模型插入一个新的 Comment
。这是你无须为 Comment
手动设置 posts
属性,直接在关联上使用 save
方法插入 Comment
即可:
$comment = new AppComment(['message' => '一条新的评论。']);
$post = AppPost::find(1);
$post->comments()->save($comment);
需要注意的是,我们没有使用动态属性形式访问 comments
关联。相反,我们调用了 comments
方法获得关联实例。save
方法会自动在新的 Comment
模型中添加正确的 post_id
值。
如果您需要保存多个关联模型,可以使用 saveMany
方法:
$post = AppPost::find(1);
$post->comments()->saveMany([
new AppComment(['message' => '一条新的评论。']),
new AppComment(['message' => '另一条评论。']),
]);
create
方法
除了 save
和 saveMany
方法,您也可以使用 create
方法,它接收一个属性数组、创建模型并插入数据库。还有,save
和 create
的不同之处在于,save
接收的是一个完整的 Eloquent 模型实例,而 create
接收的是一个纯 PHP 数组:
$post = AppPost::find(1);
$comment = $post->comments()->create([
'message' => '一条新的评论。',
]);
{tip} 在使用
create
方法前,请确认您已经浏览了本文档的 批量赋值 章节。
您可以使用 createMany
方法保存多个关联模型:
$post = AppPost::find(1);
$post->comments()->createMany([
[
'message' => '一条新的评论。',
],
[
'message' => '另一条新的评论。',
],
]);
更新 belongsTo
关联
当更新 belongsTo
关联时,可以使用 associate
方法。此方法会在子模型中设置外键:
$account = AppAccount::find(10);
$user->account()->associate($account);
$user->save();
当删除 belongsTo
关联时,可以使用 dissociate
方法。此方法会设置关联外键为 null
:
$user->account()->dissociate();
$user->save();
多对多关联
附加 / 移除
Eloquent 也提供了几个额外的辅助方法,让操作关联模型更加便捷。例如:我们假设一个用户可以拥有多个角色,并且每个角色都可以被多个用户共享。给某个用户附加一个角色是通过向中间表插入一条记录实现的,使用 attach
方法:
$user = AppUser::find(1);
$user->roles()->attach($roleId);
使用 attach
方法时,您也可以通过传递一个数组参数向中间表写入额外数据:
$user->roles()->attach($roleId, ['expires' => $expires]);
当然,有时也需要移除用户的角色。删除多对多关联记录,使用 detach
方法。detach
方法会移除掉正确的记录;当然,这两个模型数据依然保存在数据库中:
// 移除用户的一个角色...
$user->roles()->detach($roleId);
// 移除用户的所有角色...
$user->roles()->detach();
为了方便,attach
和 detach
都允许传入 ID 数组:
$user = AppUser::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires]
]);
同步关联
您也可以使用 sync
方法来构造多对多关联。sync
方法可以接收 ID 数组,向中间表插入对应关联数据记录。所有没放在数组里的 IDs 都会从中间表里移除。所以,这步操作完成后,只有在数组里的 IDs 会被保留在中间表中。
$user->roles()->sync([1, 2, 3]);
您可以通过 ID 传递其他额外的数据到中间表:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
如果您不想移除现有的 IDs,可以使用 syncWithoutDetaching
方法:
$user->roles()->syncWithoutDetaching([1, 2, 3]);
切换关联
多对多关联也提供了一个 toggle
方法用于「切换」给定 IDs 的附加状态。如果给定 ID 已附加,就会被移除。同样的,如果给定 ID 已移除,就会被附加:
$user->roles()->toggle([1, 2, 3]);
在中间表上保存额外数据
当处理多对多关联时,save
方法还可以使用第二个参数,它是一个属性数组,包含插入到中间表的额外字段数据。
AppUser::find(1)->roles()->save($role, ['expires' => $expires]);
更新中间表记录
如果您需要更新中间表中已存在的记录,可以使用 updateExistingPivot
方法。此方法接收中间记录的外键和一个属性数组进行更新:
$user = AppUser::find(1);
$user->roles()->updateExistingPivot($roleId, $attributes);
更新父级时间戳
当一个模型 belongsTo
或者 belongsToMany
另一个模型,比如一个 Comment
属于一个 Post
,有时更新子模型导致更新父模型时间戳非常有用。例如,当一个 Comment
模型更新时,您要自动「触发」父级 Post
模型的 updated_at
时间戳的更新,Eloquent 让它变得简单。只要在子模型加一个包含关联名称的 touches
属性即可:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
/**
* 所有会被触发的关联。
*
* @var array
*/
protected $touches = ['post'];
/**
* 获得此评论所属的文章。
*/
public function post()
{
return $this->belongsTo('AppPost');
}
}
现在,当你更新一个 Comment
时,对应父级 Post
模型的 updated_at
字段也会被同时更新,使其更方便得知何时让一个 Post
模型的缓存失效:
$comment = AppComment::find(1);
$comment->text = '编辑了这条评论!';
$comment->save();
https://learnku.com/docs/laravel/5.5/eloquent-relationships/1333
通常情况下我们在做leftjoin连接时需要对不止一个条件进行进行匹配,这时候就需要使用闭包方式,如下:
leftjoin多条件查询,无非以下三种情况。
-
并且关系(&&)且为字段名称,使用on,代码示例如下:
-
或者关系(||),将on改为orOn,代码示例如下:
- 多条件查询,使用where,并使用use传递参数,代码示例如下: