MongoDB數(shù)據(jù)庫(kù)設(shè)計(jì)實(shí)例 - KeystoneJS

前言

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

使用官方提供的簡(jiǎn)單方式配置,可以配出標(biāo)準(zhǔn)類型的博客系統(tǒng)颖杏,包括文章系統(tǒng)(含有分類機(jī)制)蛋辈、相冊(cè)系統(tǒng)、私信系統(tǒng)胃碾、用戶系統(tǒng)。若需要更高級(jí)的自定義配置,需要手寫一些js文件曹体。

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

此篇即用最簡(jiǎn)單、標(biāo)準(zhǔn)的Keystone博客模版盖奈,記錄KeystoneJS是如何使用MongoDB存儲(chǔ)內(nèi)容的混坞。

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

概覽

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

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

> show collections
app_updates
enquiries
galleries
postcategories
posts
users

除了app_updates存儲(chǔ)版本升級(jí)信息绘趋,這里不細(xì)說(shuō),其他的看下文颗管。

博客系統(tǒng)

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

分類(PostCategories)

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

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

其中__v字段是mongoose(一個(gè)Node上常用的MongoDB數(shù)據(jù)庫(kù)ORM)增加的帽馋,mongoose用這個(gè)字段配以一些機(jī)制,增強(qiáng)數(shù)據(jù)一致性比吭、安全性绽族,與存儲(chǔ)的內(nèi)容無(wú)關(guān)。

剩下的有效字段包括_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默認(rèn)博客配置中岁诉,需要通過(guò)_id或其字符串查詢,少有直接通過(guò)name進(jìn)行的查詢跋选。

文章(Posts)

創(chuàng)建了兩篇范例文章后涕癣,查看數(shù)據(jù)庫(kù)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部分,大概是一句話的簡(jiǎn)介前标。</p>",
        "extended" : "<p>這里是Content Extended部分坠韩,應(yīng)該是正文。</p>\r\n<p>所以多寫一句話炼列,讓字?jǐn)?shù)稍微多多多多多多那么一點(diǎn)只搁。</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
}
>

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

對(duì)于categories域,表達(dá)了多對(duì)多關(guān)系焰望。MongoDB可以有多種多對(duì)多關(guān)系的表達(dá)方式骚亿,此處使用一個(gè)數(shù)組存儲(chǔ)所有對(duì)Category的引用。因?yàn)樵贙eystoneJS中Category經(jīng)常需要單獨(dú)查詢(列出所有Category等操作)熊赖,所以把所有Category放到一個(gè)單獨(dú)的集合postcategories是更合適的做法来屠,不適合使用純粹的內(nèi)嵌文檔模式。而傳統(tǒng)SQL用專門一張表表達(dá)多對(duì)多關(guān)系的方式震鹉,只能說(shuō)MongoDB對(duì)Join操作支持不好俱笛,這不是NoSQL該用的模式。

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

對(duì)于author域蝶棋,表達(dá)一對(duì)多關(guān)系(一個(gè)author多個(gè)post)。直接存儲(chǔ)author的引用忽妒,標(biāo)準(zhǔn)的做法玩裙。

content是存粹的內(nèi)嵌文檔,因?yàn)镃ontent完全屬于Post段直,不存在使得Content獨(dú)立于Post單獨(dú)查詢的場(chǎng)景吃溅,所以是MongoDB的標(biāo)準(zhǔn)做法。

image類似于content鸯檬。額外解釋一下KeystoneJS的圖片機(jī)制:上傳圖片的時(shí)候會(huì)保存到cloudinary(圖片存儲(chǔ)决侈、CDN服務(wù),和國(guó)內(nèi)的七牛云差不多)喧务,并保存URL赖歌,本機(jī)不存圖片本身。

索引方面功茴,getIndexes()結(jié)果太長(zhǎng)庐冯,只寫簡(jiǎn)單結(jié)果:_idstate坎穿、author展父、publishDate返劲、slug設(shè)置了索引,其中slug索引設(shè)置了unique屬性保證唯一性栖茉。

評(píng)論(Comments)

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

任意發(fā)布一篇文章之后添加一條評(píng)論衡载。文章(post)的文檔沒有變化搔耕,沒有comments之類的字段。數(shù)據(jù)庫(kù)中會(huì)有一個(gè)單獨(dú)的postcomments集合痰娱,存放整個(gè)系統(tǒng)中所有的評(píng)論:

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

對(duì)于[文章-評(píng)論]這種一對(duì)多的關(guān)系弃榨,只在“多”的部分加入對(duì)“一”的引用,即post字段梨睁。

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

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

或者

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

KeystoneJS Demo中的方法坡贺,和之后列出的方法1官辈、方法2,是MongoDB中表達(dá)一對(duì)多關(guān)系的三種常見方式遍坟。

方法2是最有MongoDB風(fēng)格的方法拳亿,在單一場(chǎng)景下(查詢文章以及其下的評(píng)論),性能最好(只需一次查詢同時(shí)獲取文章和評(píng)論)愿伴。同時(shí)靈活性較差肺魁,例如查詢“所有文章中的未讀評(píng)論”就會(huì)很麻煩,性能也很差隔节,對(duì)于博客系統(tǒng)鹅经,這種情況可以考慮添加專門的通知功能代替上述的場(chǎng)景,用以彌補(bǔ)怎诫。

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

方法1在我看來(lái)算是折中幻妓,也能夠兼顧多種場(chǎng)景蹦误,對(duì)比SQL的傳統(tǒng)方法,從屬關(guān)系以人的角度看起來(lái)更直觀肉津。

在索引上胖缤,字段_idauthor阀圾、post哪廓、commentStatepublishedOn包括索引初烘,沒有unique索引的域涡真。

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

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

> db.galleries.find().pretty()
{
    "_id" : ObjectId("59f9396170871a41d3ff7d68"),
    "key" : "59f9396170871a41d3ff7d68",
    "name" : "第一個(gè)相冊(cè)",
    "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是相冊(cè)封面哆料。這里使用內(nèi)嵌文檔的數(shù)組保存相冊(cè)內(nèi)的圖片對(duì)象缸剪。

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

若能保證文件都小于16M,可以把所有“文件”獨(dú)立進(jìn)一個(gè)collection壮啊,在gallaries集合的images數(shù)組中嫉鲸,保存文件的引用。

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

索引比較簡(jiǎn)單藤树,有_idkey,其中key索引有unique屬性拓萌。

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

除了系統(tǒng)初始化創(chuàng)建了一個(gè)Admin用戶外也榄,還手動(dòng)創(chuàng)建了一個(gè)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
}
>

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

索引方面腰根,_id激才、emailisAdmin設(shè)置了索引额嘿,應(yīng)當(dāng)是為了“通過(guò)email賬號(hào)登陸”和“列出所有管理員”的應(yīng)用場(chǎng)景瘸恼。其中email有unique屬性保證唯一性。

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

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

> 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" : "只是測(cè)試一下contact...",
        "html" : "<p>只是測(cè)試一下contact...</p>\n"
    },
    "name" : {
        "first" : "你好"
    },
    "__v" : 0
}
>

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

踩的坑

KeystoneJS官方新手教程使用yo(Yeoman)搭建默認(rèn)配置靠闭。yo在監(jiān)測(cè)到當(dāng)前用戶為root時(shí)帐我,會(huì)切換為使用自己的UID,導(dǎo)致一系列權(quán)限問(wèn)題愧膀。

因?yàn)榘惭b時(shí)生成的配置文件等是root:root且rw權(quán)限只給了u沒有g(shù)o拦键,導(dǎo)致無(wú)法讀取自己的配置文件。離奇的是手動(dòng)chmod增加權(quán)限后檩淋,yo依舊會(huì)失敗芬为,且權(quán)限恢復(fù)成原來(lái)的樣子。

最后我是為此創(chuàng)建了一個(gè)新的普通用戶才跑起來(lái)KeystoneJS蟀悦。對(duì)于只有root用戶的機(jī)器(VPS等)要留意這一點(diǎn)媚朦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市熬芜,隨后出現(xiàn)的幾起案子莲镣,更是在濱河造成了極大的恐慌,老刑警劉巖涎拉,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑞侮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鼓拧,警方通過(guò)查閱死者的電腦和手機(jī)半火,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)季俩,“玉大人钮糖,你說(shuō)我怎么就攤上這事∽米。” “怎么了店归?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)酪我。 經(jīng)常有香客問(wèn)我消痛,道長(zhǎng),這世上最難降的妖魔是什么都哭? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任秩伞,我火速辦了婚禮,結(jié)果婚禮上欺矫,老公的妹妹穿的比我還像新娘纱新。我一直安慰自己,他們只是感情好穆趴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布脸爱。 她就那樣靜靜地躺著,像睡著了一般未妹。 火紅的嫁衣襯著肌膚如雪阅羹。 梳的紋絲不亂的頭發(fā)上勺疼,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音捏鱼,去河邊找鬼执庐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛导梆,可吹牛的內(nèi)容都是我干的轨淌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼看尼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼递鹉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起藏斩,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤躏结,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后狰域,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體媳拴,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年兆览,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屈溉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抬探,死狀恐怖子巾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情小压,我是刑警寧澤线梗,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站怠益,受9級(jí)特大地震影響仪搔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜溉痢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望憋他。 院中可真熱鬧孩饼,春花似錦、人聲如沸竹挡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)揪罕。三九已至梯码,卻和暖如春宝泵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轩娶。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工儿奶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鳄抒。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓闯捎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親许溅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瓤鼻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容