Laravel-Eloquent ORM(上)

Introduction

Eloquent ORM 為您的database操作提供了一個優雅且簡單的 ActiveRecord 的實作。每個 database 的 table 有一個對應的 "Model",這個 Model 會被用來和 table 互動。

在我們開始之前,請確認在 app/config/database.php 裡有先設定好一個資料庫的連結。

Basic Usage

首先,新增一個 Eloquent Model。Models 一般來說存放於 app/models 的目錄中,但和 controller一樣,您可以將它存放於任何您希望的位置,只要確保檔案能夠根據 composer.json 定義的路徑自動讀取到它們。

定義一個Eloquent Model

<?php

class User extends Eloquent {}

注意,我們目前還沒告訴 Eloquent User 是和哪個 table 對應。如果沒有特別指定的話,小寫複數型態的 Model 類別名稱會被當做對應的 table 名稱。所以在這個例子中,User Model 對應的是 users table。您可以藉由定義一個table屬性自行指定一個對應的 table 名稱:

<?php

class User extends Eloquent {

    protected $table = 'my_users';

}

注意: Eloquent 會假設每個 table 都有一個欄位名稱為 id 的主鍵,您可以定義一個 primaryKey 屬性去覆寫這個轉換規則。除此之外,您也可以定義一個 connection 屬性去覆寫使用 model 時的資料庫連結名稱。

取得所有的 Models
<?php

$users = User::all();
根據主鍵取得一筆記錄
<?php

$user = User::find(1);

var_dump($user->name);

注意: 所有在 Query Builder 可以使用的方法,在Eloquent models 也可以使用!!

藉由主鍵取得一個 Model 或是丟出一個 exception

有時候您可能會希望在找不到 model 時丟出一個 exception,可以使用如下方法:

<?php

$model = User::findOrFail(1);

$model = User::where('votes', '>', 100)->firstOrFail();

註冊一個 error handler,並且監聽 ModelNotFoundException

<?php

use Illuminate\Database\Eloquent\ModelNotFoundException;

App::error(function(ModelNotFoundException $e)
{
    return Response::make('Not Found', 404);
});

使用 Eloquent 執行 Query
<?php

$users = User::where('votes', '>', 100)->take(10)->get();

foreach ($users as $user)
{
    var_dump($user->name);
}

當然,您也可以使用Query Builder 的統計函式。

Eloquent 統計
<?php

$count = User::where('votes', '>', 100)->count();

如果您無法藉由該介面產生您要的 query,可以使用 whereRaw()方法:

<?php

$users = User::whereRaw('age > ? and votes = 100', array(25))->get();

將結果切塊

如果您需要處理非常大量的 Eloquent 記錄,使用 chunk()方法將結果將成許多小塊可以避免您的暫存記憶體被耗盡:

<?php

User::chunk(200, function($users)
{
    foreach ($users as $user)
    {
        //
    }
});

第一個參數是設定每個切塊包含的記錄數量,而第二個閉包參數則於每次從資料庫抓取切塊時呼叫。

指定 Query 使用的連結

您可以在執行 Eloquent Query 時指定要使用哪個資料庫連結:

<?php

$user = User::on('connection-name')->find(1);

Mass Assignment

當新增一個 model 時,我們會傳遞一個屬性的陣列給 model 的建構子,而該陣列將會對 model內對應的屬性賦值。如下:

<?php

$user = new User;
$user->fill(array('name'=>'Jordan', 'email'=>'jordan@hotmail.com'));
$user->save();

這相當方便。然而,如果盲目的將使用者提供的數值送入 model,讓使用者可以任意修改 model 的屬性,可能會造成嚴重的安全問題,因此所有的 Eloquent Model 預設都會防止 Mass Assignment。

fillable屬性可以指定哪些屬性是Mass-assignable,可以在類別或是實體設定。

在 Model 定義一個 Fillable 屬性
<?php

class User extends Eloquent {

    protected $fillable = array('first_name', 'last_name', 'email');

}

在這個例子中,只有陣列中的三個屬性是Mass-assignable。fillable 的相反是 guarded。

在 Model 定義一個 Guarded 屬性
<?php

class User extends Eloquent {

    protected $guarded = array('id', 'password');

}

在上面的例子中, id 和 password 無法被 mass assignment。其他的屬性則接可以被mass assignment。您也可以直接使用guard屬性限制任何屬性都不可被mass assignment。

<?php

protected $guarded = array('*');

Insert, Update, Delete

透過新增一個 Model 實體並且呼叫 save()方法,從Model 中新增一筆記錄進入資料庫:

<?php

$user = new User;

$user->name = 'John';

$user->save();

注意: 一般來說,Eloquent Model 會有一個自動累加的鍵,然而,如果您想要指定一個自行定義的鍵,可以把 incrementing 屬性設置為 false。

您也可以使用 create() 方法儲存一個新增的 model。該方法最後會回傳新增的 Model 之實體。然而,在這們做之前,您需要指定 fillable 或是 guarded 屬性,否則 Eloquent 會自動防止所有的 mass-assignment。

在使用自動累加ID 新增或儲存完一個 Model 後,您可以藉由 id 屬性取得 ID。

<?php

$insertedId = $user->id;
在 Model 上設定 Guarded屬性
<?php

class User extends Eloquent {

    protected $guarded = array('id', 'account_id');

}
使用 Model 的 create() 方法
<?php

// Create a new user in the database...
$user = User::create(array('name' => 'John'));

// Retrieve the user by the attributes, or create it if it doesn't exist...
$user = User::firstOrCreate(array('name' => 'John'));

// Retrieve the user by the attributes, or instantiate a new instance...
$user = User::firstOrNew(array('name' => 'John'));

要更新一個 Model,您可以先取得它,在重新設置它的屬性,最後儲存。

更新一個取得的 Model
<?php

$user = User::find(1);

$user->email = 'john@foo.com';

$user->save();

有時候我們不只想要儲存一個 Model,同時也想要將其關連的 model 也一起儲存。這是可以使用 push()方法:

<?php

$user->push();

您也可以一次對某範圍的 models 執行 update():

<?php

$affectedRows = User::where('votes', '>', 100)->update(array('status' => 2));

刪除一個已經存在的 Model
<?php

$user = User::find(1);

$user->delete();

根據鍵值刪除一個已經存在的 Model
<?php

User::destroy(1);

User::destroy(array(1, 2, 3));

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

當然,也可以一次刪除多個 Model

<?php

$affectedRows = User::where('votes', '>', 100)->delete();

如果您僅只是想更新 model 上的 timestamp,可以使用 touch()方法:

<?php

$user->touch();

Soft Deleting

當您軟刪除( sofe delete )一個 model時,其實並沒有真正的將資料從database刪除。取而代之的是,一個 deleted_at 的timestamp 會被設置在該筆記錄上。設置 softDelete屬性讓 model 允許使用軟刪除。

<?php

class User extends Eloquent {

    protected $softDelete = true;

}

要在表格增加一個 deleted_at欄位,您可以在 migration 使用 softDeletes()方法:

<?php

$table->softDeletes();

之後,當您在 model 上使用 delete()方法時,deleted_at欄位得值會被設置為現在的時間。

被軟刪除的 model 不會包含在之後query 的結果中,必需使用 withTrashed()方法取得:

強制被軟刪除的 model 可以在 query 的結果中取得
<?php

$users = User::withTrashed()->where('account_id', 1)->get();

withTrashed() 方法也可以用在已經定義的關聯 model 上:

<?php

$user->posts()->withTrashed()->get();

如果您只想要取得被軟刪除的 model,可以使用 onlyTrashed()方法:

<?php

$users = User::onlyTrashed()->where('account_id', 1)->get();

使用 restore()方法將被軟刪除的 model 復原。

<?php

$user->restore();

也可以將 restore()方法在 query 上使用:

<?php

User::withTrashed()->where('account_id', 1)->restore();

和 withTrashed()方法一樣, restore()方法也可以用在關連 model 上:

<?php

$user->posts()->restore();

如果您想要將 model 真的從 database移除,可以使用 forceDelete()方法。

<?php

$user->forceDelete();

forceDelete()方法也可以使用在關連的 model 上:

<?php

$user->posts()->forceDelete();

判斷一個取得的實體是否已經被軟刪除,可以使用 trashed()方法:

<?php

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

Timestamps

只要您有加上created_at 和 updated_at 這些 timestamp 欄位, Eloquent 預設會自動幫我們維護它們。如果您不希望 Eloquent 自動幫我們維護這些欄位,可以在 model 加入以下屬性:

取消自動 Timestamps
<?php

class User extends Eloquent {

    protected $table = 'users';

    public $timestamps = false;

}

如果您希望客製化您的 timestamp 格式,可以在您的 model 裡覆寫 getDateFormat()方法。

<?php

class User extends Eloquent {

    protected function getDateFormat()
    {
        return 'U';
    }

}

Query Scopes

Scopes 讓我們可以重複使用 model 裡面的 query 邏輯。要定義一個 scope,只要簡單的在 model 方法加上一個前綴"scope"。

