cakephp1.3 saveメソッドで更新日時がudpateされない

大分ご無沙汰しております。ビールがおいしい季節になりました。

cakePHPは、modified,createdなんてフィールドを予め作成しておくと、新規作成・更新の際に勝手にデータをinsert,updateしてくれるというお便利機能があります。

先日、以前と同じように更新のため、

$this->User->save($this->data);

なんて処理をしていたときに、突然、「更新日時が更新されない」という現象に遭遇しました。

実は、探してみると

  • mysqlの設定が悪い
  • そもそも、hiddenとかでPOSTしてた
  • という人たちの記事があって、はじめはそこを疑いました。ですが以前はきちんと動いていたのに、突然save()時に更新日時modifiedが更新されない現象だったので、他の原因を探ってみました。

    結論からいうと、ここのsaveの直前に

    $this->User->id = user_id;
    $this->User->read();
    

    というreadメソッドを呼び出すことで、コアのmodel.php側のsave()内で$this->dataを参照すると、
    read()時にセットしたであろう、更新日を含めた一行まるごとのデータ構成になっていました。
    その後、処理としては実際にsave()に投げられた引数$dataとmodel内の$this->dataとを、マージしているらしく、結果として、

    パスワードだけ変更する画面だったけど、実際はすべてのデータをupdateしてた(実際はパスワード以外のデータは
    データベースから参照された値なので、変更点はなく、更新日時すらも含まれているので結果、更新されていない)
    という現象でした。
    なぜ、直前でreadなんぞしているかというと、変更点を調べたいために、事前に更新前のデータを取得しておく必要があったのです。

    解決方法
    解決方法はいくつかあると思うのですが今回は・・・このようにしてみました(これが推奨されるわけではないと思います)。

    $this->User->data  = null;//readしたデータをリセットする。
    $this->User->save($this->data);
    

    本来の正しい修正方法としては第3引数に更新対象のフィールドを含めて、余計なフィールドは含ませないことがポイントだと思われます。
    恥ずかしながら・・・controller側で参照している$this->data と、model 側で参照している$this->data は完全に一致しているものと思い込んでいましたが、実際のところ違う、ということですね。2.0からは明確に表示が変わるので、今後は意識して使ったほうがいいのだろうか・・。

    おなかへりました!ビールのみにさまよってきまーす。

    cakePHP2.1でファイルのアップロードとダウンロード

    cakePHP2.1の
    ファイルアップロード
    ファイルダウンロード
    をやってみようと思います。ひとまずファイルアップロード用にテーブル作成します。
    今回は、ファイルの実体は特定のディレクトリに保存するので、ファイルの実体はDBには保存しません。


    mysql> desc upload_files;
    +----------------+--------------+------+-----+---------+----------------+
    | Field | Type | Null | Key | Default | Extra |
    +----------------+--------------+------+-----+---------+----------------+
    | id | int(11) | NO | PRI | NULL | auto_increment |
    | user_id | int(11) | NO | | NULL | |
    | name | varchar(50) | NO | | NULL | |
    | file_name | varchar(100) | NO | | NULL | |
    | extension | varchar(10) | NO | | NULL | |
    | size | int(11) | NO | | 0 | |
    | download_count | int(11) | NO | | 0 | |
    | created | datetime | YES | | NULL | |
    | deleted | tinyint(1) | YES | | 0 | |
    | deleted_date | datetime | YES | | NULL | |
    +----------------+--------------+------+-----+---------+----------------+

    次にファイルアップロード用のViewです。
    Viewにはこんな感じでアップロードフォームを作ります。

    echo $this->Form->create(null, array('action' => 'index', 'enctype' => 'multipart/form-data'));
    echo $this->Form->input(null, array('type'=>'file','label'=>'対象ファイル' ));
    echo $this->Form->submit('ファイルアップロード');
    

    次はアップロード用のコントローラー。index()メソッドです。一覧とアップロードされた
    ページを一緒にしているので一覧もとりに行っています。
    定数化したり、モデルに処理を移動する必要があったりしますが、まずはこれで。

    public function index() {
    	$this->UploadFile->recursive = 0;
    	$this->set('uploadFiles', $this->paginate());
    	if(!empty($this->request->data)){
    		$uploaddir = self::FILE_SAVE_PATH.DS;//事前にパスを宣言して、パーミッション変えておいてください。
    		$uploadfile = $uploaddir . date("YmdHis");
    		$this->request->data['UploadFile']['user_id']	 = $this->Auth->User('id');
    		$this->request->data['UploadFile']['file_name'] = self::FILE_SAVE_PATH.DS.basename($uploadfile);
    		$this->request->data['UploadFile']['extension'] = pathinfo($this->request->data['UploadFile']['name'],PATHINFO_EXTENSION);
    		if(!move_uploaded_file($this->request->data['UploadFile']['tmp_name'], $uploadfile)){
    			return;
    		}
    		if($this->request->data['UploadFile']['size'] > 1000000){
    			$this->UploadFile->invalidate('name','ファイルサイズが大きすぎます');
    			return;
    		}
    		if($this->UploadFile->save($this->request->data)){
    			$this->Session->setFlash('アップロードを完了しました', true);
    		}else{
    			$this->Session->setFlash('アップロードに失敗しました', true);
    		}
    		$this->redirect('index');
    	}
    }
    

    さて次は一覧のViewです。ダウンロードリンクですな。

    echo $this->Html->link($uploadFile['UploadFile']['name'], array('controller' => 'upload_files', 'action' => 'download', $uploadFile['UploadFile']['id'])); 
    

    それでは、ダウンロード用のメソッドを見ていきます。ここでポイントは、MediaViewを使うとすんごい便利なんです。
    ダウンロードは断然、cakePHPのコアのMediaViewを使うとダウンロードが簡単にできるのでおすすめ。
    1.3では「$this->view = ‘Media’;」という使い方だったのですが、(2.0はわからない・・・)2.1では
    「$this->viewClass = ‘Media’;」という利用方法に変わっている、というのが本日の重要ポイントです。
    lib/Cake/View にあるMediaView.phpをみると利用方法が書いて有ります。
    ヘッダーの書き出しとか自前でしなくてよくて、このMediaViewが勝手に判断してくれるのでかなり便利です。
    これで、ダウンロードリンクをクリックしてもページ遷移なしでファイルがダウンロードできます。お便利。
    本当はダウンロードされるたびにカウントアップしたものにしようと思っていますが・・今回はとりあえずスルーしました。

    public function download($id) {
    	$this->viewClass = 'Media';
    	$this->UploadFile->id = $id;
    	$this->UploadFile->recursive = -1;
    	$data = $this->UploadFile->read();
    	$params = array(
    		'id'=>basename($data['UploadFile']['file_name']),
    		'name'=>preg_replace("/.[^.]+$/","",$data['UploadFile']['name']),
    		'download'=>true,
    		'extension'=>$data['UploadFile']['extension'],
    		'path'=>dirname($data['UploadFile']['file_name']).DS
    	);
    	$this->set($params);
    }
    

    月末に怒涛のブログ記事を書きましたが、まだまだ2.1関連の内容をお届けできればと思います!

    cakePHP2.1でSoftDeleteBehaviorを使う

    何かというとcakeでは通常$model->delete();すると物理削除でレコードごとなくなりますが、
    これがあら不思議!このビヘイビアを使うと論理削除にしてくれます。

  • 通常のdelete()呼び出しで物理削除ではなく、deletedに1 が入り、delete_dateフィールドに日時が入る
  • 該当するモデルは何もしなければdelete = 0 である条件を必ずつけてくれる
  • というものです。いや幸せ。
    さて準備です。GitHubのcakeDCから
    ここにあるSoftDeleteBehavior.phpだけをapp/Model/Behaviorにぶっこんでみます。
    例としてモデルは、User.phpにしてみましょう。
    私のモデルは、既にACLのビヘイビアが設定されていますが、それに追加してあげればいいですね。

    	public $actsAs = array(
    					'Acl' => array(
    						'type' => 'requester',
    					),
    					'SoftDelete'
    					);
    

    さて、ここでもうひとつ準備。
    テーブルにフィールドがあれば良いですが、usersテーブルに該当するフィールドを用意しましょう。
    mysqlを例にして下記のようなフィールドを追加します。
    ここで注意・・deletedは指定しないで追加するとtinyint(4)として登録されますのでちゃんとtinyint(1)で指定しましょう。
    これが間違ってると検索時に deleted = ”という謎の条件になってしまいます。

    deleted   tinyint(1)
    deleted_date datetime

    さてこれで用意が整ったので、一覧画面を覗いてみましょう。
    WHERE `User`.`id` = 1 AND `User`.`deleted` = ‘0’ LIMIT 1
    こんな条件が一覧についていたら、やりたいことができたも同然です。
    それでは試しに、ユーザを削除してみると、どうやらエラーがでるみたいですが・・・。

    SoftDeleteBehavior は通常のdelete()を呼ぶとどうやらfalseを返すようです。
    なのでひとまず・・こんなかんじでしのいでみました。

    if (!$this->User->delete()) {
    	$this->redirect(array('action'=>'index'));
    }
    

    さてこれで削除したデータを見てみると、
    いえい!削除されていますよぅ!


    mysql> select deleted,deleted_date from users where id =6;
    +---------+---------------------+
    | deleted | deleted_date |
    +---------+---------------------+
    | 1 | 2012-03-24 19:43:51 |
    +---------+---------------------+
    1 row in set (0.00 sec)

    補足*管理画面などで削除されているユーザ自体も表示したい場合は、現在追加されているSoftDeleteを
    detachしてあげればよいです。例えばこんな感じ。

    public function index(){
    	$this->User->Behaviors->detach('SoftDelete');
    	$this->User->recursive = 0;
    	$this->set('users', $this->paginate());
    }
    

    補足その2*UserモデルにhasManyしているPostモデルにもこのSoftDeleteを摘要して、hasManyの条件として
    depend => trueを設定してみたのですが、どうも連動してSoftDeleteはしてくれませんでした。
    UserモデルのSoftDeleteを外して、物理削除状態にすると、PostモデルのSoftDeleteが聞いて削除状態になりました。
    ここらへんはもうちょっと調べて挙動を確認してみないといけないですね。新しいことがわかったらまた追記したいと思います。

    cakePHP2.1でSearchPlugin hasManyを扱う

    先日からSearchPluginを使った画面周りの実装してます。
    今回は特に、一対多データの扱いについてです。
    ブログにタグをつけたチュートリアルが結構あるので、HABTMデータは結構ありますね。
    今回の実際はHABTAMではなく、中間テーブルを介在しません。

    その1.企業モデルに「属性」を外だしで複数持たせる。(いわば、タグと同じ)
    その2.属性は複数もてるが、マスターテーブルは持たない。
    その3.属性テーブルはid,company_id,nameという構成。

    というちょっと特殊なケースかもしれない。でも よくあるUseとPostの関係だと思ってもらってもいいです。
    ただし、属性自体を「AND」条件で検索したい。というのが味噌です。

    SearchPluginの準備
    app配下にSearchというディレクトリ掘って、GitHubのこちら
    一式ごっそりおきます。
    bootstrap.phpのこの箇所が生きていれば、問題ないでしょう。

    CakePlugin::loadAll(); // Loads all plugins at once
    

    次に読みこむコントローラー側で利用できるように

    public $components = array('Search.Prg'); 
    

    こちらもセット。これで準備完了。

    次に親となる企業モデルCompany.phpにこの設定。当然、テーブル構成にこのフィールドが必要です。
    それと、SearchPluginで使う設定も一緒に書き足しておきましょう。
    $filterArgsのmethodについては後ほど。

    	public $hasMany = array(
    		'CompaniesAttribute' => array(
    			'className' => 'CompaniesAttribute',
    			'foreignKey' => 'company_id',
    			'dependent' => true,//企業が削除されたら、属性は一緒に削除されていい
    			'conditions' => '',
    			'fields' => '',
    			'order' => 'order',
    			'limit' => '',
    			'offset' => '',
    			'exclusive' => '',
    			'finderQuery' => '',
    			'counterQuery' => ''
    		),
    	);
    
    	public $filterArgs = array(
    		array('name' => 'attribute_id', 'type' => 'subquery', 'field' => 'Company.id', 'method' => 'searchByAttributes'),
     	);
    

    次に、CompaniesAttribute.phpの方にも設定をしておきます。

    	public $belongsTo = array(
    		'Company' => array(
    			'className' => 'Company',
    			'foreignKey' => 'company_id',
    			'conditions' => '',
    			'fields' => '',
    			'order' => ''
    		)
    	);
    
    

    モデル側の設定はひとまずここまで。次は検索画面ですね。
    先ほど、プラグインを読み込んだコントローラーにこんな検索メソッドを準備。
    属性テーブルCompaniesAttributeから選択させる属性一覧を取得してViewに渡しています。
    あとは検索結果をpaginateするための条件設定です。
    検索画面と検索結果を一緒に表示しています。

    	function search(){
        	  $this->Prg->commonProcess();
        	  $conditions = $this->Company->parseCriteria($this->passedArgs);//SearchPlugin
    		$this->paginate = array(
    		  'conditions' => $conditions,
    		  'limit' => 5,
    		  'order'=>'Company.id asc'
    		);
    		$attributes = $this->Company->CompaniesAttribute->findAllByProjectId($this->Session->read('selectedProjectId'));
    		$attributes = Set::combine($attributes, '{n}.CompaniesAttribute.name', '{n}.CompaniesAttribute.name');
    		$this->set('pager_numbers', $this->pager_numbers);
    		$this->set(compact('attributes'));
    		$this->set('companies', $this->paginate('Company'));  
    	}
    

    次にViewですね。検索フォームの箇所だけ、こんな感じ。
    本当はいろんな検索条件があるのですが、属性だけにしてみました。
    ひとまず属性を複数選択できるドロップダウン形式で表示しています。

    <?php echo $this->Form->create('Company');?>
    <fieldset>
    	<legend><?php echo __('Search Company'); ?></legend>
    <?php
    	echo $this->Form->input('attribute_id',array('multiple'=>true,'options'=>$attributes));
    ?>
    </fieldset>
    <?php echo $this->Form->end(__('検索'));?>
    

    これで肝心の企業モデルCompany.phpに設定していたsearchByAttributesを実装します。
    こちらは @kanonjiさんのこちらの記事が非常に参考になりました。
    属性自体は、IDを持たないので、値としては数字ではなくテキストが入ってきます。
    検索画面のドロップダウンのリストのvalue値は「製造業」のような日本語が入ってくることを前提にしています。

        public function searchByAttributes($data = array()){
            $this->Behaviors->attach('Search.Searchable');
            $attribute_id = Set::extract($data,'/attribute_id');
    	$options = array(
                'conditions' => array('CompaniesAttribute.name'  => $attribute_id),
                'contain' => $this->alias,
            );
            if (( $c = count ( $attribute_id )) !== 1 ){
            	//ここの条件を通すとAND条件として構成される。ここを通らないとOR条件として構成される。
                $options['group'] = 'CompaniesAttribute.company_id HAVING COUNT(CompaniesAttribute.company_id) = '.$c;
            }
    	$data = $this->CompaniesAttribute->find('all',$options);
            $condition = implode(', ', Set::extract($data,'/Company/id'));
            if ( empty( $condition )){
                $condition = 'NULL';
            }
            return $condition;
        }
    

    若干いじったところ
    属性モデルから企業モデルを参照するのでcontainでCompanyモデルを利用しようとしたら、
    find(‘list’)ではLEFTJOINされなかったのでall指定にしてextractでIDを引っ張り出しました。

    ArraySourceの勧め

    今日もお酒のまないんで、ひとつ記事かいてねることにします。本日もcakePHP2.1のお話です。
    さて、設定ファイルでデータベースに持つ程でもない情報というのは、かつてconst.phpに書いてました。
    ところが例えば男・女 とか、普遍的なものだったりあまり情報が書き換わらないものって結構あります。
    で、設定ファイルが結構カオスになってたりします。

    今回は、設定情報もこんなふうに持つと良いよってことで、https://github.com/cakephp/datasources/tree/2.0/Model/Datasource
    ここからArraySource.phpだけをいただきます。そんでapp/Model/Datasourceにポロリと入れます。

    次に、設定です。database.phpにこんな記述を足します。既存のデータベースの設定に追加してくださいね。

    	var $ArraySource = array(
    		'datasource' => 'ArraySource',
    	);
    

    これで、利用できる準備が整いました。
    次にGender.phpっていうモデルをModel配下に作成します。

    <?php
    App::uses('AppModel', 'Model');
    /**
     * Gender Model
     *
     */
     class Gender extends AppModel
    {
        public $name = 'Gender';
    
        public $displayField = 'display';
    
        public $useDbConfig = 'ArraySource';//ここにdatabase.phpで宣言した変数名をセット
    
        public $records = array(
                array(
                    'id' => 1,
                    'display' => '男性',
                ),
                array(
                    'id' => 2,
                    'display' => '女性',
                ),
            );
    
        const MAN     = 1;//一応定数としても利用できるようにしてみた。
        const WOMAN = 2;
    
    }
    

    これで、このモデルをbelongToに持つモデルにこんなふうに設定すると、(注1)

    	public $belongsTo = array(
    		'Gender' => array(
    			'className' => 'Gender',
    			'foreignKey' => 'gender_id',
    			'fields' => 'display',//これがちょっときになる指定
    		)
    	);
    

    親のモデルから芋ずるに引っ張ってくるとあたかもGenderモデルがDBから取得したデータのように、「女性」や「男性」を返してくれます。
    設定ファイルとかから参照しなくて良いので、かなりストレスフルフリーです。

    echo h($value['Gender']['display']);
    

    (注1)
    ここで一点だけ。どうもcontainableを使う場合、$belongsToの条件に‘fields’ => ‘display’,
    指定をするか、あるいはcontainableのconditionsの中に、フィールドを指定する、
    のどちらかをしないと取ってこれませんでした。
    親モデルからfind(‘all’)の場合は別に指定はいらないのですが、ビヘイビアを使う場合には
    上記指定をしないと結果がarray()になってしまい、途中でフィールド名がunsetされてしまうようでした。
    これ仕様なのかちょっとわからないです。
    でもcontainable使ってるだけだからなあ。。。できればフィールド名は指定しなくてもいいようになるといいんですが。

    ここからは確認していませんが、新規登録の時にもちゃんとlistでとってこれれば

     echo $this->Form->input('gender');
    

    こんなかんじでアソシエーションの恩恵を受けられるのではないでしょうか。

    というわけで、設定ファイルの値をあたかもDBから取得したように振る舞えるArraySource。ぜひみなさんも使ってみてください。

    CakePHP Advent Calendar 2011 12/09

    こんにちは。本日12月9日担当の@yashioです。
    来週はいよいよcake勉強会and忘年会ですね(∩´∀`)∩ワーイ皆さんにあえるの楽しみにしています。

    さて今回は、1.3系で・・しかも出尽くされていると思いますがViewキャッシュ導入について紹介したいと思います。
    いろいろな方がこちらは紹介されていますが、私でもできるヨ!ってことで生暖かく見てください。
    頑張って図つけたわ。。これが一番時間かかったとか・・・・。

    ・登録者向けのサービスだが、ログイン前にランディングページなどの静的なページを多用している
    ・できるだけ、ウェブサーバーへの負荷を軽減したい

    などの時にぜひ。

    レシピ:cake1.3、 https://github.com/mcurry/html_cacheからダウンロードしてきた一式

    手順その1:
    https://github.com/mcurry/html_cache からファイルをダウンロードしてapp/plugins/html_cache/の下につっこむ。

    手順その2:
    App::import(‘core’, ‘File’); をapp_controller.phpのbeforeFilterに記述する(これ重要です。忘れてすごい悩んだ・・)
    特定のコントローラーだけに処理をさせたい場合は、該当するcontrollerのbeforeFilterに記述が望ましいでしょう。

    手順その3:webroot/cache配下に静的に出力したいファイルのパスでディレクトリをつくる。app/webroot/cache/controller名/action名/のような。

    手順その4:
    3で作成したディレクトリにアクセス権限をつける。実際にここに静的なHTMLファイルが作成されます。

    手順その5:
    静的表示をしたいコントローラー、のアクションに、

    $this->helpers[] = ‘HtmlCache.HtmlCache’;

    を追記する。
    ※妙な怒りのデバッグが入っていますがおきになさらずに。ちゃんと表示されます。

    これは、3の手順で作成したディレクトリのパスにcontroller,actionが一致している必要があります。

    手順その6:
    webroot直下にある/htaccessを表1.のように変更する(簡単に言うと、webroot/cache配下に該当のファイルがある場合にはそちらを優先的参照する。ない場合は通常通りフレームワークのルールに則り、/controller/actionへ飛ぶ。)

    <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_METHOD} ^GET$ #追加部分
    RewriteCond %{DOCUMENT_ROOT}/cache/$1/index.html -f #追加部分
    RewriteRule ^(.*)$ /cache/$1/index.html [L]  #追加部分
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
    </IfModule>

    手順その7:
    美味しくいただく。 さあ該当ページにアクセスしてみましょう。ファイルが作成されない場合はパーミッションの確認をしてください。
    3で作られたディレクトリにファイルがつくられていますか?日付を見てみてくださいね。
    この場合、controllerも通らないので不必要なロードがされないので結構表示速度を体感できるのではないかと思います。
    (コンテンツにもよるでしょうが)


    試しに削除してもう一度作成してみましょう。【やったー!】
    注意すべきことはctpファイルの中身に変更が合った場合などは、

    こちらにあるキャッシュを一度削除しないと新しいファイルが作成されません。

    そのため頻繁に変更するファイルに関してはあまり向かないかもしれませんが、キャッシュで表示する方が
    明らかに表示速度は上昇しますので、お手軽にできますし皆さん、試してみてはいかがでしょうか。

    さて、明日はおなじみ@mon_satさんです。よろしくおねがいしまーす!

    Containable Behaviorを追いかけてみました。

    cakephp 1.3.3で開発しております。
    今回は先日わたくしがハマった事象についてお話します。多少書きなぐり気味ですが><許してください。

    前提条件

    1.A_controllerでpaginateしている
    2.paginate対象のモデル(User)には色々なモデルが関連付けられているが、その1部であるHogehogeというモデルをLEFTJOINではなく、INNERJOINで連結したいので一度unbindする
    3.このメソッドが呼ばれる前にapp_controllerのbeforeFilter()にUserモデルで検索をかけている箇所がある。そこはContainableBehaviorが使われている

    という前提でした。

    1.A_controllerのpaginateの箇所はこんなかんじ。

    $this->User->unbindModel(array('belongsTo'=>array('Hogehoge')), false);//第2引数をfalseにすることでモデルの関連性を維持させる
    
    			'joins'=>array(
    				array(
    					'type'	=>'INNER',
    					'fields'=>array('Hogehoge.name'),
    					'table'=>'`Hogehoges`',
    					'alias'=>'`Hogehoge`',
    					'conditions'=>array(
    						'Hogehoge.id = Hogehoge.user_id',
    					),
    				),
    			),
    //このあとpaginate実行
    
    

    でハッピー! にならなかった・・・というお話。

     

    “Containable Behaviorを追いかけてみました。” の続きを読む