Skip to content

Instantly share code, notes, and snippets.

@zmts
Last active November 13, 2020 01:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save zmts/cb22cd51ecf43479d2f6d5368a6f3061 to your computer and use it in GitHub Desktop.
Save zmts/cb22cd51ecf43479d2f6d5368a6f3061 to your computer and use it in GitHub Desktop.
About Tags (Имплементация тегов)

Задача найти наиболее правильное и удобное решение имплементации функцианала 'Теги'

Дано:

  • RESTful API\PostgreSQL
  • Сущность TAG (таблица tags)
  • Все теги всех юзеров хранятся в одной таблице(tags). Названия тегов уникальны(qnique).
  • Сущность POST(новость, таблица posts)
  • Все посты всех юзеров хранятся в одной таблице(posts)
  • Отношение ManyToMany через posts_tags связующую таблицу
  • На момент добавления тега сущность поста создана и имеет свой уникальный ID

Задачи:

  • Получение всех постов по тегу
  • Получение поста со всеми связанными тегами
  • Добавление тега у клиента происходит во вьюхе редактирования поста
  • Добавление существующего тега к посту
  • Добавление нового тега к посту

Вариант первый:

  • Теги вводятся в специально отведенный для них блок(аля medium, evernote, github)
  • Теги в одном посте уникальны и не могут повторяться

Story:

  • Юзер начинает вводить название тега
    • Система автокомплита производит поиск тега(глобально по всем тегам в БД)
      • Юзер выбирает предложенный тег системой (1)
        • Система проверяет присуцтвует ли тег в посте
          • FALSE >> аттачит тег к посту(делает запись в связующей таблице posts_tags)
          • TRUE >> выдает ошибку
    • Юзер вводит новый тег(юзер игнорирует автокомплит)
      • Система смотрит нет ли такого же тега в БД
        • FALSE >> создает тег и аттачит тег к посту(делает запись в связующей таблице posts_tags)
        • TRUE >> Система проверяет присуцтвует ли этот тег в посте
          • FALSE >> аттачит тег к посту(делает запись в связующей таблице posts_tags)
          • TRUE >> выдает ошибку

Вариант второй:

  • Теги вносятся в тело поста(аля twitter, facebook)
  • Юзер может добавлять скольнеобходимое количество тегов(в разумных пределах)
  • Теги в одном посте не уникальны и могут повторяться

Story:

  • Юзер начинает вводить название тега
    • Система автокомплита производит поиск тега(глобально по всем тегам в БД)
      • Юзер выбирает предложенный тег системой (1)
        • Система проверяет присуцтвует ли тег в посте
          • FALSE >> добавляет ссылку на тег в контент поста и аттачит его к посту(делает запись в связующей таблице posts_tags)
          • TRUE >> добавляет только ссылку на тег в контент поста(дабы избежать дублирования в связующей таблице)
    • Юзер вводит новый тег(юзер игнорирует автокомплит)
      • Система смотрит нет ли такого же тега в БД
        • FALSE >> создает тег и аттачит его к посту(делает запись в связующей таблице posts_tags)
        • TRUE >> Система проверяет присуцтвует ли этот тег в посте
          • FALSE >> добавляет ссылку на тег в контент поста и аттачит его к посту(делает запись в связующей таблице posts_tags)
          • TRUE >> добавляет только ссылку на тег в контент поста(дабы избежать дублирования в связующей таблице)

(1) - Если желаемынй тег присуцтвует в списке предложенный системой, система принудительно вибирает тег из списка и аттачит его к посту. Что предотвращает ошибку в БД при попытке создать дубликат тега.

P.S.

  • Правильно ли представленны story для решения задачи ?
  • Каково ваше видение реализации подобного функционала ?
  • Если посмотреть на API github или twitter то у них ID в тегах не используются. Почему так ? Или они хайдят ID или ... ???
  • Прошу в комментарии, буду рад получить советы от более опытных разработчиков !)
// в итоге реализации первого варианта (node.js) >>
router.post('/:post_id/attachTag/:tag_id', attachTagToPost());
router.post('/:post_id/attachTag/', createAndAttachTagToPost());
/**
* ------------------------------
* description: attach Tag to Post
* ------------------------------
* access: owner, SU, ADMINROLES
* url: posts/:post_id/attachTag/:tag_id
* method: POST
*/
function attachTagToPost() {
return function (req, res) {
Post.checkTagByIdInPost(req.params.post_id, req.params.tag_id)
.then(function () {
return Post.attachTagToPost(req.params.post_id, req.params.tag_id);
})
.then(function (post) {
res.json({success: true, data: post});
})
.catch(function (error) {
res.status(error.statusCode || 403).send({success: false, description: error});
});
};
}
/**
* ------------------------------
* description: create and attach Tag to Post
* ------------------------------
* access: owner, SU, ADMINROLES
* url: posts/:post_id/attachTag/
* method: POST
* request: {"name": "string"}
*/
function createAndAttachTagToPost() {
return function (req, res) {
Tag.create(req.body)
.then(function (tag) {
return Post.attachTagToPost(req.params.post_id, tag.id);
})
.then(function (post) {
res.json({success: true, data: post});
})
.catch(function (error) {
res.status(error.statusCode || 403).send({success: false, description: error});
});
};
}
@zbitname
Copy link

zbitname commented Feb 27, 2017

Post - это "посляшка"? :)
https://translate.google.com/#en/ru/post
Если вы пытаетесь реализовать что-то вроде блога или новостного движка, то лучше называть вещи своими именами, а слово "пост" в русском языке (да и в английском тоже) имеет несколько (читай как "совсем") другое значение, нежели "статья".

Насчёт тэгов:
Если бы вы работали с не реляционной СУБД, то такого вопроса бы не появилось, т.к. там подобные отношения являются плохой практикой. Но и в РСУБД не нужно пытаться из всего сделать сущность и везде использовать отношения, когда введение отношений усложняет понимание и работу с даными.

Если рассматривать "тэг" как самостоятельную сущность, то получается, что это сущность с одним признаком. Но мне кажется, что "тэг" - это всё таки признак сущности "статья". Если нет никакого смысла делать из признака сущности отдельную сущность - то лучше этого не делать. Как говорится: не плодите сущности без необходимости (бритва Оккама).

Я в одном своём проекте где нужно было реализовать работу со статьями сделал без отдельных таблиц и отношений, получилось намного быстрее реализовать и понятно сразу что к чему, к тому же для такого решения проще писать и бэкэнд и фронтэнд, т.к. нет "лишних" методов для работы с тэгами.

Ответ на вопрос "Если посмотреть на API github или twitter то у них ID в тегах не используются. Почему так ? Или они хайдят ID или ... ???": в чём проблема сделать PK из тэга в таблице, которая связывает тэги и статьи?

@zmts
Copy link
Author

zmts commented Feb 27, 2017

@zbitname Спасибо за развернутый ответ!
С постом согласен намудрил. Нужно было назвать сущность ARCTICLE.

Сперва сначала подумал сделать как вы и писали. Добавить теги как свойство новости.
Но есть задача получение всех постов по ID тега и наоборот всех тегов по ID новости. Варианта кроме как использовать ManyToMany я не нашел.

А вот на счет создать unique index по двум полям в таблице posts_tags это мысль. Пускай будет проверка на уровне базы также

@zbitname
Copy link

Зачему тегу идентификатор? Мне стало интересно откуда появилось такое требование? :)

@zmts
Copy link
Author

zmts commented Feb 27, 2017

@zbitname А как хранить теги в таблице без ID ? Можно конечно искать и чисто по названию тега.
То есть так: select from tags where name == 'super_tag' >> смотрим ID >> выгребаем по ID все связанные новости

@zbitname
Copy link

zbitname commented Feb 27, 2017

А зачем для тегов таблица своя? Аргументируйте решение :)

Получить все статьи по тегу - не проблема, будет что-то вроде этого:

SELECT * FROM articles WHERE :tag = ANY tags

или теги по статье:

SELECT unnest(tags) FROM articles WHERE id=:article_id

или все уникальные теги:

SELECT DISTINCT unnest(tags) FROM articles

или как-то так, по памяти пишу :)

@zbitname
Copy link

zbitname commented Feb 27, 2017

А как хранить теги в таблице без ID ?

Колонка с PK не обязательно должна называться id, хоть наличие колонки id(PK) и является хорошим тоном.

@zmts
Copy link
Author

zmts commented Feb 27, 2017

@zbitname

А зачем для тегов таблица своя? Аргументируйте решение :)

Изначально казалось правильно организовать отношение ManyToMany.
Создание аля вот такой таблицы мне показалось не правильно поскольку добавление в поле более чем одного тега нарушает первое правило нормализации.

posts table
------------------------------------------------
id | name | content | tags
------------------------------------------------
1 | post_name1 | post content | car, moto, auto 
------------------------------------------------

@zbitname
Copy link

Если не рассматривать "тэг" как отдельную сущность, а рассматривать как признак сущности, наряду с такими признаками как "name" или "content", то всё нормально. И предложенный мной вариант не подразумевает отношений с другими таблицами/сущностями, так что ниодно из правил нормализации в этом случае не нарушается и вовсе не применимы даже.
Если выделять "тэг" как отдельную сущность - пожалуйста, только зачем? Вы подумайте насколько это усложнит вам работу и насколько сложнее этот вариант будет поддерживать, чем вариант без "лишней" реляционной составляющей.

@olegkoskin
Copy link

@zbitname разве тэг как признак сущности не нарушает нормальную форму, что атрибут кортежа должен быть простым? Не предлагает же вы хранить несколько записей об статье, если тэгов несколько?

@zbitname
Copy link

zbitname commented Feb 28, 2017

@0136, о каких нормальных формах может быть речь если в предложенном мной решении нет отношений? Храненике массива вместо скалярного типа данных никак не относится ни к одной из нормальных форм. Где написано, что хранение не скалярного значения нарушает хоть что-нибудь? :)

Какой смысл вы вкладываете в эту ↓ фразу?

тэг как признак сущности не нарушает нормальную форму, что атрибут кортежа должен быть простым?

Меня больше всего интересует что вы подразумеваете под словом "простым".

Да и что вы к этим нормальным формам прицепились. Если вводить отношения/сущности при любой возможности у вас получится слишком академический подход к построению структуры БД, а академический подход не всегда является удобным, понятным, да и его не всегда можно назвать объективно правильным. Я найду много причин не вводить новую сущность, а вы найдёте столько же причин для того, чтобы её ввести. Разница есть только в том, что вы потом придёте к мысли, что вы зря это сделали, а я быстрее решу поставленную задачу. Поэтому вопрос стоит в том - насколько это необходимо. Я думаю, что в данном кейсе, да и во всех похожих, вынесение тега в отдельную сущность - лишнее.

У меня очень богатый опыт работы как с SQL так и с NoSQL, поверьте мне, я знаю о чём говорю :)

@olegkoskin
Copy link

@zbitname Ну, тс будет решать эту задачу на sql, надо найти оптимальное решение с учетом требований. НФ иногда помогают в этом.
Под простым я имел в виду: "Переменная отношения находится в первой нормальной форме тогда и только тогда, когда в любом допустимом значении отношения каждый его кортеж содержит только одно значение для каждого из атрибутов" (с) wiki
Я не вижу ничего зазорного сделать связь many-to-many. А так созревает больше вопрос к тс: "Почему sql", документоориентированная бд все таки лучше ложится на данную задачу

@zbitname
Copy link

zbitname commented Mar 1, 2017

@0136, в приведённой вами цитате ключевое слово "Переменная отношения находится". Нормальные формы применимы только в реляционных моделях. Реляционная модель - это когда значение ячейки имеет ссылку на другую сущность. Если ссылки на другую сущность нет - значит это не реляционная модель и ни о каких "нормальных формах" по отношению к этой модели говорить нельзя и не правильно вовсе.
Если вы используете РСУБД - это ещё не означает, что у вас там обязаны быть реляционные модели, вы их можете так организовать, но это не обязательно. Везде должен быть здравый смысл :)

И я тоже не вижу ничего зазорного в many-to-many в данном случае, но настаиваю на том, что в данном случае это слишком академический подход и именно в случае с тегами такой подход будет не так удобно реализовывать/поддерживать/использовать как подход без реляционной составляющей.

Не плодите сущности без необходимости :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment