node学习笔记——初识MongoDB

数据库

数据库就是存储数据的,那么存储数据就用txt就行了啊,为什么要有数据库?

  • 数据库有行、列的概念,数据有关系,数据不是散的。
  • 据库能够提供非常方便的接口,让增删改查操作变得简单
  • 数据库不能自己玩儿,要给向PHP、.net、jsp等语言提供接口。

老牌数据库,比如MySQL、SQL Server、Oracle、Access。这些数据库,我们管他们叫做结构型数据库。每个表中,都有明确的字段,每行记录,都有这些字段,不能有的行有,有的行没有。

非结构型数据库NoSQL

在大数据时代,如果数据库中存储的数据很多,如果突然需要改变某个字段,很不方便,因为所谓的字段,是表的一个结构,所有的行都必须拥有,不能有的行有这个字段,有的行没有这个字段。

所以非结构型数据库 NoSQL 应运而生,NoSQL 是个怪胎,无法挑战老牌数据库,但是在大数据时代有自己的意义。NoSQL 非结构型数据库,没有行、列的概念,用JSON来存储数据,集合就相当于“表”,文档就相当于“行”。

在 NoSQL 中,最小的“数据条目”,不是“行”,而是“文档”。文档就是键值对的一个集合,实际上表达方式和JSON一样。

文档就是JSON,但是要比JSON多了一些限制:

  • 每个文档必须有一个特殊的键 _ID ,这个键在集合中必须是唯一的。
  • 文档中的所有键不能重复;大小写不同的键,视为不同的键。
  • 文档中键的命名,不能含有.和$,其他不限,甚至可以用中文命名、阿拉伯数字。
  • 文档中值的类型,比JavaScript中多了一些,比如日期、ObjectId()、正则表达式。
  • 文档给程序员看的时候,是JSON的表示模式,但是实际存储的时候,是BSON方式,即用二级制方式存储。

NoSQL数据库包括四大类:键值存储数据库、列存储数据库、文档型数据库,图像数据库。

NoSQL数据库在以下的这几种情况下比较适用:

  1. 1、数据模型比较简单;
  2. 需要灵活性更强的IT系统;
  3. 对数据库性能要求较高;
  4. 不需要高度的数据一致性;
  5. 对于给定key,比较容易映射复杂值的环境。

mysql 与 mongoDB 对比图:

MongoDB

MongoDB 属于NoSQL 文档型数据库,主要命令以三种开头:

  • mongo 使用数据库
  • mongod 开启
  • mongoimport 导入数据

打开命令行 CMD,开启命令为:mongod --dbpath E:\mongo,其中 –dbpath 就是选择数据库文档所在的文件夹。这个 CMD 不能动了,不能关,不能ctrl+c。 一旦这个cmd有问题了,数据库就自动关闭了。所以,应该再开一个cmd。输入:mongo

如果是3.0以上的 mongdb 则这样开启 mongod --storageEngine mmapv1 --dbpath 数据目录,3.2 版本开始,MongoDB 支持多数据存储引擎(Storage Engine),MongoDB 支持的存储引擎有:WiredTiger,MMAPv1 和 In-Memory。

那么,运行环境就是mongo语法了,下面是一些经常用的增删改查方法:

  • 列出所有数据库:show dbs
  • 使用某个数据库:use 数据库名字,use一个不存在的,就是新建
  • 查看当前所在数据库:db
  • 插入数据:db.student.insert({"name":"xiaoming","age":"12","sex","male"}),student就是所谓的集合。集合中存储着很多json。如果student是第一次使用,集合将自动创建。
  • 清屏:cls
  • 删除数据库,删除当前所在的数据库:db.dropDatabase();
  • 将 JSON 导入数据库:mongoimport --db test --collection restaurants --drop --file primer-dataset.json:
    • –db test 想往哪个数据库里面导入
    • –collection restaurants 想往哪个集合中导入
    • –drop 把集合清空
    • –file primer-dataset.json 哪个文件
  • 查找数据,用find(),findOne() 则只获取第一条:
    • find中没有参数,那么将列出这个集合的所有文档:db.restaurants.find()
    • 精确匹配:db.student.find({"score.shuxue":70});
    • 多个条件:db.student.find({"score.shuxue":70 , "age":12})
    • 大于条件:db.student.find({"score.yuwen":{$gt:50}})
    • 或者,寻找所有年龄是9岁,或者11岁的学生:db.student.find({$or:[{"age":9},{"age":11}]})
    • 查找完毕之后,打点调用sort,表示升降排序:db.restaurants.find().sort({ "borough": 1, "address.zipcode": 1 })
  • 查找名字叫做小明的,把年龄更改为16岁:db.student.update({"name":"小明"},{$set:{"age":16}})
  • 查找数学成绩是70,把年龄更改为33岁:db.student.update({"score.shuxue":70},{$set:{"age":33}})
  • 默认情况下,update() 方法更新单个文档。要更新多个文档,请在update() 方法中使用多重选项 {multi: true}:db.student.update({"sex":"男"},{$set:{"age":33}},{multi: true});
  • 完整替换,不出现$set关键字了:db.student.update({"name":"小明"},{"name":"大明","age":16});
  • 删除数据,默认删除全部匹配项:db.restaurants.remove({ "borough": "Manhattan" }),如果需要删除单项,请加{justOne: true} :db.restaurants.remove({ "borough": "Queens" }, { justOne: true })
  • 删除子文档数据:db.collection.update({ field: <query> }, { $pull: { field: <query> }});例如:db.test.update({"userId":100000077},{"$pull":{"cartList":{"productId":201710009}}});

需要说明的是,find() 的第二个参数限制返回的 filed 的个数,0代表不返回,1代表返回:

1
2
3
4
5
6
> db.Student.find({name:"jack"},{name:1,age:1})
{ "_id" : ObjectId("5437383157abafe09d99cbfc"), "name" : "jack", "age" : 33 }
{ "_id" : ObjectId("543738b857abafe09d99cbfd"), "name" : "jack", "age" : 33 }
> db.Student.find({name:"jack"},{sex:0})
{ "_id" : ObjectId("5437383157abafe09d99cbfc"), "name" : "jack", "age" : 33 }
{ "_id" : ObjectId("543738b857abafe09d99cbfd"), "name" : "jack", "age" : 33 }

“$lt”,”$lte”,”$gt”,”$gte”分别对应<,<=,>,>= ,此外还有:逻辑操作符:”$or”, “$and”,”$not”,”$nor”,以及包含与不包含:”$in”,”$nin”。

MongoDB 分页

假如我们有有一个学生列表需要分页,那么有两种做法:

  1. 错误的做法: 就是讲所有的result都读取到数组,然后进行数据操作,进行分页;
  2. 正确的做法: 就是真的在数据库中,只读取这么多内容。

如果利用第一种错误的方式,我们试图每次都读取全部数据,但是这样开销很大:

1
2
3
4
5
6
7
var a = [];
db.find("news",{},function(err,result){
for(var i = 10 * page ; i < 10 * (page + 1) ; i++){
a.push(result[i]);
}
res.send(a);
});

所以,mongodb提供了傻傻的两个函数: limit() 和 skip():

假如第一页是page=0。每页10条,所以当前页的查询语句:

1
db.student.find({}).limit(10).skip(page*10)

得到数据总数:

1
2
db.student.stats().count
db.student.find().count();

mongoDB 索引

在数据库操作中,根据一个字段的值,来寻找一个文档,是很常见的操作,比如根据学号来找一个学生。

这个学号,是唯一的,只要有学号,就能唯一确认一个学生的文档(类似于结构型数据库中的主键)。学号这个属性,就非常适合建立索引,这样一来,查找学生就变得简单了。

学生的姓名是唯一的,为了快速的进行检索,所以就把name属性建立成为“索引”:

1
db.student.createIndex({"name":1});

这样,今后通过 name 寻找 student 文档的时候,速度非常快。因为能够快速的从索引表中,找到这个文档。缺点就是插入每条数据的时候,时间变慢了,效率低了。但是换回来的就是寻找的速度快了。

索引这个属性,所有的文档都不能相同:

1
db.members.createIndex({"user_id": 1}, {unique: true });

需要注意的是:前面有说到过,在 mongoDB 中,每条数据都有一个特殊的键 _id ,这个键在集合中是唯一的,只是来实际查找中没有那么方便,使用时需要引入require('mongodb').ObjectID;,且数值是随机的一串,不是我们自己写入的,比较不好控制。

mongoDB 使用

一个简易的留言板增删改查:

数据库增删改查封装,db.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//这个模块里面封装了所有对数据库的常用操作
var MongoClient = require('mongodb').MongoClient;
var settings = require("../settings.js");
//不管数据库什么操作,都是先连接数据库,所以我们可以把连接数据库
//封装成为内部函数
function _connectDB(callback) {
var url = settings.dburl; //从settings文件中,获取数据库地址"mongodb://localhost:27017/mongotest"
//连接数据库
MongoClient.connect(url, function (err, db) {
if (err) {
callback(err, null);
return;
}
callback(err, db);
});
}

//插入数据
exports.insertOne = function (collectionName, json, callback) {
_connectDB(function (err, db) {
db.collection(collectionName).insertOne(json, function (err, result) {
callback(err, result);
db.close(); //关闭数据库
})
})
};

//查找数据,找到所有数据。args是个对象{"pageamount":10,"page":10}
exports.find = function (collectionName, json, C, D) {
var result = []; //结果数组
if (arguments.length == 3) {
//那么参数C就是callback,参数D没有传。
var callback = C;
var skipnumber = 0;
//数目限制
var limit = 0;
} else if (arguments.length == 4) {
var callback = D;
var args = C;
//应该省略的条数
var skipnumber = args.pageamount * args.page || 0;
//数目限制
var limit = args.pageamount || 0;
//排序方式
var sort = args.sort || {};
} else {
throw new Error("find函数的参数个数,必须是3个,或者4个。");
return;
}

//连接数据库,连接之后查找所有
_connectDB(function (err, db) {
var cursor = db.collection(collectionName).find(json).skip(skipnumber).limit(limit).sort(sort);
cursor.each(function (err, doc) {
if (err) {
callback(err, null);
db.close(); //关闭数据库
return;
}
if (doc != null) {
result.push(doc); //放入结果数组
} else {
//遍历结束,没有更多的文档了
callback(null, result);
db.close(); //关闭数据库
}
});
});
}

//删除
exports.deleteMany = function (collectionName, json, callback) {
_connectDB(function (err, db) {
//删除
db.collection(collectionName).deleteMany(
json,
function (err, results) {
callback(err, results);
db.close(); //关闭数据库
}
);
});
}

//修改
exports.updateMany = function (collectionName, json1, json2, callback) {
_connectDB(function (err, db) {
db.collection(collectionName).updateMany(
json1,
json2,
function (err, results) {
callback(err, results);
db.close();
});
})
}
//获取数据总条数
exports.getAllCount = function (collectionName,callback) {
_connectDB(function (err, db) {
db.collection(collectionName).count({}).then(function(count) {
callback(count);
db.close();
});
})
}

后台代码,backstage.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//这个模块是后台的接口代码
var express = require("express");
var app = express();
var db = require("./model/db.js");
var formidable = require('formidable');
var ObjectId = require('mongodb').ObjectID;


//设置ejs模板引擎
app.set("view engine", "ejs");

//静态
app.use(express.static("./public"));
//显示留言列表
app.get("/", function (req, res, next) {
db.getAllCount("liuyanben",function(count){
res.render("index",{
"pageamount" : Math.ceil(count / 20)
});
});
});

//读取所有留言,这个页面是供Ajax使用的
app.get("/du", function (req, res, next) {
//可以接受一个参数
var page = parseInt(req.query.page);

db.find("liuyanben",{},{"sort":{"shijian":-1},"pageamount":20,"page":page},function(err,result){
res.json({"result":result});
});
});

//处理留言
app.post("/tijiao", function (req, res, next) {
var form = new formidable.IncomingForm();

form.parse(req, function (err, fields) {
//写入数据库
db.insertOne("liuyanben", {
"xingming" : fields.xingming,
"liuyan" : fields.liuyan,
"shijian" : new Date()
}, function (err, result) {
if(err){
res.send({"result":-1}); //-1是给Ajax看的
return;
}
res.json({"result":1});
});
});
});


//删除
app.get("/shanchu",function(req,res,next){
//得到参数
var id = req.query.id;//根据唯一_id删除
db.deleteMany("liuyanben",{"_id":ObjectId(id)},function(err,result){
res.redirect("/");
});
})

app.listen(3000);

前端html模板,index.ejs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>简易留言板</title>
<link href="css/bootstrap.min.css" rel="stylesheet"/>
<style type="text/css">
#chenggong, #shibai {
display: none;
}

.liuyankuai {
padding: 10px 0;
border-bottom: 1px solid #ccc;
}
</style>
</head>
<body>
<h1>我的留言本</h1>

<div class="container">
<div class="row">
<form class="form-horizontal col-lg-6">
<div class="form-group">
<label for="xingming" class="col-sm-2 control-label">姓名</label>

<div class="col-sm-10">
<input type="text" class="form-control" id="xingming" name="xingming" placeholder="姓名">
</div>
</div>
<div class="form-group">
<label for="liuyan" class="col-sm-2 control-label">留言</label>

<div class="col-sm-10">
<textarea class="form-control" rows="3" name="liuyan" id="liuyan"></textarea>
</div>
</div>

<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button id="tijiao" type="button" class="btn btn-success">提交</button>
</div>
</div>
<div class="alert alert-success" role="alert" id="chenggong">
<a href="#" class="alert-link">表单已经成功提交</a>
</div>
<div class="alert alert-danger" role="alert" id="shibai">
<a href="#" class="alert-link">表单提交失败</a>
</div>
</form>
</div>
<nav>
<ul class="pagination">

<% for(var i = 1 ; i <= pageamount ; i++){%>
<li class="yemaanniu" data-page="<%=i%>"><a href="#"><%=i%></a></li>
<%}%>

</ul>
</nav>
<div id="quanbuliuyan">

</div>
</div>

