zz log

zaininnari Blog

cakephp model::saveAll を使って、新規作成時に別モデルに初期値を作る

Project hasMany State なモデルにて、
新規に ProjectsController::add をした時、State の初期値を持たせたい。

  • model::saveAll を使う
    • バリデーションとトランザクションを同時に行うことが出来ます。
    • State を保存する際、「project_id」が必要になりますが、アソシエーションを設定していれば、自動的にセットしてくれます。

下準備

データベーステーブル
--
-- テーブルの構造 `projects`
--
CREATE TABLE IF NOT EXISTS `projects` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
  `description` text COLLATE utf8_unicode_ci NOT NULL,
  `type` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
  `license` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
  `user_id` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=6 ;

--
-- テーブルの構造 `states`
--

CREATE TABLE IF NOT EXISTS `states` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
  `hex` varchar(6) COLLATE utf8_unicode_ci NOT NULL,
  `position` int(11) NOT NULL,
  `type` int(1) NOT NULL,
  `project_id` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=41 ;
モデル
<?php
// [app/app_model.php]
class AppModel extends Model {
  public function setInitData(&$data, $options = array())
  {
    $ids = array(
// project.id を取得する、なければ、「null」
      'project_id' => self::getProjectId(),
// user.id を取得する、なければ、「null」
      'user_id' => self::getUserId(),
    );
// スキーマになければ削除
    foreach (array_keys($ids) as $n => $v) {
      if (!array_key_exists($v, $this->_schema)) unset($ids[$v]);
    }
    $default = array_merge($ids, $options);
    $data[$this->alias] = array_merge($data[$this->alias], $default);
    return $data;
  }
}

// [app/models/project.php]
class Project extends AppModel {

  var $name = 'Project';
  var $alias = 'Project';
  /**
   * @var State
   */
  var $State;
  var $validate = array(
// 略
  );

  var $belongsTo = array(
    'User' => array(
      'className' => 'User',
      'foreignKey' => 'user_id',
      'conditions' => '',
      'fields' => '',
      'order' => ''
    )
  );

  var $hasMany = array(
    'State' => array(
      'className' => 'State',
      'foreignKey' => 'project_id',
      'dependent' => true,
      'conditions' => '',
      'fields' => '',
      'order' => array('State.position', 'State.type', 'State.id'),
      'limit' => '',
      'offset' => '',
      'exclusive' => '',
      'finderQuery' => '',
      'counterQuery' => ''
    )
  );

  public function setInitData(&$data, $options = array())
  {
    parent::setInitData($data, $options);
    $this->State->setDefaultData($data);
    return $data;
  }

}

// [app/models/state.php]
class State extends AppModel {

  var $name = 'State';
  var $validate = array(
// 略
  );

  var $order = array('State.position', 'State.type', 'State.id');

  //The Associations below have been created with all possible keys, those that are not needed can be removed
  var $belongsTo = array(
    'Project' => array(
      'className' => 'Project',
      'foreignKey' => 'project_id',
      'conditions' => '',
      'fields' => '',
      'order' => ''
    )
  );

  public function setDefaultData(&$data)
  {
    $stateData = array(
      array('name' => 'new', 'hex' => 'ff1177', 'position' => '0', 'type' => '0'),
      array('name' => 'open', 'hex' => 'aaaaaa', 'position' => '1', 'type' => '0'),
      array('name' => 'hold', 'hex' => 'EEBB00', 'position' => '2', 'type' => '0'),
      array('name' => 'resolved', 'hex' => '66AA00', 'position' => '3', 'type' => '1'),
      array('name' => 'duplicate', 'hex' => 'AA3300', 'position' => '4', 'type' => '1'),
      array('name' => 'wont-fix', 'hex' => 'AA3300', 'position' => '5', 'type' => '1'),
      array('name' => 'invalid', 'hex' => 'AA3300', 'position' => '6', 'type' => '1'),
    );
    $data['State'] = $stateData;
    return $data;
  }

}

コントローラー

まずは、全体。

<?php
// [app/controllers/projects_controller.php]
class ProjectsController extends AppController {

  var $name = 'Projects';

  /**
   * @var Project
   */
  var $Project;

  function add() {
    if (!empty($this->data)) {
      $this->Project->create();
      $this->Project->setInitData($this->data);
      if ($this->Project->saveAll($this->data)) {
        $this->Session->setFlash(__('The Project has been saved', true));
        $this->redirect(array('action' => 'index'));
      } else {
        $this->Session->setFlash(__('The Project could not be saved. Please, try again.', true));
      }
    }
    $users = $this->Project->User->find('list');
    $this->set(compact('users'));
  }

}

解説

以下の値がPOSTされたとして解説します。

<?php
$this->data = array('Project' => array(
  'name' => 'name',
  'description' => 'description',
  'type' => 'type',
  'license' => 'license',
));
<?php
  function add() {
    if (!empty($this->data)) {
      $this->Project->create();

新規保存する際のおまじないです。
Project モデル内の $this->id 、$this->data、$this->validationErrors を初期化します。

  • $this->id はプライマリーキー の値です。これの有無で、INSERT か UPDATE を判別します。
  • $this->data は、保存するデータの配列です。
  • $this->validationErrors は、バリデートに失敗した際のエラーが記録されます。
<?php
      $this->Project->setInitData($this->data);

独自関数です。2つの役割があります。
(1) POSTされたデータにユーザIDを付加します。
$this->data['user_id'] = $this->Session->read('Auth.User.id') と同じ結果です。

  • >
<?php
    $this->data = array(
      'Project' => array(
         'name' => 'name',
         'description' => 'description',
         'type' => 'type',
         'license' => 'license',
         'user_id' => '1', // これが追加されます。
      )
    );

(2) POSTされたデータにState の初期値を付加します。

  • >
<?php
    $this->data = array(
      'Project' => array(
         'name' => 'name',
         'description' => 'description',
         'type' => 'type',
         'license' => 'license',
         'user_id' => '1',
      ),
      'State' => array( // これらが追加されます。
         array('name' => 'new', 'hex' => 'ff1177', 'position' => '0', 'type' => '0'),
         array('name' => 'open', 'hex' => 'aaaaaa', 'position' => '1', 'type' => '0'),
         array('name' => 'hold', 'hex' => 'EEBB00', 'position' => '2', 'type' => '0'),
         array('name' => 'resolved', 'hex' => '66AA00', 'position' => '3', 'type' => '1'),
         array('name' => 'duplicate', 'hex' => 'AA3300', 'position' => '4', 'type' => '1'),
         array('name' => 'wont-fix', 'hex' => 'AA3300', 'position' => '5', 'type' => '1'),
         array('name' => 'invalid', 'hex' => 'AA3300', 'position' => '6', 'type' => '1'),
      )
    );
<?php
      if ($this->Project->saveAll($this->data)) {
// 保存が成功
        $this->Session->setFlash(__('The Project has been saved', true));
        $this->redirect(array('action' => 'index'));
      } else {
// 保存が失敗
        $this->Session->setFlash(__('The Project could not be saved. Please, try again.', true));
      }
saveAll の用法

cakebook と同じことを書いています。
http://book.cakephp.org/ja/view/1031/Saving-Your-Data

セットされたデータを使って、
(a) 単一のモデルに、個別のレコードを複数記録する。
(b) あるレコードと同様に、関連したレコードも全て記録する。

のどちらかを行います。


ここでは、(b)の用法で呼ばれ、boolean を返します。
(内部では、(a)の用法でも呼ばれています。)


saveAll(array $data = null, array $options = array())


$options は、内部では、save メソッドに引き継がれますが、
saveAll独自のパラメータとして、
array('validate' => 'first', 'atomic' => true)
の初期値が与えられます。

validate:
false をセットすると、バリデーションが行われません。
true をセットすると各レコードが保存される前にバリデーションが行われます。
'first' をセットすると、レコードの保存が行われる前に、全てのレコードにバリデーションが行われます。
'only' をセットすると、バリデートだけを行い、保存は行われません。

atomic:
true(デフォルト)をセットすると、全てのレコードの保存を単一のトランザクションとして行うよう試みます。データベースやテーブルがトランザクションをサポートしていない場合は、false にセットするようにしましょう。
もし false にセットされているなら、渡した $data 配列に似た形式のものが、配列として返されます。ただし、値は各レコードが保存されたかどうかを表す true または false がセットされます。


ここでは、
第二引数になにも設定していないため、

  • validate: 'first' をセットすると、レコードの保存が行われる前に、全てのレコードにバリデーションが行われます。
  • atomic: true(デフォルト)をセットすると、全てのレコードの保存を単一のトランザクションとして行うよう試みます。

を行います。


保存を行う順番は、
'belongsTo' -> 自分自身のモデルデータ -> 'hasOne' -> 'hasMany'
です。


ここでは、
$data['Project'] の保存がまず試みられます。
次に、
$data['State'] の保存が試みられます。
先に、Project モデルにて保存が行われたため、挿入したIDが判明します。
(ここでは、2 とします)
よって、
$data['State'] のデータに、アソシエーションで設定した外部キー「foreignKey」('project_id')に判明したIDが付加されます。

<?php
array('State' => array( // 「'project_id' => '2'」が追加されます。
  array('name' => 'new', 'hex' => 'ff1177', 'position' => '0', 'type' => '0', 'project_id' => '2'),
  array('name' => 'open', 'hex' => 'aaaaaa', 'position' => '1', 'type' => '0', 'project_id' => '2'),
  array('name' => 'hold', 'hex' => 'EEBB00', 'position' => '2', 'type' => '0', 'project_id' => '2'),
  array('name' => 'resolved', 'hex' => '66AA00', 'position' => '3', 'type' => '1', 'project_id' => '2'),
  array('name' => 'duplicate', 'hex' => 'AA3300', 'position' => '4', 'type' => '1', 'project_id' => '2'),
  array('name' => 'wont-fix', 'hex' => 'AA3300', 'position' => '5', 'type' => '1', 'project_id' => '2'),
  array('name' => 'invalid', 'hex' => 'AA3300', 'position' => '6', 'type' => '1', 'project_id' => '2'),
))

また、
上記のデータ形式は、
saveAll の用法 (a) の「単一のモデルに、個別のレコードを複数記録する。」に該当するため、
State モデル の saveAll を内部で呼びます。
(save メソッドをループで回した回数分呼び出します。)


State::saveAll の結果と validationErrors プロパティが空であることを確認して、
最終的に、boolean を返します。


保存したモデルの結果です。
(project_id など細かい部分は異なります)

array
'Project' =>
array
'id' => string '6' (length=1)
'name' => string 'Name' (length=4)
'description' => string 'Description' (length=11)
'type' => string 'Type' (length=4)
'license' => string 'License' (length=7)
'user_id' => string '41' (length=2)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
'User' =>
array
'id' => string '41' (length=2)
'created' => string '2010-06-13 16:19:57' (length=19)
'modified' => string '2010-06-13 16:19:57' (length=19)
'username' => string 'aaaa' (length=4)
'password' => string '886cda4198f50ec0167c8eb38edf7036c838c3aa' (length=40)
'State' =>
array
0 =>
array
'id' => string '41' (length=2)
'name' => string 'new' (length=3)
'hex' => string 'ff1177' (length=6)
'position' => string '0' (length=1)
'type' => string '0' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
1 =>
array
'id' => string '42' (length=2)
'name' => string 'open' (length=4)
'hex' => string 'aaaaaa' (length=6)
'position' => string '1' (length=1)
'type' => string '0' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
2 =>
array
'id' => string '43' (length=2)
'name' => string 'hold' (length=4)
'hex' => string 'EEBB00' (length=6)
'position' => string '2' (length=1)
'type' => string '0' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
3 =>
array
'id' => string '44' (length=2)
'name' => string 'resolved' (length=8)
'hex' => string '66AA00' (length=6)
'position' => string '3' (length=1)
'type' => string '1' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
4 =>
array
'id' => string '45' (length=2)
'name' => string 'duplicate' (length=9)
'hex' => string 'AA3300' (length=6)
'position' => string '4' (length=1)
'type' => string '1' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
5 =>
array
'id' => string '46' (length=2)
'name' => string 'wont-fix' (length=8)
'hex' => string 'AA3300' (length=6)
'position' => string '5' (length=1)
'type' => string '1' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)
6 =>
array
'id' => string '47' (length=2)
'name' => string 'invalid' (length=7)
'hex' => string 'AA3300' (length=6)
'position' => string '6' (length=1)
'type' => string '1' (length=1)
'project_id' => string '6' (length=1)
'created' => string '2010-06-23 00:35:17' (length=19)
'modified' => string '2010-06-23 00:35:17' (length=19)