CakePHP1.3.6:テーブルのジョイン(join)

ORM(Object-Relational Map、もしくは、Object-Relational Mapper) モジュールでは、エンティティー間のリレーションが定義できることができるのが、一般的だ。話が脇にそれるが、先日、会社で教育をやったらエンティティー(Entity)という言葉に違和感を覚える人が多い(私もそうだったなぁ)。PHPのコーディングに特化して考えれば、ORMでいうエンティティーはテーブルと考えればいい。

CakePHPのモデルもこの機能をもっていて、モデル間で関連を定義することで、cakeが勝手にjoinの結果を返してくれる。

今回は、これまでの顧客テーブル(customers)に追加して、新しく売上(sales)テーブルを追加して、以下のような売上一覧を作成してみる。salesテーブルのDDLは以下。

CREATE TABLE IF NOT EXISTS `sales` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `customer_id` int(11) NOT NULL COMMENT '顧客ID',
  `item_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '商品名',
  `amount` int(11) NOT NULL COMMENT '金額',
  `purchase_date` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '購入年月日',
  `memo` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'メモ',
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'タイムスタンプ',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='売上' AUTO_INCREMENT=1 ;

上の画面のデータは、salesテーブルにcustomerテーブルをLEFT JOINすれば得られる。

関係性を定義する場合にも、CakePHPの規約がある。

  • 外部キーとなるフィールド名は、アンダースコア記法で「モデル名_モデル名のキー」とする。

salesテーブルの「顧客ID」が外部キーになっていて、相手は、モデルcustomerのキーidである。この場合、customer_idがフィールド名となる。

モデルの作成

/app/modelsにsale.phpを以下のように定義する。

<?php 
class Sale extends AppModel {
	public $name = 'Sale';
	
	public $belongsTo = 'Customer'; 
	
	public $validate = array(
				'customer_id'=>array(
							'rule' => 'notEmpty',
							'message' =>'入力してください'),
				'item_name'=>array(
							'rule' => 'notEmpty',
							'message' =>'入力してください'),
				'amount'=>array(
							'rule' => 'numeric',
							'required' => true,
							'message' =>'数字を入力してください'),
				'purchase_date'=>array(
							'rule' => 'date',
							'required' => true,
							'message' =>'yyyy-mm-ddの形式で日付を入力してください')
	);
	
}
?>

レフトジョインするための定義は、

	public $belongsTo = 'Customer'; 

だけで済む。これは、外部キーの命名を規約に沿って行ったからで、そうでない場合には、以下のように細かく定義することもできる。

	public $belongsTo = array(
		"Customer"	=> array(
			'className' => 'Customer',
			'conditions' => '',
			'order' => '',
			'dependent' => false,
			'foreignKey' => 'customer_id'
		)
	);

組み込みのdateダリデータを使ってみたが、yyyy-mm-ddの形式でカレンダーの不正をチェックできる。たとえば、4月31日など入れるとエラーになって戻ってくる。便利。

データの登録

テスト用のデータを1つ登録する。
この際、scaffoldでデータを登録したのだが、外部キーが「空っぽ」のselectボックスになってしまう問題がある模様。未入力でも登録できてしまうので、適当に登録後、SQLでcustomer_idを更新した。

ビューの作成

/app/viewsにsalesディレクトリを作成し、index.ctpを作成する。

<h2>売上の一覧</h2>
<br>
商品名で絞り込みをします。
<?php 
// 絞り込み用フォーム
echo $form->create(null,array('type'=>'post','action'=>'.'));
echo $form->text('Sale.name', array('size' => 10));
echo $form->end('送信');
?>

<?php 
// ページネーション
echo $paginator->numbers(
	array(
		'before'=>$paginator->hasPrev() ? $paginator->first('<<').' ' : '',
		'after'=>$paginator->hasNext() ? ' '.$paginator->last('>>') : '',
		'modulus'=>4,
		'separator'=>' '
	)
);
?>

<table>
<tr>
	<th><?php echo $paginator->sort('ID','Sale.id') ?></th>
	<th><?php echo $paginator->sort('商品名','item_name') ?></th>
	<th><?php echo $paginator->sort('顧客名','Customer.name') ?></th>
	<th><?php echo $paginator->sort('売上金額','amount') ?></th>
	<th><?php echo $paginator->sort('売上日','purchase_date') ?></th>
	<th><?php echo $paginator->sort('更新日','Sale.timestamp') ?></th>
</tr>
<?php 
	foreach ($data as $arr){
		echo '<tr>';
		echo '<td>'.$html->link($arr['Sale']['id'],
  				array('controller'=>'sales', 'action'=>'show',$arr['Sale']['id'])).'</td>';
		echo "<td>{$arr['Sale']['item_name']}</td>";
		echo "<td>{$arr['Customer']['name']}</td>";
		echo "<td>{$arr['Sale']['amount']}</td>";
		echo "<td>{$arr['Sale']['purchase_date']}</td>";
		echo "<td>{$arr['Sale']['timestamp']}</td>";
		echo '</tr>';
	}
?>
</table>
<?php
echo $html->link('登録',
  array('controller'=>'sales', 'action'=>'add'));
?>

コントローラー

/app/controllersに、以下のようにsales_controller.phpを作成する。

<?php
require_once '../vendors/MyConverter.class.php';

class SalesController extends AppController{
	public $name = 'Sales';
	public $layout = 'myznala';
	public $uses = array('Sale','Customer');
	
	/*
	 * Paginatorの定義
	 */
	public $paginate = array(
		'page'=>1,
		'conditions'=>array(),
		'fields'=>array(),
		'order'=>array('Sale.timestamp'=>'desc'),
		'limit'=>5,
		'recursive'=>0
	);
	
	/**
	 * 
	 * 初期画面(一覧表示)
	 */
	function index(){
		$this->set('title_for_layout', "売上の一覧");
		$req=null;
		if(!empty($this->data)){
			//サニタイズ
			$req = MyConverter::getRequestParams($this->data["Sale"]);
			//絞り込みの場合には、コンディションを書き換える。
			$this->paginate['conditions'] = array('Sale.item_name like ?' => array("%{$req["name"]}%"));
		}
		$data = $this->paginate();
		
		$this->set('data',$data);
	}
	
	
}
?>

このとき、CakePHPが発行するSQLは以下のようになり、ジョインされていることがわかる。

SELECT `Sale`.`id`, `Sale`.`customer_id`, `Sale`.`item_name`, `Sale`.`amount`, `Sale`.`purchase_date`, `Sale`.`memo`, `Sale`.`timestamp`, `Customer`.`id`, `Customer`.`name`, `Customer`.`zip`, `Customer`.`address`, `Customer`.`tel`, `Customer`.`mobile`, `Customer`.`mail`, `Customer`.`memo`, `Customer`.`timestamp` FROM `sales` AS `Sale` LEFT JOIN `customers` AS `Customer` ON (`Sale`.`customer_id` = `Customer`.`id`) WHERE 1 = 1 ORDER BY `Sale`.`timestamp` desc LIMIT 5

取得されるデータをprint_rすると、以下のようになっている。

Array ( [0] => 
Array ( [Sale] => 
  Array ( 
    [id] => 1 
    [customer_id] => 1 
    [item_name] => おまんじゅう 
    [amount] => 101 
    [purchase_date] => 2010-12-01 
    [memo] => 
    [timestamp] => 2010-12-10 12:29:48 ) 
   
       [Customer] => 
  Array ( 
    [id] => 1 
    [name] => 山田太郎 
    [zip] => 000-0000 
    [address] => 東京都新宿区xxx 1-1-1 
    [tel] => 03-0000-0000 
    [mobile] => 090-0000-0000 
    [mail] => taro.yamada@localhost.localdomain 
    [memo] => 山田商事社長です。 
   [timestamp] => 2010-12-06 21:13:18 ) 
) )