[CakePHP note.] 理解 Model 關聯模式

由於 CakePHP 的模組本身就是 ORM 的一種,所以在操作上有著迅速,便利,低風險(例如 SQL Injection)爾等好處。當然也不是沒有缺點,大概就是要犧牲掉一點效能吧。原生的 SQL 語法當然可以最佳化方式很多,當然衍生的問題也多。

首先,這裡先理解 Model 的運作方式,先是在 models 資料夾中建立一個模組,我們叫他 my_test 好了。他的內容基本上是這樣:

<?php
	Class MyTest extends AppModel
	{
	    var $name = "MyTest";
	}

當我們這樣設定的時候,我們的資料庫也必須要有相對應的資料表(前輟 app_ 是我自己的設定)。

然後我們就可以直接在 Controller 中使用。

<?php
	Class HomepageController extends AppController
	{
	    var $name = "Homapage";
	    var $uses = array("MyTest");
	    function index()
	    {
	        $result = $this->MyTest->find(...);
	    }
	}

接下來要提到的是比較複雜的資料表關聯。在 CakePHP 中有幾種資料表之間的關聯模式,其實大家都一樣,就都是這幾種。

  • 一對一(hasOne)
  • 一對多(hasMany)
  • 多對一(belongsTo)
  • 多對多(hasAndBelongsToMany)

而不管是多少對多少,資料表之間一定得有個外鍵(foreignKey)來相互關聯。在 CakePHP 中預設是使用該資料加上底線 ID(_id)的方式來作為預設的外鍵。所以如果我上述的例子有這種情況就是:

<?php
	Class MyTest extends AppModel
	{
	    var $name = "MyTest";
	    /**
	     * 我要跟 YourTest 關聯,那麼在 YourTest 就必須有一個外鍵欄位,預設是 my_test_id。
	     */
	    var $hasOne = array(
	        'YourTest' => array (
	            'className' => 'YourTest'
	        )
	    );
	}
	SELECT
	`MyTest`.`id`, `MyTest`.`label`, `MyTest`.`value`, `YourTest`.`id`, `YourTest`.`my_test_id`, `YourTest`.`value` FROM `app_my_tests`
	AS `MyTest`
	LEFT JOIN `app_your_tests` AS `YourTest`
	ON (`YourTest`.`my_test_id` = `MyTest`.`id`)
	WHERE 1 = 1;

而在 CakePHP 中的 belongsTo 則剛好跟 hasOne 相反,如果我的 YourTest 模組中設定了 belongsTo 給 MyTest,則所產生出來的查詢語法會跟上面的語法相似(幾乎是一樣的,只是主從關係對調)。

	SELECT
	`MyTest`.`id`, `MyTest`.`label`, `MyTest`.`value`, `YourTest`.`id`, `YourTest`.`my_test_id`, `YourTest`.`value` FROM `app_your_tests`
	AS `YourTest`
	LEFT JOIN `app_my_tests` AS `MyTest`
	ON (`MyTest`.`id` = `YourTest`.`my_test_id`)
	WHERE 1 = 1;

那麼,所謂的 hasMany 跟 hasAndBelongsToMany 的應用呢?我們先列出一個簡單的例子:

<?php
	Class MyTest extends AppModel
	{
	    var $name = "MyTest";
	    /**
	     * 我要跟 YourTest 關聯,那麼在 YourTest 就必須有一個外鍵欄位,預設是 my_test_id。
	     */
	    var $hasOne = array(
	        'YourTest' => array (
	            'className' => 'YourTest'
	        )
	    );
	    /**
	     * 我要跟 OtherTest 關聯,那麼在 OtherTest 就必須有一個外鍵欄位,預設是 my_test_id。
	     */
	    var $hasMany = array(
	        'OtherTest' => array (
	            'className' => 'OtherTest'
	        )
	    );}

那我要怎麼使用?

<?php
	Class HomepageController extends AppController
	{
	    var $name = "Homapage";
	    var $uses = array("MyTest");
	    function index()
	    {
	        $result = $this->MyTest->find(...);
	        /**
	         * 一對多,我就可以這樣去查詢。他會自動幫你把外鍵關聯做好。
	         */
	        $result = $this->MyTest->OtherTest->find(...);
	    }
	}

請看:

<?php
	Class OtherTest extends AppModel
	{
	    var $name = "OtherTest";
	    /**
	     * 我要跟 MyTest 關聯。
	     */
	    var $belongsTo = array(
	        'MyTest' => array (
	            'className' => 'MyTest'
	        )
	    );
<?php
	Class HomepageController extends AppController
	{
	    var $name = "Homapage";
	    var $uses = array("MyTest", "OtherTest");
	    function index()
	    {
	        $result = $this->MyTest->find(...);
	        /**
	         * 倘若 OtherTest 有設定 belongsTo MyTest 的話,可以直接這樣查詢。
	         * 請留意上面的 $uses 有多了一組 OtherTest 的設定。
	         */
	        $result = $this->OtherTest->find(...);    }
	}
	SELECT
	`OtherTest`.`id`, `OtherTest`.`my_test_id`, `OtherTest`.`value`, `MyTest`.`id`, `MyTest`.`label`, `MyTest`.`value`
	FROM `app_other_tests`
	AS `OtherTest`
	LEFT JOIN `app_my_tests` AS `MyTest`
	ON (`OtherTest`.`my_test_id` = `MyTest`.`id`)
	WHERE 1 = 1;
<?php
	Class MyTest extends AppModel
	{
	    var $name = "MyTest";
	    /**
	     * 這裡做多對多查詢,子查詢是 YourTest,然後關聯 OtherTest 做條件比對。
	     */
	    var $hasAndBelongsToMany = array(
	        'YourTest' => array (
	            'className' => 'YourTest',
	            'joinTable' => 'other_tests',
	            'foreignKey' => 'my_test_id',
	            'associationForeignKey' => 'your_test_id'
	            // 後面參數就先略過了。
	        )
	    );}

首先,他會先去 MyTest 去找資料,找到之後,再去 YourTest 與 OtherTest 做關聯查詢。我把關聯查詢的部份 Query 語法貼出來給大家參考一下:

	# 其中 bf076000-81de-11e0-93d9-d8d3852f9be4 是 MyTest 查詢出來的 my_test_id 值。
	SELECT
	`YourTest`.`id`, `YourTest`.`your_test_id`, `YourTest`.`my_test_id`, `YourTest`.`value`, `OtherTest`.`id`, `OtherTest`.`other_test_id`, `OtherTest`.`my_test_id`, `OtherTest`.`value`
	FROM `app_your_tests` AS `YourTest`
	JOIN `app_other_tests` AS `OtherTest`
	ON (
	`OtherTest`.`my_test_id` = 'bf076000-81de-11e0-93d9-d8d3852f9be4'
	AND `OtherTest`.`other_test_id` = `YourTest`.`id`
	)

再解釋一次多對多的設定方法:

<?php
	    var $hasAndBelongsToMany = array(
	        /**
	         * 我的次查詢將交給 YourTest 去做。
	         */
	        'YourTest' => array (
	            'className' => 'YourTest',
	            /**
	             * 這個查詢將會加入 other_tests 這個表格(注意沒有前輟)。
	             */
	            'joinTable' => 'other_tests',
	            /**
	             * 設定加入的表格(OtherTest)的外鍵對應欄位:my_test_id
	             * 加入的表格的外鍵欄位值,必須符合第一次查詢(MyTest)傳入值。
	             * MyTest 傳入預設是使用 MyTest 的主鍵值(MyTest.id)。
	             */
	            'foreignKey' => 'my_test_id',
	            /**
	             * 設定加入的表格(OtherTest)與次查詢表格的外鍵對應欄位為:your_test_id
	             * 次查詢 YourTest 的主鍵值,需要符合加入的表格(OtherTest)的值。
	             */
	            'associationForeignKey' => 'your_test_id'
	            // 後面參數就先略過了。
	        )
	    );}

順序大致上是這樣的:

  • 我先查詢(MyTest)
  • 將主鍵值丟給最後一個資料表(OtherTest)
  • 最後一個資料表比對外鍵,並且自身與第二資料表外鍵關聯。
  • 最後取出所有資料(最後一個表會直接建立在第二資料表的輸出陣列內)。

輸出的結果大致上會像是這樣:

	Array
	(
	    [MyTest] => Array
	        (
	            [id] => bf076000-81de-11e0-93d9-d8d3852f9be4
	            [your_test_id] => 1
	            [other_test_id] => 1
	            [label] => 1
	            [value] => 1
	        )
	    [YourTest] => Array
	        (
	            [0] => Array
	                (
	                    [id] => c7a46992-81de-11e0-93d9-d8d3852f9be4
	                    [your_test_id] => 1
	                    [my_test_id] => bf076000-81de-11e0-93d9-d8d3852f9be4
	                    [value] => 1
	                    [OtherTest] => Array
	                        (
	                            [id] => b6041304-81de-11e0-93d9-d8d3852f9be4
	                            [other_test_id] => 1
	                            [my_test_id] => bf076000-81de-11e0-93d9-d8d3852f9be4
	                            [your_test_id] => c7a46992-81de-11e0-93d9-d8d3852f9be4
	                            [value] => 1
	                        )
	                )
	        )
	)

私心建議,資料結構簡單清楚明瞭,會比用複雜的查詢來得好得多。

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei