21 августа 2008 г.

Составные первичные индексы и CakePHP

Общеизвестный факт: 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, видимо, не удастся, на его значение, а также просто наличие много чего завязано.