欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

深入理解 Laravel Eloquent(三)——模型间关系(关联)

shiping1 的头像
在本篇文章中,我将跟大家一起学习 Eloquent 中最复杂也是最难理解的部分——模型间关系。官方英文文档中叫 Relationships,个人认为翻译成 “模型间关系” 比现在的 “关联” 更好理解一点哈哈。

Eloquent是什么

Eloquent 是一个 ORM,全称为 Object Relational Mapping,翻译为 “对象关系映射”(如果只把它当成 Database Abstraction Layer 数组库抽象层那就太小看它了)。所谓 “对象”,就是本文所说的 “模型(Model)”;对象关系映射,即为模型间关系。中文文档: http://laravel-china.org/docs/eloquent#relationships

下面我们开始一个一个地学习。

一对一关系

顾名思义,这描述的是两个模型之间一对一的关系。这种关系是不需要中间表的。

假如我们有两个模型:User 和 Account,分别对应注册用户和消费者,他们是一对一的关系,那么如果我们要使用 Eloquent 提供的一对一关系方法,表结构应该是这样的:

user: id ... ... account_id

account: id ... ... user_id

假设我们需要在 User 模型中查询对应的 Account 表的信息,那么代码应该是这样的。 `/app/models/User.php`:

<?php

class User extends Eloquent {

  

  protected $table = 'users';

  public function hasOneAccount()

  {

      return $this->hasOne('Account', 'user_id', 'id');

  }

}

然后,当我们需要用到这种关系的时候,该如何使用呢?如下:

$account = User::find(10)->hasOneAccount;

此时得到的 `$account` 即为 `Account` 类的一个实例。


这里最难的地方在于后面的两个 foreign_key 和 local_key 的设置,大家可以就此记住:在 User 类中,无论 hasOne 谁,第二个参数都是 `user_id`,第三个参数一般都是 `id`。由于前面的 `find(10)` 已经锁定了 id = 10,所以这段函数对应的 SQL 为: `select * from account where user_id=10`。


这段代码除了展示了一对一关系该如何使用之外,还传达了三点信息,也是我对于大家使用 Eloquent 时候的建议:

1. 每一个 Model 中都指定表名

2. has one account 这样的关系写成 `hasOneAccount()` 而不是简单的 `account()`

3. 每次使用模型间关系的时候都写全参数,不要省略

相应的,如果使用 belongsTo() 关系,应该这么写:

<?php

class Account extends Eloquent {

  protected $table = 'accounts';

  

  public function belongsToUser()

  {

    return $this->belongsTo('User', 'user_id', 'id');

  }

}

一对多关系

学会了前面使用一对一关系的基础方法,后面的几种关系就简单多了。

我们引入一个新的Model:Pay,付款记录。表结构应该是这样的:

user: id ... ...

pay: id ... ... user_id

User 和 Pay 具有一对多关系,换句话说就是一个 User 可以有多个 Pay,这样的话,只在 Pay 表中存在一个 `user_id` 字段即可。 `/app/models/User.php`:

<?php

class User extends Eloquent {

  

  protected $table = 'users';

  public function hasManyPays()

  {

    return $this->hasMany('Pay', 'user_id', 'id');

  }

}

然后,当我们需要用到这种关系的时候,该如何使用呢?如下:

$accounts = User::find(10)->hasManyPays()->get();

此时得到的 `$accounts` 即为 `Illuminate\Database\Eloquent\Collection` 类的一个实例。大家应该也已经注意到了,这里不是简单的 `-> hasOneAccount` 而是 `->hasManyPays()->get()`,为什么呢?因为这里是 `hasMany`,操作的是一个对象集合。

相应的 belongsTo() 的用法跟上面一对一关系一样:

<?php

class Pay extends Eloquent {

  protected $table = 'pays';

  

  public function belongsToUser()

  {

    return $this->belongsTo('User', 'user_id', 'id');

  }

}

