MongoDB上手笔记4——在很多数据中进行查询

上一次把简单的Restful API搭建起来了,接下来让我们往数据库里面填充一些数据。

俗话说的好,一天一个苹果,考不上博士。假设小明每天吃一个苹果,为了记录截止到某一天小明一共吃了多少个苹果,我在db logs的collection applesIAte中,记录了截止到某一天小明一共吃了多少苹果。

假设小明命中注定不会读博,所以小明骨骼惊奇的生下来就不吃奶,而是出生第一天就吃苹果,每天以苹果为生(好像一个并吃不饱的样子,请不要在意这些细节)。然后每天为了建设祖国而奋斗,一直活到了100岁,最后在100岁生日的当晚因为看到了祖国的建设成果过于兴奋(以及吃了太多的苹果)而含笑九泉。

按照这以上假设,我写了个脚本,记录了无私而强韧的小明的不平凡的一生中吃了的苹果数量。这段脚本中用了个insert_many()的方法,在一次性写入很多条document的时候比较快。

from flask_pymongo import MongoClient
mongo = MongoClient(host="localhost", port=27999)
db = mongo.logs
auth_res = db.authenticate("readwriter", "123456")
apples = [{
    "day": day,
    "apple": day
} for day in range(1, 100*365+1)]
db.applesIAte.insert_many(apples)

然后我们看一看小明这光辉的一生。

> db.applesIAte.stats()
{
        "ns" : "logs.applesIAte",
        "count" : 36500,
        "size" : 1752064,
        "avgObjSize" : 48,
        "numExtents" : 5,
        "storageSize" : 2793472,
        "lastExtentSize" : 2097152,
        "paddingFactor" : 1,
        "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
        "userFlags" : 1,
        "capped" : false,
        "nindexes" : 1,
        "totalIndexSize" : 1193696,
        "indexSizes" : {
                "_id_" : 1193696
        },
        "ok" : 1
}

恩,小明活了36500天(原谅我忘了闰年这事),真是了不起!

在回顾小明这光辉的一生(中吃了多少苹果)之前,让我们先做一些小手脚。这样我们才能看看我们能以多快的速度回顾(查询)了小明的一生(丫可是活了100年口阿!)

> db.setProfilingLevel(2)
{ "was" : 0, "slowms" : 100, "ok" : 1 }

上面这句话的意思是,我们所有的回顾小明一生的行为(查询)的相关信息都会被记录下来。

接下来我们看看小明在10000天的时候一共吃了多少苹果。(此处也可以用findOne({day: 10000}),不过并不影响结论)

> db.applesIAte.find({day: 10000})
{ "_id" : ObjectId("56a9c39cabf8322aa0828dec"), "day" : 10000, "apple" : 10000 }

显然他吃了10000个,awesome!那让我们看看我们花了多长时间发现他吃了这么多苹果。

> db.system.profile.find()
{
    "op":"query",
    "ns":"logs.applesIAte",
    "query":{
        "day":10000
    },
    "ntoreturn":0,
    "ntoskip":0,
    "nscanned":0,
    "nscannedObjects":36500,
    "keyUpdates":0,
    "writeConflicts":0,
    "numYield":285,
    "locks":{
        "Global":{
            "acquireCount":{
                "r":NumberLong(572)
            }
        },
        "MMAPV1Journal":{
            "acquireCount":{
                "r":NumberLong(286)
            }
        },
        "Database":{
            "acquireCount":{
                "r":NumberLong(286)
            }
        },
        "Collection":{
            "acquireCount":{
                "R":NumberLong(286)
            }
        }
    },
    "nreturned":1,
    "responseLength":62,
    "millis":20,
    "execStats":{
        "stage":"COLLSCAN",
        "filter":{
            "day":{
                "$eq":10000
            }
        },
        "nReturned":1,
        "executionTimeMillisEstimate":10,
        "works":36502,
        "advanced":1,
        "needTime":36500,
        "needFetch":0,
        "saveState":285,
        "restoreState":285,
        "isEOF":1,
        "invalidates":0,
        "direction":"forward",
        "docsExamined":36500
    },
    "ts":    ISODate("2016-01-28T07:53:57.857    Z"),
    "client":"127.0.0.1",
    "allUsers":[
        {
            "user":"boss",
            "db":"admin"
        },
        {
            "user":"readwriter",
            "db":"logs"
        }
    ],
    "user":"readwriter@logs"
}

