Eloquent 是基于查询构造器的数据库管理工具。 ActiveRecord ORM,为多种数据库类型提供单一抽象接口。MVC中的M层。

创建 Eloquent

  1. php artisan make:model Contact
  2. # 使用迁移 -m 或者 --migration
  3. php artisan make:model Contact --migration

将得到 app/Contact.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Contact extends Model
{
    //
}

表名

默认 SecondaryContact 将会得到 secondary_contacts 表;

如果 自定义,请显式的设置:

protected $table = 'contacts_secondary';

主键

默认 id

如果 自定义,请显式设置:

protected $primaryKey = 'contact_id';

如果 不设置自增长:

public $incrementing = false;

时间戳

默认 created_atupdated_at

如果 你没有他们,请禁用:

public $timestamps = false;

如果 你想自定义存储到数据库的时间格式,就像 php 的 date 函数解析的那样,参考如下例子(记录 从 Unix 纪元(January 1 1970 00:00:00 GMT)开始至今的秒数)

protected $dateFormat = 'U';

获取数据

查询构造器 + 链式操作,你可以做很多事。

Get one

first() , firstOrFail() , find(id) , or findOrFail(id)

tip: 在控制器中使用 *OrFail() 方法时,我们不需要捕获异常,laravel 的
路由系统会捕获它,并返回我们一个404错误。

Get many

get() , all()

$vipContacts = Contact::where('vip', true)->get();

$contacts = Contact::all();

tip: 忘记 all() 的存在,优先使用 get() , get() 可以增加 where() 过滤。

chunk()

处理大量数据时,内存占用过多,chunk() 可以帮你分块处理,类似 yii2 中的 each()batch()

Contact::chunk(100, function ($contacts) {
    foreach ($contacts as $contact) {
        // Do something with $contact
    }
});

统计

$countVips = Contact::where('vip', true)->count();
$sumVotes = Contact::sum('votes');
$averageSkill = User::avg('skill_level');

插入和更新模型

插入

// 方法1
$contact = new Contact;
$contact->name = 'Ken Hirata';
$contact->email = 'ken@hirata.com';
$contact->save();

// 方法2
$contact = new Contact([
    'name' => 'Ken Hirata',
    'email' => 'ken@hirata.com',
]);
$contact->save();

// or
$contact = Contact::make([
    'name' => 'Ken Hirata',
    'email' => 'ken@hirata.com',
]);
$contact->save();

以上方法 save() 之前不保存到数据库,即 没有 id, 也没有自动填充的字段,如 created_at

// 直接插入数据库  不需要 save()
$contact = Contact::create([
    'name' => 'Ken Hirata',
    'email' => 'ken@hirata.com',
]);

tip: new Model(), Model::make(), Model::create(), Model::update(), 传入的数组必须是 mass assignment(见下文)。

更新

// 单个更新
$contact = Contact::find(1);
$contact->email = 'natalie@parkfamily.com';
$contact->save();
// or
$contact = Contact::find(1);
$contact->update(['email' => 'natalie@parkfamily.com']);

// 批量更新
Contact::where('created_at', '<', now()->subYear())->update(['longevity' => 'ancient']);

Mass assignment 批量分配

假设您有一个 students 表,其字段为 "student_type, first_name, last_name"。您可能想批量分配 " first_name, last_name ",但是您想保护 student_type 避免被直接更改,这是 可填充的(fillable)被守护的(guarded) 的地方。

更新程序 如下:

// StudentController
public function update(Student $student, Request $request)
{
    $student->update($request->all());
}

如果恶意用户传入了 student_type 字段,则程序就会被直接修改,我们不希望用户自行修改 student_type 字段。 试想一些如果在更新 user 数据时,用户传入 password 字段,这是非常危险的。

laravel 提供了 Mass assignment 解决这个问题

class Student extends Model
{
    protected $fillable = ['first_name', 'last_name'];

    // or
    protected $guarded = ['id', 'created_at', 'updated_at', 'student_type'];
}

这样就解决了,上述问题.

其中 非 fillable 的字段还是可以通过直接分配修改的, eg: $user->password = '123456';

