数据库:模型(Active Record ORM)

Introduction

Winter 提供了一个漂亮而简单的 Active Record 实现来处理你的数据库,基于Laravel 雄辩.每个数据库表都有一个对应的“模型”,用于与该表进行交互。模型允许您查询表中的数据,以及将新记录插入表中。

模型类驻留在models 插件目录的子目录。模型目录结构的示例:

📂 plugins
 ┗ 📂 acme
   ┗ 📂 blog
     ┣ 📂 models
     ┃ ┣ 📂 user              <=== Model config directory
     ┃ ┃ ┣ 📜 columns.yaml    <=== Model config files
     ┃ ┃ ┗ 📜 fields.yaml     <==^
     ┃ ┗ 📜 User.php          <=== Model class
     ┗ 📜 Plugin.php

模型配置目录可以包含模型的列表栏表单字段 定义。模型配置目录名称与小写的模型类名称匹配。

定义模型

在大多数情况下,您应该为每个数据库表创建一个模型类。所有模型类都必须扩展Model 班级。插件中使用的模型的最基本表示如下所示:

namespace Acme\Blog\Models;

use Model;

class Post extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'acme_blog_posts';
}

$table protected 字段指定模型对应的数据库表。表名是作者、插件和复数记录类型名称的蛇形命名。

支持的属性

除了由模型特征.例如:

class User extends Model
{
    protected $primaryKey = 'id';

    public $exists = false;

    protected $dates = ['last_seen_at'];

    public $timestamps = true;

    protected $jsonable = ['permissions'];

    protected $guarded = ['*'];
}
Property Description
$primaryKey 用于标识模型的主键名称。
$incrementing 布尔值,如果为 false,则表示主键不是一个递增的整数值。
$exists 布尔值,如果为真则表示模型存在。
$dates 值在获取后转换为 Carbon/DateTime 对象的实例。
$timestamps 布尔值,如果为真,将自动设置 created_at 和 updated_at 字段。
$jsonable 值在保存之前被编码为 JSON,并在获取之后转换为数组。
$fillable 值是可访问的字段批量分配.
$guarded 值是受保护的字段批量分配.
$visible 值是在以下情况下可见的字段序列化模型数据.
$hidden 值是隐藏的字段序列化模型数据.
$connection 包含的字符串连接名称 这是模型默认使用的。

首要的关键

模型将假定每个表都有一个名为id.你可以定义一个$primaryKey 属性来覆盖此约定。

class Post extends Model
{
    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'id';
}

Incrementing

模型将假设主键是一个递增的整数值,这意味着默认情况下主键将自动转换为整数。如果您希望使用非递增或非数字主键,则必须设置 public$incrementing 属性为假。

class Message extends Model
{
    /**
     * The primary key for the model is not an integer.
     *
     * @var bool
     */
    public $incrementing = false;
}

Timestamps

默认情况下,模型会期望created_atupdated_at 表中存在的列。如果您不希望自动管理这些列,请将$timestamps 模型上的属性false:

class Post extends Model
{
    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    public $timestamps = false;
}

如果您需要自定义时间戳的格式,请设置$dateFormat 您模型上的属性。此属性确定日期属性在数据库中的存储方式,以及模型序列化为数组或 JSON 时的格式:

class Post extends Model
{
    /**
     * The storage format of the model's date columns.
     *
     * @var string
     */
    protected $dateFormat = 'U';
}

存储为 JSON 的值

当属性名称传递给$jsonable 属性,这些值将从数据库中序列化和反序列化为 JSON:

class Post extends Model
{
    /**
     * @var array Attribute names to encode and decode using JSON.
     */
    protected $jsonable = ['data'];
}

检索模型

当从数据库请求数据时,模型将主要使用get 或者first 方法,取决于你是否愿意检索多个模型 或者检索单个模型 分别。从模型派生的查询返回一个实例Winter\Storm\Database\Builder.

NOTE:所有模型查询都有启用内存缓存 默认情况下。虽然缓存在大多数情况下会自动失效,但有时您需要使用$model->reload() 为更复杂的用例刷新缓存的方法。

检索多个模型

一旦你创建了一个模型并且其关联的数据库表,您已准备好开始从数据库中检索数据。将每个模型视为一个强大的查询生成器 允许您查询与模型关联的数据库表。例如:

$flights = Flight::all();

访问列值

如果你有一个模型实例,你可以通过访问相应的属性来访问模型的列值。例如,让我们遍历每个Flight 我们的查询返回的实例并回显name 柱子:

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

添加额外的约束

all 方法将返回模型表中的所有结果。由于每个模型都作为一个查询生成器,您还可以向查询添加约束,然后使用get 检索结果的方法:

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

