Общеизвестный факт: CakePHP не поддерживает составные первичные индексы. Разработчики предлагают добавить в качестве первичного индекса поле id, а составной индекс из нескольких полей просто сделать уникальным.
Это, в общем-то, разумно — первичный индекс используется в моделях для установления и проверки связей, поэтому добавление процедур для работы с составными индексами неплохо увеличит объем кода, что, в свою очередь, скажется на производительности. Более того, использование составных индексов в качестве первичных — редкое явление, хотя я сходу придумал пару ситуаций когда это оправданно.
Схема с добавлением еще одного поля, предлагаемая разработчиками Cake неплохо функционирует, однако контроль за уникальностью составного индекса перекладывается на веб-разработчика.
Для определения, вставляется-ли новая запись (INSERT…) или обновляется старая (UPDATE…) при вызове из метода save(), Cake поступает очень просто: продеряет наличие в элемента массива $this->data[‘Model’][‘id’] и, если он есть, проверяет наличие записи в БД с таким ID. Если запись есть, значит UPDATE, если нет – INSERT.
В своем контроллере, который работает с моделью с составным индексом я сначала искал id с помощью Model->field() и, если находил, присваивал его $data[‘Model’]'[‘id’], после чего Cake искренне считал, что запись надо обновить, а не добавить. Это не совсем Cake Way — не дело контроллера определять, режимы записи данных.
Никаких указаний на то, как влиять на выбот UPDATE/INSERT, в учебнике по CakePHP я не нашел. Тяжело вздохнув решил сначала переопределить метод save(), добавив в него код поиска поля ID по моему уникальному индексу, а потом вызывать родительский метод save. В процессе разгладывания API, в частности кода метода save, обнаружил, что save() вызывает метод getID, который и ищет id записи во всех вариантах ее указания.
Тогда я и добрался до этого самого метода exists в cake/model.php. Нужный мне кусок кода выглядел вот так:
if ($this->getID() === false || $this->useTable === false) {
return false;
}
В коде своей модели я переопределил этот метод, скопировав его весь, а вышеуказанные строчки заменил на такие:
if ($this->useTable === false) { //Модель без таблицы
return false;
}
/* Три поля таблицы supplier_id, supplier_param1 и supplier_param2
* это у меня как раз и есть уникальный индекс
*/
if ($this->getID() === false) // $this->id не указан
{
$id = $this->field('id', array(
$this->alias . '.supplier_id'=>
$this->data[$this->alias]['supplier_id'],
$this->alias . '.supplier_param1'=>
$this->data[$this->alias]['supplier_param1'],
$this->alias . '.supplier_param2'=>
(string)$this->data[$this->alias]['supplier_param2']));
if ($id !== false) { // Удачно получили id
$this->data[$this->alias]['id']=$id;
$this->id = $id;
$this->__exists = true;
return true;
}
return false;
}
Т.е. фактически вставил выполнение запроса на получение id записи в этот метод. Теперь я из контроллера, как и положено, просто вызываю Model->save() и никаких проблем с ошибками про уникальный индекс нет. :-)
Обидно, что в документации на CakePHP никаких упоминаний про этот самый волшебный метод exists я не нашел, он упоминается только в перечислении методов класса в описании API.
Пока писал пост, подумалось: в принципе, можно сделать и более общий код — в модель добавить свойство-массив, с перечислением полей уникального индекса, а в этом коде в запрос подставлять имена полей из этого массива.
Совсем избавиться от поля ID, видимо, не удастся, на его значение, а также просто наличие много чего завязано.