Blog: Alter World

Виртуальный мир является своеобразным Alter Ego мира реального…

Организация списка меток в статье

2 комментария 19 июля 2010, 16:52 • Проектирование, Таги

Вчера поздно вечером в разговоре с twitter-пользователем TheNatd всплыл вопрос об организации управлением списком меток (или тагов) к некоторой статье. В твиттере свои соображения на этот счет публиковать не очень удобно из-за краткости сообщений, поэтому я решил описать их здесь и в твиттере дать ссылку.

Итак, таблица тагов в обсуждаемой задаче представлена двумя полями — id и TagName, таблица связи тага и статьи представлена классическим видом из трех полей id, PostID и TagID. Остальные таблицы из модели я пока трогать не буду — они в данном случае меня не интересуют. Как видим из модели, связи в таблице связи между статьями и тагами реализованы как многое-к-многому, что хорошо для нашего случая.

Условимся, что ключевые слова задаются в виде списка термов, разделенных запятой и набираемых в поле textarea.

Опишем случай добавления статьи с набором тагов.
При добавлении статьи данные на обработчик обычно передаются методом POST и после обработки складируются в базу данных. Допустим, что список тагов передается в переменной tags (Атрибут name элемента textarea). В обработчике разбиваем строку tags на элементы массива, в качестве разделителя для разбивки используем запятую. После разбиения делаем следующие итерации для каждого элемента массива:

  1. Делаем SELECT-запрос к таблице Tag вида: SELECT id FROM Tag WHERE TagName='".$tags[$i]."';
  2. Если предыдущий запрос вернул непустой результат, то делаем INSERT-запрос в таблицу связи: INSERT INTO TagsInPost SET TagID='".$TagID."', PostID='".$PostID."';иначе делам три запроса (или два — зависит от конкретной СУБД и ее «драйвера»): INSERT INTO Tag SET TagName='".$tags[$i]."'; SELECT id FROM Tag ORDER BY id DESC LIMIT 1; INSERT INTO TagsInPost SET TagID='".$TagID."', PostID='".$PostID."';

То есть, говоря обычными словами мы реализуем следующий алгоритм: Берем очередное ключевое слово, проверяем, если ли оно уже в базе данных и если есть, то выставляем связь иначе сперва добавляем новое ключевое слово в БД, а потом уже выставляем связь.

Опишем случай удаления статьи с набором тагов.
В этом случае все делается элементарно — удаляем все связи между конкретной статьей и списком тагов. Сами таги не удаляем, потому что их могут использовать в других статьях. На языке SQL описать этот случай можно так: DELETE FROM TagsInPost WHERE PostID='".$PostID."';.

Опишем случай обновления статьи с набором тагов.
В случае обновления статьи мы не можем без дополнительных проверок точно знать, обновился ли список тагов или нет. Поэтому для этого случая обычно поступают следующим способом: сначало удаляют все связи (случай «удаления статьи»), а потом считают набор тагов новым и добавляют их с проверками на наличие (случай «добавления статьи»).

Минусом этого случая является то, что первичные ключи в таблице связи будут расти при каждом редактировании статьи, независимо от того, обновлялся ли список тагов («меток» или «ключевых слов») или нет.

Если уменьшение числа значений первичного ключа критично, то можно поступить другим способом, но он будет более накладен из-за большего числа SQL-запросов.

Шаги таковы:

  1. Делаем SELECT-запрос в две связанных таблицы, чтобы выяснить, если ли такое ключевое слово в базе данных и есть ли связь между ним и статьей: SELECT Tag.id FROM TagsInPost p JOIN Tag t ON t.TagID=p.TagID WHERE p.PostID='".$PostID."' AND t.TagName='".$tags[$i]."';
  2. Если запрос вернул ненулевой результат, то такое слово есть и есть связь — ничего не делам, иначе проверяем, есть ли уже такое слово в базе данных в принципе: SELECT id FROM Tag WHERE TagName='".$tags[$i]."';
  3. Если предыдущий запрос вернул непустой результат, то делаем INSERT-запрос в таблицу связи: INSERT INTO TagsInPost SET TagID='".$TagID."', PostID='".$PostID."';иначе делам три запроса (или два — зависит от конкретной СУБД и ее «драйвера»): INSERT INTO Tag SET TagName='".$tags[$i]."'; SELECT id FROM Tag ORDER BY id DESC LIMIT 1; INSERT INTO TagsInPost SET TagID='".$TagID."', PostID='".$PostID."';

В этом методе рост значений первичного ключа будет менее прогрессирующим, но этот способ будет более дорогим, из-за большего числа SQL-запросов.

Первое примечание: При написании SQL-запросов я предполагал, что по каким-либо причинам хранимые процедуры в СУБД не используются (либо их не умеет СУБД, либо есть ограничения модели данных).

Примечание второе: Для организации вывода списка из n последних связанных статьей обычно выборка делается как раз по тагам. В этом случае может быть критично знать порядок ключевых слов, т.к. более весомые слова обычно указывают первыми. Для учета этого факта в таблицу связи нужно добавить еще одно поле, которое хранило бы данные о порядковом номере слова. Соответственно, обновление данных по тагам несколько усложнится, особенно во втором случае, где нужно будет дополнительно проверять «местоположение связи» и при необходимости проводить UPDATE-запрос для актуализации информации.

Ещё заметки на эту тему:

2 комментария

Вы можете подписаться на комментарии к этой статье через RSS или отправить к ней TrackBack.

  1. TheNatd • 19 июля 2010 г. в 19:37

    Статья очень хорошая, я принципе всё так и думал, меня немного смущает рост первичного ключа. Но мы же работаем с Entity Framework запросы у нас Linq вместо sql, я это к тому, что оперируем мы объектами.

    Я думал сделать так:

    До обновления получаем все теги у статьи, сравниваем с списком который пришел из формы. Если каких то объектов нет — мы их удаляем, если есть новые заносим в базу. (И покамись мы сделали только один запрос, всех тегов у статьи) а дальше уже работаем с обьектами(списком, масивом, лучше всего типизированным списком).

    Но так ли критичен рост PK, и намного ли мы замедлим его рост?

    Насчёт весомости тега, чем больше записей у тега тем он весомей, разве нет?

    Статья хорошая в закладочки обязательно:)

  2. Александр Вольф • 19 июля 2010 г. в 20:19

    Но так ли критичен рост PK, и намного ли мы замедлим его рост?

    Размер ключа имеет смысл сделать побольше, с пристрелкой на будущее, но в любом случае при проектировании нужно ориентироваться на примерные объемы информации и сразу же заложить возможность дальнейшей модернизации.

    Я как-то в одном движке форума нарвался на то, что со временем вдруг «сломались» прикрепления — новые не добавляет, или после «добавления» отображалось совсем не то, что добавляли. Разбор полетов дал причину: разработчики пожадничали на размере первичного ключа и для внесения новой записи просто не хватало разряда.

    Насчёт весомости тега, чем больше записей у тега тем он весомей, разве нет?

    Вот тут некоторое недопонимание — если брать абсолютное число связей для одного из тагов, то это будет «вес» этого тага. А как быть, если нужно указать веса тагов для конкретной статьи? Если брать веса по количеству, то мы получим среднюю температуру по палате и значительные накладные расходы на подсчет количества вхождений всех тагов. Вот тут как раз и пригодится указание веса тага для конкретной статьи. И на основе, скажем, 3-х самых весомых метки строить список из 5-10 связанных записей.

Оставить свое мнение

XHTML: Вы можете использовать эти тэги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>