Model

第一節

何謂model?

Model用於將企業邏輯與使用者介面邏輯分離,使應用程式各部分的邏輯獨立。 Model通常是一個資料庫存取點,更精確的說,Model相當於是一個資料表。 CakePHP將Model的名字預設為資料表名稱的單數型,比如名為'User'的Model,使用的資料表名為'users'。 Model也可含有資料檢驗的規則、關聯的資訊與資料表特有的方法。 這兒列出Cake中User model應該有的長像。

User Model範例, 存放於 /app/models/user.php

<?php

//AppModel具有所有Cake的Model具有的功能

class User extends AppModel
{
    // 包含這個變數是個好習慣
    var $name = 'User';

    // 這兒使用了資料檢驗。請參考"資料檢驗"一章
    var $validate = array();

    // 這兒定義了資料表間的關聯。詳細資料請見第四節。

    var $hasMany = array('Image' =>
                   array('className' => 'Image')
                   );

    // 這是自己的函式:
    function makeInactive($uid)
    {
        // 自訂的邏輯...
    }
}

?>

第二節

Model 函式

從PHP的角度看來,Model為繼承自AppModel的類別。AppModel類別的原始定義放在cake/ 目錄內。如果想定義自己的AppModel,可以把它放在app/app_model.php內。自訂的AppModel可以實作多數Model可能共用的方法。AppModel本身繼承自Model類別,在標準Cake函式庫中,被定義於cake/libs/model.php

這節我們使用的是Cake的Model類別中最常使用的函式,可以到http://api.cakephp.org 取得完整的資料。

使用者自訂函式

下面的範例幫資料表定義了專用的方法,功能是隱藏與顯示blog內的文章內容。

Model 函式範例

<?php
class Post extends AppModel
{
   var $name = 'Post';

   function hide ($id=null)
   {
      if ($id)
      {
          $this->id = $id;
          $this->saveField('hidden', '1');
      }
   }

   function unhide ($id=null)
   {
      if ($id)
      {
          $this->id = $id;
          $this->saveField('hidden', '0');
      }
   }
}
?>

撈出資料

下面是使用model取得資料的標準方法:

  • findAll
  • string $conditions
  • array $fields
  • string $order
  • int $limit
  • int $page
  • int $recursive

傳回條件符合$conditions的所有資料的$fields欄位值,以$order指定的欄位排序,最多傳回 $limit 筆, 由第$page頁開始列(預設由第一頁開始)。 $conditions格式與SQL裡定義的相同,例如:$conditions = "race = 'wookie' AND thermal_detonators > 3"。

當$recursive被設成比1大的整數時,findAll()會繼續向有關聯的Model找下去,層數由$recursive指定。 若某個資料表和另一個資料表相關,另一個資料表又和另一個資料表相關,findAll()就會把這堆相關的資料,一層層撈出。

  • find
  • string $conditions
  • array $fields
  • string $order
  • int $recursive

傳回第一筆符合條件的資料中的$fields欄位值。如果沒指定欄位,就全部傳回。

$recursive設成1到3的整數時可以叫find()繼續向關聯的Model找下去,層數最多3層。

  • findAllBy<fieldName>
  • string $value

filedName可以擺放任何欄位名稱。 這堆神奇的函式是以單一欄位值檢索的捷徑。 只要把想要查詢的欄位放在fieldName的位置,把值放在$value ,即可傳回符合條件的值。 下面有幾個例子:

$this->Post->findByTitle('My First Blog Post');
$this->Author->findByLastName('Rogers');
$this->Property->findAllByState('AZ');
$this->Specimen->findAllByKingdom('Animalia');

傳回值是一組陣列,格式就和find()或findAll()傳回的格式相同。

  • findNeighbours
  • string $conditions
  • array $field
  • string $value

查出符合$field=$value條件的上一筆和下一筆資料內的$field欄位值。$conditions用於濾除不要的資料。

這個函式在需要取得上一筆和下一筆資料時非常有用,可用來一一檢索Model內的內容。指定的欄位資料型態只能是數字或日期。