上面这一大段基本上都没啥用,我们只需要两条——"millis":20,"docsExamined":36500。也就是查询一共花了20ms,查看了36500条document才找到了这条记录。(好慢!)

如果小明不是一个普通的人,是一个脱离了低级趣味的神仙,他活了一万年,甚至十万年前他就和猛犸象谈笑风生了(可他依旧不想读博)。那么我们的记录会越来越多,查询速度会越来越慢。

而我们为days加上索引可以有效的解决这个问题。因为day是单调增长的,所以参数是{day: 1}。

> db.applesIAte.createIndex({day: 1})
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> db.applesIAte.stats()
{
        "ns" : "logs.applesIAte",
        "count" : 36500,
        "size" : 1752064,
        "avgObjSize" : 48,
        "numExtents" : 5,
        "storageSize" : 2793472,
        "lastExtentSize" : 2097152,
        "paddingFactor" : 1,
        "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
        "userFlags" : 1,
        "capped" : false,
        "nindexes" : 2,
        "totalIndexSize" : 2117584,
        "indexSizes" : {
                "_id_" : 1193696,
                "day_1" : 923888
        },
        "ok" : 1
}

从stats()返回的结果来看,day的索引确实是加上了。那么结果如何呢?

{
    "op":"query",
    "ns":"logs.applesIAte",
    "query":{
        "day":20000
    },
    "ntoreturn":0,
    "ntoskip":0,
    "nscanned":1,
    "nscannedObjects":1,
    "keyUpdates":0,
    "writeConflicts":0,
    "numYield":0,
    "locks":{
        "Global":{
            "acquireCount":{
                "r":NumberLong(2)
            }
        },
        "MMAPV1Journal":{
            "acquireCount":{
                "r":NumberLong(1)
            }
        },
        "Database":{
            "acquireCount":{
                "r":NumberLong(1)
            }
        },
        "Collection":{
            "acquireCount":{
                "R":NumberLong(1)
            }
        }
    },
    "nreturned":1,
    "responseLength":62,
    "millis":0,
    "execStats":{
        "stage":"FETCH",
        "nReturned":1,
        "executionTimeMillisEstimate":0,
        "works":2,
        "advanced":1,
        "needTime":0,
        "needFetch":0,
        "saveState":0,
        "restoreState":0,
        "isEOF":1,
        "invalidates":0,
        "docsExamined":1,
        "alreadyHasObj":0,
        "inputStage":{
            "stage":"IXSCAN",
            "nReturned":1,
            "executionTimeMillisEstimate":0,
            "works":2,
            "advanced":1,
            "needTime":0,
            "needFetch":0,
            "saveState":0,
            "restoreState":0,
            "isEOF":1,
            "invalidates":0,
            "keyPattern":{
                "day":1
            },
            "indexName":"day_1",
            "isMultiKey":false,
            "direction":"forward",
            "indexBounds":{
                "day":[
                    "[20000.0, 20000.0]"
                ]
            },
            "keysExamined":1,
            "dupsTested":0,
            "dupsDropped":0,
            "seenInvalidated":0,
            "matchTested":0
        }
    },
    "ts":    ISODate("2016-01-28T08:05:46.904    Z"),
    "client":"127.0.0.1",
    "allUsers":[
        {
            "user":"boss",
            "db":"admin"
        },
        {
            "user":"readwriter",
            "db":"logs"
        }
    ],
    "user":"readwriter@logs"
}