多对多关系

多对多关系和之前的关系完全不一样,因为多对多关系可能出现很多冗余数据,用之前自带的表存不下了。

我们定义两个模型: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 右键 “查看源代码”,就可以看到非常整齐的对象/数组展开了。

在这里给大家展示一个少见用法(奇技淫巧):

public function parent_video()

{

    return $this->belongsToMany($this, 'video_hierarchy', 'video_id', 'video_parent_id');

}

public function children_video()

{

    return $this->belongsToMany($this, 'video_hierarchy', 'video_parent_id', 'video_id');

}

对,你没有看错,可以 belongsToMany 自己。

其他关系

Eloquent 还提供 “远层一对多关联”、“多态关联” 和 “多态的多对多关联” 这另外三种用法,经过上面的学习,我们已经掌握了 Eloquent 模型间关系的基本概念和使用方法,剩下的几种不常用的方法就留到我们用到的时候再自己探索吧。

重要技巧:关系预载入

你也许已经发现了,在一对一关系中,如果我们需要一次性查询出10个 User 并带上对应的 Account 的话,那么就需要给数据库打 1 + 10 条 SQL,这样性能是很差的。我们可以使用一个重要的特性,关系预载入:http://laravel-china.org/docs/eloquent#eager-loading

直接上代码:

$users = User::with('hasOneAccount')->take(10)->get()

这样生成的 SQL 就是这个样子的:

select * from account where id in (1, 2, 3, ... ...)

这样 1 + 10 条 SQL 就变成了 1 + 1 条,性能大增。

 


至此,深入理解 Laravel Eloquent 系列文章到此结束。推荐继续了解 软删除 、转换成数组/JSON

END

WRITTEN BY

avatar
 
2015.1.7   /   热度:23336   /   分类: Laravel

评论:

xhq 
2016-04-30 22:54
我想问下关联模型中两个表存在相同字段的时候怎么办?原生sql是as。只要查询部分字段怎么办?刚接触laravel,小白一个
Panfen 
2016-04-21 10:19
能简单描述一下“belongsToMany 自己”的应用场景吗?不是很理解这一点
ark 
2016-02-13 03:05
@JohnLui,我想问下hasOne的问题。
$account = User::find(10)->hasOneAccount;
和Account::where('user_id',10)->first();
有什么区别呢?如果要同时得到user和account的数据,也是需要写2条语句,
我一直以为hasOne能同时得到User和Account的数据呢。
JohnLui 
2016-02-13 14:31
@ark:他可以把多条数据放到一个对象中,让数据展示更简单,而不能干所谓的“提高性能”的活儿,我一直认为大多数人嘴里说的“性能较高”都是扯淡。
Jaydon 
2016-01-13 14:38
我想问下,大佬,就本页面而言的评论的数据关系图是怎么样的?Comments一张表进行hasOne自己?(id,comment_id,reply_id,user_id,content)。纠结了好多天,想参考下大佬们的!!
laravel 小菜鸟 
2015-12-04 14:05
多态的多对多关联
public function terms()
    {
        return $this->morphToMany('Facsimile\Term','term_relationship');
    }
public function posts()
    {
        return $this->morphedByMany('Facsimile\Post','term_relationship');
    }
我在PostController 里里需要查询 所有文章关联下的 terms  
term_relationship 是关联 post 和terms 的  
请问 下 这个 查询的 语句 该怎么来写呢
改局 
2015-12-02 13:05
有两张表关联。
分别:businessInfo(商家表) 和 favorable(优惠表)
关联:businessInfo表的id  对应  favorable的shop_id
商家表:businessInfo表有个状态字段,state(0为隐藏/1为显示)
我再调用favorable(优惠表)的数据时,要查找businessInfo(商家表)state字段为1的商家。
favorable(优惠表)实体中这样写的:
public function ShopState()
{
        return $this->belongsToMany('App\BusinessInfo','favorable_information','shop_id','id');
}
调用的方法的位置这样写的:
$tfi = FavorableInfo::take(10)->orderby("sort","asc")->get()->ShopState()->where("state","=",1)->get();
但是出现错误:
Call to undefined method Illuminate\Database\Eloquent\Collection::ShopState()
我应该怎样弄才能做到这个多表关联,麻烦大家了。
甘向东 
2015-11-23 14:49
感谢楼主的好文,请问在使用预载入时如何获取关联模型中的某些字段?
刘进 
2016-01-13 14:05
@甘向东:同问。。。。。。
tupgu 
2015-11-17 14:56
我想写自连接这个应该怎么写啊?老师
xiaohan 
2015-10-23 20:41
博主,请问一对多关系怎么插入数据?比如现在为 user id=1 插入新的pay数据
JohnLui 
2015-10-24 01:18
@xiaohan:一对多不能自动插入数据,得自己写。
sam 
2015-09-22 11:31
我想问个问题.我的一对多关系是用逗号分隔储存的.
例如一个产品表和一个证书表
product 字段 id,name,cert_ids
cert 字段 id,name
cert_ids字段保存多个cert表里的id.如1,2,3这样
那在product这个model里面应该如何写,令到返回的数据可以带cert表里的数据呢?
``public function certs()
    {
        $certs = Cert::whereIn('id', explode(',', $this->attributes['cert_ids']))->get();
        return $certs ? $certs : FALSE;
    }``
我是这样写的.但我发觉用with预加载会报错.
冰沐 
2015-09-21 17:28
请问 一个模型 比方说 Category 可以hasMany自己吗
JohnLui 
2015-09-21 18:15
@冰沐:完全没问题
冰沐 
2015-09-29 15:48
@JohnLui:真是感谢,我已经在原文找到答案了,看来还是得好好阅读你的文章啊。我还想了解一下laravel对数据库的视图能不能进行操作,因为我目前做的东西,类似于“子类继承所有父类属性”的东西,为了获得子类的所有属性可能会用到数据库视图
tlijian1989 
2015-08-25 23:48
不太理解 ,User::find(10)->hasManyPays()->get();  一定要用find吗,我想用where去限制怎么做?我自己试的时候find替换成where就报错了,另一个如何同时取出user ,Account 2个表的字段呢?
陈祥 
2015-08-06 15:04
老师 with预载入之后,想要按照其中一个表的某个字段排序,该怎么做啊?您这里有例子吗?
JohnLui 
2015-08-06 15:22
@陈祥:可以取到结果之后再排序嘛
陈祥 
2015-08-06 15:28
@JohnLui:遍历数组排序还是有相应的语法函数啊
beysong 
2015-05-13 00:32
要是一对多,多对多的关系,需要建中间表吗?还是设置好belondtomany关系,会自动建立中间表的?就是保存两个表id对应关系的中间表。
JohnLui 
2015-05-13 10:52
@beysong:需要手动建立
nickwang 
2015-05-13 10:57
@JohnLui:我看有的有使用中间表,有的是加外键,这个数据库执行效率有啥区别没,感谢回答。
JohnLui 
2015-05-13 10:58
@nickwang:中间表是多对多关系才会使用。中间表效率会下降。
ZeroDeng 
2015-03-03 16:02
你好,我想问一下,LARAVEL的这个ORM如果数据库关系很复杂的话,我是说有4个以上的JOIN才可以完整查出所要的结果的话,这个ORM可以很好的工作不,或者是说便捷不?
JohnLui 
2015-03-03 17:30
@ZeroDeng:两个 join 就手写 SQL 吧
ZeroDeng 
2015-03-03 22:11
@JohnLui:你好,还有些问题想请教下。
1.业务逻辑一般是放在CONTROLLER的吧?
2.MODEL中方法很多的化命名实在和苦恼,是不是需要分割MODEL结构分用户MODEL,管理MODEL,将职能细分,然后再继承一个MODEL共用某些API?
3.有些数据输出可能需要格式化,这个操作在CONTROLLER做合适点还是在MODEL处理好合适点,小到时间戳的转换。
4.其实上面的那些疑问都是为了在MVC框架底下,如何构建一个灵活的部署,不知道博主有经验可以分享下不。

