cakephp雑記帳 ITかあさん

ITかあさん

ちょっと意外?Cakephpは同一のテーブルでbindできる

Cakephpは同一のテーブルでbindできる

まさか~と思って試したところ、出来てしまいました。
一つのテーブルに、親子関係があるようなデータ構造があったとします。
例えば都道府県と市区町村を一つのテーブルで管理するareaテーブルがあったとします。

areaテーブルの例

下記の場合だと、都道府県が親、市区町村が子の関係になり、都道府県は親を持たないデータ、市区町村はそれぞれの都道府県を親に持つデータ構造になります。
この場合、子を複数持つ、hasManyの形で『たった一つのテーブルでbind』したいのです。

areaテーブルのデータの例

id name parent_id
1 東京 0
2 埼玉 0
3 千葉 0
4 神奈川 0
5 栃木 0
6 新宿 1
7 さいたま市 2
8 浦安 3
9 横浜 4
10 宇都宮 5

Cakephp hasManyでbind

親(都道府県)が、子(市区町村)をたくさん持つのですから、この場合hasManyですね。

$this->Area->bindModel(
array(
hasMany‘=>array(
Area‘=>array(
‘className’=>’Area‘,
‘conditions’=>”,
‘order’=>”,
‘foreignKey’=>’parent_id’,
)
)
),false);

hasMany bindの実行結果

実行結果は以下のようになり、単一のテーブルでも何ら問題ないhasManyで結果が返ってきます。

Array
(
    [0] => Array
        (
            [Area] => Array
                (
                    [id] => 1
                    [name] => 東京
                    [parent_id] => 0
                    [0] => Array
                        (
                            [id] => 6
                            [name] => 新宿
                            [parent_id] => 1
                        )

                )

        )
    [1] => Array
        (
            [Area] => Array
                (
                    [id] => 2
                    [name] => 埼玉
                    [parent_id] => 0
                    [0] => Array
                        (
                            [id] => 7
                            [name] => さいたま市
                            [parent_id] => 2
                        )

                )

        )
)

単一のテーブルでhasManyのSQL

bindは単一のテーブルでも可能なことは分かりました。
最後にどのようなSQLが発行されていたか確認しました。

