Laravel-Eloquent ORM(下)

Eager Loading

關於什們是 Eager loading 的簡單介紹:

Eager loading: 把所有的事情在詢問時就做完。典型的例子是,當您將兩個矩陣增加時,就把所有的運作都做完了。
Lazy loading: 只有在需要的時候才去進行運算。以上述例子來說,您在取得矩陣內某個元素時才會去進行運算。
Over-eager loading: 預測使用者將會詢問什們然後預先將起加載完畢。

Eager Loading 是為了解決 N+1 query(關於 N+1 query請參考此文)的問題而存在的。舉例來說,一本書(Book)和一個作者(Author)關連。它們的關聯定義如下:

<?php

class Book extends Eloquent {

    public function author()
    {
        return $this->belongsTo('Author');
    }

}

請思考以下程式碼:

<?php

foreach (Book::all() as $book)
{
    echo $book->author->name;
}

這個迴圈會先執行一個取得所有書本的 query,然後再根據每一本書去取得作者。因此,如果我們有 25 本書,那們將會執行 26 次的 query。

幸好,我們可以使用 Eager loading 去大幅度的減少 query 的數量。我們可以藉由 with()方法去指定此關連應該使用 eager loaded:

<?php

foreach (Book::with('author')->get() as $book)
{
    echo $book->author->name;
}

在上面的迴圈中,只有兩條 query 會被執行:

<?php

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

適當的使用 Eager Loading 可以大大的增加應用程式的效能。當然,您也可以一次對多個關連做 eager load:

<?php

$books = Book::with('author', 'publisher')->get();

也可以對巢狀關連做 Eager Load:

<?php

$books = Book::with('author.contacts')->get();

在上面的例子中,author 的將會被 eager load,而 author 的 contacts關連也會被 eager load。

Eager Load 限制

有些時候您可能會想要在 Eager load 上做一點限制。如下:

<?php

$users = User::with(array('posts' => function($query)
{
    $query->where('title', 'like', '%first%');

}))->get();

當然,您也可以加上排序:

<?php

$users = User::with(array('posts' => function($query)
{
    $query->orderBy('created_at', 'desc')

}))->get();

Lazy Eager Loading

我們也可以從已經存在的model 集合中去 eager load相關連的 model。當我們需要動態的決定使否讀取關連或是和 cache 結合使用的時候,這會很有用:

<?php

$books = Book::all();

$books->load('author', 'publisher');

Inserting Related Models

您可能也會常常需要去新增一個關連 model。舉例來說,您想要新增一個新的 comment,這個comment 屬於某個 post。您可以直接從 Post model 直接新增一筆 comment取代手動的去設置 comment 的外鍵。

增加一個關連 model
<?php

$comment = new Comment(array('message' => 'A new comment.'));

$post = Post::find(1);

$comment = $post->comments()->save($comment);

在上面的例子中,新增的comment記錄中的 post_id 欄位被自動被賦值。

Associating Models (Belongs To)

您可以使用 associate()方法去更新 belongsTo關連,這個方法會對子model的外鍵賦值。

<?php

$account = Account::find(10);

$user->account()->associate($account);

$user->save();

新增關連模組 (多對多)

同樣的我們可以在多對多的關聯中新增關連model。延續前面的 User 和 Role model 當做我們的例子。我們可以透過 attach()方法很簡單的給予某個 user 新的 role。

附上多對多 model
<?php

$user = User::find(1);

$user->roles()->attach(1);

您也可以傳送一個屬性陣列去修改關連 model 的值:

<?php

$user->roles()->attach(1, array('expires' => $expires));

當然,有 attach()方法就有detach()方法:

<?php

$user->roles()->detach(1);

您也可以使用 sync()方法去附加關連 model。sync()方法會接受一個 ID 的陣列,並將它存放在樞紐表格。在完成這個操作後,只有陣列中的ID 會出現在中介表格。

使用同步方法去附加多對多 model
<?php

$user->roles()->sync(array(1, 2, 3));

您也可以藉由給定的 ID 陣列改變樞紐表格的值

同步時更改樞紐表單資料
<?php

$user->roles()->sync(array(1 => array('expires' => true)));

有時候您可能會希望新增一個關連model並且將它附加上去。這時可以使用 save()方法:

<?php

$role = new Role(array('name' => 'Editor'));

User::find(1)->roles()->save($role);

您也可以傳入一個屬性陣列修改關連的表格資料。

<?php

User::find(1)->roles()->save($role, array('expires' => $expires));

Touching Parent Timestamps

當一個 model 屬於另一個 model 時,例如 comment 屬於一個 Post,在子model被呼叫時同時更新父 model 的 timestamp會很有幫助。舉例來說,當一個 Comment model被更新時,您可能會想要自動的去 touch 擁有該 comment 的 Post 的 updated_at timestamp。Eloquent 使得這件事情很容易。只要加一個 touches 屬性,該屬性為一個包含關連model 名稱的陣列。

