Eloquent:快速入门

简介

Laravel 的 Eloquent ORM 为数据库提供了漂亮简洁的 ActiveRecord 实现。每张数据表都有一个对应的「模型」,用于与之交互的。模型允许您在数据表中查询数据,并将新记录插入到数据表。

首先,确保在 config/database.php 中配置了数据库连接。有关配置数据库的更多信息,可以查看 文档

定义模型

首先,我们创建一个 Eloquent 模型。模型通常位于 app 目录中,但您可以自由放置它们,只要可以通过 composer.json 文件自动加载。所有 Eloquent 模型都继承自 Illuminate\Database\Eloquent\Model 类。

创建一个模型实例最简单的方法是使用 Artisan 命令 make:model

php artisan make:model Flight

如果要在生成模型时一并生成 数据库迁移,可以使用 --migration-m 选项:

php artisan make:model Flight --migration

php artisan make:model Flight -m

Eloquent 模型约定

现在,我们看一个 Flight 模型示例,我们会使用它从 flights 数据表中获取并存储信息:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    //
}

数据表名称

需要注意的是,我们并没有告诉 Eloquent 为 Flight 模型使用哪张表。按照约定,除非明确指定了另一个表名,否则,会使用类名的复数形式的「蛇形命名」作为表名。因此,在此示例中,Eloquent 会假定 Flight 模型在 flights 表中存储记录。可以通过在模型上定义一个 table 属性指定自定义数据表:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型关联的表
     *
     * @var string
     */
    protected $table = 'my_flights';
}

主键

Eloquent 还会假定每张表都有一个字段名为 id 的主键。可以定义一个受保护的 $primaryKey 属性覆盖此约定。

此外,Eloquent 假定主键是一个自增的整数值,意味着默认的主键会被自动转换为 int。如果要使用一个非自增的或非数字的主键,必须在模型中将公有属性 $incrementing 设置为 false。如果主键不是一个整数,应该在模型中将私有属性 $keyType 设置为 string

时间戳

默认情况下,Eloquent 认为数据表中存在 created_atupdated_at 字段。如果不希望 Eloquent 自动管理这些字段,可以在模型中将 $timestamps 属性设置为 false

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 表明模型是否应该自动进行时间戳管理
     *
     * @var bool
     */
    public $timestamps = false;
}

如果需要自定义时间戳格式,可以在模型上设置 $dateFormat 属性。此属性决定日期属性应该怎样被存储到数据库中,以及当模型被序列化为数组或 JSON 时日期的格式:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型日期字段的存储格式
     *
     * @var string
     */
    protected $dateFormat = 'U';
}

如果需要自定义用于存储时间戳的字段名,可以在模型中设置 CREATED_ATUPDATED_AT 常量:

class Flight extends Model
{
    const CREATED_AT = 'creation_date';
    const UPDATED_AT = 'last_update';
}

数据库连接

默认情况下,所有 Eloquent 模型都会使用应用配置的默认数据库连接。如果要为模型指定不同的连接,可以使用 $connection 属性:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型的连接名称
     *
     * @var string
     */
    protected $connection = 'connection-name';
}

获取模型

创建模型和 对应的数据表 后,就可以开始从数据库获取数据了。可以将每个 Eloquent 模型想象成一个强大的 查询构造器,允许您流畅地查询模型关联的数据表。例如:

$flights = App\Flight::all();

foreach ($flights as $flight) {
    echo $flight->name;
}

添加其它约束

Eloquent 的 all 方法会返回模型数据表中的所有结果。由于每个 Eloquent 模型都是一个 查询构造器,因此还可以给查询添加约束,然后使用 get 方法获取结果:

$flights = App\Flight::where('active', 1)
               ->orderBy('name', 'desc')
               ->take(10)
               ->get();

由于 Eloquent 模型是查询构造器,因此应查看 查询构造器 上可以使用的所有方法。可以在 Eloquent 查询中使用任何的这些方法。

刷新模型

可以使用 freshrefresh 方法刷新模型。fresh 方法会从数据库重新获取模型。不影响已存在的模型实例:

$flight = App\Flight::where('number', 'FR 900')->first();

$freshFlight = $flight->fresh();

refresh 方法会从数据库中获取新的数据合并到现有模型。此外,模型加载的关联也会同样被刷新:

$flight = App\Flight::where('number', 'FR 900')->first();

$flight->number = 'FR 456';

$flight->refresh();

$flight->number; // "FR 900"

集合

allget 这样获取多个结果的 Eloquent 方法,会返回一个 Illuminate\Database\Eloquent\Collection 实例。Collection 类为处理 Eloquent 结果提供了 各种有用的方法

$flights = $flights->reject(function ($flight) {
    return $flight->cancelled;
});

当然,也可以像数组一样循环集合:

foreach ($flights as $flight) {
    echo $flight->name;
}

对结果分块

如果需要处理上千条 Eloquent 记录,可以使用 chunk 命令。chunk 方法会获取 Eloquent 模型中的「一块」,将其传递到给定闭包进行处理。当处理很大的结果集时,使用 chunk 方法会节省内存:

Flight::chunk(200, function ($flights) {
    foreach ($flights as $flight) {
        //
    }
});

第一个传递给此方法的参数是每个「块」希望接收的记录条数。作为第二个参数传递的闭包会在从数据库中获取块时被调用。用于执行后获取每块记录的数据库查询会传递给闭包。

使用游标

cursor 方法允许您使用游标遍历数据库记录,它只会执行单个查询。当处理大量数据时,cursor 方法可用于大幅减少内存使用:

foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
    //
}

获取单个模型/聚合

当然,除了在给定数据表中获取所有记录外,还可以使用 findfirst 获取单条记录。这些方法会返回单个模型实例,而不是返回模型的集合:

// 通过主键获取模型
$flight = App\Flight::find(1);

// 获取匹配查询条件的第一个模型
$flight = App\Flight::where('active', 1)->first();

也可以使用包含主键的数组调用 find 方法,它会返回匹配的记录的集合:

$flights = App\Flight::find([1, 2, 3]);

未找到异常

有时希望在未找到模型时抛出异常。这在路由或控制器中尤其有用。findOrFailfirstOrFail 方法会获取查询的第一个结果;但是,如果没有找到结果,会抛出一个 Illuminate\Database\Eloquent\ModelNotFoundException

$model = App\Flight::findOrFail(1);

$model = App\Flight::where('legs', '>', 100)->firstOrFail();

如果没有捕获此异常,会自动返回一个 404 HTTP 响应给用户。使用这些方法时,无需编写代码检查来返回 404 响应:

Route::get('/api/flights/{id}', function ($id) {
    return App\Flight::findOrFail($id);
});

获取聚合

也可以使用 countsummax查询构造器 提供的其它 聚合方法。这些方法会返回对应的标量值而不是完整的模型实例:

$count = App\Flight::where('active', 1)->count();

$max = App\Flight::where('active', 1)->max('price');

插入 & 更新模型

插入

在数据库中创建新记录,可以先创建一个新的模型实例,在模型上设置属性,然后调用 save 方法:

namespace App\Http\Controllers;

use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class FlightController extends Controller
{
    /**
     * 创建新的航班实例
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        // 验证请求

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();
    }
}

在此示例中,我们将传入的 HTTP 请求的 name 参数赋值给 App\Flight 模型实例的 name 属性。当我们调用 save 方法时,会插入一条记录到数据库中。created_atupdated_at 时间戳会在调用 save 方法时自动设置,因此无需手动设置它们。

更新

save 方法可用于更新数据库中已存在的模型。要更新模型,应该先获取它,设置任何要更新的属性,然后调用 save 方法。同样,updated_at 时间戳会自动更新,因此无需手动设置其值:

$flight = App\Flight::find(1);

$flight->name = 'New Flight Name';

$flight->save();

批量更新

还可以更新与给定查询相匹配的任意数量的模型。在如下示例中,所有有效并且目的地为圣迭戈的航班都会被标记为延误:

App\Flight::where('active', 1)
          ->where('destination', 'San Diego')
          ->update(['delayed' => 1]);

update 方法接收一个表示应该被更新字段的字段和对应值的数组。

当通过 Eloquent 模型进行批量更新时,不会触发被更新模型的 savedupdated 模型事件。这是因为批量更新时,实际上不会获取模型。

批量赋值

也可以使用 create 方法在单行中保存新模型。此方法会返回插入的模型实例。但是,在此之前,需要在模型上指定 fillableguarded 属性,因为默认情况下所有 Eloquent 模型都不能批量赋值。

当用户通过请求传递预期之外的 HTTP 参数,并且参数改变了数据库中不希望改变的字段时,就会发生批量赋值攻击。例如,恶意用户可能通过 HTTP 请求发送一个 is_admin 参数,接着它被传递到模型的 create 方法,从而允许用户将自己晋升为管理员。

因此,要进行批量赋值,应该在模型中定义想要批量赋值的属性。可以使用模型的 $fillable 属性完成此操作。例如,我们将 Flight 模型的 name 属性设置为可批量赋值:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 可批量赋值的属性
     *
     * @var array
     */
    protected $fillable = ['name'];
}