结果发现"docsExamined":1,"millis":0。我们只查询了一条数据就直接找到了到20000天的时候小明吃了多少苹果,花的时间更是小到近似于0了!所以说当小明成仙之后,我们必须对day加上索引才能知道他为了不读博吃了多少苹果XD。

后记:最近看了一本书,作者比鸟哥还谐,整本书写的很像《春物》这种轻小说的风格。于是尝试着『学习』了一下这种风格,试过之后感觉有些浮夸啊XD

MongoDB上手笔记3——写个简单的Restful API读写数据库

上一篇中设置好了db logs的两个用户,并尝试往collection gen1中写入了一条简单的document。使用上一篇中的同样的方法,又写入了新的两条document,现在gen1中有了三条document。

> db
logs
> db.gen1.find()
{ "_id" : ObjectId("56a0daabcf03a917cb662aef"), "time" : 1, "value" : 1 }
{ "_id" : ObjectId("56a0dab3cf03a917cb662af0"), "time" : 2, "value" : 2 }
{ "_id" : ObjectId("56a0dabacf03a917cb662af1"), "time" : 3, "value" : 3 }

在命令行中写js语句来操作mongoDB自然是可行的,可是有很多情况需要使用别的语言来读取数据库。js可能缺少某些别人造好的轮子,比如我想用python来做科学计算,数据存在mongoDB里面,我就需要用python的驱动来操作数据库。

提供这个功能的轮子已经有很多了,mongoEngine和pymongo之类的。但是代码变多之后,感觉维护变得比较麻烦。代码中往往操作数据库和进行计算的部分混合在一起,看着条理不是很清晰。

所以想是不是可以将两部分分开来做,中间用个什么东西连接起来。后来知道了很Fancy(大概?)的Restful API,于是想着可以把各个部分分开,中间用http连接起来。

于是用flask和做了个十分简单的Restful API,功能就只有读取db logs的collection gen1中的最新一条数据以及向其中写数据。

api的验证部分使用了最简单的basic http authentication。request header中的auth部分就只是使用了base64加密而已(都不能算得上加密吧),十分简陋,完全没有安全性。每次请求都会创建一个MongoClient,简单测试了一下,目前没看出来有什么性能影响。

# 创建MongoClient
mongo = MongoClient(host="localhost", port=27999)
# 使用创建的对象验证身份
mongo["logs"].authenticate(request.authorization.username, request.authorization.password)

以下就是所有的代码了,一共只有50行。

simple_restful.py

from flask import request
from flask_restful import Resource
from bson.json_util import loads, dumps
from flask_pymongo import MongoClient
from pymongo.errors import OperationFailure
from flask import Flask
from flask_restful import Api


class Data(Resource):
    def get(self):
        mongo = MongoClient(host="localhost", port=27999)
        try:
            mongo["logs"].authenticate(request.authorization.username, request.authorization.password)
            last_log = mongo["logs"]["gen1"].find().sort("_id", -1)[0]
            return {
                       "err": "False",
                       "message": "Successfully get data",
                       "result": {"data": dumps(last_log)}
            }
        except OperationFailure, err:
            return {
                "err": "True",
                "message": "Failed getting data",
                "result": err.details
            }

    def post(self):
        mongo = MongoClient(host="localhost", port=27999)
        try:
            mongo["logs"].authenticate(request.authorization.username, request.authorization.password)
            data = request.data
            l = loads(data)
            result = mongo["logs"]["gen1"].insert_one(l["data"])
            return {
                "err": "False",
                "message": "Successfully post data",
                "result": {"data": dumps(l["data"]), "ack": str(result.acknowledged)}
            }
        except OperationFailure, err:
            return {
                "err": "True",
                "message": "Failed post data",
                "result": err.details
            }

app = Flask(__name__)
api = Api(app)
api.add_resource(Data, '/data')
app.run(debug=True)

使用curl简单测试一下吧。

