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

2015-1-7   /   阅读数:113648   /   分类: Laravel

在本篇文章中,我将跟大家一起学习 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-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对数据库的视图能不能进行操作,因为我目前做的东西,类似于“子类继承所有父类属性”的东西,为了获得子类的所有属性可能会用到数据库视图
qiancen
2016-06-03 15:37
@JohnLui:你好,我想请问怎么操作数据库的视图呢,也可以在model里定义吗,就像指定数据库一样protect $table=? ?可以指定数据库视图吗,很苦恼现在
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:遍历数组排序还是有相应的语法函数啊
COOLER
2016-10-04 12:10
@陈祥:同问啊
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
2015-01-08 11:02
@zxy:
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);
JohnLui
2015-01-08 10:02
@zxy:这些是现有的验证规则,大部分情况应该都覆盖了: http://laravel-china.org/docs/validation#available-validation-rules

发表评论:

© 2011-2019 岁寒  |  Powered by Emlog