tip: 上例中,Request 对象的 all() 获取了用户的所有输入,其实 Request 还有一个 only() 方法用于采取用户的输入。那么,上述问题,也可以按照如下方式解决:

$student->update($request->only('first_name', 'last_name'));

firstOrCreate() 和 firstOrNew() 还有 updateOrCreate()

业务需求: 查找一条符合某个属性的记录,如果没有,则创建。

$contact = Contact::firstOrCreate(['email' => 'luis.ramos@myacme.com']);
// or
$contact = Contact::firstOrNew(['email' => 'luis.ramos@myacme.com']);

区别: firstOrCreate() 查找完直接入库了,firstOrNew() 要手动 save() 才能存入数据库。

第二个参数

$contact = Contact::firstOrCreate(['email' => 'luis.ramos@myacme.com'], ['name' => 'Luis Ramos']);
// or
$contact = Contact::firstOrNew(['email' => 'luis.ramos@myacme.com'], ['name' => 'Luis Ramos']);

如果根据条件(第一个参数),没查找到记录,则创建记录,并追加第二个参数的值到记录中; 如果找到了记录,则忽略第二个参数的值。

如果你想,无论记录找到与否,都按照第二个参数修改记录,则可以使用 updateOrCreate() 方法。

删除

正常删除

// 方法1
$contact = Contact::find(5);
$contact->delete();

// 方法2
Contact::destroy(1);
// or
Contact::destroy([1, 5, 7]);

// 方法3
Contact::where('updated_at', '<', now()->subYear())->delete();

软删除

软删除是,数据库状态标记为删除,但并非真正删除,方便管理员恢复和查看数据。

软删除的麻烦之处在于, 每次查询都要排除软删除。

幸运的是 laravel 的软删除默认会自动排除软删除。你只需在数据库加入 delete_at 列,并在模型中启用软删除。

启用软删除

// 1. 迁移中加入 delete_at 列
Schema::table('contacts', function (Blueprint $table) {
    $table->softDeletes();
});

// 2. 模型中启用软删除
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Contact extends Model
{
    use SoftDeletes; // use the trait

    protected $dates = ['deleted_at']; // mark this column as a date
}

只要你完成了以上改变,每次调用 delete() 或 destroy() 时,就自动修改 delete_at 类为当期时间(标记删除),而不是直接删除它,而且所有的查询也将排除此行。

查询软删除

那么怎么查询软删除。

// 查询 包含 软删除的内容
$allHistoricContacts = Contact::withTrashed()->get();

// 判断记录是否是已经删除的
if ($contact->trashed()) {
    // do something
}

// 只获取软删除的内容
$deletedContacts = Contact::onlyTrashed()->get();

恢复软删除

$contact->restore();
// or
Contact::onlyTrashed()->where('vip', true)->restore();

强制删除

强制删除开启了软删除的的记录。

$contact->forceDelete();
// or
Contact::onlyTrashed()->forceDelete();

Scopes

截止目前为止,我们一直都在手动创建查询构造器。

Scope 运行我们预构建查询过滤器,Scope 分为局部 Scope,可以直接链式调用;和 全局 Scope, 每次查询将自动调用。

Local scope (局部)

// 活跃的vip
$activeVips = Contact::where('vip', true)->where('trial', false)->get();

我们可能在很多地方用到这个查询,而且这个写法对开发者并不友好。

// 这样就好多了 
$activeVips = Contact::activeVips()->get();

这里使用了 local scope ,要使以上代码生效,我们还要做。

class Contact
{
    public function scopeActiveVips($query)
    {
        return $query->where('vip', true)->where('trial', false);
    }

    // code more...

使用参数

class Contact
{
    public function scopeStatus($query, $status)
    {
        return $query->where('status', $status);
    }

    // code more...
$friends = Contact::status('friend')->get();

global scope (全局)

使用闭包创建全局 scope

class Contact extends Model
{
     protected static function boot()
    {
        parent::boot();

        // 增加了名为 active 的 scope
        static::addGlobalScope('active', function (Builder $builder) {
            $builder->where('active', true);
        });
    }

这样,每次查询,就只会找到 activetrue 的记录。

为 scope 单独定义类,实现 scope 接口

<?php
namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

// 定义了名为 active 的 scope类
class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        return $builder->where('active', true);
    }
}

要想使用它,要做如下操作

<?php
use App\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Model;

class Contact extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new ActiveScope);
    }
}

