文章出處

首先,標簽(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 模型設計,我最偏向于第二種和第四種,如果非要選擇一種的話,我可能會選擇第二種,為什么呢?因為誰寫過應用操作的實現代碼,誰知道,哈哈!!!

相關參考資料:

未完待續。。。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()