NODEJS AND MONGODB 评论列表模型

评论列表算是编程领域极具代表性的问题了,算法上使用树结构。现在我们一步一步来使用Javascript、Node、mongodDB来解决评论模型问题。

#Step 1 建立模型

User:
    _id       : OBJECTID 用户ID
    username  : STRING   用户名
    
Article:
    _id       : OBJECTID 文章ID 
    userID    : OBJECTID 用户ID
    username  : STRING   用户名  反常规化
    title     : STRING   标题
    text      : STRING   内容
    
Comment:
    _id       : OBJECTID 评论ID
    userID    : OBJECTID 用户ID
    username  : STRING   用户名  反常规化
    articleID : OBJECTID 文章ID
    parentID  : OBJECTID 上级评论ID,可以没有
    text      : STRING   内容

在这里,使用了反常规化,在Article、Comment中不但存储了用户ID,还存储了用户名。 这样会增加数据文件的容量,但是查询的时候可以不用去关联User,从而减少一次请求,提升速度。所谓的“以空间换时间”。

#Step 2 使用node-mongodb-native,编写MongoDB数据库服务器连接代码 dbm.js:

/// dbm.js ///

var mongodb = require("mongodb");
// 数据库连接缓存
var cache = {};

// 连接数据库
function connect (url, options) {
    var fns = [];
    var status = 0;
    var _db = cache[url];
    var args;

    return function (f) {
        args = arguments;
        if (_db !== null && typeof _db === "object") {
            f(_db);
            return;
        } 
            
        fns.push(f);
        // 当有一个连接初始化请求时,挂起其他初始化请求
        // 连接池建立完后,使用该连接处理挂起的请求
        if (status === 0) {
            status = 1;
            mongodb.MongoClient.connect(url, options, function (err, db) {
                if (err) { throw err; }
                _db = cache[url] = db;
                for (var i = 0, len = fns.length; i < len; i++) {
                    fns.shift().call(null, _db);
                }
            });
        }
    };
}

// 关闭数据库
function close (url) {
    var db = cache[url]; 
    if (db !== null && typeof db === "object") {
        db.close();
        delete cache[url];
    }
}

exports.connect = connect;
exports.close = close;

这段代码封装了连接MongoDB的细节,只需要提供url和options(详情见MongoDb MongoClient连接配置)。 好处是可以并发访问MongoDB,在展示并发之前,还需要下面这个小工具:

/// dbm.js ///

function roll (count, f) {
    return function () { 
        count--;
        if (count === 0) {
            f();
        }
    };
}

function rollData (count, f) {
    var cache = {};
    return function (name, data) { 
        count--;
        if (typeof name === "string") {
            cache[name] = data;
        }
        if (count === 0) {
            f(cache);
        }
    };
}

exports.roll = roll;
exports.rollData = rollData;

roll rollData 可以测试并发请求是否全部结束,并在结束后启动回调函数,我们来写个Example:

var dbm = require("./dbm.js");
var connect = dbm.connect("mongodb://127.0.0.1:27017/teste", {
    "server" : {
        "poolSize" : 10  // 10条连接数
    }
});
var roll = dbm.roll(3, function () {  // 3个并发请求
    console.log("并发请求结束.");
});

console.log("并发请求开始.");
connect(function (db) {  // db就是映射的数据库teste
    db.collection("users").find({}, function (err, result) {
        roll();
    });
});
connect(function (db) {  // db就是映射的数据库teste
    db.collection("articles").find({}, function (err, result) {
        roll();
    });
});
connect(function (db) {  // db就是映射的数据库teste
    db.collection("comments").find({}, function (err, result) {
        roll();
    });
});

代码是并列写的,每个connect会向MongoDB服务器发送请求,并侦听结果。 每个返回会测试当前结束请求的个数,并在全部结束后,启动回调函数,输出console.log("并发请求结束.");

#Step 3 编写个查找评论的函数

/// dbm.js ///

var COLL_COMMENTS = "comments";
var contact = util.connect("mongodb://127.0.0.1:27017/teste", {
    "server" : {
        "poolSize" : 10  // 10条连接数
    }
});

function checkId (id) {
    return String(id).length === 24;
}

function findComment (id, f) {
// id : STRING, Comment ID
// f(err, result)
//     err    : OBJECT | NULL 服务器错误
//                            id格式错误
//     result : OBJECT 评论文档
//              NULL   没有该评论文档
    if(!checkId(id)) { return f({ name:"IDError", err:"Comment id invalid" }); }
    var selector = { _id: new mongodb.ObjectID(id) };
    contact(function (db) {
        db.collection(COLL_COMMENTS).findOne(selector, f);
    });
}

exports.findComment = findComment;

#Step 4 第2、3步都不是本章要关注的,现在才是我们主要关心的: 树渲染

当我们取到评论列表的时候应该如何渲染?

取到的结果会是这样的:

comments:
[
    { _id:"1", text:"foo", ... },
    { _id:"2", text:"foo", parentID:"1", ... },
    { _id:"3", text:"foo", parentID:"1", ... },
    { _id:"4", text:"foo", ... },
    { _id:"5", text:"foo", parentID:"2", ... },
    { _id:"6", text:"foo", parentID:"5", ... },
    ...
]

如何解决嵌套评论?

A lalala
  B lalala
    C lalala
D lalala
  E lalala
F lalala

这是个树递归的问题,每个项会有parentID来指定当前的判断KEY,如果没有parentID,那么这个KEY就是undefined.

=> 每当用KEY去comments列表查找,会找到一组结果。 => 我们再跳到结果第一个,保存上次的KEY,并设置KEY=当前的查找parentID。 => 又找到一组结果,然后保存上次的KEY,跳到新的结果,并设置KEY=当前的查找parentID。 => ... => 当树最左边的底层叶子完成的时候,向上回跳进行下一个叶子。 => ...

画成图形,可以是这样的过程:

[]
[]                            []   []
[          ][][] 
[完成]

[]
[]                            []   []
[          ][][] 
[完成][完成]

[]
[]                            []   []
[   完成    ][          ][] 
[完成][完成]  [完成]

[] 
[]                            []   []
[   完成    ][          ][] 
[完成][完成]  [完成][完成]

...

[] 
[]                            []   []
[   完成    ][     完成     ][] 
[完成][完成]  [完成][完成]

...

[] 
[完成]                         [完成]   [开始]

...

[]
[完成]                         [完成]   [完成]
...

从树的最左一支,最底点完成,向上向右破动,依次完成。 每个点在自己的栈内存中,跳到上一级会提取当前栈保存的KEY。

代码很简单,如下:

/// dbm.js ///

function serilize (comments, format) {
    var tree = { childs:[] }; 
    var UNDEFINED;

    function walk (key, value) {
        var i = 0;
        var comment = comments[i]; 
        while (comment) {
            var parentID = comment.parentID ? String(comment.parentID) : comment.parentID;
            if (parentID === key) {
                var child = {};
                var childKey = String(comment._id);
                var childValue = format(comment);
                childValue.childs = [];
                child[childKey] = childValue; 
                value.childs.push(child);
                walk(childKey, childValue);
            }
            comment = comments[++i];
        }
    }

    walk(UNDEFINED, tree);
    return tree;
}

exports.serilize = serilize;

当然了,我们可以写一个测试,看看ta是不是能成功渲染一棵树:

       var serilize = require("./dbm.js").serilize;
       var assert = require("assert");       

       var samples = [
            {
                _id: "id1",
                text: "foo 1"
            },
            {
                _id: "id2",
                parentID: "id1",
                text: "foo 2"
            },
            {
                _id: "id3",
                text: "foo 3"
            },
            {
                _id: "id4",
                parentID: "id2",
                text: "foo 4"
            },
            {
                _id: "id5",
                parentID: "id2",
                text: "foo 5"
            },
            {
                _id: "id6",
                parentID: "id4",
                text: "foo 6"
            }
        ];

        var result = serilize(samples, function (cmt) {
            return {
                text: cmt.text,
            };
        });
             
        assert.strictEqual(result["childs"][0]["id1"]["childs"][0]["id2"]["text"], "foo 2");
        assert.strictEqual(result["childs"][0]["id1"]["childs"][0]["id2"]["childs"][0]["id4"]["text"], "foo 4");
        assert.strictEqual(result["childs"][1]["id3"]["text"], "foo 3");

#Step 5 完成的评论查找测试

var dbm = require("./dbm.js");
var assert = require("assert");

dbm.findComments("53fded391f6769586a5e7fe1", function (err, result) {    
    var tree = dbm.serilize(result, function (cmt) {
        return {
            text: cmt.text,
            username: cmt.username
        };
    });
    console.log("%j", tree); 
    assert.ok(result instanceof Array);
    assert.ok(result.length > 0);
});

这里有个文章评论创建查找修改的代码,太长了,如果你感兴趣,可以来github: node-dbm看看。 当然,单元测试在这儿node-dbm-test

转载于:https://my.oschina.net/tulayang/blog/307825

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值