NOTE: 由于模型是查询构建器,您应该熟悉查询生成器.您可以在模型查询中使用这些方法中的任何一种。

Collections

对于像这样的方法allget 它检索多个结果,一个实例Collection 将被退回。这个类提供各种有用的方法 用于处理您的结果。当然,你可以像数组一样简单地遍历这个集合:

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

分块结果

如果您需要处理数千条记录,请使用chunk 命令。这chunk 方法将检索模型的“块”,将它们提供给给定的Closure 进行处理。使用chunk 使用大型结果集时,方法将节省内存:

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

传递给该方法的第一个参数是您希望每个“块”接收的记录数。作为第二个参数传递的闭包将为从数据库中检索到的每个块调用。

检索单个模型

除了检索给定表的所有记录外,您还可以使用以下方法检索单个记录findfirst.这些方法不返回模型集合,而是返回单个模型实例:

// Retrieve a model by its primary key
$flight = Flight::find(1);

// Retrieve the first model matching the query constraints
$flight = Flight::where('active', 1)->first();

未发现异常

有时您可能希望在未找到模型时抛出异常。这在路由或控制器中特别有用。这findOrFailfirstOrFail 方法将检索查询的第一个结果。但是,如果没有找到结果,Illuminate\Database\Eloquent\ModelNotFoundException 将被抛出:

$model = Flight::findOrFail(1);

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

什么时候开发API, 如果异常没有被捕获,一个404 HTTP 响应会自动发送回用户,因此无需编写显式检查来返回404 使用这些方法时的响应:

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

检索聚合

您也可以使用count,sum,max, 和别的聚合函数 由查询生成器提供。这些方法返回适当的标量值而不是完整的模型实例:

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

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

插入和更新模型

插入和更新数据是模型的基石特性,与传统的 SQL 语句相比,它使这个过程变得毫不费力。

基本刀片

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

$flight = new Flight;
$flight->name = 'Sydney to Canberra';
$flight->save();

在这个例子中,我们简单地创建了一个新的实例Flight 建模并分配name 属性。当我们调用save 方法,一条记录将被插入到数据库中。这created_atupdated_at 时间戳也会自动设置,因此无需手动设置。

基本更新

save 方法也可用于更新数据库中已存在的模型。要更新模型,您应该检索它,设置您希望更新的任何属性,然后调用save 方法。再次,updated_at timestamp 会自动更新,所以不需要手动设置它的值:

$flight = Flight::find(1);
$flight->name = 'Darwin to Adelaide';
$flight->save();

还可以针对与给定查询匹配的任意数量的模型执行更新。在此示例中,所有航班active 并有一个destinationSan Diego 将被标记为延迟:

Flight::where('is_active', true)
    ->where('destination', 'Perth')
    ->update(['delayed' => true]);

update 方法需要一个列和值对的数组,表示应更新的列。

更新或插入 /upsert() (批量查询在一个数据库调用中处理多行)

如果您想在单个查询中执行多个“更新插入”,那么您应该使用upsert 方法代替。该方法的第一个参数包含要插入或更新的值,而第二个参数列出了在关联表中唯一标识记录的列。该方法的第三个也是最后一个参数是一个列数组,如果匹配记录已存在于数据库中,则应更新这些列。这upsert方法将自动设置created_atupdated_at 如果在模型上启用了时间戳,则为时间戳:

MyVendor\MyPlugin\Models\Flight::upsert([
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], ['departure', 'destination'], ['price']);

NOTE:: 除 SQL Server 外的所有数据库都需要第二个参数中的列upsert 具有“主要”或“唯一”索引的方法。

批量分配

您也可以使用create 在一行中保存新模型的方法。插入的模型实例将从该方法返回给您。但是,在这样做之前,您需要指定一个fillable 或者guarded 模型上的属性,因为默认情况下所有模型都防止质量分配。请注意,两者都不fillable 或者guarded 影响后台表单的提交,只有使用create 或者fill 方法直接在模型上。

当用户通过请求传递意外的 HTTP 参数时,就会出现批量分配漏洞,并且该参数会更改数据库中您未预料到的列。例如,恶意用户可能会发送is_admin 通过 HTTP 请求传递参数,然后将其映射到模型的create 方法,允许用户将自己升级为管理员。

首先,您应该定义要使哪些模型属性可批量分配。您可以使用$fillable 模型上的属性。例如,让我们制作name 我们的属性Flight 模型质量分配:

class Flight extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

一旦我们使属性可以批量分配,我们就可以使用create 方法在数据库中插入一条新记录。这create 方法返回保存的模型实例:

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

使用$fillable 属性明确哪些字段允许被批量分配,但确实需要 开发人员在将新列添加到数据库时向此数组添加其他字段。相反,您可以使用$guarded 属性而不是假设所有字段都是可填写的except 对于中定义的字段$guarded 大批。这对于具有大量字段的模型很有用。