class ImagesController extends AppController
{
    function view($id)
    {
        // 假設我們要顯示圖片...

        $this->set('image', $this->Image->find("id = $id");

        // 但同時也想要上一個和下一個圖片...

        $this->set('neighbours', $this->Image->findNeighbours(null, 'id', $id);

    }
}

在這個範例中,首先我們會得到整個 $image['Image'] 陣列,接下來得到$neighbours['prev']['Image']['id'] 與 $neighbours['next']['Image']['id']二個值。

  • field
  • string $name
  • string $conditions
  • string $order

查出條件合乎$conditions的資料,依$order指定的欄位排序,傳回$name欄位的值。

  • findCount
  • string $conditions

計算符合條件的資料筆數。

  • generateList
  • string $conditions
  • string $order
  • int $limit
  • string $keyPath
  • string $valuePath

此函式是取得多組鍵/值組合最簡便的方法-在利用查詢結果建立HTML時,特別有用。 $conditions, $order, 和 $limit 的使用方法和findAll一樣。 $keyPath 與 $valuePath 則是告訴model要拿什麼建立鍵和值的配對。 如果要用Role model的role_name為值,id為鍵,呼叫方法就如同下面所示:

$this->set(
    'Roles',
    $this->Role->generateList(null, 'role_name ASC', null, '{n}.Role.id', '{n}.Role.role_name')
);

// 傳回值像這樣:
array(
    '1' => 'Account Manager',
    '2' => 'Account Viewer',
    '3' => 'System Manager',
    '4' => 'Site Visitor'
);
  • read
  • string $fields
  • string $id

用此函式取回目前所指那筆記錄的欄位值,也可以由$id指定。

請注意一點,read()只取第一層的資料,無視於model中的$recursive。若想取得更深層的資料,請用find()或findAll().

  • query
  • string $query
  • execute
  • string $query

如果想要執行SQL指令,可使用Model的query()或execute()其中之一。二者的差別在於query()的SQL指令是SQL查詢(需要傳回查詢結果的SQL指令);execute()的SQL指令則是SQL命令(不需要傳回查詢結果的SQL指令)。

使用query()執行SQL指令

<?php
class Post extends AppModel
{
    var $name = 'Post';

    function posterFirstName()
    {
        $ret = $this->query("SELECT first_name FROM posters_table
                                 WHERE poster_id = 1");
        $firstName = $ret[0]['first_name'];
        return $firstName;
    }
}
?>

複雜的查詢條件(使用陣列)

Model裡的查詢函式指定查詢條件的方法很多,其中最簡單的是使用SQL的WHERE子句。 如果需要更複雜的條件控制,則可使用陣列。 陣列讓程式更容易閱讀也更容易建立查詢,使用的語法同時將查詢條件裡各元素(欄位、值、運算子等)分離,便於操作。 如此一來不僅能讓Cake查詢動作更具效能,更能確定SQL語法的語意是否合於需求。

最基本的陣列式查詢看起來就像這樣:

使用陣列查詢條件的基本範例:

$conditions = array("Post.title" => "This is a post");

// Model內使用範例
$this->Post->find($conditions);

這個結構一看就知道他的意思是:找到所有標題是"This is a post"的文章。 這裡可以注意一點,我們可以只輸入"title"指定欄位名稱。 但實際要建立查詢指令時,最好連Model名稱一起指定。 如此可以讓程式碼更清析,也可以避免未來發生不相容的問題。

那麼別的條件查詢起來又如何呢?一樣容易。 假設我們要找所有標題不是"This is a post"的文章:

array("Post.title" => "<> This is a post")

只要在表示式前加上'<>'就全部完成。 Cake看得懂SQL裡所有合法的運算元,包括比較用的像LIKE、BETWEEN或REGEX,只要在運算元和運算式(或值)間留個空白。 其中一個例外是IN(...)的比對。 假設我們想要找到標題等於三個之一的文章,程式得這麼寫:

array("Post.title" => array("First post", "Second post", "Third post"))

加入更多的條件就和在陣列中加入鍵/值對一樣容易:

array
(     "Post.title" => array("First post", "Second post", "Third post"),
    "Post.created" => "> " . date('Y-m-d', strtotime("-2 weeks"))
)

Cake預設動作會把所有條件以AND結合: 上頭的條件表示把建立時間在這二星期內,且標題在三個其中之一的文章找出來。 如果想要改變這個預設行為也很容易:

array
("or" =>
    array
    (
        "Post.title" => array("First post", "Second post", "Third post"),
        "Post.created" => "> " . date('Y-m-d', strtotime("-2 weeks"))
    )
)

Cake接受所有SQL裡可以使用的布林運算元,如AND,OR,NOT,XOR等,同時不分大小寫。 查詢條件也可以無限地串連。 例如在Posts和Authors間有hasMany/belongsTo的關係,就表示要在Post上使用LEFT JOIN。 再例如想找到Bob所發表的文章,且內容含有某關建字或發表日期在最近二星期內:

array
("Author.name" => "Bob", "or" => array
    (
        "Post.title" => "LIKE %magic%",
        "Post.created" => "> " . date('Y-m-d', strtotime("-2 weeks")
    )
)

儲存資料

想要把資料存入Model中,必須提供要存的資料內容,透過save()方法儲存。 資料格式看起來像這樣:

Array
(     [ModelName] => Array
    (
        [fieldname1] => 'value'
        [fieldname2] => 'value'
    ) )

如果你想用同樣的格式把資料顯示在網頁上,最簡單的方法就是使用HTML Helper。 它會負責建立表單元件,同時以Cake想要的方式命名。 然而你也可以不使用它:只要確定表單元件的名稱格式像這樣data[Modelname][fieldname]。 $html->input('Model/fieldname')還是最容易,不是嗎?

自網頁表單上傳來的資料會自動以這種格式存放,然後被放在controller裡的$this->data變數裡。 如此一來,要把網頁上輸入的資料存起來,就變得相當容易。 Controller裡的edit()函式看起來會像這樣:

function edit($id)
{

   //註:Property model自動被載入,放在$this->Property。

   // 檢查是否有表單資料...
   if (empty($this->data))
   {
        $this->Property->id = $id;
        $this->data = $this->Property->read();// 產生表單欄位
   }
   else
   {
      // 試著儲存資料,save()函式會自動進行資料檢驗
      if ($this->Property->save($this->data['Property']))
      {
         // 快速顯示一段訊息,然後重新導向
         $this->flash('Your information has been saved.',
                     '/properties/view/'.$this->data['Property']['id'], 2);
      }
      // 如果發現有任何錯誤,處理的程式碼放這兒
   }
}

注意一下儲存動作被放在條件式中:當我們企圖存放資料時,Cake會自動使用Model內設定的方法檢驗資料的合理性。 如果想知道更多關於資料檢驗的內容,請參考"資料檢驗"一章。 如果資料存入前不需要檢驗資料,可以這麼呼叫save($data, false)

另一個有用的函式是:

  • del
  • string $id
  • boolean $cascade

刪除$id指定的資料,或刪除model目前所指的資料。

如果這個model和別的model相關聯(關聯陣列中設有相依的鍵),那麼當$cascade被設成true時,這個方法也會同時把關聯model中的資料刪除。

成功刪除時,傳回true。

  • saveField
  • string $name
  • string $value

用來儲存單一欄位的資料。

  • getLastInsertId

傳回最新建立的資料的ID。

Model的回呼函式

我們幫Model加了一些回呼函式,讓您在Model做任何動作之前與之後,有機會額外做一些事。要 在您的應用程式內加入這樣的功能,只要在Cake model的子類別內重新定義這些函式即可。

  • beforeFind
  • string $conditions

beforeFind()回呼函式會在查詢動作執行前被呼叫,請在此放入任何詢查前想做的事。 當您回呼函式內因為某些因素而不想讓查詢動作繼續,可以傳回false,查詢動作會在函式結束後也一併中止; 相反的,如果一切正常,也記得傳回true,否則不會有任何結果傳回。

  • afterFind
  • array $results

透過這個回呼函式修改查詢結果,或放入任何查詢後想做的事。 這函式的參數就是查詢的結果,傳回值則是修改後的資料。

  • beforeValidate

這回呼函式內放著檢驗資料前要做的事。 在資料被檢驗之前可以修改資料內容。 也可以使用model的data變數($this->data)取得資料,一一檢驗內容是否合理, 再配合Model::invalidate()便可設計更複雜的檢驗邏輯。 函式傳回false時,save()函式會中斷執行。

  • beforeSave

這回呼函式內放著儲存前要做的事。 這函式會在資料確認合理後立即被執行,接下來資料才會被儲存(如果資料被認為不合理,save()就會中斷,這個回呼函式當然也就不會被執行)。 同樣的,如果你想讓儲存動作繼續,就讓函式傳回true,否則傳回false

下面的用法是在資料儲存前,依不同的資料庫引擎更改不同的時間格式:

// 由HTML Helper建立Date/time 欄位:
// 這段程式會被放在view裡

$html->dayOptionTag('Event/start');
$html->monthOptionTag('Event/start');
$html->yearOptionTag('Event/start');
$html->hourOptionTag('Event/start');
$html->minuteOptionTag('Event/start');

/*=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-*/

// Model的回呼函式同時轉換時間與資料,以便儲存
// 這段程式碼可以在Event model中找到:

function beforeSave()
{
    $this->data['Event']['start'] = $this->_getDate('Event', 'start');

    return true;
}

function _getDate($model, $field)
{
    return date('Y-m-d H:i:s', mktime(
        intval($this->data[$model][$field . '_hour']),
        intval($this->data[$model][$field . '_min']),
        null,
        intval($this->data[$model][$field . '_month']),
        intval($this->data[$model][$field . '_day']),
        intval($this->data[$model][$field . '_year'])));
}
  • afterSave

這裡放的是儲存動作結束後想做的事。

  • beforeDelete

這裡放的是資料刪除前要做的事。傳回false可以中止刪除動作,傳回true則會繼續。

  • afterDelete

把資料刪除後想做這事放在這個回呼函式中。

第三節

Model的變數

在建立model時,我們可以設一些特殊的變數,啟用Cake的某些功能:

$primaryKey

如果這個model的對應資料表,主鍵不叫'id',就必須使用這個變數告訴Cake主鍵的名稱。

$recursive

這個值可以指定Cake在執行find()和findAll()時向內尋找的層數。 想像一下有很多小組(Groups),每個小組內有很多位成員(Users),每位成員擁有很多文章(Articles),則不同的設定會有以下不同的結果:

Model::recursive options

$recursive = 0 Cake只撈出小組資料就結束了
$recursive = 1 Cake撈出小組與小組內的成員資料
$recursive = 2 Cake 不但撈出小組、組內的成員,也找到成員所發表的文章

$transactional

告訴Cake是否啟用交易(就是begin/commit/rollback)。設定值為true或false。此變數只有在資料庫支援交易時才有用。

$useTable

如果model使用的資料表名稱不是model名稱的複數型(加s),而你又不想改變資料表的名稱,這個變數能設定model使用的資料表。

$validate

用來檢驗傳入model的資料的陣列,請參考"資料檢驗"一章。

$useDbConfig

記得一開始我們在/app/config/database.php設定資料庫連接的資料嗎?用這個變數可以選擇要使用那一組設定- 先在資料庫設定檔內做好幾組不同的設定,再把這個變數設定成欲使用設定的名稱即可。預設會使用的正是你所知的'default'。

第四節

關聯

引言

CakePHP具有一項強而有力的特色,就是model所提供的相關性對應。 CakePHP中資料表透過關聯來連結。 關聯是邏輯相關的各單元間的黏著劑。

CakePHP含有四種關聯:

  1. hasOne

  2. hasMany

  3. belongsTo

  4. hasAndBelongsToMany

當model間定義了關聯設定,Cake會自動由資料表內撈出資料。 舉個例來說,如果有個Post model使用hasMany關聯與Author model連結。 當呼叫controller中的$this->Post->findAll()時,會同時撈出Post與相關的Author資料。

如果想正確的使用關聯,最好能尊守CakePHP的命名規則(參考附錄"命名規則")。 正確地使用CakePHP命名規則,即可啟用Scafold機制將資料以視覺元件顯示出來,因為Scafold機制會偵測並使用model間的關聯。 當然你也可以不透過Cake的命名規則,以自己的方法建立model間的關聯,方法我們稍後再介紹,目前先以命名規則的方法來建立關聯。 現在要介紹資料表名稱、model名稱和外部鍵的命名規則。

下面列出來的是Cake期待這幾個元素應該有的名稱:

  1. 資料表名稱: [代表物件的複數]。例如我們想存blog的文章(post)和文章的作者(author),資料表名稱就定為posts和authors。

  2. Model名稱: [資料表名稱的單數]。 "posts"資料表對應的model名稱就是"Post",而"authors"資料表對應的model名稱就叫"Author"。

  3. 外部鍵: [model名稱的單數]_id。 例如,"authors"資料表內含一個外部鍵指向"posts"資料表,這個外部鍵就叫"post_id".

CakePHP的Scafold機制把欄位順序的視為關聯的順序。 例如,有個Article資料表關聯於Author,Editor和Publisher三個model , 需要三個外部鍵:author_id,,editor_id與publisher_id。 Scafold機制就會自動以三個欄位在資料表中順序建立關聯,第一是Author,第二是Editor,最後是Publisher。

為了要顯示這些關聯的運作情形,我們繼續使用blog程式當範例。 整個情境是這樣的,現在我們想要建立一個簡單的使用者管理系統,這麼做當然是為了持續追蹤使用者。 我們希望讓每個使用者有一組資料設定(User hasOne Profile),同時也可以發表很多自己的意見(User hasMany Comments)。 使用者系統建立後,我們又想讓文章擁有很多標籤(Post hasAndBelongsToMany Tags)。

定義與使用hasOne查詢

在操作接下來的動作前,我假設你已經事先建立好User和Profile model了。 要在他們之間建立hasOne關聯,必須在model中加入一個陣列告訴Cake他們的關聯性,看起來像這樣:

/app/models/user.php hasOne

<?php
class User extends AppModel
{
    var $name = 'User';
    var $hasOne = array('Profile' =>
                        array('className'    => 'Profile',
                              'conditions'   => '',
                              'order'        => '',
                              'dependent'    =>  true,
                              'foreignKey'   => 'user_id'
                        )
                  );
}
?>

hasOne陣列正是Cake用來建立User和Profile二個model之間關聯的關鍵。陣列中的每個設定可以讓您更仔細地設計關聯性:

  1. className (必須):相要建立關聯的model名稱。

    以我們的例子來說,就設定成'Profile'。

  2. conditions:用SQL語法陳述此關係成立的額外限制

    我們可以用這個設定告訴Cake只有在Profile的標題顏色為綠色時才建立關聯。 此時,只要把conditions的值設成像這樣:"Profile.header_color = 'green'"。

  3. order: 關聯的model排序的規則

    如果想讓model照某個規則排序,可以用SQL語法寫好,設定在此:例如"Profile.name ASC"。

  4. dependent:如果設定成true,則當model資料被刪除時,相關的model也會一併被刪。

    例如,如果"Cool Blue"關聯於"Bob",當我刪除"Bob"時,"Cool Blue"也會一併被刪除。

  5. foreignKey: 指向關聯model的外部鍵名稱。

    如果沒有依照Cake的命名規則建立資料表,就必須在此設定外部鍵的名稱。

現在執行User的find()或findAll()函式,可以看到Profile model也跟著被撈出相關的資料。

$user = $this->User->read(null, '25');
print_r($user);

//輸出:

Array
(
    [User] => Array
        (
            [id] => 25
            [first_name] => John
            [last_name] => Anderson
            [username] => psychic
            [password] => c4k3roxx
        )

    [Profile] => Array
        (
            [id] => 4
            [name] => Cool Blue
            [header_color] => aquamarine
            [user_id] = 25
        )
)

定義與使用belongsTo查詢

現在User可以看到它的Profile了。 接著再建立另一種關聯,讓Profile也能看到User。 這就是belongsTo在Cake中所發揮的功能。 我們接著在Profile model裡做這件事:

/app/models/profile.php belongsTo

<?php
class Profile extends AppModel
{
    var $name = 'Profile';
    var $belongsTo = array('User' =>
                           array('className'  => 'User',
                                 'conditions' => '',
                                 'order'      => '',
                                 'foreignKey' => 'user_id'
                           )
                     );
}
?>

$belongsTo陣列就是Cake用來建立User和Profile model間關聯的關鍵。 裡頭的每個選項可以用來更仔細的設計此關聯。

  1. className (必須):想要建立關聯的model名稱

    以我們的例子為例,這個值就是'User'。

  2. conditions:用SQL語法陳述此關係成立的額外限制

    用這個設定值告訴Cake只有在User啟用時,此關聯才成立。 此時這個值要設定成"User.active = '1'"或類似的樣子。

  3. order: model的排序規則

    如果想要讓model依特定的方式排序,就用SQL命定陳述規則,放在這裡,例如:"User.last_name ASC"。

  4. foreignKey:指向關聯model的外部鍵

    若沒有依Cake的命名規則建立資料表,就必須在此設定外部鍵名稱。

現在我們再執行一次Profile的file()或findAll(),就可以很明顯的看到User model也一起被查詢出相關資料:

$profile = $this->Profile->read(null, '4');
print_r($profile);

//output:

Array
(

    [Profile] => Array
        (
            [id] => 4
            [name] => Cool Blue
            [header_color] => aquamarine
            [user_id] = 25
        )

    [User] => Array
        (
            [id] => 25
            [first_name] => John
            [last_name] => Anderson
            [username] => psychic
            [password] => c4k3roxx
        )
)

定義與使用hasMany查詢

現在,已經將User和Profile二個model建立起關聯,且運作正常,接下來試著為User和Comment間建立關聯。 結果,User model看起來會像這樣:

/app/models/user.php hasMany

<?php
class User extends AppModel
{
    var $name = 'User';
    var $hasMany = array('Comment' =>
                         array('className'     => 'Comment',
                               'conditions'    => 'Comment.moderated = 1',
                               'order'         => 'Comment.created DESC',
                               'limit'         => '5',
                               'foreignKey'    => 'user_id',
                               'dependent'     => true,
                               'exclusive'     => false,
                               'finderQuery'   => ''
                         )
                  );

    // 這是我們先前建立的另一個關聯
    var $hasOne = array('Profile' =>
                        array('className'    => 'Profile',
                              'conditions'   => '',
                              'order'        => '',
                              'dependent'    =>  true,
                              'foreignKey'   => 'user_id'
                        )
                  );
}
?>

$hasMany陣列就是Cake建立User和Comment間關聯的關鍵。陣列內的每個項目可以 讓你更仔細的設計關聯:

  1. className (必須): 想要建立關聯的model名稱

    以我們的例子為例,這個值就是'Comment'。

  2. conditions: 用SQL語法陳述此關係成立的額外限制

    若想告訴Cake只找出"溫合"的意見,只要把這個值設成"Comment.moderated = 1"或類似的值。

  3. order: model的排序規則

    如果想讓關聯資料表以特定的方式排序,可以將SQL指令敍述放在這裡,例如:"Comment.created DESC"。

  4. limit:Cake從關聯資料表撈出的資料數上限

    例如,我們不想看所有人的見意,只想看5條。

  5. foreignKey: 指向關聯model的外部鍵

    若沒有依Cake的命名規則建立資料表,就必須在此設定外部鍵名稱。

  6. dependent: 如果設成true,則在這個資料被刪時,關聯的資料也會一併被刪。

    例如,假設"Cool Blue"關聯於"Bob",當我把"Bob"刪除時,"Cool Blue"也會一起被刪掉。

  7. exclusive: 如果設成true,則關聯物件被刪除前,不會呼叫beforeDelete回呼涵數。

    對簡單的關聯非常有用,可以加快執行速度。

  8. finderQuery: 用SQL指令建立關聯

    這兒可以對多個資料表進行複雜的關聯。如果Cake自動做的關聯不合你用,就在這兒自己寫一個吧。

現在執行User model的find()或findAll()看看,應該會發現相關聯的Comment model也一併被撈出資料了:

$user = $this->User->read(null, '25');
print_r($user);

//output:

Array
(
    [User] => Array
        (
            [id] => 25
            [first_name] => John
            [last_name] => Anderson
            [username] => psychic
            [password] => c4k3roxx
        )

    [Profile] => Array
        (
            [id] => 4
            [name] => Cool Blue
            [header_color] => aquamarine
            [user_id] = 25
        )

    [Comment] => Array
        (
            [0] => Array
                (
                    [id] => 247
                    [user_id] => 25
                    [body] => The hasMany assocation is nice to have.
                )

            [1] => Array
                (
                    [id] => 256
                    [user_id] => 25
                    [body] => The hasMany assocation is really nice to have.
                )

            [2] => Array
                (
                    [id] => 269
                    [user_id] => 25
                    [body] => The hasMany assocation is really, really nice to have.
                )

            [3] => Array
                (
                    [id] => 285
                    [user_id] => 25
                    [body] => The hasMany assocation is extremely nice to have.
                )

            [4] => Array
                (
                    [id] => 286
                    [user_id] => 25
                    [body] => The hasMany assocation is super nice to have.
                )

        )
)

雖然這理並沒有提及詳細的步驟,但最好同時定義"Comment bedongsTo User"的關聯,讓二個model可以互相看到。 當使用scaffold時就常常會沒有定義雙向的關聯。

定義與使用hasAndBelongsToMany

現在應該已經對這簡單的關聯機制很熟析了。 讓我們看看最後一種關聯:hasAndBelongsToMan(或簡稱HABTM)。 最後一種是最難理解的,但它也是最常用的的關聯。 當有二個資料表靠一個Join資料表連結在一起時,HABTM關聯立即發揮它的功用。 Join資料表通常只有二欄,分別關聯到二個資料表。

hasMany和hasAndBelongsToMany的相異處在於hasMany的關連性無法共享。 例如,一個使用者(User)有很多(hasMany)建議(comment),這樣的程式只能讓Comment資料與單一筆User資料產生關聯, 若用HABTM,Comment的資料可以和多筆User資料產生關聯。 馬上舉個例子看看:建立Post和Tag二個model的關聯。 如果一個Tag只能讓一個Post使用,Tag馬上就會用完,但我們不想這様,我們想要讓一個Tag可以讓多個Post使用。

為達此需求,我們必須先設計好正確的資料表。 所以,我們需要為Tag model建立"tags"資料表,為Post model建立"posts"資料表。 接下來要為他們建立一個join資料表,這是一個新的資料表, Cake對這種資料表的命名規則為[第一組model名稱的複數]_[第二組model名稱的複數],model名稱要依字母排放:

HABTM Join資料表: 範例model和他們的join資料表名稱

  1. Posts 和Tags: posts_tags

  2. Monkeys 和 IceCubes: ice_cubes_monkeys

  3. Categories 和 Articles: articles_categories

HABTM Join資料表最少需為連結的model建立二個外部鍵。以我們的例子來說就是"post_id"和"tag_id"。

要建立這三個資料表的SQL指令如下:

--
-- posts資料表的結構
--

CREATE TABLE `posts` (
`id` int(10) unsigned NOT NULL auto_increment,
`user_id` int(10) default NULL,
`title` varchar(50) default NULL,
`body` text,
`created` datetime default NULL,
`modified` datetime default NULL,
`status` tinyint(1) NOT NULL default '0',
PRIMARY KEY (`id`)
) TYPE=MyISAM;

-- --------------------------------------------------------

--
-- posts_tags資料表的結構
--

CREATE TABLE `posts_tags` (
`post_id` int(10) unsigned NOT NULL default '0',
`tag_id` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`post_id`,`tag_id`)
) TYPE=MyISAM;

-- --------------------------------------------------------

--
-- tags資料表的結構
--

CREATE TABLE `tags` (
`id` int(10) unsigned NOT NULL auto_increment,
`tag` varchar(100) default NULL,
PRIMARY KEY (`id`)
) TYPE=MyISAM;

資料表建定好後就可以在Post model內定義關聯了:

/app/models/post.php hasAndBelongsToMany

<?php
class Post extends AppModel
{
    var $name = 'Post';
    var $hasAndBelongsToMany = array('Tag' =>
                               array('className'    => 'Tag',
                                     'joinTable'    => 'posts_tags',
                                     'foreignKey'   => 'post_id',
                                     'associationForeignKey'=> 'tag_id',
                                     'conditions'   => '',
                                     'order'        => '',
                                     'limit'        => '',
                                     'unique'       => true,
                                     'finderQuery'  => '',
                                     'deleteQuery'  => '',
                               )
                               );
}
?>

$hasAndBelongsToMany陣列是Cake用來建立Post和Tag關聯的關鍵。每個項目可以用來更仔細的設定這個關聯的內容:

  1. className (必要):想要建立關聯的model名稱

    在我們這個例子中要設為'Tag'

  2. joinTable:當資料庫沒照Cake命名規則建立時,就必須在此指定資料表名稱。

  3. foreignKey:Join資料表內指向目前model的外部鍵名稱。

    當資料庫沒照Cake命名規則建立時,就必須在此指定外部鍵名稱。

  4. associationForeignKey:指向關聯資料表的外部鍵名稱。

  5. conditions: 用SQL語法陳述此關係成立的額外限制

    我們可以使用這個項目告訴Cake只找Tag資料表中有確認過的標簽籤。 這時要把這個值設定成"Tag.approved = 1"或類似的樣子。

  6. order: model的排序規則

    這個項目可以讓關聯model以特定的方式排列。得用SQL語法敍述,例如:"Tag.tag DESC"。

  7. limit: 從關聯資料表中撈出的資料量上限

    用來限制撈出的資料筆數上限。

  8. unique: 如果設成true,則忽略重復的資料

    基本上,如果關聯很明確,就可以把它設成true。 那麼"Awesomeness"就只能被指定到"Cake Model Associations"一次,只會在結果中出現一個。

  9. finderQuery: 用完整的SQL指令描述關聯

    這兒可以對多個資料表進行複雜的關聯。 如果Cake自動做的關聯不合你用,就在這兒自己寫一個吧。

  10. deleteQuery: 用來刪除HABTM model間關聯資料的完整SQL指令

    如果你不喜歡Cake的刪除行為,可以在此建立自己的方法來刪除。

當我們現在執行Post model裡的find()或findall(),應該看到Tag model同時也被撈出資料了:

$post = $this->Post->read(null, '2');
print_r($post);

//output:

Array
(
    [Post] => Array
        (
            [id] => 2
            [user_id] => 25
            [title] => Cake Model Associations
            [body] => Time saving, easy, and powerful.
            [created] => 2006-04-15 09:33:24
            [modified] => 2006-04-15 09:33:24
            [status] => 1
        )

    [Tag] => Array
        (
            [0] => Array
                (
                    [id] => 247
                    [tag] => CakePHP
                )

            [1] => Array
                (
                    [id] => 256
                    [tag] => Powerful Software
                )
        )
)

儲存相關聯的Model資料

使用關聯式model時有個重點要緊記在心: 所有相關聯的model內,只要其中一個進行儲存,其他的也會跟著儲存。 例如現在要儲存Post和關聯的Comment,想透過Post或Comment model儲存都可以。

如果相關聯的model內其中一個資料還沒建立(例如,我們想同時存入新的Post和它的註解),則必須先存主(或父)model。 舉個例子,相像一下在PostController裡有個動作是儲存新的Post和相關的Comment。 下面的動作會假設你已經發表一個單一的Post和單一個Comment。

/app/controllers/posts_controller.php (部分)

function add()
{
    if (!empty($this->data))
    {
        //我們可以存Post資料:
        //它應該會被放在$this->data['Post']
       
        $this->Post->save($this->data);

        //現在我們需要儲存Comment資料
        //但我們必須先得知剛剛存入的Post的id

        $post_id = $this->Post->getLastInsertId();

        //然後把這個資料加入欲儲存的資料中
        //並且儲存comment

        $this->data['Comment']['post_id'] = $post_id;

        //因為Post有很多(hasMany)Comments(他們有關聯),我們可以
        //透過Post model存取Comment model:

        $this->Post->Comment->save($this->data);

    }
}

然而,如果父model已經存於系統中(例如,現在要在已經存在的Post上再加一個Comment), 在儲存前需要先知道這個Post在父model內的ID值。 你可以透過URL參數(或當作表單中一個隱藏的元件)把ID傳過來。

/app/controllers/posts_controller.php (部分)

//如果用URL參數傳遞,則看起來會長這様
function addComment($post_id)
{
    if (!empty($this->data))
    {
        //你可能會覺得這麼做對$post_id的安全性不高,
        //其實這麼做只是為了做示範,夠用了..

        $this->data['Comment']['post_id'] = $post_id;

        //因為我們的Post和Comment間有hasMany的關聯,所以可以直
        //接透過Post model存取Comment model:

        $this->Post->Comment->save($this->data);
    }
}

如果透過表單中隱藏欄位傳遞資訊,那麼先幫欄位取個名字(如果是使用HtmlHelper),它就會出現在POST資料裡應該出現的地方:

如果post的ID是在$post['Post']['id']...

<?php echo $html->hidden('Comment/post_id', array('value' => $post['Post']['id'])); ?>

透過這樣的方式,Post資料的ID自動被填到$this->data['Comment']['post_id']裡, 儲存時就只需呼叫$this->Post->Comment->save($this->data)

同樣的技巧也可以用在儲存多個子model,只要把這些save()的呼叫放在迴圈中(記得使用Model::Create()清除model資訊)。

總而言之,如果要儲存相關聯的資料(對belongsTo,hasOne,和hasMany來說),就是要先取得資料在父model的ID值,再存到子model中。

儲存 hasAndBelongsToMany 的關聯資料

儲存具有hasOne、belongsTo和hasMany關聯的資料十分容易: 只要把外部鍵和ID值開放,結束時就於model裡呼叫save()即可,所有事都會自動連結好。

但如果是儲存具有hasAndBelongsToMany的資料,就有點技巧,但我們會用我們的方法讓它盡可能的簡單。 跟著我們的範例走前,必須先做一些資料表,建立Tags與Posts的關聯。 先建立一個用來新增post的表單,然後把他們關聯到一組已存在的Tag上。

或許你想建一個可以新增Tag然後動態進行關聯動作的表單。 但為了讓事情單純化,這裡只示範如果進行關聯動作,其他的留給你做。

在Cake中讓model自己儲存資料的話,tag的名字看起來會類似'Model/field_name'(如果你是使用Html Helper)。 我們直接從新增文章的表單那部分開始說:

/app/views/posts/add.thtml 新增文章的表單

<h1>Write a New Post</h1>
<table>   
    <tr>   
        <td>Title:</td> 
        <td><?php echo $html->input('Post/title')?></td>
    </tr>
    <tr>       
        <td>Body:<td>
        <td><?php echo $html->textarea('Post/body')?></td>
    </tr>
    <tr>
        <td colspan="2">
            <?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
            <?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
            <?php echo $html->submit('Save Post')?>
        </td>
    </tr>
</table>
            

這樣的表單只能新增一筆文章資料。再加上一些程式碼,可以在文章上加一個或多個標籤:

/app/views/posts/add.thtml (加入了標籤相關的程式碼)

<h1>Write a New Post</h1>
<table>
    <tr>
        <td>Title:</td>
        <td><?php echo $html->input('Post/title')?></td>
    </tr>
    <tr>
        <td>Body:</td>
        <td><?php echo $html->textarea('Post/body')?></td>
    </tr>
    <tr>
        <td>Related Tags:</td>
        <td><?php echo $html->selectTag('Tag/Tag', $tags, null, array('multiple' => 'multiple')) ?>
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <?php echo $html->hidden('Post/user_id', array('value'=>$this->controller->Session->read('User.id')))?>
            <?php echo $html->hidden('Post/status' , array('value'=>'0'))?>
            <?php echo $html->submit('Save Post')?>
        </td>
    </tr>
</table>
        

為了在呼叫controller裡的$this->Post->save()後可以把新的文章和關聯的標籤存起來, 欄位名稱要叫作"Tag/Tag"(欄位屬性會看起來像'data[ModelName][ModelName][]')。 傳回的資料必須是單獨一個ID或一組ID的陣列。因為這裡我們使用多選元件,傳回的結果會是一個ID的陣列。

$tags變數是一個陣列,內部的鍵是所有可能的標籤ID,值會在多選選單元件上顯示標籤的名稱。

使用bindModel()和unbindModel()動態改變關聯

有時候我們會想在某些狀況時動態改變資料間的關聯性。 如果你覺得model檔裡定義了太多關聯的資訊,可以用bindModel和unbindModel為下一個查詢呼叫時建立或解除關聯性。

我們先做二個model來示簵bindModel()和unbindMode()的運作狀況:

leader.php 和 follower.php

<?php

class Leader extends AppModel
{
    var $name = 'Leader';

    var $hasMany = array(
        'Follower' => array(
            'className' => 'Follower',
            'order'     => 'Follower.rank'
        )
    );
}

?>

<?php

class Follower extends AppModel
{
    var $name = 'Follower';
}

?>

在LeadersController中可以用Leader Model的find()方法找到一個領袖和他的追隨者。 如上面所示,Leader Model裡定義了一組關聯陣列,說明了"領袖有很多追隨者"。 為了示範,讓我們用unbindModel()解除這個領袖的追隨者。

leaders_controller.php (部分)

function someAction()
{
    //這裡會撈出領袖和追隨者的資料
    $this->Leader->findAll();

    //移除hasMany關聯...
    $this->Leader->unbindModel(array('hasMany' => array('Follower')));
   
    //現在再查詢一次,就只會傳回一個沒有追隨者的領袖
    $this->Leader->findAll();

    //註: unbindModel只會影響下一個查詢函式
    //接下來的查詢函式會自動恢復原始設定

    //我們已經在unbindModel()後呼叫過一次findAll(), 所以
    //這次查詢結果會再度撈出領袖和他的追隨者...
    $this->Leader->findAll();
}

將unbindModel函式用在其他種類的關聯上的方法也都類似: 只要改變關聯名稱和model的名稱就行了。unbindModel()的基本用法如下:

通用的 unbindModel() 範例

$this->Model->unbindModel(array('associationType' => array('associatedModelClassName')));

現在已經成功的動態解除關聯,接下來讓我們動態加上一個關聯。 這個看起來沒有什麼信條的領袖,需要加上一些信條。 Principle model的程式碼很簡單,裡頭什麼都沒有,只有一行指定$name的程式。 現在就動態為領袖加上一些信條(一樣只有下一個查詢有用):

leaders_controller.php (部分)

funciton anotherAction()
{
    //在leader.php的model檔中沒有在Leader和Priciple間建立hasMan關聯,
    //所以這個查詢動作只會撈出領袖資料
    $this->Leader->findAll();

    //用bindModel()在Leader和Priciple間建立hasMany的關聯
    $this->Leader->bindModel(
        array('hasMany' => array(
                'Principle' => array(
                    'className' => 'Principle'
                )
            )
        )
    );

    //現在已經建立好關聯,在下一個查詢動作中可以同時撈出領袖和
    //他的信條
    $this->Leader->findAll();
}

bindModel()可以輕易的加入新的關聯,如果想要改變排列規則或其他參數也很有用。

現在你已了解,bindModel的基本用法就是把一般的關聯陣列包在另一個鍵是關聯名稱的陣列中:

通用的bindModel()範例

$this->Model->bindModel(
        array('associationName' => array(
                'associatedModelClassName' => array(
                    // normal association keys go here...
                )
            )
        )
    );

請注意動態設定時,必須確保執行時資料是正確的。


附錄:讀者筆記

翻譯問題 

jaceju說:這裡的「涵式」 (?) 一詞應該是用「函式」。

Clar回答說:改好啦~ 謝謝

jeremy回答說: moderated 一般是指 留言需要先經過管理者同意後才會顯示出來,也就是 approve 的過程,翻譯成 溫和 可能較不適當。