>curl -v -H "Content-Type: application/json" --user readwriter:123456 http://localhost:5000/data
* Adding handle: conn: 0x140cf10
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x140cf10) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* Server auth using Basic with user 'readwriter'
> GET /data HTTP/1.1
> Authorization: Basic cmVhZHdyaXRlcjoxMjM0NTY=
> User-Agent: curl/7.33.0
> Host: localhost:5000
> Accept: */*
> Content-Type: application/json
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 189
< Server: Werkzeug/0.11.3 Python/2.7.10
< Date: Thu, 21 Jan 2016 13:59:24 GMT
<
{
    "err": "False",
    "message": "Successfully get data",
    "result": {
        "data": "{\"_id\": {\"$oid\": \"56a0dabacf03a917cb662af1\"}, \"value\":
3.0, \"time\": 3.0}"
    }
}
* Closing connection 0

确实读出来了最新的一条document。再写入一条document试试。

C:\Users\Juiceyang\Desktop>curl -v -H "Content-Type: application/json" --user re
adwriter:123456 http://localhost:5000/data -X POST -d "{\"data\":{\"time\":4,\"v
alue\":4}}"
* Adding handle: conn: 0x85cf10
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x85cf10) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* Server auth using Basic with user 'readwriter'
> POST /data HTTP/1.1
> Authorization: Basic cmVhZHdyaXRlcjoxMjM0NTY=
> User-Agent: curl/7.33.0
> Host: localhost:5000
> Accept: */*
> Content-Type: application/json
> Content-Length: 29
>
* upload completely sent off: 29 out of 29 bytes
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 210
< Server: Werkzeug/0.11.3 Python/2.7.10
< Date: Thu, 21 Jan 2016 14:02:58 GMT
<
{
    "err": "False",
    "message": "Successfully post data",
    "result": {
        "ack": "True",
        "data": "{\"_id\": {\"$oid\": \"56a0e512612bf10dcce366d7\"}, \"value\":
4, \"time\": 4}"
    }
}
* Closing connection 0

结果来看也确实写入成功了。

MongoDB上手笔记2——新建用户并设置权限

上一篇说到了把MongoDB安装完毕,并设置了开机启动服务。接下来记录下怎么创建用户,并通过MongoDB的权限管理功能进行管理。

mongodb的用户权限管理的方式是分组的,不同的用户组有着不同的权限,对应的就是可以执行不同的命令。

形象地说就是警察用户组的只有抓捕的权力,医生用户组的只有开处方的权力等等。MongoDB的用户组的设置也秉承了这样的方法,在保证能正常使用的前提下,只给特定用户最少的权力。

首先,先启动mongod,此处使用配置文件的启动方法。

mongod --config E:/MongoDB/mongod.conf

然后连接mongodb。

mongo 127.0.0.1:27999
show dbs
use admin

这个时候可以看到成功的连接了进去,只有一个local的数据库。然后切换到admin数据库,准备创建用户。

MongoDB内置了许多类型的用户类型,大致分为了几类:

  • 单个数据库用户(Database User Roles)
  • 单个数据库管理员(Database Administration Roles)
  • 集群管理员(Cluster Administration Roles)
  • 备份恢复管理员(Backup and Restoration Roles)
  • 全部数据库的管理员(All-Database Roles)
  • 超级用户(Superuser Roles)

不同的用户类别下都有若干个用户类型,分别对应的这不同的权力。比如现在有一个数据库里记录着不同时间段的测量数据,我们想把特定时间段的测量数据读出来,然后在脚本里输出成csv文件,我们就需要一个read用户就够了,而不需要一个root用户(对应的就是上面说的『最少权力』的原则)。

回到正题,简单的举个例子,我们需要一个read用户用来读测量数据用来显示输出,一个readWrite用户用来写数据进入数据库,还有一个权力最大的用户来管理用户和数据库。然我就像建立一个公司一样,我们先任命一个boss,然后再由boss找员工(当然如果这个公司只有一个人的话,boss又要当老板,又要当苦力,那么在roles里面填多个role就可以了)。