非常感谢。
Salon 
2015-01-24 19:54
我想问问 那个with方法的预加载是一对一的情况才能使用吗。但我看文档,它是一对多(authors - books)而且是通过book来查找对应的一个author也行。那么多对多呢?能用这种预加载的方式提高性能吗?大谢作者回复~~
JohnLui 
2015-01-24 23:27
@Salon:预加载本质上是改变 SQL 结构,减少 SQL 请求次数以提高性能。多对多在目前情况下本身就不建议使用,复杂度太高导致学习成本太高,而且重复字段等细节的处理还有不少问题,所以建议直接写 SQL。
Shocker 
2015-01-20 12:06
首先感谢@JohnLui不光制作了这个教程,并且耐心地回答所有朋友的问题。我看到@JohnLui回答某一位网友的问题里说@JohnLui创建这个工程并且完成以上步骤是顺利的,没有碰到蛋疼的编码问题。但是我和那位提问的网友一样无论是composer出来的工程文件还是generate自动生成的php都是默认ansi编码,这样当你完成教程一、二、三后逐个去尝试新建、编辑、删除等功能时妥妥会碰壁,碰到的问题无非是
json_encode():Invalid UTF-8 sequence in argument.....
Namespace declaration statement has to be the very first......
trying to get function delete() on non-object......
这全是编码的问题,其中第二个namesapce那个是因为编码方式为utf-8+bom(对此我很蛋疼,全是ansi也就算了,连bom都出来了)
解决上述问题的方法也很简单,就是哪个文件报错就去把它的编码方式改为utf-8,貌似没有捷径
最初我下了ansi-utf8转换工具,尝试把整个工程全部转换成utf-8格式,然后碰到了很诡异的问题,在此就不赘述;然后我又用该工具把所有文件转回ansi格式,碰到了更诡异的问题。不知道是工具的原因还是啥,
之后又做了一遍,老老实实跟着调试页面提示的信息一个一个改,然后就好呐
JohnLui 
2015-01-20 14:06
@Shocker:
大赞
zgldh 
2015-01-10 22:10
解释foreign_key和local key应当从根本上的 “主表从表,什么是外键,外键是相对于哪个表来说的” 来解释。
有表Cars和表Doors。 简单思考即可得一个车有多个车门,主表是车,从表是门(也可以奇葩的做出从表是车主表是门,不过不在讨论范围内)。
那么两表之间有关联的那两个键,在主表里的是local key(一般究竟是主键了),从表里的是foreign key。

这么记,不管再变,也不会混乱。
yuqifeng 
2015-10-21 10:01
@zgldh:你说的不错,我个人感觉本文作者再解释这两个key的时候过于简单,解释的也很牵强,我说话直作者不要在意。
你有一点说的很对,主表是local,从表是foreign,核心问题是,作者在用上面的例子来解释时使用的是主从双向的关系。按照zgldh的例子,是只有一个model用的上这两个键,就是door这个model,laravel的官方文档说的是,如果不加这两个key, Eloquent会自己认为你在从表中使用的外键名称是基于主表的名字,也就是car_id(默认的foreign key), 而local key 就是id, 本文作者在解释这个地方的时候解释的很笼统。最后附上官方文档关于这块的解释:

Additionally, Eloquent assumes that the foreign key should have a value matching the id column of the parent. In other words, Eloquent will look for the value of the user's id column in the user_id column of the Phone record. If you would like the relationship to use a value other than id, you may pass a third argument to the hasOne method specifying your custom key:

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