设置可批量赋值的属性后,我们可以使用 create 方法插入新记录到数据库中。create 方法返回保存的模型实例:

$flight = App\Flight::create(['name' => 'Flight 10']);

如果已经有了模型实例,可以使用 fill 方法将属性数组填充到模型:

$flight->fill(['name' => 'Flight 22']);

保护属性

$fillable 相当于一个允许批量赋值的「白名单」,还可以选择使用 $guarded$guarded 属性应该包含一个不想被批量赋值的属性的数组。所有不在此数组中的其它属性都可以被批量赋值。因此,$guarded 像一个「黑名单」。当然,应该使用 $fillable$guarded 其中之一 —— 而不是两个一起使用。在以下示例中,price 以外 的所有属性都可以批量赋值:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 不允许批量赋值的属性
     *
     * @var array
     */
    protected $guarded = ['price'];
}

如果想要所有属性都可以批量赋值,可以将 $guarded 属性定义为一个空数组:

/**
 * 不允许批量赋值的属性
 *
 * @var array
 */
protected $guarded = [];

其它创建方法

firstOrCreate / firstOrNew

有两个可以通过批量赋值属性创建模型的其它方法:firstOrCreatefirstOrNewfirstOrCreate 方法会尝试使用给定键/值对添加数据库记录。如果在数据库中找不到模型,会从第一个参数获取属性插入一条记录,还会从可选的第二个参数中获取属性。

firstOrCreate 一样,firstOrNew 方法会尝试添加与给定属性相匹配的记录到数据库中。不过,如果找不到模型,会返回一个新的模型实例。需要注意的是,firstOrNew 返回的模型还没有被存储到数据库中。需要手动调用 save 存储它:

// 通过名称获取航班,或者,如果不存在就创建
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);

// 通过名称获取航班,或者,如果不存在就使用「name」和「delayed」属性创建
$flight = App\Flight::firstOrCreate(
    ['name' => 'Flight 10'], ['delayed' => 1]
);

// 通过名称获取,或者实例化
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);

// 通过名称获取,或者使用「name」和「delayed」属性实例化
$flight = App\Flight::firstOrNew(
    ['name' => 'Flight 10'], ['delayed' => 1]
);

updateOrCreate

也可能遇到模型存在则更新,不存在则创建的情况。Laravel 提供了 updateOrCreate 方法一次完成上述操作。与 firstOrCreate 方法一样,updateOrCreate 会存储模型,因此无需调用 save

// 如果有从奥克兰到圣迭戈的航班,将其价格设为 $99
// 如果匹配的模型不存在,就创建一个
$flight = App\Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99]
);

删除模型

删除模型,可以在模型实例上调用 delete 方法:

$flight = App\Flight::find(1);

$flight->delete();

通过键删除已存在模型

上述示例中,在调用 delete 方法前我们从数据库获取了模型。不过,如果您知道模型的主键,可以调用 destroy 方法删除模型,而不用获取模型。除了使用单个主键作为其参数外,destroy 方法还接收多个主键,主键数组或主键集合:

App\Flight::destroy(1);

App\Flight::destroy(1, 2, 3);

App\Flight::destroy([1, 2, 3]);

App\Flight::destroy(collect([1, 2, 3]));

通过查询删除模型

当然,也可以在模型集合上运行删除语句。在此示例中,我们会删除所有无效的航班。与批量更新一样,批量删除不会为删除的模型触发任何模型事件:

$deletedRows = App\Flight::where('active', 0)->delete();