移除 全局scope

// 闭包情况
$allContacts = Contact::withoutGlobalScope('active')->get();

// 独立类
Contact::withoutGlobalScope(ActiveScope::class)->get();
Contact::withoutGlobalScopes([ActiveScope::class, VipScope::class])->get();

// 移除所有
Contact::withoutGlobalScopes()->get();

获取器、修改器和属性转换

获取器

获取器,允许自定义字段,可能你想自定义已存在字段的输出形式,或者直接获取一个数据库不存在的字段。

编写 get{PascalCasedPropertyName}Attribute 方法来定义获取器。

// 定义:
class Contact extends Model
{
    // 修饰已存在的列
    public function getNameAttribute($value)
    {
        return $value ?: '(No name provided)';
    }

    // 修饰不存在的列
    public function getFullNameAttribute()
    {
        return $this->first_name . $this->last_name;
    }
}

// 使用:
$name = $contact->name;

$fullName = $contact->full_name;

修改器

和获取器一样,修改器用于设置数据,而不是获取数据。你可以用于写入已存在的字段,或者设置一个数据库不存在的字段。

编写 set{PascalCasedPropertyName}Attribute 的方法来定义修改器

// 定义:
class Order extends Model
{
    // 设置存在的字段
    public function setAmountAttribute($value)
    {
        $this->attributes['amount'] = $value > 0 ? $value : 0;
    }

    // 设置不存在的值
    public function setWorkgroupNameAttribute($value)
    {
        $this->attributes['email'] = "{$workgroupName}@ourcompany.com";
    }
}

// 使用:
$order->amount = '15';
$order->workgroup_name = 'jstott';

属性转换

属性转换 允许你 将所有整数类型字段转换为整数,对JSON进行编码和解码以存储在TEXT类型的列中,或者将TINYINT 0和1与布尔值进行相互转换。

所有的属性转换列表

Type Description
int|integer Casts with PHP ( int )
real|float|double Casts with PHP ( float )
string Casts with PHP ( string )
bool boolean
object Parses to/from JSON, as a stdClass object
array Parses to/from JSON, as an array
collection Parses to/from JSON, as a collection
date|datetime Parses from database DATETIME to Carbon, and back
timestamp Parses from database TIMESTAMP to Carbon, and back

使用模型转换

class Contact
{
    protected $casts = [
        'vip' => 'boolean',
        'children_names' => 'array',
        'birthday' => 'date',
    ];
}

选定列为时间戳列

class Contact
{
    protected $dates = [
        'met_at',
    ];

    // or

    protected $casts = [
        'met_at' => 'timestamp',
    ];
}

默认 created_atupdated_at 已经在 dates 数组中了。

Collections

使用模型时,返回 collection 而不是纯数组。

Collection 基础

laravel 的 collection 类似于一个对象的聚合,它对外释放的方法非常有用,如果您想在非 laravel 项目中使用它,请引入 Tightenco/Collect 包。

// 创建
$collection = collect([1, 2, 3]);

// 过滤掉偶数
$odds = $collection->reject(function($item){
    return $item % 2 === 0;
});

// 整体扩大10倍
$multiplied = $collection->map(function ($item) {
    return $item * 10;
});

// 偶数 扩大10倍 再求和
$sum = $collection
    ->filter(function ($item) {
        return $item % 2 == 0;
    })->map(function ($item) {
        return $item * 10;
    })->sum();

你会发现部分功能 array_map()array_reduce() 也可以实现,但是 collection 可以让你从原生 php 混乱的参数顺序中解放出来。

collection 提供超过60种方法, 详细见 https://laravel.com/docs/master/collections

tip: collection 可以被用于任何地方,只要这个地方可以用数组(除强制指定数组)。例如,你可以 foreach 一个 collection, 或者 collection['key'] 这样获取 collection 中的特定值。

Eloquent 的 collection

每个模型都返回一个 正常的 collection, 而且对这个 collection 进行了功能扩充。

例如: modelKeys() 返回模型的主键。

另外一项功能是,为模型的 collection 扩充功能,见下例。

// 为模型自定义 collection 类
class OrderCollection extends Collection
{
    public function sumBillableAmount()
    {
        return $this->reduce(function ($carry, $order) {
            return $carry + ($order->billable ? $order->amount : 0);
        }, 0);
    }
}


class Order extends Model
{
    public function newCollection(array $models = [])
    {
        return new OrderCollection($models);
    }

    // ...

现在,任何时候你得到 order collection 时,其实你得到了一个 OrderCollection 的实例。

$orders = Order::all();
$billableAmount = $orders->sumBillableAmount();

Eloquent 的序列化

$contactArray = Contact::first()->toArray();
$contactJson = Contact::first()->toJson();
$contactsArray = Contact::all()->toArray();
$contactsJson = Contact::all()->toJson();

 $string = (string) $contact; // 这是允许的,本质是运行 toJson() 方法

路由默认转json

// routes/web.php
Route::get('api/contacts', function () {
    return Contact::all();
});

Route::get('api/contacts/{id}', function ($id) {
    return Contact::findOrFail($id);
});

json中隐藏部分属性

class Contact extends Model
{
    protected $hidden = ['password', 'remember_token'];

    // or 
    protected $visible = ['name', 'email', 'status'];
// 模型关联中也可以
class User extends Model
{
    protected $hidden = ['contacts'];

    public function contacts()
    {
        return $this->hasMany(Contact::class);
    }

    // ...
}

tip: 默认当你获取数据库记录时是不显示关联信息的,所有隐藏与否没什么关系。但是也有要包含关联的情况,所以就需要这个功能。如果要显示隐藏,如下

$user = User::with('contacts')->first();

某次查询可能要显示隐藏的内容,如下:

$array = $user->makeVisible('remember_token')->toArray();

tip: 如果你创建了一个字段不存在的获取器,你想添加该字段到json中,如下:

class Contact extends Model
{
    // 每次都追加
    protected $appends = ['full_name'];

    public function getFullNameAttribute()
    {
        return "{$this->first_name} {$this->last_name}";
    }
}

// 单次追加
return $user->append('is_admin')->toArray();

return $user->setAppends(['is_admin'])->toArray();

日期序列化

// 自定义默认日期格式
protected function serializeDate(DateTimeInterface $date)
{
    return $date->format('Y-m-d');
}
// 为每个字段单独定义日期格式 
protected $casts = [
    'birthday' => 'date:Y-m-d',
    'joined_at' => 'datetime:Y-m-d H:00',
];

Eloquent 关联

One to one

class Contact extends Model
{
    public function phoneNumber()
    {
        // 默认 contact_id 可以省略,
        // 等价于 return $this->hasOne(PhoneNumber::class, 'contact_id');
        return $this->hasOne(PhoneNumber::class);
    }

    // ...

使用

$contact = Contact::first();
$contactPhone = $contact->phoneNumber;

从 phoneNumber 获取 contact

class PhoneNumber extends Model
{
    public function contact()
    {
        return $this->belongsTo(Contact::class);
    }

    // ...

使用

$contact = $phoneNumber->contact;

tip: hasOne 和 belongsTo 主要区别在于关系的哪一侧持有关系的外键,以上例为主, phoneNumber 持有 Contact 的外键 contact_id 故,Contact hasOne phoneNumber && phoneNumber belongsTo Contact.

创建关联

实质是传递一个实例给 save() 或 传递一个实例数组给 saveMany(), 当然你也可以传递参数给 create()createMany(), 他们将创造新的实例。

$contact = Contact::first();
$phoneNumber = new PhoneNumber;
$phoneNumber->number = 8008675309;
$contact->phoneNumbers()->save($phoneNumber);

// or
$contact->phoneNumbers()->saveMany([
    PhoneNumber::find(1),
    PhoneNumber::find(2),
]);

// or
$contact->phoneNumbers()->create([
    'number' => '+13138675309',
]);

// or
$contact->phoneNumbers()->createMany([
    ['number' => '+13138675309'],
    ['number' => '+15556060842'],
]);

One to many

class User extends Model
{
    public function contacts()
    {
        return $this->hasMany(Contact::class);
    }

