読者です 読者をやめる 読者になる 読者になる

CakePHP2で複数フィールドにまたがる一意性制約

CakePHP

複数フィールドの値の組み合わせがユニークかどうかというValidationです。
結論から言うと下記サイトを参考にさせていただき解決しました。


isUnique制約を使って複数フィールドの値の組み合わせをみているってことは
ユニットテストが通るようになったので結果はOKなんですが何やってるのか意味分からんのでisUniqueの実装をみたという話。


ちなみにlib/Cake/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
 */
    public function isUnique($fields, $or = true) {
        if (!is_array($fields)) {
            $fields = func_get_args();
            if (is_bool($fields[count($fields) - 1])) {
                $or = $fields[count($fields) - 1];
                unset($fields[count($fields) - 1]);
            }
        }

        foreach ($fields as $field => $value) {
            if (is_numeric($field)) {
                unset($fields[$field]);

                $field = $value;
                if (isset($this->data[$this->alias][$field])) {
                    $value = $this->data[$this->alias][$field];
                } else {
                    $value = null;
                }
            }

            if (strpos($field, '.') === false) {
                unset($fields[$field]);
                $fields[$this->alias . '.' . $field] = $value;
            }
        }
        if ($or) {
            $fields = array('or' => $fields);
        }
        if (!empty($this->id)) {
            $fields[$this->alias . '.' . $this->primaryKey . ' !='] =  $this->id;
        }
        return ($this->find('count', array('conditions' => $fields, 'recursive' => -1)) == 0);
    }

具体的に関係してくるのはforeachしてるところあたりになります。

実際の動き

参考サイト様を参考にさせていただき作ったisUniqueWithを使ってみましょう

適当ですがこんな指定を想定してください。

    public $validate = array(
        'column1' => array(
            'rule' => array('isUniqueWith', 'column2')
        ),
    );

    public function isUniqueWith($data, $fields) {
        if (!is_array($fields) {
            $fields = array($fields);
        }
        $fields = array_merge($data, $fields);
        return $this->isUnique($fields, false);
    }

まず単純にisUniqueWithにきた時点で第一引数である$dataには制約を設けているカラム名と値の連想配列が入っています
上の例で値が1だとするとこんな感じです

array('column1' => 1);

第二引数である$fieldsには渡している'column2'が入ります。
この配列をmergeしてただisUniqueに投げているだけですので投げた状態の$fieldsは下記のような感じです

array('column1' => 1, 'column2');

ここからisUniqueにきます。foreachのところで$field => $valueというkeyとvalue別々に展開されます。


最初は$field => 'column1', $value => 1になるのはすぐわかると思います。
is_numeric($field)は条件に合致しないので通過してstrposのところにきます。


.があるかどうかをみているのですがこれはModel.column = valueというcountのconditionsの形式を作っています。


それを$fieldsにいれます。


次に'column2'がきますがこの時keyは特に明示されてないので添字が使われます
$field => 0, $value => 'column2'となります。



この状態だとis_numericはtrueになるのでこの処理を通ります。
これが関数定義のところに書いてあるコメントにもかいてあるのだけど
$this->Model->dataの中から$valueの名前のkeyを探しだしてもしあったら
その値を$valueにいれるみたいなことをやっているわけです。


カラム名は$fieldに退避ずみなので$field => 'column2', $value => 2とかになるわけです
(column2の値は2ということにしておきます)


だからisUniqueに渡した時点でカラム名だけでも値みてValidationがはれるわけです。
これでカラム名なんで渡してるのかわかったわけです。


以後は一緒です。conditionsように組み立てて最終的に$fieldsはこんな感じになるはずです
(column2の値は2がおくられていたことにします)

['Model.column1' => 1, 'Model.column2' => 2]

あと絡んでくるのがisUniqueの第二引数orです。
デフォルトだとtrueなのですがこれだとor検索が走ってしまうので意図した動作ではないです。
falseを渡しておきます。


すると上記conditionsでcountをみるわけです。それで0件ならばtrueが返り0件じゃなければfalseが返ります。