当通过 Eloquent 运行批量删除语句时,不会触发被删除模型的 deletingdeleted 模型事件。这是因为运行批量删除语句时,实际上不会获取模型。

软删除

除了从数据库中真的删除记录外,Eloquent 还可以「软删除」模型。模型被软删除时,它们不会真的从数据库中删除。而是,设置 deleted_at 属性的值然后插入到数据库中。如果模型有一个非空的 deleted_at 值,模型就已经被软删除了。要为模型启用软删除,可以在模型上使用 Illuminate\Database\Eloquent\SoftDeletes Trait 并将 deleted_at 字段添加到 $dates 属性:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
    use SoftDeletes;

    /**
     * 应该被转换为日期的属性
     *
     * @var array
     */
    protected $dates = ['deleted_at'];
}

当然,还应该将 deleted_at 字段添加到数据表中。Laravel 的 数据库结构生成器 包含一个创建此字段的辅助方法:

Schema::table('flights', function (Blueprint $table) {
    $table->softDeletes();
});

现在,当在模型上调用 delete 方法时,deleted_at 字段会被设置为当前的日期和时间。并且,查询使用软删除的模型时,软删除的模型会自动被排除在所有查询结果之外。

要判断给定模型实例是否已被软删除,可以使用 trashed 方法:

if ($flight->trashed()) {
    //
}

查询软删除的模型

包含软删除的模型

如之前所述,软删除的模型会自动被排除在查询结果之外。不过,可以在查询上使用 withTrashed 方法让软删除的模型包含在结果集中:

$flights = App\Flight::withTrashed()
                ->where('account_id', 1)
                ->get();

withTrashed 方法也可以用在 关联 查询上:

$flight->history()->withTrashed()->get();

只获取软删除的模型

onlyTrashed 方法会获取软删除的模型:

$flights = App\Flight::onlyTrashed()
                ->where('airline_id', 1)
                ->get();

恢复软删除的模型

有时可能希望「恢复」软删除的模型。要将软删除的模型恢复到有效状态,可以在模型实例上使用 restore 方法:

$flight->restore();

也可以在查询中使用 restore 方法快速恢复多个模型。同样,与其它「批量」操作一样,此操作不会触发被恢复模型的任何模型事件:

App\Flight::withTrashed()
        ->where('airline_id', 1)
        ->restore();

withTrashed 方法一样,restore 方法也可以用在 关联 上:

$flight->history()->restore();

永久删除模型

有时可能需要从数据库中真的删除模型。要从数据库中永久移除软删除的模型,可以使用 forceDelete 方法:

// 强制删除单个模型实例
$flight->forceDelete();

// 强制删除所有关联的模型
$flight->history()->forceDelete();

查询作用域

全局作用域

全局作用域允许您为给定模型的所有查询添加约束。Laravel 自带的 软删除 功能就使用了全局作用域从数据库中只获取「未删除」的模型。编写自己的全局作用域可以提供一种便捷简单的方式,确保给定模型的每个查询都受到某些约束。

编写全局作用域

编写全局作用域很简单。定义一个类并实现 Illuminate\Database\Eloquent\Scope 接口。此接口需要实现一个方法:apply。在 apply 方法中,可以添加查询所需的 where 条件:

namespace App\Scopes;

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