    // ...

使用

$user = User::first();
// 返回一个 对象实例的 collection 
$contactList = $user->contacts;

因为是 collection 所以, 你可以像下边那样使用

$donors = $user->contacts->filter(function ($contact) {
    return $contact->status == 'donor';
});

$lifetimeValue = $contact->orders->reduce(function ($carry, $order) {
    return $carry + $order->amount;
}, 0);

// ...

contact 获取 user

class contact extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

使用
$userName = $contact->user->name;

关联和取消关联

大部分情况下,我们可以在 父级 上运行 save() 并传递参数来创建关联(就像 one to one中创建关联一样)。如:$user->contacts()->save($contact)。 但是当我们要在 子级 上创建和取消关联,请使用 associate()dissociate() 。如下:

// 创建关联 User::first() 和 Contact::first()
$contact = Contact::first();
$contact->user()->associate(User::first());
$contact->save();


// 取消关联
$contact->user()->dissociate();
$contact->save();

作为查询构建器使用关联

// 返回一个 collection 
$user->contact; 

// 返回一个 预处理的 查询构建器, 即 如果 user 的 id = 1, 调用 contact() 后,将获得一个 "获取所有 contact 记录中 user_id=1 的记录 "的预处理查询。
$user->contact(); 

// 所以你可以像以下这样使用
$user->contact()->where('status', 'donor')->get();

查找有关联关系的记录

// 查询有关联记录的
$usersWithContacts = User::has('contact')->get();

// 筛选关联记录数量
$usersWithManyContacts = User::has('contact', '>=', 5)->get();

// 嵌套关联  (user 与 contact 有关联,且 contact 与 phoneNumber 有关联)
$usersWithPhoneBook = User::has('contact.phoneNumbers')->get();

// 自定义查询
// Gets all contacts with a phone number containing the string "867-5309"
$jennyIGotYourNumber = Contact::whereHas('phoneNumbers', function ($query) {
    $query->where('number', 'like', '%867-5309%');
});

Has many through

例子: 每个用户(user)有很多联系人(contact), 每个联系人有很多电话号码(phoneNumber), 现在想获得用户的联系人电话列表. 这就是 Has many through 的关系.

class User extends Model{

    // 默认 phone_numbers 表有一个 contact_id 的列, 并且 contacts 表 有一个 user_id 的列
    public function phoneNumbers()
    {
        return $this->hasManyThrough(PhoneNumber::class, Contact::class);
    }

    // or 自定义关联列名 
    //  contacts 表 有一个 user_id 的列, phone_numbers 表有一个 owner_id 的列
    public function phoneNumbers()
    {
        return $this->hasManyThrough(PhoneNumber::class, Contact::class, 'user_id', 'owner_id');
    }
}

$user = User::find(1);
$phoneNumbers = $user->phoneNumbers;

Has one through

和 Has many through 一样, 只不过 不是通过 多个 中间项, 访问 多个 项, 而是 通过 一个 中间项, 访问 一个 项.

例子: 用户(user)属于一个公司(company), 公司有一个电话(phoneNumber),现在通过获得公司的电话来获取用户的电话.

class User extends Model
{
    public function phoneNumber()
    {
        return $this->hasOneThrough(PhoneNumber::class, Company::class);
    }
}

Many to many

例子: 一个用户(user)有多个联系人(contact), 每个联系人与多个用户有关系.

/**
 *  User 模型
 */
class User extends Model
{