db.createUser({user: "boss",pwd: "123456",roles:[{ role: "userAdminAnyDatabase", db: "admin" }]})

我们创建了一个叫boss的用户,他的角色是userAdminAnyDatabase,。从名字上也能看出来,userAdminAnyDatabase类型的用户可以管理所有数据库下的用户。(如果想在创建这个用户之前就开启权限管理,则需要在配置文件中开启localhost exception)

完成了用户管理员的创建之后就可以重新启动启用了权限管理的MongoDB了(此处在管理员权限的cmd中安装了mongoDB的服务)。

mongod --config E:/MongoDB/mongod.conf --auth --install

如果这个时候直接登陆的话,会发现有的命令是无法执行的,因为没有这个权限。比如最简单的显示所有的数据库都无法执行。

C:\Users\Juiceyang>mongo 127.0.0.1:27999/admin
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27999/admin
> db
admin
> show dbs
2015-12-13T20:29:44.926+0800 E QUERY    Error: listDatabases failed:{
        "ok" : 0,
        "errmsg" : "not authorized on admin to execute command { listDatabases:
1.0 }",
        "code" : 13
}
    at Error (<anonymous>)
    at Mongo.getDBs (src/mongo/shell/mongo.js:47:15)
    at shellHelper.show (src/mongo/shell/utils.js:630:33)
    at shellHelper (src/mongo/shell/utils.js:524:36)
    at (shellhelp2):1:1 at src/mongo/shell/mongo.js:47
> ^C
bye

而登录了之后,就获得了部分权限,比如可以看都有哪些数据库了。

C:\Users\Juiceyang>mongo 127.0.0.1:27999/admin -u boss -p 123456
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27999/admin
> show dbs
admin  0.078GB
local  0.078GB

之后就可以开始创建其他的用户了,创建的方法和创建boss的方法一样。假设我们需要对logs数据库进行读写。

use logs
db.createUser({user: "reader",pwd: "123456",roles:[{ role: "read", db: "logs" }]})
db.createUser({user: "readwriter",pwd: "123456",roles:[{ role: "readWrite", db: "logs" }]})

之后尝试用reader用户往gen1集合写一次数据。

> db.gen1.insert({"time":1, "value":1});
WriteResult({
        "writeError" : {
                "code" : 13,
                "errmsg" : "not authorized on logs to execute command { insert:
\"gen1\", documents: [ { _id: ObjectId('566d691840d302a5c4e9cfc6'), time: 1.0, v
alue: 1.0 } ], ordered: true }"
        }
})

发现无法写入,原因是没有权限执行insert。

之后换readwriter尝试一次。

> db.auth("readwriter","123456")
1
> db.gen1.insert({"time":1, "value":1});
WriteResult({ "nInserted" : 1 })

发现成功写入了。

再换回reader,读一次数据。

> db.auth("reader","123456")
1
> db.gen1.find()
{ "_id" : ObjectId("566d696a40d302a5c4e9cfc7"), "time" : 1, "value" : 1 }

发现能够成功读数据。

MongoDB上手笔记1——在windows desktop上安装并设置MongoDB服务

最近一个活儿要从设备那里每秒取一些数据回来,出于查询简单的想法,想把数据存在数据库里。之前不知道是因为错觉还是什么,一直觉得node.js非常的酉告(cooooool好像只能这样翻译了?),所以花了几天的时间学了学mongodb。大致整理成了如下这个类似于quick start之类的东西。

之所以注明是在windows desktop上安装,是因为这个活儿里,数据从设备到远程的PC中间数据通过GPRS传到第三方的服务器上,再通过第三方提供的软件从服务器上将数据取回,通过一个虚拟串口将数据读取出来。而第三方提供的软件只能运行在windows上,又只有盗版windows desktop,反正也只是做做样子只能凑活用下了。

1.简单启动MongoDB

从mongodb网站上下载完安装文件之后自行安装完毕。在此不表。

进入mongodb的安装路径中的放exe的文件夹下,可以看到里面有两个exe文件,分别叫mongo和mongod。

mongod就是启动数据库所需要的可执行文件。如果没有理解错误的话,mongod的名字应该是指mongo daemon process的意思,他会运行在后台,默默的完成数据库中的操作。

mongodb在安装的时候并不会把bin文件夹放到环境变量下,所以首先要将bin文件夹对应的路径放到系统环境变量PATH里。这样在cmd中输入mongo和mongod时,它才会知道这两个东西在哪里。

运行数据库,自然需要对数据库进行一系列的配置。在cmd中运行mongod的时候可以完成一些简单的配置,比如数据库存储数据的路径、日志文件的路径、端口的设置等等。例如如下的配置方式会在D:/dbs/data下存储数据文件,将日志文件存储在D:/dbs/log.txt中。

mongod --dbpath d:/dbs/data --logpath d:/dbs/log.txt

2.配置文件启动MongoDB

不过因为在cmd中输入一大长串字符串十分的不方便;而且会在桌面上留下一个cmd大黑框,很不美观。所以还是推荐使用配置文件进行数据库的配置。(当然如果不觉得cmd大黑框无所谓的话,桌面上放个bat文件也可以)

先建立一个mongodb的配置文件,假设放在了D:/dbs/mongo.conf这个路径下。随便用个什么编辑器打开mongo.conf。数据库配置文件使用的是YAML格式,简单使用的话,YAML和JSON没什么不一样的。不过需要注意的一个地方就是YAML不支持tab缩进,所以需要使用空格。以下是我的配置文件,简单配置的话大概有这么几个参数。

systemLog:
    destination: file
    path: "e:/MongoDB/log.txt"
    logAppend: true
storage:
    dbPath: "e:/MongoDB/data"
    journal:
        enabled: true
net:
    bindIp: 127.0.0.1
    port: 27999

其中,各个选项的意思如下。

systemLog.destination的值可以为file或是syslog,因为之后我们会将mongod设置为一个系统服(也就是不会出现cmd大黑框,并可以设置为开机启动)。所以设置为syslog没有意义,此处选择file。

当systemLog.destination设置为file的时候,systemLog.path也必须被确定(也就是上面对应的--logpath的参数)此参数设置了日志文件的路径。

systemLog.logAppend设置为true之后,日志文件就会每次都在现有的log.txt之后续写。默认的是false,则每次mongod重启之后都会同文件夹下备份上次的日志文件,同时新建一个日志文件。

storage.dbPath对应的就是之前--dbpath之后的参数,设置数据文件的存储目录。

storage.journal.enabled对应是否启用mongodb的日志机制。简单的可以理解为启用之后,mongodb崩溃之后可以快速恢复,即可以满足ACID中的一致性。

net.bindIp这个参数是设置绑定ip用的,默认使用本地地址。

net.port这个参数是设置绑定的端口的,默认使用是27017,此处改了改,改成了27999。

假设配置文件mongo.conf放在了d:/dbs/mongo.conf路径下。在这之后,就可以win+r打开cmd,然后输入如下命令启动mongodb。

mongod --config d:/dbs/mongo.conf

3.安装MongoDB服务

按照上面的做法,还是会出现一个cmd大黑框,而且每次都要输入一遍这个命令,还是很麻烦。

这个问题可以通过设置为MongoDB服务来解决(推荐在设置完用户权限之后在安装服务)。

首先以管理员权限打开cmd,然后输入如下命令。

mongod --config d:/dbs/mongo.conf --install

之后就可以在计算机管理->服务和应用程序->服务中看到MongoDB服务了。也可以修改是否要开机自动启动。这样就不会出现cmd的大黑框了。

如果需要修改配置文件,需要先删除现有的MongoDB服务,命令如下。

mongod --remove

之后修改配置文件,并重新安装MongoDB服务即可。