<script type="text/template" id="moban">
<div class="liuyankuai">
<p>【姓名】{{= xingming }}</p>
<p>【留言】{{= liuyan }}</p>
<p>【时间】{{= shijian }}</p>
<p><a href="/shanchu?id={{=id}}" class="shanchu">删除</a></p>
</div>
</script>

<script src="js/jquery-1.11.3.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script type="text/javascript" src="js/underscore-noflect.js"></script>

<script type="text/javascript">
var nowpage = 1;

//给第一个页面,补一个active
$(".yemaanniu:first").addClass("active");

//给所有的页码按钮,添加监听
$(".yemaanniu").click(function(){

nowpage = parseInt($(this).attr("data-page"));
//重新发起请求,即可
getData(nowpage);

$(this).addClass("active").siblings().removeClass("active");
});

//默认请求第一页数据
getData(1);

//Ajax请求数据
function getData(page) {
//真实page是从0开始算的
$.get("/du?page=" + (page - 1), function (result) {
//这里接收是result,但是这个json里面有一个key叫做result。
//得到模板,弄成模板函数
var compiled = _.template($("#moban").html());
//清空全部留言中的所有节点
$("#quanbuliuyan").html("");
for (var i = 0; i < result.result.length; i++) {
//数据绑定
var html = compiled({
liuyan: result.result[i].liuyan,
xingming: result.result[i].xingming,
shijian: result.result[i].shijian,
id: result.result[i]._id
});
//DOM操作,添加节点
$("#quanbuliuyan").append($(html));
}
});
}

//Ajax提交表单
$("#tijiao").click(function () {
$("#shibai").hide();
$("#chenggong").hide();
$.post("/tijiao", {
"xingming": $("#xingming").val(),
"liuyan": $("#liuyan").val()
}, function (result) {
if (result.result == -1) {
$("#shibai").fadeIn();
} else if (result.result == 1) {
//提交成功
$("#chenggong").fadeIn();
//数据库真的存储了,但是当前页面无法显示。这是因为需要刷新
//才能用ajax从/du中得到新的。所以我们先用一个假盒子凑出来。
var compiled = _.template($("#moban").html());
var html = compiled({liuyan: $("#liuyan").val(), xingming: $("#xingming").val(), shijian: new Date()});
$(html).insertBefore($("#quanbuliuyan"));
}
});
});
</script>
</body>
</html>

通过配置文件的形式启动MongoDB

a.在c:\MongoDB(可随意起)下面建一个data文件夹 c:\MongoDB\data

b.在c:\MongoDB(可随意起)下面建一个logs文件夹 c:\MongoDB\logs ,在里面建一个文件mongo.log

c.在c:\MongoDB(可随意起)下面建一个etc(随意起,放配置文件)文件夹 c:\MongoDB\etc ,在里面建一个文件mongo.conf

d.打开mongo.conf文件,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#数据库路径
dbpath=e:\mongomallsystem\data\

#日志输出文件路径
logpath=e:\MongoDB\logs\mongodb.log

#错误日志采用追加模式,配置这个选项后mongodb的日志会追加到现有的日志文件,而不是从新创建一个新文件
logappend=true

#启用日志文件,默认启用
journal=true

#这个选项可以过滤掉一些无用的日志信息,若需要调试使用请设置为false
quiet=false

#端口号 默认为27017
port=27017

#指定存储引擎(默认先不加此引擎,如果报错了,再加进去):
storageEngine=mmapv1

完成以上操作后,我们就可以启动我们的mongo数据库了:

在 cmd 命令行运行:

1
mongod --config e:\mongomallsystem\etc\mongo.conf

注意:运行这条命令后,命令行没有任何反应,查看27017的端口也无法查看连接,但是数据库是开启状态的,这时候打开另外一个 cmd 就可以进行数据库操作了。

如果希望查看,也可以开启端口查看:

1
mongod --config e:\mongomallsystem\etc\mongo.conf --httpinterface

通过以上命令就完成了,并且可以通过默认28017查看数据库状态日志,以后每次运行数据库执行 mongo.conf 配置文件就行

当然,以上是通过配置的形式,来启动我们的MongoDB,也可以把参数直接在启动的时候,传递进去如下:

1
mongod --dbpath e:\mongomallsystem\data --logpath e:\mongomallsystem\logs\mongodb.log --journal

安装到window 服务当中

输入命令:

1
mongod --config e:\mongomallsystem\etc\mongo.conf --install --serviceName "MongoDB"

安装成功后,打开window服务(win10在任务管理器上可以看到window服务),我们可以看到里面已经安装了MongoDB。

每天,我们用的时候,只需要启动服务即可,如果大家不安装此服务也可以,每次启动的时候,手动进入到安装目录里面,输入命令启动,两种方式都可以。

删除服务命令:mongod –config c:\MongoDB\etc\mongo.conf –remove 然后在服务里面刷新一下,就会发现已经删掉了。



完~