SELECT `Area`.`id`, `Area`.`name`, `Area`.`parent_id` FROM `areas` AS `Area` WHERE `Area`.`parent_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

なるほど、INですか。

Cakephpって複合キーNGだけど、検索するくらいなら問題無い

Cakephpって複合キーNGだけど、検索するくらいなら問題無い

Cakephpは複合キーは使えないらしい。そんなことは今日の今日知りました。CakePHPerとして恥ずかしいです。

これによると、

CakePHP は複合主キーをサポートしません。 join テーブルのデータを直接操作したい場合は、直接 query を呼び出すか、通常のモデルのように振舞えるよう主キーを追加してください。

と、あります。
中には無理やりやってしまわれる方もおられるようですが、けっこう面倒なようです。

でも実は検索するくらいなら問題無い

仮にこのようテーブルがあったとします。赤字は複合プライマリーキーです。

CREATE TABLE IF NOT EXISTS `schedules` (
`workday` date NOT NULL,
`person_id` int(11) unsigned NOT NULL,
`start` datetime DEFAULT NULL,
`end` datetime DEFAULT NULL,

PRIMARY KEY (`workday`,`person_id`),
KEY `person_id` (`person_id`),
KEY `workday` (`workday`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Cakephpの規約に反したテーブルを利用する時はどうしたでしょうか?
過去にこんな記事を書いたことがあります。

その癖もありまして、modelをこのように記述していたのですが

class Schedule extends AppModel
{
var $name = 'Schedule';
function __construct() {
$id = array("id"=>false,
"table"=>"schedules",
);
}
}

これすらする必要は無く、普通のmodelを作成するときと同様でした。

$con = array(
	'limit' => 5,
	);
	$Schedule = $this->Schedule->find('all',$con);

普通に検索することが出来ました。bindして、テーブルを結合する場合も全く同様でした。
まあ、データ登録は相当厳しいんですけどね。

なお、今回試したのはCakephp1.3.14です。

Cakephp PostgreSQLでランダムページング

少し前ですが、Cakephp ランダムな pagenateを実装するという記事を書いたのですが、同じランダムなページングでも、PostgreSQLでは全く同じとはいかないようでちょっと悩んでしまいましたので、メモ的に残しておきたいと思います。
前置きはそれくらいにして、Cakephp ランダムな pagenate in PostgreSQLの実装方法です。

結論:Sessionとランダム浮動小数点数、それからseed関数

(オレとお前となんとかかんとかみたいな。)
ポイントはSessionと浮動小数点を使うこと。基本的には以前のMysqlのランダムページングと考え方は一緒ですが、PostgreSQLにはseed関数が容易されていますのでそれを使います。

1.浮動小数点数を発生させる
2.浮動小数点数をSessionに保存する。
3.Sessionに保存した乱数を使ってページング処理を実行する

以上です。Sessionの使い方についてはこちらを参考にしてください
『CakePHP』を使ってみる ~11~ セッションの使い方確認 ざ・わーるど.jp@はてな

浮動小数点数

浮動小数点数とは、-1から1の間の小数点つきの数字のことです。
今回はこれをランダムで発生させなければなりません。
ランダムな浮動小数点数の発生のさせ方はPHPマニュアルを参考にしました。
PHP: mt_getrandmax – Manual

浮動小数点数をSessionに保存するComponentの作成

浮動小数点数をSessionに保存するComponentを作成します。
前回とほとんど一緒です。ただ浮動小数点数を保存しているだけです。

class SeedComponent extends Object {
 function initialize(&$controller) {
  $this->controller =& $controller;
  }
  function seed($min = 0, $max = 1) {
  if($this->controller->Session->read('seed') == ""){
  
  $int = $min + mt_rand() / mt_getrandmax() * ($max - $min);
  
  $this->controller->Session->write('seed', $int);
  }
  
  $seed = $this->controller->Session->read('seed');
  
  return $seed;
 }
}

Sessionに保存した浮動小数点数を使ってページング処理を実行

$seed = $this->Seed->seed();//浮動小数点数
//PostgreSQL seed
$this->Model->query('select setseed('. $seed .')');
$this->paginate = array(
'conditions'=>$conditions,
'limit'=>20,
'order' => array("RANDOM()"),
);

以上です。
Cakephp1.3はSessionを利用する際はSessionコンポーネントを呼び出すことを忘れないで下さい。
PostgreSQLはseed関数があらかじめ用意されているため、やり方さえ分かっていれば、Mysqlよりもランダムページングは簡単かもしれません。

Cakephp ランダムな pagenateを実装する

Cakephp ランダムな pagenateを実装しようとすると、次のページに送った時や戻るボタンを押した時、その都度randomが実行されて、どうしても理想的なランダムなページング処理が出来ずに悩んでいましたが、今日その悩みをズバリ解決することが出来ました!x

前置きはそれくらいにして、Cakephp ランダムな pagenateの実装方法です。

結論:Sessionと乱数を使う

ポイントはSessionと乱数を使うこと。seedな考え方が必要だったのです。

1.乱数を発生させる
2.乱数をSessionに保存する。
3.Sessionに保存した乱数を使ってページング処理を実行する

以上です。Sessionの使い方についてはこちらを参考にしてください
『CakePHP』を使ってみる ~11~ セッションの使い方確認 ざ・わーるど.jp@はてな

1.乱数を発生させる

 $seed = mt_rand(0,9999);

2.発生させた乱数をSessionに保存

発生させた乱数をSessionに保存します。
Cakephp1.3ではあらかじめSessionコンポートとして呼び出す必要があります。

var $components = array('Session');

Sessionに関する処理は以下。
Sessionを呼び出して、空だったら、新たにSessionを書き込みます。

if($this->Session->read('seed') == ""){
//以下random seedの処理
$seed = mt_rand(0,9999);
$this->Session->write('seed', $int);
}
$seed = $this->Session->read('seed');

3.Sessionに保存した乱数を使ってページング処理を実行

$this->paginate = array(
'conditions'=>$con,
'order' => array("rand('$seed')"),//エラーにならないよう、外側はダブルクウォーテーション、内側シングル
'limit'=>5,
);

たったこれだけです。

Sessionが無ければ乱数を発生させてそれをSessionに保存する。
Sessionがあればその整数を継続して利用する。

Sessionが有効な間はゼロからランダムになりませんので、乱数を発生させてSessionに保存することが一番大事です。

後は保存したSessionをpagenateのrandの()の中にぶちこんであげるだけです。

一番分かりやすいのは何ページかページを送った後んに戻るボタンを押してみること。
ページを戻しても前に見た時と表示内容に変化がないと思います。

本当に乱数が発生できているかは違うブラウザを2つ立ち上げて確認してみることですね。
それぞれのブラウザが別々の結果を出力出来ていれば成功というわけです。

おまけ:CakePHP以外でランダムページャー

今回はCakephpで実装しましたが、この考え方は例えばPEAR:pagerなどでも実装可能です。
生でSQLを書いてこの処理を実装させるには以下の通り。

order by rand($seed)

ごめんなさい、本当にこれだけです。
ここでもポイントは何らかの形で乱数を必ず一定時間保存するということです。
『ちゃんとしたランダム』を実装してあげるためには、何かで一定時間Sessionを保持してあげるプラグインをかましてあげなければなりません。
発生させた乱数がリロードするごとに変わってしまうのでは、何の意味もないからです。

最後に:ページャー以外でも使ってあげるのがよい

今回のやり方はpagenateのために考えたわけですが、
ランダムって、通常のORDER BY id DESCやASCに比べると取得時間に大幅な時間がかかります。
だいたい5~8倍くらい通常の引っ張り方に比べて速度に差が出てくるので、
ページング処理以外でもランダムで取得してあげているならぜひ実装するのがオススメです。

ランダムなページング処理ってIDの降順、昇順に並べてページングすることほど多くはないですが、全てのデータを平等に見せたいという意味では有効な手段といえそうです。

bindModelを複数するときはrecursiveが大事

複数のbindModelをするときはrecursiveが大事

久々にCakephpの話題なのですが、複数bindModelしたい時はrecursiveが非常に大事というお話。

例:4つのテーブルを連携する

今回私は4つのテーブル(Model) を使いました。これだけ多数のModelを使うと、4つ全てが関連付け(foreignKey)されあっていることはないですわね。言い換えると、あるModelの中で別のModelを扱うということ。
例だと、最終行を見てもらった通りGroupShopが一番の親になるわけです。

各Modelの関係性

この関係性だと、GroupShop Prefectureこの2つが関係しあってないので、GroupShopに対してbindModelできません。どう頑張っても。
そこで、唯一関係しあっているShopモデルと先にbindModelで別にbindしておく必要があります。

ShopとPrefecture 関連
ShopとGroupShopとGroup 関連
GroupShopとPrefecture 関連していない

下記はコントローラーの内容です。

$this->Shop->bindModel(
array(
'belongsTo'=>array(
'Prefecture'=>array(
'className'=>'Prefecture',
'conditions'=>'',
'order'=>'',
'foreignKey'=>'prefecture_id'
)
)
));


$this->GroupShop->bindModel(
array(
'belongsTo'=>array(
'Group'=>array(
'className'=>'Group',
'conditions'=>'',
'order'=>'',
'foreignKey'=>'group_id',
),
'Shop'=>array(
'className'=>'Shop',
'conditions'=>'',
'order'=>'',
'foreignKey'=>'Shop_id',
)
)
));

$con = array(
'conditions'=>array('group_id'=>$id) ); $groups = $this->GroupShop->find('all',$con);

上記の書き方ではあるものが抜けているので 最初にbindeModelしたPrefectureのデータまで拾うことは出来ません。
なお、Modelで結合させても結果は同じです。忘れがちなあるものが無いのです。

bindModelはデフォルトでは第一階層までしか見ない

Cakephpは特に指定しなければ結合してあるModelの第一階層、つまり自分が直接結合しているModelしか読みに行かず、孫(ここではShopと関連付けされているPrefecture)
を一切見てくれません。そこで、さらに下の階層まで読みにいかせる場合はrecursiveを指定してあげる必要があります。

recursiveとは深度のことです。

findするModelにrecursiveを指定する

今回、自分は直接結合していない孫のModelの内容まで見る場合は’recursive’ => 2にしてあげれば 孫まで読みに行くことが出来るようです。

$con = array(
'conditions'=>array('group_id'=>$id),
'recursive' => 2
); $groups = $this->GroupShop->find('all',$con);

単純なことのようで、知らないってことってあるんですね。
Cakephpの参考書にもrecursiveについては載っているのですが、深度ですよ~との説明しかなかったので、
今まで全く気にしていなかったのですが、今回のように自分が直接bindしていないModelを読みにいく場合は使えそうです。

3.7.8.6 recursive
なお、Cakephpのマニュアルにあるとおり、必要以上に設定すると処理が重たくなるので必要最低限にしましょうとのこと。

このまま見過ごしていたら、サブクエリで全部やるところでした。ヤレヤレ。

CSVダウンロード機能ならやっぱりCakephp CSV Hepler!

CakephpではCSVダウンロードに便利なヘルパー CSV Heplerが存在した

マスター管理画面の追加機能でCSVダウンロードを機能追加することになり、CSV Heplerがすごく便利だったのでメモしてみんなにも伝えたい!
基本的な使い方は下記のURLの通りです。
なお、PHP5より利用可能です。
私はCakephp1.3.5にて作業し、問題なく動作することが確認できました。

参考URL

CakePHP – CSVダウンロード(エクスポート)機能を実装する『CSV Helper』 まとめ 高橋です

ちょっとだけ解説

基本的には参考URLの通りに作業すれば何ら問題ありません。すごく簡単です。任意のファイル名は日本語でもなんでも自由に設定できます。int型のデータを日本語に置き換えることもできるようで、この場合変数名はフィールド名と一致させておけばよいようです。
実は長らくCakephp触っていて、初めてcompactという関数に出会ったのですが、setと違い、変数を一度にまとめて設定できる関数のようです。

//CSVダウンロード	
function csv() {
        Configure::write('debug', 0); // 警告を出さない
        $this->layout = false;
        $filename = '相互リンク' . date('YmdHis'); // 任意のファイル名
       $category= array('1' => 'カテゴリー1', '2' => 'カテゴリー2');// データを文字列に変換できる
        $th = array('id','permission','category','url','name','email','created'); // 表の一行目を作成
        $td = $this->Link->find('all'); //表の内容を取得
        $this->set(compact('filename', 'th', 'td'));
    }

いや~、Cakephpってホント素晴らしいですよね。

Cakephp saveエラーでの出力

Cakephp saveエラーでデータの登録が失敗した場合の処理を別ける方法です。

Controller

if ($this->Item->save($this->data['Item'])) {
$this->redirect('/items');
}else{
$this->set('error', 'データを登録できませんでした');
}

View

if (!empty($error)) {
        echo '' . $error . '';
    }

ifで分岐させただけでした・・・でもデータが登録出来なかったときとするならば、単純にこれで十分ですね。

Cakephpの命名規則を完全無視して作ってみた

Cakephpにはテーブル名をつけるのにも、フィールド名をつけるのにも、コントローラーにも命名規則がありますが、この命名規則に沿わない、いや、むしろ完全無視してアプリケーションを作ってみました。
元々Cakephpベースで作られていないサイトに対して、新規で別の管理画面を作ろうと思った時、命名規則に全く沿わないテーブル、フィールド名ですでに構築されていまして、そのデータのままCakephpを何とか利用しようと思って今回の記事の結果にたどり着いたわけです。

命名規則に反したテーブル、id

本来命名規則に沿って作ることがCakephp開発の前提なのですが、Cakephpではなから作るつもりなかったものを、Cakephpで機能を追加しようと思ったら、現状のテーブル名、フィールド名のまま開発していかなければなりません。

  元の名前 命名規則
テーブル名 banner banners
連番(プライマリ) seq id

テーブル名が命名規則から外れていた時のModelの書き方

  
class Banner extends AppModel
{
var $name = ‘Banner’;
     function __construct() {
      $id = array("id"=>false,//idでプライマリならtrue
                      "table"=>"banner",//テーブル名
);
//$idになりうるprimaryKeyのフィールド名を定義
      parent::__construct($id);
      $this->primaryKey = "seq";
  }
}

コントローラーが命名規則と外れた場合

コントローラーについては悩む点は特になく、ただ自動でdataに入らないので、$this->Post->read();で読み込んだものが、自動でview側に渡らないので、$this->setしてあげる必要あり。

function edit($id = null) {
$this->Banner->id = $id;
if (empty($this->data)) {
$this->data = $this->Banner->read();
$this->set(‘data’, $this->data);
} else {
if ($this->Banner->save($this->data['Banner'])) {
$this->redirect(‘/url’);
}
}
} 

一通り命名規約から外れた際のフォームヘルパー(formヘルパー)の書き方

モデル名とコントローラー名に影響されることが多いのがフォームヘルパー(formヘルパー)だったりします。
例えば、RecCompanyモデルを使っているのに、コントローラーの名前がCompaniesだった時。
基本的には毎回正しいモデル名を割り当ててあげる必要があり、けっこう面倒です。

$form->create

  
echo $form->create(‘RecCompany’, 
array(‘type’ => ‘post’, ‘url’ => ‘/companies/recruit/’.$id)); 

$form->input

  
echo $form->input(‘RecCompany.message’,array(
‘label’=> false,
‘size’ => false,
‘div’=>false,
‘class’=>’inp-form required’,
‘id’=>false
)
); 

何事も規約を守ったほうがCakephpの機能を最大限に活かすことが出来ますね。

Cakephp1.3 postgresqlの設定

Cakephp1.3での postgresqlのdatabase.phpの設定についてです。

class DATABASE_CONFIG {
var $default = array(
'driver' => 'postgres',
'persistent' => false,
'host' => 'localhost',
'login' => 'postgres',
'password' => 'mfree2010',
'database' => 'hello30_kanto',
'encoding' => 'utf8',//1.3はUTF-8のときは書いたほうがよい
'prefix' => '',
);
}

Cakephp1.3 conditions 比較演算子

Cakephp1.3にて、WHEREの条件(conditions) に悩んでしまったのでメモ。

Cakephp1.3 conditions 比較演算子

9より小さい

$conditions = array(
'conditions' => array('Model.id >' => '9')
);

$hoge = $this->Model->find('all',$conditions);

9以下

$conditions = array(
'conditions' => array('Model.id >=' => '9')
);

$hoge = $this->Model->find('all',$conditions);