MongoDB數(shù)據(jù)庫設計實例 - KeystoneJS

前言

先簡單介紹一下KeystoneJS。這是一個依靠Node.js + MongoDB打造的,能夠靈活配置的CMS系統(tǒng)。

使用官方提供的簡單方式配置,可以配出標準類型的博客系統(tǒng),包括文章系統(tǒng)(含有分類機制)、相冊系統(tǒng)、私信系統(tǒng)、用戶系統(tǒng)。若需要更高級的自定義配置,需要手寫一些js文件。

官網(wǎng)地址 http://keystonejs.com/
中文官網(wǎng) http://keystonejs.com/zh/

此篇即用最簡單、標準的Keystone博客模版,記錄KeystoneJS是如何使用MongoDB存儲內(nèi)容的。

KeystoneJS中的數(shù)據(jù)庫

概覽

初始化之后,會帶有一個Admin賬戶,登陸賬戶,創(chuàng)建一個文章分類(PostCategory),創(chuàng)建兩篇文章(Post),創(chuàng)建一個相冊(Gallary)并上傳少量圖片。創(chuàng)建另一個用戶guest,并向管理員發(fā)起一個信息。

此時查看數(shù)據(jù)庫中的集合,如下所示:

> show collections
app_updates
enquiries
galleries
postcategories
posts
users

除了app_updates存儲版本升級信息,這里不細說,其他的看下文。

博客系統(tǒng)

默認的博客系統(tǒng)包括文章(Post)和文章分類(PostCategory)。

分類(PostCategories)

首先創(chuàng)建一個叫做瞎扯的分類,然后查看postcategories集合。

> db.postcategories.find().pretty()
{
    "_id" : ObjectId("59f9384970871a41d3ff7d66"),
    "key" : "59f9384970871a41d3ff7d66",
    "name" : "瞎扯",
    "__v" : 0
}
>

其中__v字段是mongoose(一個Node上常用的MongoDB數(shù)據(jù)庫ORM)增加的,mongoose用這個字段配以一些機制,增強數(shù)據(jù)一致性、安全性,與存儲的內(nèi)容無關。

剩下的有效字段包括_id,key,name,且key只是_id的字符串版本。沒有其他多余的東西。

接著查看索引:

> db.postcategories.getIndexes()
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "r-blog.postcategories"
    },
    {
        "v" : 1,
        "unique" : true,
        "key" : {
            "key" : 1
        },
        "name" : "key_1",
        "ns" : "r-blog.postcategories",
        "background" : true
    }
]
>

可以看到_idkey有索引,key額外添加了unique屬性。在KeyStone默認博客配置中,需要通過_id或其字符串查詢,少有直接通過name進行的查詢。

文章(Posts)

創(chuàng)建了兩篇范例文章后,查看數(shù)據(jù)庫posts集合:

> db.posts.find().pretty()
{
    "_id" : ObjectId("59f9388a70871a41d3ff7d67"),
    "slug" : "59f9388a70871a41d3ff7d67",
    "title" : "這是一篇瞎扯的文章",
    "categories" : [
        ObjectId("59f9384970871a41d3ff7d66")
    ],
    "state" : "published",
    "__v" : 1,
    "author" : ObjectId("59f937eb70871a41d3ff7d64"),
    "content" : {
        "brief" : "<p>這里是Content Brief部分,大概是一句話的簡介。</p>",
        "extended" : "<p>這里是Content Extended部分,應該是正文。</p>\r\n<p>所以多寫一句話,讓字數(shù)稍微多多多多多多那么一點。</p>"
    },
    "image" : {
        "public_id" : "tqcx3wzhgshzjp22zfh0",
        "version" : 1509505196,
        "signature" : "89f18cac7b111d0865515cf25455c10c6824a59b",
        "width" : 640,
        "height" : 640,
        "format" : "jpg",
        "resource_type" : "image",
        "url" : "http://res.cloudinary.com/keystone-demo/image/upload/v1509505196/tqcx3wzhgshzjp22zfh0.jpg",
        "secure_url" : "https://res.cloudinary.com/keystone-demo/image/upload/v1509505196/tqcx3wzhgshzjp22zfh0.jpg"
    },
    "publishedDate" : ISODate("2017-10-31T16:00:00Z")
}
{
    "_id" : ObjectId("59f939cb70871a41d3ff7d6c"),
    "slug" : "this-is-an-example-post-with-english-title",
    "title" : "This is an example post with english title",
    "categories" : [ ],
    "state" : "published",
    "__v" : 0,
    "author" : ObjectId("59f937eb70871a41d3ff7d64"),
    "content" : {
        "brief" : "<p>Just to try the slug...</p>",
        "extended" : "<p>hmmmmmm.</p>"
    },
    "publishedDate" : null
}
>

第一篇文章盡可能用到了全部的域;第二篇僅僅是為了測試slug。在slug不被支持的場景(中文標題等)直接使用ID作為slug;在slug正確支持的場景(一般的英文標題等)會用傳統(tǒng)的小寫單詞+橫線連接的方式做slug。

對于categories域,表達了多對多關系。MongoDB可以有多種多對多關系的表達方式,此處使用一個數(shù)組存儲所有對Category的引用。因為在KeystoneJS中Category經(jīng)常需要單獨查詢(列出所有Category等操作),所以把所有Category放到一個單獨的集合postcategories是更合適的做法,不適合使用純粹的內(nèi)嵌文檔模式。而傳統(tǒng)SQL用專門一張表表達多對多關系的方式,只能說MongoDB對Join操作支持不好,這不是NoSQL該用的模式。

state期望表達的是個枚舉類型,在MongoDB中直接使用字符串表達狀態(tài),區(qū)別于傳統(tǒng)SQL數(shù)據(jù)庫中,定義一個整形數(shù)字表達特定含義。暫且沒看到MongoDB直接提供有枚舉限制的機制。在應用中,通常需要手動編程做限制,例如mongoose定義Schema的時候可以添加enum屬性,限定域的值是合法的。

對于author域,表達一對多關系(一個author多個post)。直接存儲author的引用,標準的做法。

content是存粹的內(nèi)嵌文檔,因為Content完全屬于Post,不存在使得Content獨立于Post單獨查詢的場景,所以是MongoDB的標準做法。

image類似于content。額外解釋一下KeystoneJS的圖片機制:上傳圖片的時候會保存到cloudinary(圖片存儲、CDN服務,和國內(nèi)的七牛云差不多),并保存URL,本機不存圖片本身。

索引方面,getIndexes()結果太長,只寫簡單結果:_idstate、author、publishDate、slug設置了索引,其中slug索引設置了unique屬性保證唯一性。

評論(Comments)

此部分是之后補充的。使用keystone-demo包含有評論系統(tǒng)。

任意發(fā)布一篇文章之后添加一條評論。文章(post)的文檔沒有變化,沒有comments之類的字段。數(shù)據(jù)庫中會有一個單獨的postcomments集合,存放整個系統(tǒng)中所有的評論:

> db.postcomments.find().pretty()
{
    "_id" : ObjectId("59f96344bd9d6a6ae2edc7a6"),
    "content" : "這是一個條評論",
    "post" : ObjectId("59f962edbd9d6a6ae2edc7a5"),
    "author" : ObjectId("59f9629bbd9d6a6ae2edc7a2"),
    "publishedOn" : ISODate("2017-11-01T06:01:40.748Z"),
    "commentState" : "published",
    "__v" : 0
}
>

對于[文章-評論]這種一對多的關系,只在“多”的部分加入對“一”的引用,即post字段。

對于“文章/帖子保存評論”這種場景,我見到很多是在“一”的文檔中添加“多”的內(nèi)嵌文檔或者引用,例如對于一篇文章在數(shù)據(jù)庫中的文檔:

// 方法1
{
    "_id": ObjectId("..."),
    "title": "...",
    "content": "...",
    "comments": [
        ObjectId("......"),  // 引用一個comment文檔
        ObjectId("......")
    ]
}

或者

// 方法2
{
    "_id": ObjectId("..."),
    "title": "...",
    "content": "...",
    "comments": [
        { content: "這是一條評論", author: ObjectiId(...) },
        { content: "這是另一條評論", author: ObjectiId(...) }
    ]
}

KeystoneJS Demo中的方法,和之后列出的方法1、方法2,是MongoDB中表達一對多關系的三種常見方式。

方法2是最有MongoDB風格的方法,在單一場景下(查詢文章以及其下的評論),性能最好(只需一次查詢同時獲取文章和評論)。同時靈活性較差,例如查詢“所有文章中的未讀評論”就會很麻煩,性能也很差,對于博客系統(tǒng),這種情況可以考慮添加專門的通知功能代替上述的場景,用以彌補。

KeystoneJS Demo中的方法是傳統(tǒng)的SQL引用方法,對絕大多數(shù)場景的性能都有兼顧。

方法1在我看來算是折中,也能夠兼顧多種場景,對比SQL的傳統(tǒng)方法,從屬關系以人的角度看起來更直觀。

在索引上,字段_id、author、post、commentStatepublishedOn包括索引,沒有unique索引的域。

相冊系統(tǒng)(Gallaries)

創(chuàng)建一個相冊(Gallary),并在相冊中包含了三張圖片后,查詢數(shù)據(jù)庫的gallaries集合

> db.galleries.find().pretty()
{
    "_id" : ObjectId("59f9396170871a41d3ff7d68"),
    "key" : "59f9396170871a41d3ff7d68",
    "name" : "第一個相冊",
    "images" : [
        {
            "public_id" : "og9nkng8sqqivtdypf1z",
            "version" : 1509505412,
            "signature" : "1dd91f44e892f8ee997b425a6eb929b3f5644cdc",
            "width" : 40,
            "height" : 40,
            "format" : "png",
            "resource_type" : "image",
            "url" : "http://res.cloudinary.com/keystone-demo/image/upload/v1509505412/og9nkng8sqqivtdypf1z.png",
            "secure_url" : "https://res.cloudinary.com/keystone-demo/image/upload/v1509505412/og9nkng8sqqivtdypf1z.png",
            "_id" : ObjectId("59f9398570871a41d3ff7d6b")
        },
        {
            "public_id" : "fqm4p1ahwzfx39omw6ej",
            "version" : 1509505412,
            "signature" : "37f70094993c047d7c899e338b1cee110dffd9d5",
            "width" : 128,
            "height" : 128,
            "format" : "png",
            "resource_type" : "image",
            "url" : "http://res.cloudinary.com/keystone-demo/image/upload/v1509505412/fqm4p1ahwzfx39omw6ej.png",
            "secure_url" : "https://res.cloudinary.com/keystone-demo/image/upload/v1509505412/fqm4p1ahwzfx39omw6ej.png",
            "_id" : ObjectId("59f9398570871a41d3ff7d6a")
        },
        {
            "public_id" : "tbawweh0prvbqaunz33g",
            "version" : 1509505412,
            "signature" : "a8ed854badac8aff4c024b703c914c9c84c4934c",
            "width" : 640,
            "height" : 640,
            "format" : "jpg",
            "resource_type" : "image",
            "url" : "http://res.cloudinary.com/keystone-demo/image/upload/v1509505412/tbawweh0prvbqaunz33g.jpg",
            "secure_url" : "https://res.cloudinary.com/keystone-demo/image/upload/v1509505412/tbawweh0prvbqaunz33g.jpg",
            "_id" : ObjectId("59f9398570871a41d3ff7d69")
        }
    ],
    "publishedDate" : ISODate("2017-11-01T03:02:57Z"),
    "__v" : 1,
    "heroImage" : {
        "public_id" : "vbu4jrpfe5bowlz8ar7s",
        "version" : 1509505412,
        "signature" : "b47d9bcfcac93ec4a453a4b80b498704b589a2b9",
        "width" : 640,
        "height" : 640,
        "format" : "jpg",
        "resource_type" : "image",
        "url" : "http://res.cloudinary.com/keystone-demo/image/upload/v1509505412/vbu4jrpfe5bowlz8ar7s.jpg",
        "secure_url" : "https://res.cloudinary.com/keystone-demo/image/upload/v1509505412/vbu4jrpfe5bowlz8ar7s.jpg"
    }
}
>

其中heroImage是相冊封面。這里使用內(nèi)嵌文檔的數(shù)組保存相冊內(nèi)的圖片對象。

由于這里保存的只有元數(shù)據(jù)和URL,體積較小,是適合的方式。如果直接保存二進制文件數(shù)據(jù),那么要考慮MongoDB中單個文檔不能超過16MB的限制,通常需要考慮其他方法。

若能保證文件都小于16M,可以把所有“文件”獨立進一個collection,在gallaries集合的images數(shù)組中,保存文件的引用。

如果文件大于16M,考慮使用把文件保存在外部,保存URL,或者使用GridFS。

索引比較簡單,有_idkey,其中key索引有unique屬性。

用戶系統(tǒng)(User)

除了系統(tǒng)初始化創(chuàng)建了一個Admin用戶外,還手動創(chuàng)建了一個guest用戶。

> db.users.find().pretty()
{
    "_id" : ObjectId("59f937eb70871a41d3ff7d64"),
    "password" : "$2a$10$rv9yNFRQiJ/jQznF2FYmguhEbM8QFHBLK6J3SiaXmAhk/GbUvJH6y",
    "email" : "changrui0608@gmail.com",
    "isAdmin" : true,
    "name" : {
        "last" : "User",
        "first" : "Admin"
    },
    "__v" : 0
}
{
    "_id" : ObjectId("59f93e7870871a41d3ff7d6d"),
    "password" : "$2a$10$La5hXQxJz8Gwn9oOQ8OBruQnbsMt4D5vdggANhbtdfo./mQJ3L6nG",
    "email" : "guest@guest.guest",
    "isAdmin" : true,
    "name" : {
        "last" : "guest",
        "first" : "guest"
    },
    "__v" : 0
}
>

密碼是哈希過的,提高安全性。name域是內(nèi)嵌文檔,類似postscontent域,比較典型。

索引方面,_id、email、isAdmin設置了索引,應當是為了“通過email賬號登陸”和“列出所有管理員”的應用場景。其中email有unique屬性保證唯一性。

信息系統(tǒng)(Enquries)

以guest登陸,向站管理員發(fā)送一個消息后查看數(shù)據(jù)庫。

> db.enquiries.find().pretty()
{
    "_id" : ObjectId("59f94ef170871a41d3ff7d6e"),
    "enquiryType" : "message",
    "phone" : "1234567",
    "email" : "guest@guest.guest",
    "createdAt" : ISODate("2017-11-01T04:34:57.971Z"),
    "message" : {
        "md" : "只是測試一下contact...",
        "html" : "<p>只是測試一下contact...</p>\n"
    },
    "name" : {
        "first" : "你好"
    },
    "__v" : 0
}
>

有意思的是message實際上保存了同樣內(nèi)容的markdown原文和html版本。
索引只有_id

踩的坑

KeystoneJS官方新手教程使用yo(Yeoman)搭建默認配置。yo在監(jiān)測到當前用戶為root時,會切換為使用自己的UID,導致一系列權限問題。

因為安裝時生成的配置文件等是root:root且rw權限只給了u沒有go,導致無法讀取自己的配置文件。離奇的是手動chmod增加權限后,yo依舊會失敗,且權限恢復成原來的樣子。

最后我是為此創(chuàng)建了一個新的普通用戶才跑起來KeystoneJS。對于只有root用戶的機器(VPS等)要留意這一點。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容