定義一個 Query Scope
<?php

class User extends Eloquent {

    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    public function scopeWomen($query)
    {
        return $query->whereGender('W');
    }

}

使用一個 Query Scope
<?php

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

動態 Scopes

有時候您會需要定義一個可以接受參數的 scope。只要很單純的增加一個變數到 scope function 即可:

<?php

class User extends Eloquent {

    public function scopeOfType($query, $type)
    {
        return $query->whereType($type);
    }

}

` 使用如下:
<?php

$users = User::ofType('member')->get();

Relationships

當然,您的資料庫表格很可能會和其他的相關。Eloquent 可以讓我們很簡單的去處理這些關連。Laravel 支援許多種類的關連。

- One To One
- One To Many
- Many To Many
- Has Many Through
- Polymorphic Relations
- Many To Many Polymorphic Relations
One to One

舉例來說,一個 User model 會有一個 Phone Model。我們可以在 Eloquent 定義這些關係。

定義一個 1對1關係
<?php

class User extends Eloquent {

    public function phone()
    {
        return $this->hasOne('Phone');
    }

}

傳入 hasOne()方法的第一個參數是關連 model 的名稱。一旦關連被建立,我們便可以使用 Eloquent 的動態屬性取得關連 model。

<?php

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

實際執行的SQL語句如下:

SELECT * FROM users WHERE id = 1

SELECT * FROM phones WHERE user_id = 1

這邊要留意的是,Eloquent 會假定關連的外鍵是基於 model 的名稱,以這個例子來說,Phone model 會假設 user_id為外鍵。如果您希望覆寫這個轉換規則,您可以傳入第二個參數給 hasOne()方法。除此之外,您也可以傳入第三個參數給這個方法,去指定要使用哪個欄位當做關連。

<?php

return $this->hasOne('Phone', 'foreign_key');

return $this->hasOne('Phone', 'foreign_key', 'local_key');
定義反向關連

使用 belongsTo()方法去定義 Phone model 的反向關連:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User');
    }

}

在上面這個例子中,Eloquent 會根據 phone table 裡的 user_id 欄位當做外鍵。如果您想要自行定義不同的欄位當外鍵,可以在 belongsTo()方法傳入第二個參數:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User', 'local_key');
    }

}

您也可以傳入第三個參數指定父表格關連欄位:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User', 'local_key', 'parent_key');
    }

}

One to Many

我們以一篇文章對應多個評論來舉例:

<?php

class Post extends Eloquent {

    public function comments()
    {
        return $this->hasMany('Comment');
    }

}


現在我們可以透過動態屬性取得 post 的 comment:

<?php

$comments = Post::find(1)->comments;

如果您想要增加限制去過濾取得的 comments,可以使用 comments()方法並且串接上條件:

<?php

$comments = Post::find(1)->comments()->where('title', '=', 'foo')->first();

同樣的,您也可以藉由傳入第二個參數給 hasMany()方法去覆寫轉換外鍵的邏輯。和 hasOne的關聯一樣,欄位可以指定如下:

<?php

return $this->hasMany('Comment', 'foreign_key');

return $this->hasMany('Comment', 'foreign_key', 'local_key');

定義 comment Model 的反向關連,可以使用 belongsTo()方法:

定義反向關連
<?php

class Comment extends Eloquent {

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

}

Many to Many

多對多關連是最複雜的關聯種類。多個使用者( users )對多個不同權限( roles )是實務上很常見的多對多關係。實現這樣的多對多關係需要三個表格: users, roles, role_user。 role_user 表格是從兩個關連 model 的名稱和照字母順序衍生出來的,該表格應該要有 user_id,role_id 兩個欄位。

我們可以使用 belongsToMany()方法定義多對多關係:

<?php

class User extends Eloquent {

    public function roles()
    {
        return $this->belongsToMany('Role');
    }

}

現在,我們可以從 User Model 中取得 roles:

<?php

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

當然,您也可以對 belongsToMany()方法傳入第二個參數去指定樞紐表格的名稱:

<?php

return $this->belongsToMany('Role', 'user_roles');

也可以對Role Model 定義反向關係:

<?php

class Role extends Eloquent {

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

}

Has Many Through

"has many through"關連提供了一個方便的方法幫助我們透過一個直接關連取得隔了一層以上關連的關連model。舉例來說,一個 Country model和許多 User model 關連,而User model 又和 Post model 關連。表格的關係如下圖:

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

如您所見,posts 表格並沒有包含一個 country_id 的欄位,但 hasManyThrough關連可以讓我們藉由 $country->posts 取得屬於某個 country下的posts。

<?php

class Country extends Eloquent {

    public function posts()
    {
        return $this->hasManyThrough('Post', 'User');
    }

}

如果您想要手動替關連指定鍵,可以把第三和第四個參數傳入方法中:

<?php

class Country extends Eloquent {

    public function posts()
    {
        return $this->hasManyThrough('Post', 'User', 'country_id', 'user_id');
    }

}

Polymorphic Relations

多型關係允許一個 model 屬於一個或多個 model 之下。舉例來說,有一個 photo model 既屬於 staff model ,也屬於 order model 之下。我們可以這樣定義他們的關聯:

<?php

class Photo extends Eloquent {

    public function imageable()
    {
        return $this->morphTo();
    }

}

class Staff extends Eloquent {

    public function photos()
    {
        return $this->morphMany('Photo', 'imageable');
    }

}

class Order extends Eloquent {

    public function photos()
    {
        return $this->morphMany('Photo', 'imageable');
    }

}

現在,不論從 staff 或是 member 都可以取得 photo 了。

取得一個多型關連
<?php

$staff = Staff::find(1);

foreach ($staff->photos as $photo)
{
    //
}

取得某個多型關連的所有者

然而,真正的"多型"魔術發生在從photo model 取得 staff 或是 order 時:

<?php

$photo = Photo::find(1);

$imageable = $photo->imageable;

Photo model 上的可視( imageable )關連會回傳 Staff 或是 Order 實體,這取決於擁有該 Photo 的是哪種 model。
為了幫助釐清,讓我們來看看多型關連在資料庫的結構:

staff
    id - integer
    name - string

orders
    id - integer
    price - integer

photos
    id - integer
    path - string
    imageable_id - integer
    imageable_type - string

特別注意 表格Photo 上的 imageable_id 以及 imageable_type 兩個欄位。id 即為對應的 id 值,而type是對應 model 的類別名稱。在取得可視關連時,這讓 ORM 能夠判斷應該回傳哪種 model。

多對多多型關連

除了傳統的多型關連之外,您也可以指定多對多的多型關連。舉例來說,部落格的 Post 和 Video模組分享一個到 Tag model 的多型關連。首先,來看看資料庫結構:

多型多對多關連表格結構
posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

接著,我們準備來設置 model 的關聯。Post 和 Video 模組都有一個透過 tags()方法的 morphToMany 關連。

<?php

class Post extends Eloquent {

    public function tags()
    {
        return $this->morphToMany('Tag', 'taggable');
    }

}

tag model 可以對每個關連定義分別方法:

<?php

class Tag extends Eloquent {

    public function posts()
    {
        return $this->morphedByMany('Post', 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany('Video', 'taggable');
    }

}

Querying Relations

當取得某個 model 的紀錄時,您可能會希望根據某個關連來限制取得的結果。舉例來說,您希望取得所有至少有一則評論的 Post。要做這點,您可以使用 has()方法:

Select資料時詢問關連
<?php

$posts = Post::has('comments')->get();

您也可以指定一個比較子和計數

<?php

$posts = Post::has('comments', '>=', 3)->get();

如果還需要更強大的功能,可以使用 whereHas 以及 orWhereHas 兩個方法,將 where 條件結合 has:

<?php

$posts = Post::whereHas('comments', function($q)
{
    $q->where('content', 'like', 'foo%');

})->get();

動態屬性( Dynamic Properties )

Eloquent允許您透過動態屬性取得關連。Eloquent 會自動的為您讀取關連,甚至聰明到可以判斷應該使用 get( 1對多關連時使用 ) 或是 first( 1對1關連時使用 )。他們將可以在動態屬性以和關連模組相同的名稱被取得。例如:

<?php

class Phone extends Eloquent {

    public function user()
    {
        return $this->belongsTo('User');
    }

}

$phone = Phone::find(1);

原本這樣的呼叫:

<?php

echo $phone->user()->first()->email;

可以改成

<?php

echo $phone->user->email;

注意: 如果關連回傳的實體為兩個以上的話,將會回傳一個 Illuminate\Database\Eloquent\Collection 的實體。

本篇回顧

這篇內容真的好多啊,整整花了我一天,比預期還要多很多的時間( 怎們跟公司交代啊Orz )。熬夜工作無妨,熬夜看書就真的毫無效率可言,明天再繼續 Eloquent ORM 的下半部吧。假日看來也要繼續趕工了。

參考資料

Laravel-Eloqunet ORM :http://laravel.com/docs/eloquent

Comments

comments powered by Disqus