标签(Tag)的各种设计方案
标签(Tag)的各种设计方案

首先,标签(Tag)是什么?
我的理解:用来具体区分某一类内容的标识,和标签类似的一个概念是分类(Category),有一个示例可以很好的区分它们两个,比如人类分为:白种人、黄种人和黑种人(可以看作分类),但又可以根据职业分为:农民、工人和程序猿等等(可以看作标签)。
概括来说就是:分类是大而全的概念(用抽象概念来区分),标签是小而具体的概念(用具体值来区分)。
在所有的 CMS 内容管理系统中(比如园子中的博客、博问、新闻、网摘等),都存在标签(Tag)的概念,而且它们大多都有相似的功能,对于这类问题,我们最好把它抽象出来,然后单独去考虑并探讨它,如果一切顺利的话,最后完成的东西就是标签领域(TagDomain),当然这是最理想的方式。
我们先从实际应用出发,今天要探讨的是:各种标签(Tag)模型设计下,各类应用操作的实现方式。
标签(Tag)模型,我大概设计了 4 种(也可以在这个基础上进行扩展),如下:
- 1. Tag 存在于 Post 中。
- 2. Tag 独立 Post(一对多关系)。
- 3. Tag 独立 Post(一对多关系),Post 中多一个 Tags。
- 4. Tag 和 Post 都独立,创建 TagMap 映射(多对多关系)。
应用操作(EF Linq 实现),我大概想了 8 种,对于 Tag 的一般操作,我想应该都包含了,如下:
- 1. 添加 Post-Tag
- 2. 单独修改 Tag
- 3. 在 Post 中修改 Tag
- 4. 单独删除 Tag
- 5. 在 Post 中删除Tag
- 6. 查询 Tag(带数量统计)
- 7. 查询 Post(Tag 展示)
- 8. 根据 Tag 查询 Post 列表
下面我们分别来探讨下。
1. Tag 存在于 Post 中。
Tag 模型图:

Tag 模型说明:这个 Tag 模型是最简单的,Tag 直接存在 Post 中,但是模型简单,就意味着应用操作实现会很复杂。
应用操作实现代码:
public void Tags1()
{
using (var context = new TagsDbContext())
{
//1.添加post-tag
var postAdd = new Post1 { UserId = 1, Title = "title", Content = "content", Tags = ".net|asp.net vnext" };
context.Post1s.Add(postAdd);
//2.4.6单独对tag进行修改、删除、查询(带数量统计),难于登天。。。
//3.在post中修改tag
var postModify = context.Post1s.FirstOrDefault(p => p.PostId == 1 && p.UserId == 1);
postModify.Tags.Replace("asp.net vnext", "asp.net5");
//5.在post中删除tag
var postTagDelete = context.Post1s.FirstOrDefault(p => p.PostId == 1 && p.UserId == 1);
postTagDelete.Tags.Replace("asp.net vnext", "");
//7.查询post(tag展示)
var postSelect = context.Post1s.FirstOrDefault(p => p.PostId == 1 && p.UserId == 1);
postSelect.Tags.Replace('|', ',');
//8.根据tag查询post
var postTagSelect = context.Post1s.Where(p => p.Tags.Contains("asp.net5") && p.UserId == 1).ToList();
context.SaveChanges();
}
}
结论:可以看到,对于 2.4.6 应用操作,这种模型根本就没办法进行操作(也可以,但实现起来太复杂),2.4.6 应用操作属于对 Tag 的单独操作,如果应用场景只要求在 Post 中进行 Tag 操作,这种模型是完全可以胜任的,但如果要对 Tag 进行单独操作,用这种 Tag 模型,那就是自杀行为。。。
2. Tag 独立 Post(一对多关系)。
Tag 模型图:

Tag 模型说明:这种设计虽然把 Tag 和 Post 分离了,但需要注意的是,Post 和 Tag 的关系是一对多,有人会说,Post 和 Tag 的关系不是多对多的吗?一个 Tag 也可能对应多个 Post,但这种模型设计并不是这样,Tag 中有一个 PostId,表示这个 Tag 属于哪个 Post,比如有这样的示例:Tag 为 ASP.NET 的 Post 有两篇,那么在 Tag 中就会有两条 Tag 为 ASP.NET 的数据,但对应不同的 PostId。
应用操作实现代码:
public void Tags2()
{
using (var context = new TagsDbContext())
{
//1.添加post-tag
var postAdd = new Post2 { UserId = 1, Title = "title", Content = "content" };
postAdd.Tag2s.Add(new Tag2 { UserId = 1, TagName = ".net" });
postAdd.Tag2s.Add(new Tag2 { UserId = 1, TagName = "asp.net vnext" });
context.Post2s.Add(postAdd);
//2.单独修改tag
var tagsModify = context.Tag2s.Where(t => t.TagName == "asp.net vnext" && t.UserId == 1).ToList();
tagsModify.ForEach(t => t.TagName = "asp.net5");
//3.在post中修改tag
var tagModify = context.Tag2s.FirstOrDefault(t => t.TagName == "asp.net vnext" && t.UserId == 1 && t.PostId == 1);
tagModify.TagName = "asp.net5";
//4.单独删除tag
var tagsDelete = context.Tag2s.Where(t => t.TagName == "asp.net vnext" && t.UserId == 1).ToList();
context.Tag2s.RemoveRange(tagsDelete);
//5.在post中删除tag
var tagDelete = context.Tag2s.FirstOrDefault(t => t.TagName == "asp.net vnext" && t.UserId == 1 && t.PostId == 1);
context.Tag2s.Remove(tagDelete);
//6.查询tag(带数量统计)
var tagsSelect = from t in context.Tag2s
where t.UserId == 1
group t by t.TagName into g
orderby g.Count() descending
select new
{
TagName = g.Key,
UseCount = g.Count()
};
//7.查询post(tag展示)
var postSelect = context.Post2s.Include(p => p.Tag2s).FirstOrDefault(p => p.PostId == 1 && p.UserId == 1);
var tags = string.Join(",", postSelect.Tag2s.Select(t => t.TagName));
//8.根据tag查询post
var postTagSelect = from p in context.Post2s
join t in context.Tag2s on p.PostId equals t.PostId
where t.TagName == "asp.net5" && p.UserId == 1
select p;
context.SaveChanges();
}
}
结论:可以看到,使用这种 Tag 模型,7种应用操作的实现都不是很复杂,但有一个缺点是:Tag 重复数据会很多,如果有 10 个 Post,每个 Post 有 3 个 Tag,不管 Tag 是否相同,那么 Tag 的数据就是 30 条。如果对于数据量要求不大的话,可以采用这种方式,毕竟实现起来不是很复杂(比如其他三种的实现),我个人也比较偏向这种 Tag 模型设计。
3. Tag 独立 Post(一对多关系),Post 中多一个 Tags。

Tag 模型说明:这种模型设计和上面第二种差不多,只不过在 Post 中多了个 Tags(String 类型),它的作用就是为了在 Post Tag 展示的时候,不用再去关联查找 Tag,方便是方便,但我们需要付出一些代码,那就是需要对 Post 中的 Tags 进行维护,利与弊,我们看下应用操作的实现,就知道了。
应用操作实现代码:
public void Tags3()
{
using (var context = new TagsDbContext())
{
//1.添加post-tag
var postAdd = new Post3 { UserId = 1, Title = "title", Content = "content", Tags = ".net|asp.net vnext" };
context.Post3s.Add(postAdd);
context.Tag3s.Add(new Tag3 { UserId = 1, TagName = ".net" });
context.Tag3s.Add(new Tag3 { UserId = 1, TagName = "asp.net vnext" });
//2.单独修改tag
var tagsModify = context.Tag3s.Where(t => t.TagName == "asp.net vnext" && t.UserId == 1).ToList();
var postsModify = (from p in context.Post3s
join t in tagsModify on p.PostId equals t.PostId
select p).ToList();
tagsModify.ForEach(t => t.TagName = "asp.net5");
postsModify.ForEach(p => p.Tags.Replace("asp.net vnext", "asp.net5"));
//3.在post中修改tag
var tagModify = context.Tag3s.FirstOrDefault(t => t.TagName == "asp.net vnext" && t.UserId == 1 && t.PostId == 1);
tagModify.TagName = "asp.net5";
var postModify = context.Post3s.FirstOrDefault(t => t.UserId == 1 && t.PostId == 1);
postModify.Tags.Replace("asp.net vnext", "asp.net5");
//4.单独删除tag
var tagsDelete = context.Tag3s.Where(t => t.TagName == "asp.net vnext" && t.UserId == 1).ToList();
var postsTagsModify = (from p in context.Post3s
join t in tagsModify on p.PostId equals t.PostId
select p).ToList();
context.Tag3s.RemoveRange(tagsDelete);
postsModify.ForEach(p => p.Tags.Replace("asp.net vnext", ""));
//5.在post中删除tag
var tagDelete = context.Tag3s.FirstOrDefault(t => t.TagName == "asp.net vnext" && t.UserId == 1 && t.PostId == 1);
context.Tag3s.Remove(tagDelete);
var postTagDelete = context.Post3s.FirstOrDefault(t => t.UserId == 1 && t.PostId == 1);
postTagDelete.Tags.Replace("asp.net vnext", "");
//6.查询tag(带数量统计)
var tagsSelect = from t in context.Tag3s
where t.UserId == 1
group t by t.TagName into g
orderby g.Count() descending
select new
{
TagName = g.Key,
UseCount = g.Count()
};
//7.查询post(tag展示)
var postSelect = context.Post3s.FirstOrDefault(p => p.PostId == 1 && p.UserId == 1);
postSelect.Tags.Replace("|", ",");
//8.根据tag查询post
var postTagSelect = from p in context.Post3s
join t in context.Tag3s on p.PostId equals t.PostId
where t.TagName == "asp.net5" && p.UserId == 1
select p;
context.SaveChanges();
}
}
结论:先不看应用操作的具体实现,单纯从代码量上和第二种进行对比,会发现这种 Tag 模型的应用操作实现代码会比较多,添加、修改和删除 Tag,都要对 Post 中的 Tags 进行操作,而我们做这些多余的工作,仅仅是换来的是,最后查询 Post 而不关联 Tag,总感觉有点得不偿失,但并不意味着这种 Tag 模型实现就无用武之地,如果我们的应用场景,要求对 Tag 操作,必须通过 Post,比如修改 Tag,则必须通过 Post 进行修改,也就是不能对 Tag 进行独立操作,那么这种 Tag 模型就很适用。
应用场景不要求对 Tag 进行独立操作,上面说到,第一种 Tag 模型设计也适用啊,它们有什么不同呢? 分离 Tag 的好处是什么呢?很简单,就是为了方便 Tag 使用数量的统计,如果应用场景要求这个操作,第一种 Tag 模型设计就不适用了。
4. Tag 和 Post 都独立,创建 TagMap 映射(多对多关系)。

Tag 模型说明:这种 Tag 模型和上面第二种形成鲜明对比,上面第二种 Post 和 Tag 是一对多关系,而这种是多对多关系,第二种会出现重复 Tag 数据,而这种则不会。从模型图中,我们可以看到,Post 和 Tag 是独立存在的,它们通过一个 TagMap 进行映射关联,Tag 中的 UserId 和 TagName 是唯一的,并且多了一个 UseCount,在第二种 Tag 模型中,因为 Tag 根据 Post 产生,我们想要统计 Tag 的使用数量,直接对 Tag 进行 GroupBy 就可以了(具体看第二种的实现代码),而这种 Tag 模型,某一特定用户下的 Tag 是唯一的,所以要想统计 Tag 的使用数量,就必须通过 TagMap(需要关联 Tag 实现),既然 Tag 是独立的,那还不如增加一个 UseCount 更加方便。
这种 Tag 模型设计是四种方案中最复杂的,好处就是模型更加健壮,方便扩展,没有荣冗余数据产生,那坏处呢?我们接着看下面。
应用操作实现代码:
public void Tags4()
{
using (var context = new TagsDbContext())
{
//1.添加post-tag
var postAdd = new Post4 { UserId = 1, Title = "title", Content = "content" };
context.Post4s.Add(postAdd);
var tagAdd1 = context.Tag4s.FirstOrDefault(t => t.TagName == ".net" && t.UserId == 1);
var tagAdd2 = context.Tag4s.FirstOrDefault(t => t.TagName == "asp.net vnext" && t.UserId == 1);
if (tagAdd1 != null)
tagAdd1.UseCount++;
else
context.Tag4s.Add(new Tag4 { UserId = 1, TagName = ".net", UseCount = 1 });
if (tagAdd2 != null)
tagAdd1.UseCount++;
else
context.Tag4s.Add(new Tag4 { UserId = 1, TagName = "asp.net vnext", UseCount = 1 });
context.TagMap4s.Add(new TagMap4 { PostId = postAdd.PostId, TagId = tagAdd1.TagId });
context.TagMap4s.Add(new TagMap4 { PostId = postAdd.PostId, TagId = tagAdd2.TagId });
//2.单独修改tag
var tagModify = context.Tag4s.Where(t => t.TagName == "asp.net vnext" && t.UserId == 1).FirstOrDefault();
tagModify.TagName = "asp.net5";
//3.在post中修改tag
var tagModify2 = (from t in context.Tag4s
where t.UserId == 1 && t.TagName == "asp.net vnext"
join m in context.TagMap4s on t.TagId equals m.TagId
where m.PostId == 1
select t).FirstOrDefault();
tagModify2.UseCount--;
var tagModify3 = context.Tag4s.FirstOrDefault(t => t.TagName == "asp.net 5" && t.UserId == 1);
if (tagModify3 != null)
tagModify3.UseCount++;
else
context.Tag4s.Add(new Tag4 { UserId = 1, TagName = "asp.net 5", UseCount = 1 });
var postModify = context.Post4s.FirstOrDefault(p => p.PostId == 1 && p.UserId == 1);
var tagMapDelete= context.TagMap4s.FirstOrDefault(p => p.PostId == 1 && p.TagId == tagModify2.TagId);
context.TagMap4s.Remove(tagMapDelete);
postModify.TagMap4s.Add(new TagMap4 { PostId = postModify.PostId, TagId = tagModify3.TagId });
//4.单独删除tag
var tagDelete = context.Tag4s.Where(t => t.TagName == "asp.net vnext" && t.UserId == 1).FirstOrDefault();
var tagMapsDelete = context.TagMap4s.Where(t => t.TagId == tagDelete.TagId).ToList();
context.Tag4s.Remove(tagDelete);
context.TagMap4s.RemoveRange(tagMapsDelete);
//5.在post中删除tag
var tagDelete2 = (from t in context.Tag4s
where t.UserId == 1 && t.TagName == "asp.net vnext"
join m in context.TagMap4s on t.TagId equals m.TagId
where m.PostId == 1
select t).FirstOrDefault();
tagDelete2.UseCount--;
var tagMapDelete2 = context.TagMap4s.FirstOrDefault(p => p.PostId == 1 && p.TagId == tagDelete2.TagId);
context.TagMap4s.Remove(tagMapDelete2);
//6.查询tag(带数量统计)
var tagsSelect = context.Tag4s.Where(t => t.UserId == 1).ToList();
//7.查询post(tag展示)
var postSelect = context.Post4s.FirstOrDefault(p =>p.PostId == 1 && p.UserId == 1);
var tagsSelect2 = (from t in context.Tag4s
where t.UserId == 1
join m in context.TagMap4s on t.TagId equals m.TagId
select t).ToList();
var tags = string.Join(",", tagsSelect2.Select(t => t.TagName));
//8.根据tag查询post
var postTagSelect = from p in context.Post4s
join m in context.TagMap4s on p.PostId equals m.PostId
join t in context.Tag4s on m.TagId equals t.TagId
where t.TagName == "asp.net5" && p.UserId == 1 && t.UserId == 1
select p;
context.SaveChanges();
}
}
结论:单从代码量上来说,这种应用操作实现代码量最多,其实大部分操作都是在判断 Tag,也就是为了利用现有的 Tag 数据,并不是像前面两种,不管 Tag 是否存在,直接添加、修改和删除,统计 Tag 使用数量实现,是四种方案中最简单的,其余的应用操作,因为模型层级越多、关联越多,操作起来就会越复杂,但不可否认,这种设计,是四种方案中“最理想”的。
5. 简要总结
深入去设计并实现这四种 Tag 模型方案,其实有很多的感触,是之前没实现体会不到的,比如:
- 模型的简单和复杂是相对的:并不是模型越简单越好,也不是越复杂越好,第一和第二种方案就说明这点。
- 模型的设计是相对于应用场景的:在不能确定应用场景的情况下,不能说哪种模型设计是好是坏,交通工具有很多种,飞机快过汽车,但飞机在陆地上跑不过汽车。
最后,简要总结下四种 Tag 模型设计的一些应用场景:
- 1. Tag 存在于 Post 中:1.3.5.7.8 应用操作,不要求独立对 Tag 进行操作和数量统计。
- 2. Tag 独立 Post(一对多关系):1-8 应用操作,数据量不是很大,对 Tag 操作比较频繁,对 Post 操作不频繁。
- 3. Tag 独立 Post(一对多关系),Post 中多一个 Tags:1-8 应用操作,数据量不是很大,对 Post 操作比较频繁,对 Tag 操作不频繁。
- 4. Tag 和 Post 都独立,创建 TagMap 映射(多对多关系):1-8 应用操作,Tag 业务变化比较频繁,对 Tag 和 Post 操作都比较频繁。
对于我自己来说,上面四种 Tag 模型设计,我最偏向于第二种和第四种,如果非要选择一种的话,我可能会选择第二种,为什么呢?因为谁写过应用操作的实现代码,谁知道,哈哈!!!
相关参考资料:
- 如何设计一款高效的TAG索引系统
- 如何进行文章分类和标签的数据库设计
- 探讨下Tag标签的数据库设计(千万级数据量)
- 开发高效的Tag标签系统数据库设计
- 「用户标签」在数据库设计时应该如何存储?
- 标签 (Tags) 是个好的设计吗?
- 标签(Tag)的数据库设计
- 关于数据库中Tag的设计
未完待续。。。
标签(Tag)的各种设计方案的更多相关文章
- 一、变量.二、过滤器(filter).三、标签(tag).四、条件分支tag.五、迭代器tag.六、自定义过滤器与标签.七、全系统过滤器(了解)
一.变量 ''' 1.视图函数可以通过两种方式将变量传递给模板页面 -- render(request, 'test_page.html', {'变量key1': '变量值1', ..., '变量ke ...
- struts2官方 中文教程 系列三:使用struts2 标签 tag
避免被爬,先贴上本帖地址:struts2 官方系列教程一:使用struts2 标签 tag http://www.cnblogs.com/linghaoxinpian/p/6901316.html 本 ...
- 转Git仓库分支(Branch)和标签(Tag)
仓库的分支(Branch)规范,影响到每个团队的工作流的一致性:标签(Tag)便于开发团队.测 试团队和其他团队识别每个项目的版本,特别是在协同处理线上问题的时候,大家可以非常清楚 地知道线上运行版本 ...
- JUnit5学习之五:标签(Tag)和自定义注解
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- golang中的反射解析结构体标签tag
package main import ( "fmt" "reflect" ) type resume struct { // 反射解析结构体标签tag Nam ...
- 『现学现忘』Git基础 — 36、标签tag(一)
目录 1.标签介绍 2.列出标签 3.创建标签 (1)标签的分类 (2)附注标签 (3)轻量标签 4.后期打标签 1.标签介绍 软件的某个发行版本所对应的,其实就是软件开发过程中,某一个阶段的最后一次 ...
- DESTOON B2B标签(tag)调用手册
路径:include/tag.func.php 1.标签格式的大致说明 {tag("moduleid=9&table=article_9&length=40&cond ...
- intellij idea之git执行打标签(tag)和删除标签
intellij idea 版本为2017.2.6 进入Version Control-->log 1.在之前版本中,右键,新建标签 2.输入标签名称,建议输入版本号的方式 3.push标签 由 ...
- git版本管理工具 标签(Tag) / 版本回退 / 分支的简单使用
a.标签 标签,可以使用这个功能来标记发布结点. 举个例子, 假如我们的项目版本目前是1.2版本, 上级要求这个版本要在半个月后再进行上传至Appstore, 并要求我们未来的半个月内,去写1.3版本 ...
- Struts2 自己定义下拉框标签Tag
自己定义标签主要包含三个步骤: 1.编写java类,继承TagSupport类. 2.创建tld文件,影射标签名和标签的java类. 3.jsp页面引入tld. 样例:自己定义下拉框标签 假设页面上有 ...
随机推荐
- DeviceIoControl 直接从磁盘扇区读文件
好久没写博客了,近期看了下DeviceIoControl 关于磁盘的应用,特记一文,以备久后查阅. 首先介绍下,文件在磁盘的存储结构(详细能够到网上查询NTFS文件系统相关的教程后者数据恢复方面教程 ...
- MQTT学习笔记——Yeelink MQTT维修 采用mqtt.js和paho-mqtt
0 前言 2014年8月yeelink推出基于MQTT协议的开关类型设备控制API.相比于基于HTTP RESTful的轮训方式,通过订阅相关主题消息,能够远程控制类应用实时性更好. 本文使用 ...
- Visual Studio 2010 将网站直接发布到远程站点
原文:Visual Studio 2010 将网站直接发布到远程站点 这次说下如何将web应用程序直接发布到IIS服务器站点!!! 问题的由来 本人每天要发布更新的程序,所以每次更新的时候要做的工作: ...
- 改动symbol link的owner
当/home/jenkins文件夹空间不足的时候,能够先查看哪个文件夹在较大的磁盘分区上,然后将jenkins文件夹移动过去 最后创建/home/jenkins link到新位置. 这时候须要改动sy ...
- ZOJ - 3822 Domination (DP)
Edward is the headmaster of Marjar University. He is enthusiastic about chess and often plays chess ...
- 查看.a架构文件
苹果公司现在要求所有新提交的评论app,我们必须支持64位架构.而我们的在线项目编制,操作员做了一堆SDK在需要访问,我们发现,在这个过程中,有些SDK的.a文件进入后,链接错误,如提示 Undefi ...
- dev layoutControl 控件使用
对于排版控件,用微软的方法都是先拉 label再拉一个 Textbox , 虽然微软的控件了有类似于 EXCEL的单元格全并功能,但用起来使终不方便, 今天研究了一下 DEV 的这个控件,比微软的 ...
- iOS开发多线程篇—多线程简介
iOS开发多线程篇-多线程简介 一.进程和线程 1.什么是进程 进程是指在系统中正在执行的一个应用程序 每一个进程之间是独立的.每一个进程均执行在其专用且受保护的内存空间内 比方同一时候打开QQ.Xc ...
- ORACLE profile列4 --CREATE PROFILE
这个博客是ORACLE profile系列第四部分.主要说一下,假设你创建profile而使用profile资源和password控制 CREATE PROFILE Note: Oracle reco ...
- 1.cocos2dx 3.2环境结构
1 所需软件 jdk-7u25-windows-i586.exe python-2.7.8.amd64.msi cocos2d-x-3.2.zip apache-ant-1.9.4.zi ...