为什么要使用 MongoDB 以及 Pymongo
在程序开发实践中,除了学习代码、算法之外,其他开发有用的程序一定离不开数据库。然而,传统的 CS 专业里面教授数据库往往会从 SQL 数据库开始学起,但是,要使用 SQL 数据库,那么第一步一定离不开如何建表,设置字段、设置键的类型等,而作为初学者,加上没有真是的项目背景的情况下,数据库的设计往往只能是那种一个 User 用户表,然后再一个 username 、 passwd、 nickname 之类的字段设计,再在这个‘小朋友’基础版本的数据库上完成一些 CURD 操作后,向再进一步开发一些小 DEMO 时,由于无法预先知道所需要的数据模型到底是什么样的,就会出现不断的改表、改数据库的情况,再面对不熟悉的项目,最后弄得焦头烂额,反倒是打击了学习的兴趣。
再者,在一些个人项目,或者是一些小微创业公司、新孵化的项目,同样存在市场不明确、需求不明确的情况,使用传统的数据库分析、设计流程,一来耗费时间经历,二来辛辛苦苦设计出来的模型可能刚设计出来又不符合需求了。
针对这些情况,使用 NoSQL 数据库,比如 MongoDB,就能很好避免上述的问题。NoSQL 数据库有一个特点,就是并没有表的概念,以 MongoDB 为例,它只有 数据库、集合、文档三种结果,到文档这个维度级别,还可以在其中创建子文档、数组等数据类型。基于这种特性,我们就很容易做到“让产品跑起来,最小可用”的理念。当然,NoSQL 数据库并不仅仅只有 MongoDB 一款,不过由于 MongoDB 安装比较简单,社群完善,网上能找到比较多的教程,因此我们可以考虑使用 MongoDB 进行 NoSQL 数据库的学习及使用。
目前人工智能非常火,在人工智能流行的同时,Python 凭借其“胶水语言”的特点,也越来越受欢迎。使用 Python,我们可以借助庞大的库的支持,很快、很方便就能创造出很多有意思的项目,因此,本文下面都将以 Python 作为开发语言,MongoDB 作为数据库,来学习如何使用 Pymongo 这一个 MongoDB 操作的封装库来实现对 MongoDB 的使用。
本文虽然标题称为教程,但我却并不认为这篇文章能称得上教程,主要有以下两个原因:
- 人都好为人师而易自满。在软件,或者说可以行业,几乎所有东西都是日新月异,很可能这篇教程发布的时候,这些库或者软件就更新了,或者当中许多方法都变了,按照文章的方法可能就行不通了,这样的教程肯定是没有多大效用的。所以,分享这篇文章,主要是想分享一下从发现一个需求,到寻求答案、解决方案,再到最终能实施落地的一连串思考、思路的分享,这才是值得分享、交流传播的东西
- 人学习的过程,需要一个科学的步骤来完成。科学表明,看一次,跟着做一次的学习效果,远没有在看完、跟着做完,再呈现出来分享给别人的效果好。再有,把东西分享出去的过程当中,能收获到不少自己没有意识到,遗漏甚至错漏的地方,这对深造非常有帮助。
文章整体脉络
本文参考了 《MongoDB权威指南》以及 MongoDB 的官方文档,Pymongo 的官方文档作为参考。实际中,《MongoDB权威指南》主要以 mongo 终端交互为例进行数据库操作,因此本文借鉴了其中第三章、第四章的部分案例进行演示。而 Pymongo 的官方文档中,有一些方法也并没有提供详细的解释,因此通过推敲摸索以及综合部分Stack Overflow的讨论形成,关于这一部分,比较多的存在于修改器的使用部分,如对游标的 min() 及 max() 的使用等。
本文主要以五大部分来讲解如何利用 Pymongo 操作 MongoDB,具体如下:
- 环境的搭建
- Pymongo 的安装
- Pymongo的基本使用
- 数据库连接
- 数据库、集合相关操作
- 基本的文档 CURD
- insert_one() insert_many()
- find() find_one()
- update()
- remove()
- 修改器在更新、查询中的使用
- $set
- $unset
- $inc
- $push
- $addToSet
- $pull
- $each
- $pop
- $lt $lte
- $gt $gte
- $ne
- $in nin
- $or
- $exist
- $all
- $size
- $slice
- min() max()
- $where
- 游标的使用
- sor()
- limit()
- skip()
PyMongo 安装
pymongo 可以使用 pip
安装,方法如下:
python -m pip install pymongo
如果是使用 Anaconda 环境的,可以使用 conda install
来安装。方法如下:
conda install pymongo
安装完成后,可以使用
conda list
命令查看 pymongo 模块是否成功安装,如果成功安装,可以在输出中看到上图结果。
Pymongo 数据库连接,数据库、集合相关操作
本文介绍使用 MongoDB 为 Docker 的镜像,系统为 Mac OS 10.13.4。MongoDB Docker 容器启动后,地址为默认的 localhost:27017
。
pymongo 连接数据库
我们先引入 pymongo
的 MongoClient
模块。再创建一个连接。
from pymongo import MongoClient
client = MongoClient()
print(client)
>> MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True)
pymongo client 数据库相关操作
pymongo client 数据库的相关操作主要有三种,分别是使用现有数据库,创建不存在的数据库,删除数据库。具体操作如下:
# 列出当前所有数据库名称
database_names = client.database_names()
# 使用现有数据库,假设已存在数据库,数据库名称为 test_database
test_database = client.test_database
# 创建不存在的数据库并使用
new_database = client.new_database
# 删除现有数据库
client.drop_database('new_database')
小结一下 pymongo client 的数据库操作,可以简单的理解为三类常见操作,一类是选择或者创建数据库,都可以直接指定一个数据库名称,如果该数据库存在就使用该数据库,否则将创建一个;第二类为删除数据库操作,可以使用 drop_database()
方法。最后则为查看当前 MongoDB 连接中的所有数据库,可以使用 database_names()
方法。
pymongo 集合相关操作
pymongo 的集合操作和数据库的操作类似,同样是指定一个集合名称,如何集合存在就使用该集合,否则创建一个新的集合。以下以使用 test_database 数据库进行集合操作:
# 查看当前数据库中的所有集合
collection_names = test_database.collection_names()
# 使用一个已存在数集合,假设集合名称为 posts
posts = test_database.posts
# 创建一个新的集合
new_collection = test_database.new_collection
# 删除一个集合
test_database.drop(''new_collection)
这里有一点值得注意,当我们创建一个新集合时,如果我们只是创建集合,但并没有向集合中添加数据,这是新的集合其实并没有创建,可以看下面的例子:
test_database.collection_names()
>> []
posts = test_database.posts
# 创建一个新集合,但未向集合中存放数据
test_database.collection_names()
>> []
# 向集合中存放数据
doc = {'test': 1}
post_doc = posts.insert_one(doc)
test_database.collection_names()
>> ['posts']
向集合中添加文档
pymongo 提供两种添加新文档的方法,分别是 insert_one
和 insert_many
。其中,insert_one
是将一个文档添加到集合中, insert_many()
是将多个文档一次性添加到集合中。例子如下:
# insert_one() 用法
foo = test_database.foo
foo.insert_one({'_id': 0})
for data in foo.find():
print(data)
>> {'_id': 0}
# insert_many() 用法
foo.insert_many([{'_id': 1}, {'_id': 2}])
for data in foo.find():
print(data)
>> {'_id': 0}
>> {'_id': 1}
>> {'_id': 2}
删除集合中的文档
我们可以使用 remove()
方法完成删除集合中的文档。
# 删除指定文档
foo.remove({'_id': 2})
for data in foo.find():
print(data)
>> {'_id': 0}
>> {'_id': 1}
# 删除所有文档
foo.remove()
foo.count()
>> 0
更新文档数据
我们先在 test_databse 数据库中创建一个 user 数据库,然后在数据库中添加一条用户文档记录,具体操作如下:
user = test_database.user
joe = {'name': 'joe', 'friends': 32, 'enemies': 2}
user.insert_one(joe)
user.find_one()
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
'enemies': 2,
'friends': 32,
'name': 'joe'}
接下来,我们将对这个文档作出几处修改,修改如下:
- 将 friedns 和 enemies 移动到 relationships 子文档之下
- 将字段 name 更改为 username
具体操作如下:
joe = user.find_one({'name': 'joe'})
print(joe)
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
'enemies': 2,
'friends': 32,
'name': 'joe'}
joe['relationships'] = {'firends': joe['friends'], 'enemies': joe['enemies']}
joe['username'] = joe['name']
del joe['name']
del joe['enemies']
del joe['friends']
print(joe)
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
'relationships': {'enemies': 2, 'firends': 32},
'username': 'joe'}
user.replace_one({'name': 'joe'}, joe)
joe = user.find_one()
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
'relationships': {'enemies': 2, 'firends': 32},
'username': 'joe'}
替换文档我们使用了replace_one()
方法,该方法传入两个参数,第一个参数是需要被替换的文档查找条件,第二个参数是更新的文档数据。
更新操作中的修改器
在实际中,更新文档往往是更新文档的一部分内容,在 MongoDB 中,我们可以使用更新修改器 (update modifier) 来对文档中某些字段进行更新,常用的修改器有以下几个:
- $set 用来指定一个字段的值,如果不存在,将创建一个新的字段
- $unset 删除一个字段
- $inc 用来增加(或减少)一个已有键的值,如果不存在将会创建一个
- $push 向已有的数组末尾添加一个元素
- $addToSet 避免插入重复数据
- $pull 删除元素,基于特定条件
- $each 遍历列表操作
- $pop 删除元素
以下将展开演示上述修改器的使用方法。
$set
# 先清除之前存储的 user 集合中的数据
user.drop()
user.count()
>> 0
joe = {'name': 'joe',
'age': 30,
'sex': 'male',
'location': 'Wisconsin' }
user.insert_one(joe)
print(user.find_one())
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 为用户添加一项 favorite 的字段
user.update_one({'_id': ObjectId('5ac9836829561f64220f6f9d')}, {'$set' : {'favorite': 'War adn Peace'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'favorite': 'War adn Peace',
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 将 favorite 字段的值修改为 Green Eggs and Ham
user.update_one({'name': 'joe'}, {'$set': {'favorite': 'Green Eggs and Ham'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'favorite': 'Green Eggs and Ham',
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 将 favorite 字段的值替换为一个数组
user.update_one({'name': 'joe'}, {'$set': {'favorite': ["Cat's Cradle": , "Foundation Trilogy", "Ender's Game"]}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'favorite': ["Cat's Cradle", 'Foundation Trilogy', "Ender's Game"],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 内嵌文档修改
blog = test_database.blog # 创建一个 blog 集合
posts = {'title': 'A Blog Post', 'content': '...', 'author': {'name': 'joe', 'email': 'joe@example.com'}} # 创建一个 posts 文档
blog.insert_one(posts)
blog.find_one()
{'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
'author': {'email': 'joe@example.com', 'name': 'joe'},
'content': '...',
'title': 'A Blog Post'}
# 将作者名称字段 name 的值修改为 joe schmoe
blog.update_one({'author.name': 'joe'}, {'$set': {'author.name': 'joe schmoe'}})
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
'content': '...',
'title': 'A Blog Post'}
$unset
# 删除 user 集合中 joe 的 favorite 字段
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'favorite': ["Cat's Cradle", 'Foundation Trilogy', "Ender's Game"],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
user.update_one({'name': 'joe'}, {'$unset': {'favorite': 1}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
$inc
# 创建一个 games 的集合,并向集合中添加数据
games = test_database.games
games.insert_one({'game': 'pinball', 'user': 'joe'})
games.find_one()
>> {'_id': ObjectId('5ac9c55f29561f64220f6f9f'), 'game': 'pinball', 'user': 'joe'}
# 增加一个分数字段 score
games.update_one({'game': 'pinball', 'user': 'joe'}, {'$inc': {'score': 50}})
games.find_one()
>> {'_id': ObjectId('5ac9c55f29561f64220f6f9f'),
'game': 'pinball',
'score': 50,
'user': 'joe'}
# 为 score 字段的值增加 5000
games.update_one({'game': 'pinball', 'user': 'joe'}, {'$inc': {'score': 5000}})
games.find_one()
>> {'_id': ObjectId('5ac9c55f29561f64220f6f9f'),
'game': 'pinball',
'score': 5050,
'user': 'joe'}
$push
# 选择 blog 数据库
blog = test_database.blog
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
'content': '...',
'title': 'A Blog Post'}
# 添加一项评论字段 comment
blog.update_one({'title': 'A Blog Post'}, {'$push' : {'comments': {'name': 'joe', 'email': 'joe@example.com', 'content': 'nice post.'}}})
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
'comments': [{'content': 'nice post.',
'email': 'joe@example.com',
'name': 'joe'}],
'content': '...',
'title': 'A Blog Post'}
# 在添加一条由 bob 发表的评论
blog.update_one({'title': 'A Blog Post'}, {'$push' : {'comments': {'name': 'bob', 'email': 'bob@example.com', 'content': 'good post.'}}})
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
'comments': [{'content': 'nice post.',
'email': 'joe@example.com',
'name': 'joe'},
{'content': 'good post.', 'email': 'bob@example.com', 'name': 'bob'}],
'content': '...',
'title': 'A Blog Post'}
$addToSet
# 为 user 集合中的 joe 文档添加 emails 字段
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
user.update_one({'name': 'joe'},{'$push': {'emails': 'joe@example.com'}})
user.update_one({'name': 'joe'},{'$push': {'emails': 'joe@gmail.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@example.com', 'joe@gmail.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 使用 $addToSet 再添加一项 joe@example.com 的记录,因为存在重复,数据不会被重复添加
user.update_one({'name': 'joe'}, {'$addToSet': {'emails': 'joe@example.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@example.com', 'joe@gmail.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 如果直接使用 $push ,记录会被重复添加
user.update_one({'name': 'joe'}, {'$push': {'emails': 'joe@example.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@example.com', 'joe@gmail.com', 'joe@example.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
$pull
# 删除 user 集合中 joe 文档重复的 emails 值 ($pull 会删除所有符合条件的记录)
user.update_one({'name': 'joe'}, {'$pull': {'emails': 'joe@example.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@gmail.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
$each
# 向 user 集合中 joe 文档的 emails 字段追加两个邮箱地址
user.update_one({'name': 'joe'}, {'$push': {'emails': {'$each': ['joe@example.com', 'joe@outlook.com']}}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@gmail.com', 'joe@example.com', 'joe@outlook.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
$pop
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@gmail.com', 'joe@example.com', 'joe@outlook.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 删除集合 user 中 joe 文档 emails 字段的第一个邮箱地址
user.update_one({'name': 'joe'}, {'$pop': {'emails': -1}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@example.com', 'joe@outlook.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
# 删除集合 user 中 joe 文档 emails 字段的最后一个邮箱地址
user.update_one({'name': 'joe'}, {'$pop': {'emails': 1}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
'age': 30,
'emails': ['joe@example.com'],
'location': 'Wisconsin',
'name': 'joe',
'sex': 'male'}
基于位置的数组修改器
如果文档中存在数组记录,而且数组记录有多个值,我们可以视同位置修噶器或者定位符 $
来修改数组其中的某些值。
# 先清除 blog 集合中的原油数据
blog.drop()
# 添加一条 posts 记录
posts = {'content': '...', 'comments': [{'comment': 'good post', 'author': 'John', 'votes': 0}, {'comment': 'i thought it was too short', 'author': 'Claire', 'votes': 3}, {'comment': 'free watches', 'auth: or': 'Alice', 'votes': -1}]}
blog.insert_one(posts)
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
'comments': [{'author': 'John', 'comment': 'good post', 'votes': 0},
{'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
{'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
'content': '...'}
# 为第一条评论 comments 的 votes 字段增加 1
post_id = blog.find_one()['_id']
blog.update_one({'_id': post_id}, {'$inc': {'comments.0.votes': 1}})
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
'comments': [{'author': 'John', 'comment': 'good post', 'votes': 1},
{'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
{'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
'content': '...'}
# 定位符的使用,通常在不知道具体数组位置,使用定位查询文档来匹配数组元素
blog.update_one({'comments.author': 'John'}, {'$set': {'comments.$.author': 'Jim'}})
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
'comments': [{'author': 'Jim', 'comment': 'good post', 'votes': 1},
{'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
{'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
'content': '...'}
PyMongo 查询相关操作
接下来,我们继续深入了解 Pymongo 的查询操作。查询的常用操作大致可以归为三部分,分别是查询的方法,可以使用find_one()
以及 find()
来进行查询;然后在实际运用中,我们可能需要使用 $
条件操作符来帮助我们定位数据;最后我们会了解关于游标的一些常用操作。
PyMongo 的 find_one() 和 find()
为方便演示,我们会在 test_database
数据库中创建一个 users
的集合,并向其中添加三条文档记录。
users = test_database.users
joe = {'name': 'joe', 'age': 26}
mike = {'name': 'mike', 'age': 28}
jake = {'name': 'jake', 'age': 26}
# 使用 insert_many() 可以一次添加多个文档记录
users.insert_many([joe, mike, jake])
for data in users.find():
print(data)
{'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
find() 的使用方法
我们使用 find()
方法,如果不传入任何参数,将返回该集合中的所有数据的一个游标,然后我们可以通过 for
来遍历游标来打印查询结果。
如果我们需要查找特定的数据,比如年龄为 28 的用户,那么我们可以给 find()
方法传入一个匹配的规则。
result = users.find({'age': 28})
# find() 的返回值是一个查询游标,在 python 中的数据类型为一个迭代器,我们可以使用 count() 查看查询结果数量
result.count()
>> 1
result.next()
>> {'_id': ObjectId('5acb225729561f64220f6fa2'), 'age': 28, 'name': 'mike'}
我们还可以通过传入多个同查询条件进行查询。
result = users.find({'age': 26, 'name': 'jake'})
# 以上表达式多个条件会被解释为 AND 关系
result.count()
>> 1
result.next()
>> {'_id': ObjectId('5acb225729561f64220f6fa3'), 'age': 26, 'name': 'jake'}
find_one() 的使用方法
上面我们演示了 find()
的用法,我们接下来将会演示 find_one()
的用法。 find_one()
的用法与 find()
的使用方法差别不大,他们的区别是使用 find_one()
最多只会返回一条文档记录,而 find()
则返回查询游标。以下是代码演示:
user.find_one()
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'age': 26, 'name': 'joe'}
# find_one() 如果没有查询条件,会返回第一条记录
type(users.find_one({'name': 'kate'}))
>> NoneType
# 如果传入查询条件,没有查询结果,则会返回一个 NoneType
users.find_one({'age': 26})
>> '_id': ObjectId('5acb225729561f64220f6fa1'), 'age': 26, 'name': 'joe'}
# 如果查询匹配多个结果,find_one() 只会返回第一条匹配记录
指定返回字段
我们在查询的时候,可能并不需要文档中的所有字段,这是我们可以在查询条件之后再传入一个参数来指定返回的字段。
# 不要 _id 字段
users.find_one({}, {'_id': 0})
>> {'age': 26, 'name': 'joe'}
# 只输出 _id 字段
users.find_one({}, {'_id': 1})
>> {'_id': ObjectId('5acb225729561f64220f6fa1')}
Pymongo 条件查询操作
比较操作符
在查询中,我们会经常用到比较字段值得大小来查询数据,实现这一功能我们会用到比较操作符,Pymongo 常用的比较操作符有以下几个:
- $lt 小于
- $let 小于等于
- $ge 大于
- $gte 大于等于
- $ne 不等于
下面我们将继续使用上面的 users
集合来演示上面几个比较操作符。
# 先遍历打印 users 集合中的数据
for data in users.find():
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询大于 26 岁的用户
for data in user.find({'age': {'$gt': 26}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
# 查询大于等于 26 岁的用户
for data in user.find({'age': {'$gte': 26}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询小于 28 岁的用户
for data in user.find({'age': {'$lt': 28}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询小于等于 28 岁的用户
for data in user.find({'age': {'$lte': 28}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询不等于 28 岁的用户
for data in user.find({'name': {'$ne': 'mike'}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
$in 和 $nin 的用法
我们可以使用 $in
和 $nin
操作符来匹配一个键的多个值,具体用法示例如下:
# 匹配 users 集合中 用户名为 joe 和 mike 的文档记录
for data in users.find({'name': {'$in': ['joe', 'mike']}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
# 匹配用户名不是 mike 的用户 注意: $in 和 $nin 条件必须是一个数组
for data in users.find({'name': {'$nin': ['mike']}}):
print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
$or 的用法
如果需要查询两个条件中其中一个为真的查询结果,可以使用 $or 操作符。具体示例如下:
# 为方便演示,我们先插入多一条文档记录
kate = {'name': 'kate', 'age': 30}
users.insert_one(kate)
for data in users.find({'$or': [{'name': 'mike'}, {'age': 30}]}):
print(data)
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb6cfc29561f64220f6fa4'), 'name': 'kate', 'age': 30}
null 值查询和 $exists 条件判定
在 Python 中,mongodb 中的 null 值以 None 表示。但在查询 null 值中,会出现比较奇怪的情况,以下为演示案例:
# 为方便演示,创建一个 c 集合,并向里面添加 3 条记录
c = test_database.c
c.insert_many({'y': None}, {'y': 1}, {'y': 2})
for data in c.find():
print(data)
>> {'_id': ObjectId('5acb738029561f64220f6fa5'), 'y': None}
{'_id': ObjectId('5acb741029561f64220f6fa6'), 'y': 1}
{'_id': ObjectId('5acb741029561f64220f6fa7'), 'y': 2}
# 查询 null 值
for data in c.find({'y': None}):
print(data)
>> {'_id': ObjectId('5acb738029561f64220f6fa5'), 'y': None}
# 查询一个不存在的键,查询条件为 null
for data in c.find({'z': None}):
print(data)
>> {'_id': ObjectId('5acb738029561f64220f6fa5'), 'y': None}
{'_id': ObjectId('5acb741029561f64220f6fa6'), 'y': 1}
{'_id': ObjectId('5acb741029561f64220f6fa7'), 'y': 2}
可以看到,当我们查找 {'z': None}
的时候,会把所有不包含这个条件的文档都查询出来,这样明显和我们的意图不一致,因此我们需要增加一个限定,具体代码如下:
for data in c.find({'z': {'$in': [None], '$exists': 1}}):
print(data)
>>
通过加上 $exists
的限定,我们可以看到代码执行完之后并没有查询结果输出,符合我们的查询意图。
查询数组
在实际使用当中,我们还可能会存在文档中有数组形式的字段值,因此我们需要一些特定的操作来查询匹配这些数组,同样的,MongoDb 提供了相关的操作符可以使用,常用的数组操作符有以下几个:
- $all 匹配多个元素数组
- $size 匹配特定长度的数组
- $slice 返回匹配数组的一个子集
下面将用代码演示以上三个操作符的用法。为方便演示,我们会先创建一个 food 的集合用来存放水果的文档记录。
food = test_database.food
food.insert_one({'_id': 1, 'fruit': ['apple', 'banana', 'peach']})
food.insert_one({'_id': 2, 'fruit': ['apple', 'kumquat', 'orange']})
food.insert_one({'_id': 3, 'fruit': ['cherry', 'banana', 'apple']})
for data in food.find():
print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 2, 'fruit': ['apple', 'kumquat', 'orange']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
$all 的用法
result = food.find({'fruit': {'$all': ['apple', 'banana']}})
for data in result:
print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
# 也可以使用位置定位匹配
result = food.find({'fruit.1': 'banana'})
for data in result:
print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
$size 的用法
为方便演示,我们会向 food 集合中的第二个文档添加多一个水果。
food.update_one({'_id': 2}, {'$push': {'fruit': 'strawbreey'}})
for data in food.find():
print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 2, 'fruit': ['apple', 'kumquat', 'orange', 'strawbreey']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
# 查找数组size为3的结果
result = food.find({'fruit': {'$size': 3}})
for data in result:
print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
$slice 的用法
$slice 可以返回某个键匹配的数组的一个子集,我们将会用 blog 集合来演示使用 $slice 操作符获取特定数量的评论记录。
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
'comments': [{'author': 'Jim', 'comment': 'good post', 'votes': 1},
{'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
{'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
'content': '...'}
# 获取前两条评论记录
blog.find_one({}, {'comments': {'$slice': 2}})
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
'comments': [{'author': 'Jim', 'comment': 'good post', 'votes': 1},
{'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3}],
'content': '...'}
# 获取最后一条评论记录
blog.find_one({}, {'comments': {'$slice': -1}})
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
'comments': [{'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
'content': '...'}
min() 和 max() 的使用
如果我们以某个区间值作为查询条件,我们可以使用比较操作符来实现,但是,如果文档中存在值,以及值组成的数组时,查询结果往往与我们的意图不一致,这是我们就需要用到 $elemMatch 来匹配非数组元素,或者使用 min() 和 max() 方法。
# 我们先清空 c 集合,并加入新的文档记录
c.drop()
c.count()
>> 0
c.insert_many([{'x': 5}, {'x': 15}, {'x': 25}, {'x': [5, 25]}])
c.count()
>> 4
for data in c.find():
print(data)
>> {'_id': ObjectId('5acc31c729561f64220f6fa8'), 'x': 5}
{'_id': ObjectId('5acc31c729561f64220f6fa9'), 'x': 15}
{'_id': ObjectId('5acc31c729561f64220f6faa'), 'x': 25}
{'_id': ObjectId('5acc31c729561f64220f6fab'), 'x': [5, 25]}
# 假设我们需要查询 [10, 20] 区间内的记录
result = c.find({'x': {'$gt': 10. '$lt': 20}})
for data in result:
print(data)
>> {'_id': ObjectId('5acc31c729561f64220f6fa9'), 'x': 15}
{'_id': ObjectId('5acc31c729561f64220f6fab'), 'x': [5, 25]}
# 这里看到 [5, 25] 这一条记录其实是不符合我们的查询预期的
# 我们可以使用 $elemMatch 来不匹配非数组元素
result = c.find({'x': {'$elemMatch': {'$gt': 10, '$lt': 20}}})
for data in result:
print(data)
>> // 没有输出结果
# 通过添加 $elemMatch 可以剔除 [5, 25] 这一记录,但正确的查询结果 {'x': 15} 却不能匹配
# 我们将使用 min() 以及 max() 方法
# 为使用者两个方法,我们需要先给 c 集合的 x 字段建立索引
c.create_index('x')
>> 'x_1'
result = c.find({'x': {'$gt': 10, '$lt': 20}}).min([('x', 10)]).max([('x', 20)])
for data in result:
print(data)
>> {'_id': ObjectId('5acc31c729561f64220f6fa9'), 'x': 15}
关于 min()
和 max()
两个方法,有两点需要注意:
- 使用这两个方法前,必须先要为需要查询的字段建立索引,否则会报错
- 这两个方法和在 mongodb 中的写法有一些不一样,在 mongodb 中,同样操作写作: min({'x': 10}) 但在 Pymongo 中,应写成 min([(‘x’, 10)]) 注意区别,否则同样会报错。
$where 查询
针对一些比较复杂的查询,我们可以使用 $where 。然而,由于 $where
可以在查询中执行任意的 Javascript,因此可能会产生出一些不安全的操作,因此,在实际生产环境中,竟可能的不用或者禁用 $where
。以下代码演示 $where
的基本用法:
# 为方便演示,我们会再次用到 foo 集合,我们先清空以下这一个集合
foo.drop()
# 向 foo 集合中添加两条文档记录
foo.insert_one({'apple': 1, 'banana': 6, 'peach': 3})
foo.insert_one({'apple': 8, 'spinach': 4, 'watermelon': 4})
for data in foo:
print(data)
>> {'_id': ObjectId('5acdbf4729561f64220f6fac'), 'apple': 1, 'banana': 6, 'peach': 3}
{'_id': ObjectId('5acdbf6a29561f64220f6fad'), 'apple': 8, 'spinach': 4, 'watermelon': 4}
# 接下来,我们需要查找存在两个水果数量相等的文档
result = foo.find({'$where':
"""
function() {
for (var current in this) {
for (var other in this) {
if (current != other && this[current] == this[other]){
return true;
}
}
}
return false;
}
"""})
for data in result:
print(data)
>> {'_id': ObjectId('5acdbf6a29561f64220f6fad'), 'apple': 8, 'spinach': 4, 'watermelon': 4}
游标
当我们进行查询操作的时候,程序执行查询指令后,返回的不是我们的查询结果,而是一个游标,在 Pymongo 库中,指定 find()
方法后,返回的是一个 pymongo.cursor.Cursor
的对象,这一个对象我们可以简单的理解成一个迭代器,因此我们就可以像上文一样,使用 for 循环来遍历所有查询结果。
type(foo.find())
>> pymongo.cursor.Cursor
以下再次演示使用 for 遍历输出查询结果,为方便演示,我们再次清空 foo 集合。
foo.drop()
# 我们创建一系列文档
import random
for i in range(20):
foo.insert_one({'x': random.randint(0, 20)})
for data in foo.find():
print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
{'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fcc'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fcd'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fce'), 'x': 14}
{'_id': ObjectId('5acdc47d29561f64220f6fcf'), 'x': 18}
{'_id': ObjectId('5acdc47d29561f64220f6fd0'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd1'), 'x': 1}
{'_id': ObjectId('5acdc47d29561f64220f6fd2'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd3'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd4'), 'x': 4}
{'_id': ObjectId('5acdc47d29561f64220f6fd5'), 'x': 5}
sort() 排序的用法
我们在基于上面的集合数据之下,对 x
进行一个排序操作,可以使用 sort() 方法,具体操作如下:
import pymongo
result = foo.find()
# 升序
result.sort([('x', pymongo.ASCENDING)])
for data in result:
print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd2'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd1'), 'x': 1}
{'_id': ObjectId('5acdc47d29561f64220f6fcc'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fcd'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fd4'), 'x': 4}
{'_id': ObjectId('5acdc47d29561f64220f6fd0'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd5'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd3'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fce'), 'x': 14}
{'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fcf'), 'x': 18}
# 降序
for data in foo.find().sort([('x', pymongo.DESCENDING)]):
print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fcf'), 'x': 18}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fce'), 'x': 14}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd3'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd0'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd5'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd4'), 'x': 4}
{'_id': ObjectId('5acdc47d29561f64220f6fcc'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fcd'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fd1'), 'x': 1}
{'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd2'), 'x': 0}
limit() 的使用
上面我们的 foo 文档中有 20 条文档记录,假如我们不需要一次获取所有文档,我们可以使用 limit() 方法来限制查询结果数量,具体操作如下:
for data in foo.find().limit(5):
print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
skip() 的使用
我们可以使用 skip() 来跳过一定数量的文档,以下为代码演示:
for data in foo.find().skip(5).limit(5):
print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
# 我们可以看到,输出的结果和上面使用 limit(5) 的数据不一样,这里是跳过前 5 条记录的后 5条记录
小结
通过以上的讲解以及代码演示,我们已经了解基本的 Pymongo 操作,我们可以自己完成一些小的项目,将以上的技能融会贯通的使用吸收,以便日后在开发中更得心应手。