在你的例子中,只有从表需要那两个key,主表应该是不需要的。
yuqifeng 
2015-10-21 16:23
@yuqifeng:不好意思,我在上面的评论中有个错误,应该是基于模型名称作为外键名,特此更正!抱歉
iralance 
2015-01-09 10:06
关于奇技淫巧的一点,方法命名必须是驼峰命名法,否则会出错。
JohnLui 
2015-01-09 10:16
@iralance:真的吗?这可是我线上运行的代码哦
iralance 
2015-01-09 10:44
@JohnLui:是关系模型 我之前用匈牙利命名法,确实出错了。。后来check之后才发现必须用驼峰。。。试验为证,我遇到的是酱紫
zgldh 
2015-01-10 22:16
@iralance:使用驼峰命名法,则在调用该关系时需要转换成下划线分割的风格(忘了学名了)
比如
class Article
{
        public function coverImage()
        {
            return $this->hasOne('Image', 'id', 'cover_image_id');
        }
}

$image = Article::find(1)->cover_image;
JohnLui 
2015-01-10 22:17
@zgldh:snake
zxy 
2015-01-08 10:27
嗯,laravel 确实提供更多的默认规则,但我的这个情况是手机验证码,需要搭配Session,得扩展,但那个自定义错误信息难到我了。
JohnLui 

zxy 
2015-01-08 09:39
请教一个问题,关于表单验证的,
我需要扩展一个验证,这个验证里有多个自定义信息
比如:(CI )
public function _check_verify_code($code = '')
    {
        if (empty($code)) {
            $this->form_validation->set_message('_check_verify_code', '请填写验证码!');
            return FALSE;
        }
        if (!$this->session->userdata('message_code') OR !$this->session->userdata('message_time') OR !$this->session->userdata('message_mobile')) {
            $this->form_validation->set_message('_check_verify_code', '手机验证码不存在!');
            return FALSE;
        }
        if ($this->session->userdata('message_mobile') != $this->input->post('mobile', true) OR $this->session->userdata('message_code') != $code) {
            $this->form_validation->set_message('_check_verify_code', '手机验证码错误!');
            return FALSE;
        }
        if (($this->session->userdata('message_time') + 600) < time()) {
            $this->form_validation->set_message('_check_verify_code', '验证码已失效请重新发送!');
            return FALSE;
        }
        return TRUE;
    }
但Laravel我没找到方法 
我现在是用这样的方式定义的,有个问题就是他会三个错误都会出现
//自定义错误提示
        $validator_messages = [
            'mobile_check' => ':attribute 输入不正确!',
            'verify_code.verify_code_check1' => ':attribute 不存在!',
            'verify_code.verify_code_check2' => ':attribute 错误!',
            'verify_code.verify_code_check3' => ':attribute 已失效请重新发送!',
        ];

        //扩展验证规则
        \Validator::extend('mobile_check', function ($attribute, $mobile, $params) {//属性  值  规则参数
            if (!preg_match('#^13[\d]{9}$|14^[0-9]\d{8}|^15[0-9]\d{8}$|^18[0-9]\d{8}$#', $mobile)) {
                return false;
            }
            return true;
        });

        \Validator::extend('verify_code_check1', function ($attribute, $code, $params) {
            if (!Session::get('message_code') OR !Session::get('message_time') OR !Session::get('message_mobile')) {
                return FALSE;
            }
            return TRUE;
        });

        \Validator::extend('verify_code_check2', function ($attribute, $code, $params) {
            if (Session::get('message_mobile') != Input::get('mobile') OR Session::get('message_code') != $code) {
                return FALSE;
            }
            return TRUE;
        });

$rules = [
            'mobile' => 'required|mobile_check:11',
            'verify_code' => 'required|verify_code_check1|verify_code_check2|verify_code_check3',
        ];
$validator = Validator::make(Input::all(), $rules, $validator_messages);
来自  https://lvwenhan.com/laravel/423.html/comment-page-2#comments
普通分类: