Многие ко многим – опасные связи

На этой неделе обновил CakePHP из SVN и тут же перестало работать добавление связей «многие-ко-многим» (hasAndBelongsToMany, HABTM). Небольшое расследование, сравнение изменений в коде и вопросы в гугл группе дали неутешительные результаты.

Во-первых у меня сложилось впечатление, что разработчики, несмотря на статус ReleaseCandidate (RC3) все еще не пришли к единому мнению относительно структуры данных, которую надо скармливать методу save(). :-/

Во-вторых естественные ключи в CakePHP практически «вне закона». Вот непонятно мне это – чем с точки зрения методов find(), save() и др. так сильно отличаются естественные ключи от синтетических? Тем более, что нечисловые первичные ключи все-таки поддерживаются. Это я про поля с UUID.

Проанализировав ситуацию, пришел к выводу, что на жанном этапе отказываться от естественных ключей пока неразумно и надо, видимо, добавлять такие связи самостоятельно. Тем более, что выборка по прежнему работает отлично. Т.е. нужно только реализовать добавление и удаление связей.

Как обычно, начал с поиска уже готовых решений и в блоге «Программируем на CakePHP» нашел код нужных методов. Работает даже лучше прежнего – передаем id записи из модели, массив id записей другой модели и все добавляется в связующую таблицу. Это, конечно, не «автомагия», но зато просто и понятно. :-)

Обратил внимание на некоторую неоптимальность в работе кода. Алгоритм метода добавления примерно такой:

  1. Как уже написал, получаем id записи и массив id связанных записей
  2. Проверяем есть-ли уже какие-нибудь пары id-id и удаляем их
  3. Добавляем связи

Мне это не очень понравилось – считаю, что один SELECT, лучше, чем 5 DELETE. :-) Поэтому немного переписал метод. Во-первых отказался от принудительного удаления уже существующих связей, просто удаляю из массива те id связанных записей, отношение с которыми уже есть. Во-вторых заменил серию INSERT’ов, по одному на каждую связь, на один общий INSERT. Получился вот такой код:

/**
 * Добавляет связь между двумя записями
 *
 * @param mixed $assoc С какой моделью установлена HABTM связь
 * @param mixed $assoc_ids Идентификатор или массив идентификаторов записей, привязываемых к выбранной записи
 * @param integer $id Идентификатор выбранной записи в этой модели
 * @return boolean Success
 */
function addAssoc($assoc, $assoc_ids, $id = null)
{
   if ($id != null) {
      $this->id = $id;
   }

   $id = $this->id;

   if (is_array($this->id)) {
      $id = $this->id[0];
   }

   if ($this->id !== null && $this->id !== false) {

      $db =& ConnectionManager::getDataSource($this->useDbConfig);

      $joinTable = $this->hasAndBelongsToMany[$assoc]['joinTable'];
      $table = $db->name($db->fullTableName($joinTable));

      $keys[] = $this->hasAndBelongsToMany[$assoc]['foreignKey'];
      $keys[] = $this->hasAndBelongsToMany[$assoc]['associationForeignKey'];
      $fields = join(',', $keys);

      if(!is_array($assoc_ids)) {
         $assoc_ids = array($assoc_ids);
      }

      $found_assoc = $db->query(
         "SELECT `{$keys[1]}` FROM $table WHERE `{$keys[0]}` = " .
         $db->value($id, $this->getColumnType($this->primaryKey)) .
         " AND `{$keys[1]}` IN (" .
         join(',', $db->value($assoc_ids)) .
         ")");

      $found_assoc = Set::classicExtract(
         $found_assoc,
         '{n}.' . $db->fullTableName($joinTable, false) . '.' . $keys[1]
      );

      $assoc_ids = array_diff($assoc_ids, $found_assoc);

      if (!empty($assoc_ids))
      {
         foreach ($assoc_ids as $assoc_id)
         {
            $insert_values[] =
               '(' .
               $db->value($id, $this->getColumnType($this->primaryKey)) .
               ',' .
               $db->value($assoc_id) .

               ')';
         }

         $db->execute("INSERT INTO {$table} ({$fields}) VALUES " . join(',', $insert_values));
         unset ($values);
      }

      return true;
   } else {
      return false;
   }
}

Метод удаления связей переделал похожим образом.

Опубликовано 15.11.2008 в 01:27 · Автор Сергей · Ссылка
Рубрики: CakePHP · Теги: , ,
  • http://com.spweb.ru/ Мета

    Доработка очень хорошая, спасибо. Если вы не против, хочу разместить у себя на блоге в разделе «сниппетов».

  • http://com.spweb.ru Мета

    Доработка очень хорошая, спасибо. Если вы не против, хочу разместить у себя на блоге в разделе «сниппетов».

  • Сергей

    Размещайте, конечно. Еслиб я против был — в интернете не публиковал бы :-)

  • Сергей

    Размещайте, конечно. Еслиб я против был — в интернете не публиковал бы :-)

  • evilbloodydemon

    ох уж мне эта самодеятельность. считаете, что что-то поломалось – заводите тикет в багтрекере, знаете как починить – добавляйте патч там же, не знаете как починить – сделайте тест, чтобы показать что не работает.
    вот куда этот ваш код добавлять, в core? и что потом с ним делать при апдейте кэйка?

  • evilbloodydemon

    ох уж мне эта самодеятельность. считаете, что что-то поломалось – заводите тикет в багтрекере, знаете как починить – добавляйте патч там же, не знаете как починить – сделайте тест, чтобы показать что не работает.
    вот куда этот ваш код добавлять, в core? и что потом с ним делать при апдейте кэйка?

  • Сергей

    А смысл заводить патч, если авторы решили в RC3 поменять в очередной раз структуру данных для HABTM и это не баг, а фича?

    Зачем в core? Я его вообще не трогаю. Можете добавить по выбору:
    1. в свою модель.
    2. в AppModel
    3. написать behavior

  • Сергей

    А смысл заводить патч, если авторы решили в RC3 поменять в очередной раз структуру данных для HABTM и это не баг, а фича?

    Зачем в core? Я его вообще не трогаю. Можете добавить по выбору:
    1. в свою модель.
    2. в AppModel
    3. написать behavior

  • Сергей

    И еще вдогонку. Начиная с RC3 «автомагически» сохраняются связи только если первичные ключи связываемых моделей — типа INT. Или CHAR(36) (UUID). Все остальное игнорируется и не сохраняется. Т.е. естественный ключ типа VARCHAR(100) не сохранится в связующей таблице. И ответ на тикет будет простым – используйте синтетические целочисленные ключи.

  • Сергей

    И еще вдогонку. Начиная с RC3 «автомагически» сохраняются связи только если первичные ключи связываемых моделей — типа INT. Или CHAR(36) (UUID). Все остальное игнорируется и не сохраняется. Т.е. естественный ключ типа VARCHAR(100) не сохранится в связующей таблице. И ответ на тикет будет простым – используйте синтетические целочисленные ключи.