    public function contacts()
    {
        return $this->belongsToMany(Contact::class);
    }
}

反过来,也一样.

/**
 * Contact 模型
 */
class Contact extends Model
{

    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

因为是多对多关联, 单个用户没有 contact_id 列, 单个联系人也没有 user_id 列, 所以,我们需要一个中间表来关联. 通常这个中间表的命名规范是, 表1的名字_表2的名字, 表1表2谁在前, 取决于模型名字的字母顺序.

本例中, 我们的中间表, 应该是 contact_user , 因为 c<u , 所以 contacts 表在前, users 在后. 当然, 你也可以自定义中间表的名字, 记得, 写关联时, 要把表名作为第二个参数传递给 belongsToMany(). 另外, 中间表需要 contact_iduser_id 两列.

从中间表获取数据

中间表的数据越少越好, 但是有时我们还要往中间表存储一些数据, 例如 created_at 存储关系创建的时间等.

  • 向中间表中加数据
public function contacts()
{
    // withTimeStamps() 增加 created_at 和 updated_at 字段
    // withPivot() 传入多个参数或数组, 表示新增的字段名
    return $this->belongsToMany(Contact::class)
        ->withTimestamps()
        ->withPivot('status');
}
  • 从中间表获得数据

当你通过关联关系获取一个模型实例时, 它将有一个 pivot 属性, 来存储中间表的相关信息, 你可以通过以下方式访问它们.

$user = User::first();
// 
$user->contacts->each(function ($contact) {
    echo sprintf(
        'Contact associated with this user at: %s',
        $contact->pivot->created_at
    );
});
  • 自定义 pivot 属性的名字
/**
 * User 模型
 */
class User extends Model
{

    public function contacts()
    {
        return $this->belongsToMany(Contact::class)
            ->withTimestamps()
            ->withPivot(['status', 'is_first'])
            ->as('client');
    }
}

// 使用
$user = User::find(1);
$user->contacts->each(function($contact){
    echo sprintf('Client is first comming? %s', $contact->client->is_first);
});

绑定与解绑关系

  • 因为中间表有自己的属性, 所以绑定多对多关系时, 你要设置这些属性, 如下
$user = User::first();
$contact = Contact::first();
$user->contacts()->save($contact, ['status' => 'donor']);
  • 你还可以使用 attach()detach() 来绑定和解绑关系, 参数不需要传递实例, 只需要传递 idid的数组.
$user = User::first();
// 绑定
$user->contacts()->attach(1);
$user->contacts()->attach(2, ['status' => 'donor']);
$user->contacts()->attach([1, 2, 3]);
$user->contacts()->attach([
    1 => ['status' => 'donor'],
    2,
    3,
]);

// 解绑
$user->contacts()->detach(1);
$user->contacts()->detach([1, 2]);
$user->contacts()->detach(); // 解绑所有,返回解绑记录数
  • 把绑定的解绑, 没绑定的绑定
$user->contacts()->toggle([1, 2, 3]);
  • 更新所有绑定的中间表的状态
$user->contacts()->updateExistingPivot($contactId, [
    'status' => 'inactive',
]);
  • 替换现有的绑定关系, 实际上解绑所有现有绑定,重新绑定
$user->contacts()->sync([1, 2, 3]);

// 等价于
$user->contacts()->detach();
$user->contacts()->attach([1, 2, 3]);

// 你还可以附加参数
$user->contacts()->sync([
    1 => ['status' => 'donor'],
    2,
    3,
]);

多态

多态意味着, 我们有多个模型以一种相同的关联关系关联到同一模型. 类如, 我们(Users)可以收藏 Contacts 也可以收藏 Events.

stars 收藏表 有 id , starrable_id , 和 starrable_type 这些列 . starrable_type 用于区分 Contacts or Events , starrable_idContactsEventsid.

构建多态

class Star extends Model
{
    public function starrable()
    {
        return $this->morphTo();
    }
}

class Contact extends Model
{
    public function stars()
    {
        return $this->morphMany(Star::class, 'starrable');
    }
}


class Events extends Model
{
    public function stars()
    {
        return $this->morphMany(Star::class, 'starrable');
    }
}

收藏一个联系人

$contact = Contact::first();
$contact->stars()->create();

获取该联系人的所有收藏记录

$contact = Contact::first();
$contact->stars->each(function ($star) {
    // Do stuff
});

获取收藏记录 收藏的内容是联系人还是事件

$stars = Star::all();
$stars->each(function ($star) {
    var_dump($star->starrable); // An instance of Contact or Event
});

怎么知道谁收藏了呢? 我们要加 user_idstars 表里. usersstars 是 一对多关系 (即, 一条收藏记录属于一个用户, 一个用户可以有多条收藏记录).

// 增加 一对多 关系
class Star extends Model
{
    public function starrable()
    {
        return $this->morphsTo;
    }
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

class User extends Model
{
    public function stars()
    {
        return $this->hasMany(Star::class);
    }
}

那么新增收藏, 如下:

// 指定收藏用户
$user = User::first();
$event = Event::first();
$event->stars()->create(['user_id' => $user->id]);

多对多的多态

这是一种最复杂, 且不常见的多态形式, 多对多的多态.

最常见的例子是标签系统, 一个标签( tags )可以标记多个项目( contactsevents ), 每个项目又可以又多个标签.

先设计数据库, contacts 表, events 表, 和 一个 tags 表, 都包含一个 id 和其余你需要的列, 自不用说, 另外我们还需要一个 taggables 表, 包含 tag_id , taggable_id , 和 taggable_type 列. 每条 taggables 记录都将关联一个标签和一个可被加标签的项目.

定义关联模型

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

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

class Tag extends Model
{
    public function contacts()
    {
        return $this->morphedByMany(Contact::class, 'taggable');
    }