class Flight extends Model
{
    /**
     * The attributes that aren't mass assignable.
     *
     * @var array
     */
    protected $guarded = ['price'];
}

在上面的示例中,所有属性除了price 将是可批量分配的。

TIP: 一般来说,我们推荐使用$fillable 超过$guarded, 就像Laravel 框架.虽然它在为您的模型创建新字段时确实涉及一个额外的步骤,但它还可以防止潜在不需要的或恶意的数据通过先前的功能写入新字段。

其他创作方式

有时您可能希望只实例化一个模型的新实例。您可以使用make 方法。这make 方法将简单地返回一个新实例而不保存或创建任何东西。

$flight = Flight::make(['name' => 'Flight 10']);

// Functionally the same as...
$flight = new Flight;
$flight->fill(['name' => 'Flight 10']);

您可以使用其他两种方法通过批量分配属性来创建模型:firstOrCreatefirstOrNew.这firstOrCreate 方法将尝试使用给定的列/值对来定位数据库记录。如果在数据库中找不到模型,将插入具有给定属性的记录。

firstOrNew 方法,比如firstOrCreate 将尝试在数据库中找到匹配给定属性的记录。但是,如果未找到模型,将返回一个新的模型实例。请注意,返回的模型firstOrNew 还没有持久化到数据库。你需要打电话save 手动持久化它:

// Retrieve the flight by the attributes, otherwise create it
$flight = Flight::firstOrCreate(['name' => 'Flight 10']);

// Retrieve the flight by the attributes, or instantiate a new instance
$flight = Flight::firstOrNew(['name' => 'Flight 10']);

删除模型

要删除模型,请调用delete 模型实例上的方法:

$flight = Flight::find(1);

$flight->delete();

按键删除现有模型

在上面的示例中,我们在调用之前从数据库中检索模型delete 方法。但是,如果您知道模型的主键,则可以删除模型而不检索它。为此,请致电destroy 方法:

Flight::destroy(1);

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

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

通过查询删除模型

您还可以对一组模型运行删除查询。在此示例中,我们将删除所有标记为无效的航班:

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

NOTE: 值得一提的是模型事件 直接从查询中删除记录时不会触发。

查询范围

本地范围

范围允许您定义可以在整个应用程序中轻松重用的通用约束集。例如,您可能需要经常检索所有被认为“受欢迎”的用户。要定义范围,只需在模型方法前加上scope:

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', 1);
    }
}

使用查询范围

定义范围后,您可以在查询模型时调用范围方法。但是,您不需要包括scope调用方法时的前缀。您甚至可以将调用链接到各种范围,例如:

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

动态范围

有时您可能希望定义一个接受参数的范围。要开始,只需将您的附加参数添加到您的范围。范围参数应在之后定义$query 争论:

class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     */
    public function scopeApplyType($query, $type)
    {
        return $query->where('type', $type);
    }
}

现在您可以在调用范围时传递参数:

$users = User::applyType('admin')->get();

全球范围

全局范围允许您为给定模型的所有查询添加约束。 Winters 自己的软删除功能利用全局范围仅从数据库中检索“未删除”模型。编写您自己的全局范围可以提供一种方便、简单的方法来确保给定模型的每个查询都收到特定的约束。

编写全局范围

编写全局范围很简单。首先,定义一个类来实现Illuminate\Database\Eloquent\Scope 界面。 Winter 没有您应该放置作用域类的常规位置,因此您可以自由地将此类放置在您希望的任何目录中。

Scope 接口要求你实现一个方法:apply.这apply 方法可以添加where 根据需要对查询进行约束或其他类型的子句:

<?php

namespace MyAuthor\MyPlugin\Scopes;

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

class AncientScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  Winter\Storm\Database\Builder  $builder
     * @param  Winter\Storm\Database\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('created_at', '<', now()->subYears(2000));
    }
}

NOTE: 如果您的全局范围正在向查询的 select 子句添加列,则您应该使用 addSelect 方法而不是 select。这将防止无意中替换查询的现有 select 子句。

应用全局范围

要为模型分配全局范围,您应该覆盖模型的booted 方法并调用模型的addGlobalScope 方法。这addGlobalScope 方法接受范围的实例作为其唯一参数:

<?php

namespace MyAuthor\MyPlugin\Models;

use MyAuthor\MyPlugin\Scopes\AncientScope;
use Winter\Storm\Database\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted()
    {
        static::addGlobalScope(new AncientScope);
    }
}

将上面示例中的范围添加到MyAuthor\MyPlugin\Models\User 模型,调用User::all() 方法将执行以下 SQL 查询:

select * from `users` where `created_at` < 0021-02-18 00:00:00

匿名全局范围

Winter 还允许您使用闭包定义全局作用域,这对于不保证它们自己的单独类的简单作用域特别有用。使用闭包定义全局范围时,您应该提供您自己选择的范围名称作为 addGlobalScope 方法的第一个参数:

<?php

namespace MyAuthor\MyPlugin\Models;

use Winter\Storm\Database\Builder;
use Winter\Storm\Database\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted()
    {
        static::addGlobalScope('ancient', function (Builder $builder) {
            $builder->where('created_at', '<', now()->subYears(2000));
        });
    }
}

删除全局范围

如果您想删除给定查询的全局范围,您可以使用withoutGlobalScope 方法。此方法接受全局范围的类名作为其唯一参数:

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

或者,如果您使用闭包定义了全局作用域,则应该传递分配给全局作用域的字符串名称:

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

如果你想删除几个甚至所有查询的全局范围,你可以使用withoutGlobalScopes 方法:

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

Events

模型触发多个事件,允许您连接到模型生命周期中的各个点。每次在数据库中保存或更新特定模型类时,事件允许您轻松执行代码。事件是通过覆盖类中的特殊方法来定义的,可以使用以下方法覆盖:

Event Description
beforeCreate 保存模型之前,首次创建时。
afterCreate 保存模型后,首次创建时。
beforeSave 在保存、创建或更新模型之前。
afterSave 保存模型后,创建或更新模型。
beforeValidate 在验证提供的模型数据之前。
afterValidate 在验证提供的模型数据之后。
beforeUpdate 在保存现有模型之前。
afterUpdate 保存现有模型后。
beforeDelete 在删除现有模型之前。
afterDelete 删除现有模型后。
beforeRestore 在恢复软删除模型之前。
afterRestore 恢复软删除模型后。
beforeFetch 在填充现有模型之前。
afterFetch 填充现有模型后。

使用事件的示例:

/**
 * Generate a URL slug for this model
 */
public function beforeCreate()
{
    $this->slug = Str::slug($this->name);
}

NOTE: 与创建的关系deferred-binding (即:文件附件)将不可用afterSave 如果尚未提交,则为模型事件。要访问未提交的绑定,请使用withDeferred($sessionKey) 关系的方法。例子:$this->images()->withDeferred(post('_session_key'))->get();

基本用法

每当第一次保存新模型时,beforeCreateafterCreate 事件将触发。如果模型已经存在于数据库中并且save 方法被调用,beforeUpdate /afterUpdate 事件将触发。然而,在这两种情况下,beforeSave /afterSave 事件将触发。

例如,让我们定义一个事件侦听器,在首次创建模型时填充 slug 属性:

/**
 * Generate a URL slug for this model
 */
public function beforeCreate()
{
    $this->slug = Str::slug($this->name);
}

回归false 从一个事件将取消save/update 手术:

public function beforeCreate()
{
    if (!$user->isValid()) {
        return false;
    }
}

可以使用original 属性。例如:

public function afterUpdate()
{
    if ($this->title != $this->original['title']) {
        // title changed
    }
}

您可以在外部绑定到当地活动 对于模型的单个实例,使用bindEvent 方法。事件名称应与方法覆盖名称相同,以model..

$flight = new Flight;
$flight->bindEvent('model.beforeCreate', function() use ($model) {
    $model->slug = Str::slug($model->name);
})

扩展模型

由于模型是准备使用行为,它们可以用静态扩展extend 方法。该方法采用闭包并将模型对象传递给它。

在闭包内,您可以向模型添加关系。在这里我们扩展Backend\Models\User 模型包含引用的配置文件(有一个)关系Acme\Demo\Models\Profile 模型。

\Backend\Models\User::extend(function($model) {
    $model->hasOne['profile'] = ['Acme\Demo\Models\Profile', 'key' => 'user_id'];
});

这种方法也可以用来绑定到当地活动,以下代码侦听model.beforeSave 事件。

\Backend\Models\User::extend(function($model) {
    $model->bindEvent('model.beforeSave', function() use ($model) {
        // ...
    });
});

NOTE: 通常放置代码的最佳位置是在你的插件注册类中boot 方法,因为这将在每个请求上运行,确保您对模型所做的扩展随处可用。

此外,还有一些方法可以扩展受保护的模型属性。

\Backend\Models\User::extend(function($model) {
    // add cast attributes
    $model->addCasts([
        'some_extended_field' => 'int',
    ]);

    // add a date attribute
    $model->addDateAttribute('updated_at');

    // add fillable or jsonable fields
    // these methods accept one or more strings, or an array of strings
    $model->addFillable('first_name');
    $model->addJsonable('some_data');
});
豫ICP备18041297号-2