class AgeScope implements Scope
{
    /**
     * 将作用域应用到给定的 Eloquent 查询构造器上
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('age', '>', 200);
    }
}

如果全局作用域在 select 语句中添加字段,应该使用 addSelect 方法而不是 select。防止无意中替换查询中已有的 select 语句。

应用全局作用域

要将全局作用域指定给模型,应该重写给定模型的 boot 方法并使用 addGlobalScope 方法:

namespace App;

use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的「启动」方法
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new AgeScope);
    }
}

添加作用域后,User::all() 查询会生成如下 SQL:

select * from `users` where `age` > 200

匿名全局作用域

Eloquent 也允许使用闭包定义全局作用域,这对不定义单独类的简单作用域尤其有用:

namespace App;

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

class User extends Model
{
    /**
     * 模型的「启动」方法
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('age', function (Builder $builder) {
            $builder->where('age', '>', 200);
        });
    }
}

移除全局作用域

如果要为给定查询移除全局作用域,可以使用 withoutGlobalScope 方法。此方法接收全局作用域的类名作为其唯一参数:

User::withoutGlobalScope(AgeScope::class)->get();

或者,如果定义了使用闭包的全局作用域:

User::withoutGlobalScope('age')->get();

如果要移除多个或者甚至所有全局作用域,可以使用 withoutGlobalScopes 方法:

// 移除所有全局作用域
User::withoutGlobalScopes()->get();

// 移除某些全局作用域
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

本地作用域

本地作用域允许定义一套通用的约束,在应用中轻松复用。例如,可能需要频繁获取所有「受欢迎的」用户。定义本地作用域,可以在 Eloquent 模型方法前加上 scope 前缀。

作用域应始终返回一个查询构造器实例:

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 将查询范围限制为只包含受欢迎的用户
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * 将查询范围限制为只包含激活用户
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
}

使用本地作用域

定义本地作用域后,就可以在查询模型时调用作用域方法了。不过,在调用作用域方法时不应包含 scope 前缀。甚至可以链式调用各种本地作用域,例如:

$users = App\User::popular()->active()->orderBy('created_at')->get();

动态作用域

有时可能希望定义接收参数的本地作用域。首先,添加额外参数到作用域。作用域参数应该在 $query 参数之后定义:

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 将查询范围限制为只包含给定类型的用户
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param mixed $type
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeOfType($query, $type)
    {
        return $query->where('type', $type);
    }
}

现在,可以在调用作用域时传递参数了:

$users = App\User::ofType('admin')->get();

比较模型

有时可能需要判断两个模型是否「相同」。is 方法可用于快速验证两个模型是否有相同的主键,数据表和数据库连接:

if ($post->is($anotherPost)) {
    //
}

事件

Eloquent 模型会触发多个事件,允许您在模型的生命周期中对如下的事件点添加钩子:retrievedcreatingcreatedupdatingupdatedsavingsaveddeletingdeletedrestoringrestored。事件允许您在模型类每次保存或更新到数据库时轻松执行相关代码。每个事件都通过其构造函数接收一个模型实例。

retrieved 事件会在从数据库获取已有模型时触发。当新模型第一次被保存时,会触发 creatingcreated 事件。如果模型在数据库中已经存在并且调用了 save 方法,那么会触发 updatingupdated 事件。但是,以上两种情况,都会触发 savingsaved 事件。

当通过 Eloquent 进行批量更新时,不会触发被更新模型的 savedupdated 模型事件。这是因为进行批量更新时,实际上不会获取模型。

首先,在 Eloquent 模型上定义一个 $dispatchesEvents 属性,将 Eloquent 模型的生命周期中的各种事件点映射到自己的 事件类

namespace App;

use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 模型的事件映射
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

定义并映射 Eloquent 事件后,就可以使用 事件监听器 处理事件了。

观察者

定义观察者

如果要监听给定类的很多事件,可以使用观察者将所有监听器放到单个类中。观察者类的方法名与到要监听的 Eloquent 事件相对应。每个方法都接收模型作为其唯一参数。创建新的观察者类最简单的方式是使用 Artisan 命令 make:observer

php artisan make:observer UserObserver --model=User

此命令会将新的观察者放到 App/Observers 目录中。如果该目录不存在,Artisan 会创建。新生成的观察者看起来像这样:

namespace App\Observers;

use App\User;

class UserObserver
{
    /**
     * 处理用户的「created」事件
     *
     * @param  \App\User  $user
     * @return void
     */
    public function created(User $user)
    {
        //
    }

    /**
     * 处理用户的「updated」事件
     *
     * @param  \App\User  $user
     * @return void
     */
    public function updated(User $user)
    {
        //
    }

    /**
     * 处理用户的「deleted」事件
     *
     * @param  \App\User  $user
     * @return void
     */
    public function deleted(User $user)
    {
        //
    }
}

注册观察者,可以在希望观察的模型上使用 observe 方法。可以在服务提供者的 boot 方法中注册观察者。在此示例中,我们会在 AppServiceProvider 中注册观察者:

namespace App\Providers;

use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 启动任何应用服务
     *
     * @return void
     */
    public function boot()
    {
        User::observe(UserObserver::class);
    }

    /**
     * 注册服务提供者
     *
     * @return void
     */
    public function register()
    {
        //
    }
}