對資源打標籤在建站過程當中是很常見的需求,有些時候咱們須要給文章打標籤,有些時候咱們須要給用戶打標籤。實現一個標籤系統其實並不難,其本質就是一個多對多的關係-我能夠對同一篇博客打多個標籤,同時也能夠把一個標籤打到不一樣的博客身上。這篇文章主要經過分析標籤系統的原理,並用PostgreSQL來實現一個可以爲多種資源打標籤的標籤系統。git
先從單一資源開始,所謂單一資源即是,咱們只給一種數據資源打標籤。假設咱們須要給博客文章打標籤,那麼咱們須要構建如下幾個表:github
posts
,用於存儲文章的基本信息。tags
,用於存儲標籤的基本信息。tags_posts
,存儲雙方的id並造成多對多的關係。表設計圖大概是sql
先進入數據庫引擎並建立對應的數據庫數據庫
postgres=# create database blog;
CREATE DATABASE
postgres=# \c blog;
blog=#
複製代碼
經過SQL語句建立上面所提到的數據表編程
CREATE TABLE posts (
id SERIAL,
body text,
title varchar(80)
);
CREATE TABLE tags (
id SERIAL,
name varchar(80)
);
CREATE TABLE tags_posts (
id SERIAL,
tag_id integer,
post_id integer
);
複製代碼
每一個表都只是包含了該資源最基礎的字段, 到這一步爲止其實已經構建好了一個最簡單的標籤系統了。接下來則是填充數據,個人策略是添加兩篇文章,五個標籤,給標題爲Ruby
的文章打上language
標籤,給標題爲Docker
的文章打上container
的標籤,兩篇文章都要打上tech
標籤編程語言
-- 填充文章數據
INSERT INTO posts (body, title) VALUES ('Hello Ruby', 'Ruby');
INSERT INTO posts (body, title) VALUES ('Hello Docker', 'Docker');
-- 填充標籤數據
INSERT INTO tags (name) VALUES ('language');
INSERT INTO tags (name) VALUES ('container');
INSERT INTO tags (name) VALUES ('tech');
-- 爲相關資源打上標籤
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'container'), (SELECT id FROM posts WHERE title = 'Docker'));
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'tech'), (SELECT id FROM posts WHERE title = 'Docker'));
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'tech'), (SELECT id FROM posts WHERE title = 'Ruby'));
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'language'), (SELECT id FROM posts WHERE title = 'Ruby'));
複製代碼
而後分別查詢兩篇文章都被打上了什麼標籤。工具
blog=# SELECT tags.name FROM tags, posts, tags_posts WHERE tags.id = tags_posts.tag_id AND posts.id = tags_posts.post_id AND posts.title = 'Ruby';
name
----------
language
tech
(2 rows)
blog=# SELECT tags.name FROM tags, posts, tags_posts WHERE tags.id = tags_posts.tag_id AND posts.id = tags_posts.post_id AND posts.title = 'Docker';
name
-----------
container
tech
(2 rows)
複製代碼
兩篇文章都被打上指望的標籤了,相關的語句有點長,通常生產線上不會這樣直接操做數據庫。各類編程語言的社區通常都對這種數據庫操做進行了封裝,這爲編寫業務代碼帶來了很多的便利性。post
若是隻須要對一個數據表打標籤的話,依照上面的邏輯來設計表已經足夠了。可是現實世界每每沒那麼簡單,假設除了要給博客文章打標籤以外,還須要給用戶表打標籤呢?咱們須要把表設計得更靈活一些。若是繼續用tags
表來存標籤數據,爲了給用戶打標籤還得另外建一個名爲tags_users
的表來存儲標籤與用戶數據之間的關係。ui
但更好的作法應該是採用名爲多態
的設計。建立關聯表taggings
,這個關聯表除了會存儲關聯的兩個id以外,還會存儲被打上標籤的資源類型,咱們根據類型來區分被打標籤的究竟是哪一種資源,這會在每條記錄上多存了類型數據,不過好處就是能夠少建表,全部的標籤關係都經過一個表來存儲。spa
Ruby比較流行的標籤系統ActsAsTaggableOn 就沿用了這個設計,不過它的類型字段直接存的是對應資源的類名,或許是爲了更方便編程吧,數據大概以下:
naive_development=# select id, tag_id, taggable_type, taggable_id from taggings;
id | tag_id | taggable_type | taggable_id
----+--------+----------------------+-------------
1 | 1 | Refinery::Blog::Post | 1
2 | 2 | Refinery::Blog::Post | 1
3 | 3 | Refinery::Blog::Post | 1
複製代碼
先經過taggable_type
獲取類名,而後再利用taggable_id
的數據就能準確獲取相關的資源了。
表設計圖大概以下
這裏我不從新建表了,而直接修改原有的表,並進行數據遷移
type
字段用於存儲資源類型。taggings
。post_id
字段改爲更通用的名字taggable_id
。type
字段統一填數據post
。ALTER TABLE tags_posts ADD COLUMN type varchar(80);
ALTER TABLE tags_posts RENAME TO taggings;
ALTER TABLE taggings RENAME COLUMN post_id TO taggable_id;
UPDATE taggings SET type='post';
複製代碼
在給用戶打標籤以前先建立用戶表,並填充數據
-- 建立簡單的用戶表
CREATE TABLE users (
id SERIAL,
username varchar(80),
age integer
);
-- 添加一個名爲lan的用戶,並添加兩個相關的標籤
INSERT INTO users (username, age) values ('lan', 26);
INSERT INTO tags (name) VALUES ('student');
INSERT INTO tags (name) VALUES ('programmer');
複製代碼
接下來須要給用戶lan
打上標籤,對原有的SQL語句作一些調整,並在打標籤的時候把type
字段填充爲user
。
INSERT INTO taggings (tag_id, taggable_id, type) VALUES ((SELECT id FROM tags WHERE name = 'student'), (SELECT id FROM users WHERE username = 'lan'), 'user');
INSERT INTO taggings (tag_id, taggable_id, type) VALUES ((SELECT id FROM tags WHERE name = 'programmer'), (SELECT id FROM users WHERE username = 'lan'), 'user');
複製代碼
上述的SQL語句爲用戶打上了student
以及programmer
兩個標籤。
爲了完成這個任務咱們依然要聯合三張表進行查詢,同時還要約束type
的類型
lan
的用戶被打上的全部標籤blog=# SELECT tags.name FROM tags, users, taggings WHERE tags.id = taggings.tag_id AND users.id = taggings.taggable_id AND taggings.type = 'user' AND users.username = 'lan';
name
------------
student
programmer
(2 rows)
複製代碼
Ruby
的文章被打上的全部標籤blog=# SELECT tags.name FROM tags, posts, taggings WHERE tags.id = taggings.tag_id AND posts.id = taggings.taggable_id AND taggings.type = 'post' AND posts.title = 'Ruby';
name
----------
language
tech
複製代碼
OK,都跟預期同樣,如今的標籤系統就比較通用了。
本文經過PostgreSQL的基礎語句來構建了一個標籤系統。實現了一個標籤系統其實並不難,各個語言的社區應該都有相關的集成。本人也就是想拋開編程語言,從數據庫層面來剖析一個標籤系統的基本原理。
PS: 另外推薦一個比較好用的Model Design工具dbdiagram,能夠用文本的方式對數據表進行設計,邊設計邊預覽。最後還能以PNG,PDF甚至SQL源文件的形式導出。本文的數據表配圖均由用該軟件製做。