はじめに
cakephp2を使って複数レコードを更新した際にレコードがうまく更新されずハマってしまったので備忘録も兼ねて記事を作成します。
筆者は現在、音声認識エンジンの開発を行っている企業でアルバイトをしており、普段はアノテーションツールやテストツールの新規機能開発や保守、モデルを効率よく学習させるためのあれこれなど、ML以外のことはなんでもやるといった業務内容です。
ML用のツールを開発することが多いため普段はDjangoをメインに触っているのですが、中にはcakephp2で書かれたツールもあり、今回記事にするのはこちらのツールの改修をする際にハマった内容になります。
やりたいこと
今回実装したかった機能はシンプルで、Vue.js + TypeScriptで書かれたフロントエンドのアプリからcakephp2で実装してあるapiにリクエストを送って、テーブルAに一対多で紐づくテーブルBのレコードを複数更新するという機能です。
元々あった機能として、テーブルBについて単体のレコードを更新できるエンドポイントは実装済みでした。
なので、パッと見た感じたと、改修が必要なのはフロントエンドのみでバックエンドについては今まで使っていたエンドポイントに複数レコードを更新するパラメータを投げればそのまま動いてくれるだろうと予想していました。
元々のコード
一部ですが、以下が元々実装されていたapiのコードになります。
public function save_a_and_b() {
$this->loadModel('A');
$this->loadModel('B');
$update_param = $this->request->data('update_param');
$a_records = json_decode($this->request->data('a_records'), true);
try {
$dataSource = $this->A->getDataSource();
$dataSource->begin();
foreach ($a_records as $r) {
$data = [
'id' => Hash::get($r, 'id'),
'update_param' => $update_param,
]
$this->A->clear();
$this->A->validator()...
$savedA = $this->A->save($data);
// Bの登録
$this->B->deleteAll(['a_id' => $savedA['A']['id']]);
if (bId = Hash::get($r, 'b_id')) {
$this->B->save([
'a_id' => $savedA['A']['id'],
'b_id' => $bId,
])
}
}
} catch(Exception $ex) {
....
}
}
かなり簡略化したコードなのでご容赦ください。
問題のテーブルBの更新についてはforeach文中のif文の中で行っています。
当初より複数レコードの更新、作成に対応できるように実装されていたのか、フロントの側からは配列の形で複数パラメータを投げ、バックではfor文で回してレコードの登録を行う実装になっていました。
意図しない挙動
当初の想像では上記のエンドポイントに複数レコードのパラメータを持つ配列を投げれば、テーブルA, Bに関して作成、更新が行えると予想していました。
paramsB_1をリクエストとして投げるとそれに対応したrecordB_1が作られると思ってください。
上の2つの図を見比べてみると、4つ分更新されるはずのテーブルBのレコードが実際には2つ分しか更新されていません。
解決策
次の一行を追加することで、4つのレコードとも更新されるようになりました。
public function save_a_and_b() {
$this->loadModel('A');
$this->loadModel('B');
$update_param = $this->request->data('update_param');
$a_records = json_decode($this->request->data('a_records'), true);
try {
$dataSource = $this->Region->getDataSource();
$dataSource->begin();
foreach ($a_records as $r) {
$data = [
'id' => Hash::get($r, 'id'),
'update_param' => $update_param,
]
$this->A->clear();
$this->A->validator()...
$savedA = $this->A->save($data);
// Bの登録
$this->B->deleteAll(['a_id' => $savedA['A']['id']]);
if (bId = Hash::get($r, 'b_id')) {
$this->B->clear(); ←この行を追加
$this->B->save([
'a_id' => $savedA['A']['id'],
'b_id' => $bId,
])
}
}
} catch(Exception $ex) {
....
}
}
clear()メソッド
公式Docでは次のような説明がされていました。
Model::clear()
このメソッドは、モデルの状態をリセットし、保存していないデータやバリデーションエラーを リセットするために使用します。
実際にcakephp2のソースコードを探してみると、clear()メソッドは次のような関数になっています。
/**
* This function is a convenient wrapper class to create(false) and, as the name suggests, clears the id, data, and validation errors.
*
* @return bool Always true upon success
* @see Model::create()
*/
public function clear() {
$this->create(false);
return true;
}
どうやらcreate()メソッドをラップした関数のようです。
clear()元となるcreate()メソッドは次のようになっています。
/**
* Initializes the model for writing a new record, loading the default values
* for those fields that are not defined in $data, and clearing previous validation errors.
* Especially helpful for saving data in loops.
*
* @param bool|array $data Optional data array to assign to the model after it is created. If null or false,
* schema data defaults are not merged.
* @param bool $filterKey If true, overwrites any primary key input with an empty value
* @return array The current Model::data; after merging $data and/or defaults from database
* @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array
*/
public function create($data = array(), $filterKey = false) {
$defaults = array();
$this->id = false;
$this->data = array();
$this->validationErrors = array();
if ($data !== null && $data !== false) {
$schema = (array)$this->schema();
foreach ($schema as $field => $properties) {
if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') {
$defaults[$field] = $properties['default'];
}
}
$this->set($defaults);
$this->set($data);
}
if ($filterKey) {
$this->set($this->primaryKey, false);
}
return $this->data;
}
コメント部を見ると、create()メソッドはレコードの書き込みをする際にデフォルト値を読み込んでモデルの初期化をするようです。ループ処理でレコードの保存をする際に活躍するとの記述もあります。
create()メソッドについて、公式Docには次のように書かれています。
Model::create(array $data = array())
このメソッドはデータを保存するためにモデルの状態をリセットします。 実際にはデータベースにデータは保存されませんが、Model::$idフィールドが クリアされ、データベースのフィールドのデフォルト値を元にModel::$data の値を セットします。データベースフィールドのデフォルト値が存在しない場合、 Model::$data には空の配列がセットされます。
$data パラメータ (上記で説明したような配列の形式) が渡されれば、 データベースフィールドのデフォルト値とマージされ、モデルのインスタンスは データを保存する準備ができます (データは $this->data でアクセスできます)。
$data パラメータへ false や null が渡された場合、 Model::$data には空の配列がセットされます。
create() vs clear()
create()は初期化を行うのに対して、clear()はid, dataとvalidation errorをリセットする。
デフォルト値が入ったフォーム入力などを行う場合はcreate()メソッドを使う方が好ましいと思われますが、基本的にclear()を使うようにすれば問題ないのかなと感じました。
まとめ
テーブルの複数レコード更新をする際にはsave()の前にclear()メソッドを使って、モデルの初期化をするようにする必要があるようです。
これにより、データのコンフリクト等を防ぐことができました。