<?php

class Comment extends Eloquent {

    protected $touches = array('post');

    public function post()
    {
        return $this->belongsTo('Post');
    }

}

現在,當您更新一個 Comment,擁有它的Post的 updated_at 欄位也會同步被更新。

<?php

$comment = Comment::find(1);

$comment->text = 'Edit to this comment!';

$comment->save();

Working With Pivot Tables

如您所知道的,處理多對多關連需要一個中介表格。Eloquent 提供了一些非常方便的方法和這些表格互動。繼續沿用 User 和Role 的例子。在取得關連後,我們可以從 model 中取得樞紐表格:

<?php

$user = User::find(1);

foreach ($user->roles as $role)
{
    echo $role->pivot->created_at;
}

每個我們取得的 Role model 都會自動被賦與一個 pivot 屬性。該屬性包含了一個代表中介表格的 model,而您也可以像使用其他 Eloquent model 般的使用它。預設只有鍵會存在樞紐物件上,如果您的樞紐表格包含了其他屬性,必須在定義關連時指定它們。

<?php

return $this->belongsToMany('Role')->withPivot('foo', 'bar');

現在 foo 和 bar 屬性可以從樞紐物件取得了。如果您想要樞紐物件自動的維護 created_at 和 updated_at 這些 timestamps,請在關連定義的地方使用withTimestamps()方法。

<?php

return $this->belongsToMany('Role')->withTimestamps();

將某個 model 的樞紐表格記錄全部刪除,可以使用 detach 方法:

刪除樞紐表格的紀錄
<?php

User::find(1)->roles()->detach();

這的動作並不會刪除 roles 表格的紀錄,僅只會刪除樞紐表格的紀錄。

定義一個客製樞紐model

Laravel 同樣允許您定義一個客製樞紐model。為了定義一個客製 model,首先您必須新增您的 "基底" model 類別,該類別需為 Eloquent類別的延伸。在您的其他 Eloquent model 中,將延伸的類別由預設的 Eloquent 改為您客製化的 base model。在您的base model中,加入下述函式以回傳一個您的客製樞紐實體。

<?php

public function newPivot(Model $parent, array $attributes, $table, $exists)
{
    return new YourCustomPivot($parent, $attributes, $table, $exists);
}

Collection

所以Eloquent取得的超過一個以上的結果,都會回傳一個 Collention 物件。這個物件實作了 PHP的 IteratorAggregate 介面,所以可以像陣列般的迭代訪問。當然,這個物件也有許多方便我們操作的方法。

舉例來說,我們可以使用 contains()方法判斷是否有包含我們指定的主鍵:

檢查 Collection 是否包含某個鍵
<?php

$roles = User::find(1)->roles;

if ($roles->contains(2))
{
    //
}

collection 可能會被轉換為陣列或是JSON。

<?php

$roles = User::find(1)->roles->toArray();

$roles = User::find(1)->roles->toJson();

如果 collection 為字串,會被自動轉換成JSON格式。

<?php

$roles = (string) User::find(1)->roles;

Eloquent 的 collection 也有一些不錯的方法可以迭代或是過濾包含的物件。

迭代 collection
<?php

$roles = $user->roles->each(function($role)
{
    //
});

過濾 collection

當使用 filter()方法時,它的 callback 函式作用相當於php 的 array_filter:

<?php

$users = $users->filter(function($user)
{
    return $user->isAdmin();
});

注意: 當過濾一個collection 並且將它轉換為 JSON時,請先呼叫 values() 函式重新設置陣列的鍵值。可參考此文

對每個 collection 物件加上 callback
<?php

$roles = User::find(1)->roles;

$roles->each(function($role)
{
    //
});
根據某個值排序 collection
<?php

$roles = $roles->sortBy(function($role)
{
    return $role->created_at;
});

也可以這樣:

<?php

$roles = $roles->sortBy('created_at');

有時候您會希望自訂的方法回傳客製的 collection 物件,這可以藉由覆寫您的 Eloquent model 中的方法來實現:

回傳一個客製的 Collection
<?php

class User extends Eloquent {

    public function newCollection(array $models = array())
    {
        return new CustomCollection($models);
    }

}

Accessors & Mutators

Laravel 提供了一個方便的方法在取得或設定您的model屬性時轉換它們。在您的 model 簡單定義一個 getFooAttribute 方法去宣告一個 accessor( 訪問器 )。注意方法的名稱必須採大小寫駝峰式,就算您的資料庫欄位名稱是小寫底線式。

定義一個取得器
<?php

