CakePHPで複合ユニークキーのバリデーションを行う方法

CakePHPで複合ユニークキーのバリデーションを行う方法と注意点をメモしておきます。

データベースのユニークキー制約

データベースにおいて、複数のフィールド(カラム)値の組み合わせがユニークであることを保証するためには、データベーステーブルのカラムにユニークキー制約を定義します。

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `first_name` varchar(255) NOT NULL,
  `last_name` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `first_name_last_name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

これで「first_name」と「last_name」の組み合わせが同じレコードは1つのみ追加できるようになります(あくまで説明用の一例です)。

CakePHPでユニークキーのバリデーション

ただ、フォームを利用してレコードの追加を行うような場合は、重複する組み合わせがあった場合にエラーメッセージを表示したいことが多いと思います。こういった場合、CakePHPのModel::isUnique()を利用すれば、複合ユニークキーのバリデーションを行うことができます。

Model::isUnique()は、CakePHP組み込みのバリデーション「isUnique」(フィールドの値がユニークかどうかを検証する)としても利用されるメソッドです。以下はModel::isUnique()の説明になります。

/cake/libs/model/model.php
/**
 * Returns false if any fields passed match any (by default, all if $or = false) of their matching values.
 *
 * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data)
 * @param boolean $or If false, all fields specified must match in order for a false return value
 * @return boolean False if any records matching any fields are found
 * @access public
 */
    function isUnique($fields, $or = true) {
    ...
    }

第一引数にフィールド名をキー、フィールド値を値としたarray(フィールド名 => フィールド値)のような配列、あるいはフィールド名のみのarray(フィールド名)のような配列を渡します。第二引数に比較演算子にORを使うかどうかを渡します。

第一引数にフィールド名のみの配列を渡した場合は、$this->dataからフィールド値が取得されます。第二引数にfalseを渡した場合は、指定したフィールド値の組み合わせがユニークかどうかを検証します。trueの場合は、いずれかのフィールド値がユニークであればtrueを返します。

通常はCookbookで説明されているように、一つのフィールド値がユニークかどうかを検証するのに使われます。これを次のようにパラメーターを指定することで、複数のフィールド値の組み合わせがユニークかどうかを検証することができるようになります。

class User extends AppModel {
    ...
    public $validate = array(
        'first_name' => array(
            'rule' => array('isUnique', array('first_name', 'last_name'), false)
        ),
        'last_name' => array(
            'rule' => array('isUnique', array('first_name', 'last_name'), false)
        )
    );
}

CakePHPで複合ユニークキーのバリデーション

同じフィールド名を何回も書くのは面倒なので、モデルに新しいメソッドを追加します。

複数のフィールド値の組み合わせがユニークかどうかを検証するためには、先ほどのModel::isUnique()を利用した新しいメソッドをモデルに追加します。

/app/models/app_model.php
class AppModel extends Model {
    ...
    public function isUniqueWith($data, $fields) {
        if (!is_array($fields)) $fields = array($fields);
        $fields = Set::merge($data, $fields);
        return $this->isUnique($fields, false);
    }
}
class User extends AppModel {
    ...
    public $validate = array(
        'first_name' => array(
            'rule' => array('isUniqueWith', 'last_name')
        ),
        'last_name' => array(
            'rule' => array('isUniqueWith', 'first_name')
        )
    );
}

これで簡単に複合ユニークキーのバリデーションが行えるようになりました。

複合ユニークキーのバリデーションでの注意点

Model::isUnique()で複数のフィールドを検証する場合や、上記のisUniqueWith()を利用する場合に注意しておくべき点があります。指定したフィールド全てのデータが必ず存在している必要があるという点です。検証するフィールドをarray(フィールド名)のように指定した場合、Model::dataにそのフィールドのデータが存在していなければなりません。指定したフィールドのデータが存在しない場合、そのフィールド値はnullとして扱われます。

例えば、「first_name」のみの更新を行うための以下の様なコードでは、User::dataに「last_name」のデータがセットされないため正しいバリデーション結果が得られず、常にバリデーションが成功したものとして扱われます(カラムにユニークキー制約を定義しているので、組み合わせが重複する場合にはレコードは更新されません)。

class UsersController extends AppController {
    ...
    public function editFirstName($id) {
        ...
        $data = array(
            $this->User->alias => array(
                $this->User->primaryKey => $id,
                'first_name' => 'hoge'
            )
        );
        $this->User->create(null);
        $this->User->set($data);
        if ($this->User->validates()) {
            $this->User->save(null, false);
            更新処理
        }
    }
}

説明とコードの実際の挙動が異なっていたため、コードを修正しました。

正しいバリデーション結果を得るには、「last_name」とともにバリデーションを行う必要があります。

ただし、組み合わせを検証するフィールドが外部キーの場合で、かつModel::saveAll()で関連するモデルとともにレコードを作成・更新する場合には、外部キーのフィールドについては省略できます。saveAll()が、自動的にModel::dataへ外部キーのフィールドを追加してくれるためです。

コメント (2)

この記事を複合ユニークキーのバリデーションの参考にさせて頂きました。
ありがとうございます。

ただ、1点だけ気になった点があったので書かせて頂きます。

“CakePHPで複合ユニークキーのバリデーション”で紹介されているisUniqueを使ったバリデーションですが、これだと動かないようです。

$validateプロパティでメソッドを指定すると、そのメソッドを呼び出す際に第一引数にバリデーション対象のフィールドとその値を含めた配列がきてしまいます。
つまり、例のようにisUniqueを指定してしまうと、引数$fieldsの値がarray(‘first_name’ => ‘斉藤’)、引数$orの値がarray(‘first_name’, ‘last_name’)のようになってしまいます。

カスタムバリデーションメソッドの第一引数にバリデート対象のデータを渡さない方法があるのでしょうか?
もしある場合は教えて頂けると幸いです。

よろしくお願いします。

tfmagicianさん、はじめまして。

>“CakePHPで複合ユニークキーのバリデーション”で紹介されているisUniqueを使ったバリデーションですが、これだと動かないようです。
>カスタムバリデーションメソッドの第一引数にバリデート対象のデータを渡さない方法があるのでしょうか?

たしかにそのままでは動かず、実際にはisUnique()を利用したバリデーション用のメソッドを作らないとダメですね(汗
どうも、記事を書くときに説明をごっちゃにしてしまったようです。

メソッドを作るとなると、結局はisUniqueWith()と同じようになってしまうので記事の該当部分を訂正することにしました。
ご指摘いただきましてありがとうございました。今後ともよろしくお願いします。

コメントフォーム

トラックバック (0)

この記事へのトラックバックはまだありません。

この記事のトラックバックURI
http://dxd8.com/archives/212/trackback/
この記事のURI
http://dxd8.com/archives/212/