    public function events()
    {
        return $this->morphedByMany(Event::class, 'taggable');
    }
}

添加标签

$tag = Tag::firstOrCreate(['name' => 'likes-cheese']);
$contact = Contact::first();
$contact->tags()->attach($tag->id);

两边都可以获取关联的信息

$contact = Contact::first();
$contact->tags->each(function ($tag) {
    // Do stuff
});

$tag = Tag::first();
$tag->contacts->each(function ($contact) {
    // Do stuff
});
$tag->events->each(function ($event) {
    // Do stuff
});

子记录变动 更新父记录时间

当有 belongsTobelongsToMany 关系时, 当子记录发生变动时,有时我们需要更新父记录的时间戳(updated_at).

如下设置:


class phoneNumber extends Model
{
    // 传入 关联的方法名 
    protected $touches = ['contact'];

    public function contact()
    {
        return $this->belongsTo(Contact::class);
    }
}

Eager loading 解决 N+1 问题

默认,在加载模型关系时, Eloquent 是懒加载, 这意味着你首次访问模型实例时, 其关联模型不会被加载,知道你在实例上访问它们时,它们才会被加载,所以是 “懒惰” 的。

这就造成了 N+1 问题。

$contacts = Contact::all();
foreach ($contacts as $contact) {
    foreach ($contact->phone_numbers as $phone_number) {
        echo $phone_number->number;
    }
}

如果你使用模型实例时, 你知道你要使用到关联模型,你可以使用 Eager Loading , 如下:

// 传递关联关系的方法名给 with 方法
$contacts = Contact::with('phoneNumbers')->all();

当使用 Eager Loading 时, 意味着,不是请求一次关联关系查询一次,而是先获取初始项(所有联系人),再获取所有初始项的关联关系(联系人电话),即简化N+1问题,为 1+1,大大提升效率。

  • 可以 加载多个关系
$contacts = Contact::with('phoneNumbers', 'addresses')->get();
  • 可以 加载关系的关系
$authors = Author::with('posts.comments')->get();
  • 使用 eager loads 但不全部使用, 可以传一个闭包给 with()
$contacts = Contact::with(['addresses' => function ($query) {
    $query->where('mailable', true);
}])->get();
  • Lazy eager loading
    有时,你并不知道你需要 eager loading , 直到你获取父模型后,某些条件下才需要
$contacts = Contact::all();
if ($someCondition) {
    $contacts->load('phoneNumbers');
}
  • 你也可以只在关联模型没被加载时加载
$contacts = Contact::all();
if ($showPhoneNumbers) {
    $contacts->loadMissing('phoneNumbers');
}
  • 仅仅 eager loading 关系数量
$authors = Author::withCount('posts')->get();
// Adds a "posts_count" integer to each Author with a count of that
// author's related posts