class User extends Eloquent {

    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }

}

在上面的例子中,first_name 欄位有了一個取得器,屬性的值會被傳入取得器中。

定義一個設置器
<?php

class User extends Eloquent {

    public function setFirstNameAttribute($value)
    {
        $this->attributes['first_name'] = strtolower($value);
    }

}

Date Mutators

Eloquent 預設會把 created_at、updated_at、以及 deleted_at 的欄位轉換成 Carbon 的實體,Carbon 類別是原生 PHPDateTime 類別的延伸,提供了許多實用的方法。

藉由覆寫 getDates()方法,您可以自行指定哪些欄位需要進行轉換,或是所有欄位都取消轉換。

<?php

public function getDates()
{
    return array('created_at');
}

當一個欄位被認為是日期時,您可以將它的值設定為 UNIX timestamp,日期字串( Y-m-d ),日期+時間字串( Y-m-d H:i:s),當然,也可以是一個 Datetime/Carbon 實體。

如果要完全取消轉換,將 getDates()方法改寫如下即可:

<?php

public function getDates()
{
    return array();
}

Model Events

Eloquent model 有一些事件驅動的方法,creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored,讓您可以在 model 的生命週期中自行添加一些處理。

每當一個新的物件第一次被儲存時, creating 和 created 事件會被觸發。如果一個舊有物件被儲存,那個updating/update 方法會被觸發。以上兩個例子中, saving 和 save 事件都會被觸發。

如果從 creating, updating, saving, deleting 事件回傳了 false,那該動作會被取消。

藉由事件取消一個儲存操作
<?php

User::creating(function($user)
{
    if ( ! $user->isValid()) return false;
});

Eloquent model 同時包含了一個靜態方法 boot(),boot() 方法提供了一個方便的空間讓您註冊事件綁定。

設置一個 model 的 Boot 方法
<?php

class User extends Eloquent {

    public static function boot()
    {
        parent::boot();

        // Setup event bindings...
    }

}

Model Observers

為了統一管理 model 的事件,您可以註冊一個 model觀察者( model observer )。一個 Observer 類別會有許多方法對應不同的 model 事件。舉例來說, creating,updating,saving 方法存在某個 observer 中,或是其他 model 中的事件名稱。

如下,一個 model observer 可能如下:

<?php

class UserObserver {

    public function saving($model)
    {
        //
    }

    public function saved($model)
    {
        //
    }

}

您也可以註冊一個 observer實體來使用 observer方法:

<?php

User::observe(new UserObserver);

Converting To Arrays / JSON

當建立 JSON API時,您可能常常需要將您的 model 或是關連model 轉換成陣列或是JSON。將 model 或是其關連轉換成陣列,您可以使用 toArray()方法:

將一個 model 轉換成陣列
<?php

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

return $user->toArray();

整個 model 的 collection 也可以被轉換為陣列

<?php

return User::all()->toArray();

將 model 轉換成 JSON,可以使用 toJson()方法:

將 model 轉換成 JSON
<?php

return User::find(1)->toJson();

如果 model 或是 collection 被預測成一個字串,它最後將會被轉成JSON輸出。這意味著您可以直接從 route 回傳 Eloquent 物件。

從 Route 回傳一個 Model
<?php

Route::get('users', function()
{
    return User::all();
});

有時候您可會希望限制某些屬性包含在 model 陣列或是 JSON 格式中,例如密碼這種敏感的資料。可以透過在 model 中增加 hidden 屬性來實現:

<?php

class User extends Eloquent {

    protected $hidden = array('password');

}

注意: 如果隱藏的是關連model 的屬性,要使用關連的方法名稱,而不是(Dynamic accessor)動態取得器的名稱。( 其實兩者大部分情況下應該都一樣吧? )

當然,您也可以使用 visible 屬性定義一份白名單:

<?php

protected $visible = array('first_name', 'last_name');

有時候您可能會需要加入一個和資料庫欄位名稱不一致的屬性。

<?php

public function getIsAdminAttribute()
{
    return $this->attributes['admin'] == 'yes';
}

一旦您建立了 accessor,在 model 中將該值加入append 屬性裡。

<?php

protected $appends = array('is_admin');

一旦該屬性被加入 appends 表單中,model array 和 JSON 都將會包含該屬性。

本篇回顧

非常非常多內容的一篇,終於翻完了好感動。( 好像還有三篇 orz )

許多內容我並沒有實際去手動實驗驗證,可能有些地方我的理解是錯的,不過就先暫時這樣了,之後開始實作再回來改正。Eloquent 沒想到東西這們多,看的好累...,不過和Doctrine比還是好多了。

參考資料

Laravel Eloquent ORM: http://laravel.com/docs/eloquent#eager-loading

Comments

comments powered by Disqus