Сен 27 2008

Добавление и удаление ассоциаций между записями

Раздел: МоделиМета @ 21:53

Во многих приложениях используются связи типа многие-ко-многим - HasAndBelongsToMany или HABTM в терминологии CakePHP. Они нужны для создания меток к записям в блоге, привязки атрибутов к товарам в интернет магазине, пользователей к пользователям в социальных сетях и т.д.

Для создания связи HABTM в CakePHP используется вспомогательная таблица, в которой хранятся два идентификатора связываемых данных. Например, если нам надо связать таблицы posts и tags, то вспомогательная таблица будет иметь вид:

1
2
3
4
5
CREATE TABLE `posts_tags` (
  `post_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY  (`post_id`,`tag_id`)
) ENGINE=MyISAM;

При создании записи, если для нее выбраны метки, т.е. присутствуют в массиве, передаваемом в $this->Post->save(), CakePHP автоматически создаст связи в этой вспомогательной таблице. Но это только при создании записи. А как сделать редактирование именно связей? Допустим, мне надо добавить метку для записи в блоге. И запись, и такая метка уже существуют, но не связаны. Как их связать?

В своей студии мы разрабатывали приложение, активно использующее редактирование связей на CakePHP 1.1. На сайте http://bakery.cakephp.org удалось найти следующие две функции, как раз подходящие для этого. Функции надо добавить в файл /app/app_model.php, если нужно чтобы они были доступны для всех моделей приложения.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
     * Добавляет связь между двумя записями
     *
     * @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);
            }
 
            // Для избежания дублирования
            $this->deleteAssoc($assoc,$assoc_ids,$id);
 
            foreach ($assoc_ids as $assoc_id) {
                $values[]  = $db->value($id, $this->getColumnType($this->primaryKey));
                $values[]  = $db->value($assoc_id);
                $values    = join(',', $values);
 
                $db->execute("INSERT INTO {$table} ({$fields}) VALUES ({$values})");
                unset ($values);
            }
 
            return true;
        } else {
            return false;
        }
    }
 
    /**
     * Удаляет связи между записями
     *
     * @param integer $id Идентификатор записи в этой модели
     * @param mixed $assoc С какой моделью установлена HABTM связь
     * @param mixed $assoc_ids Идентификатор или массив идентификаторов записей, привязанных к выбранной записи
     * @return boolean Success
     */
    function deleteAssoc($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));
 
            $mainKey = $this->hasAndBelongsToMany[$assoc]['foreignKey'];
            $assocKey = $this->hasAndBelongsToMany[$assoc]['associationForeignKey'];
 
            if(!is_array($assoc_ids)) {
                $assoc_ids = array($assoc_ids);
            }
 
            foreach ($assoc_ids as $assoc_id) {
                $db->execute("DELETE FROM {$table} WHERE {$mainKey} = '{$id}' AND {$assocKey} = '{$assoc_id}'");
            }
 
            return true;
        } else {
            return false;
        }
    }

Использовать их очень просто. Например, при редактировании записи в блоге, нам надо привязать к ней метку:

1
$this->Post->addAssoc('Tag', $tag['id'], $post_id);

Надеюсь, этот код работает и в CakePHP 1.2. Будет время — проверю.

Теги: , , , , ,

5 Responses to “Добавление и удаление ассоциаций между записями”

  1. Ярослав says:

    Не совсем удачный пример :)
    “…..надо связать таблицы posts и tags
    то..
    CREATE TABLE `vdv_articles_cats` (
    `article_id` int(11) NOT NULL,
    `cat_id` int(11) NOT NULL,
    PRIMARY KEY (`article_id`,`cat_id`)
    ) ENGINE=MyISAM; ”
    Насколько я помню, таблица должна быть типа posts_tags
    (в альфавитном порядке)
    т.е.
    CREATE TABLE `posts_tags` (
    `post_id` int(11) NOT NULL,
    `tag_id` int(11) NOT NULL,
    PRIMARY KEY (`post_id`,`tag_id`)
    ) ENGINE=MyISAM;

  2. Мета says:

    Да, точно, в примере таблицы опечатка - копировал из одного своего проекта и забыл поправить. Спасибо что заметили.
    Теперь поправил.

  3. Boris says:

    Буквально сегодня маялся с many-to-many. В скором времени опробую и этот пример.
    У меня была такая задача:
    допустим у связи есть еще и свойства, ну например (хотя в данном контексте это скорее бессмысленно) в таблице posts_tags помимо стандартных столбцов есть еще дополнительный столбец, допустим, времени отнесения того или иного тега к данному посту.
    Данный столбец “время отнесения” должен быть именно в этой таблице в силу нормализации и рефакторинга базы.
    Так вот, при такой организации не выходило выцепить это свойство (отобразить в вивере и т.п.) при связи объектов.
    Решал проблему по разному, в одном случае просто денормализовал, в другом - отказался от many-to-many, а в третьем - создал отдельную модель над данной таблицей many-to-many связями и внес в нее belongsTo к таблицам Tags и Posts. Может кто еще что подскажет? (Надеюсь, я понятно выразился)

  4. Мета says:

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

  5. WEB рукоделие » Многие ко многим - опасные связи says:

    [...] Как обычно, начал с поиска уже готовых решений и в блоге “Программируем на CakePHP” нашел код нужных

Напиши комментарий!