Python 和 MongoDB 其實(shí)很配
MongoDB 其實(shí)就是一個(gè)大大的 JSON,在 Python 的世界里 dict 也是最吃香的類型,所以,他們天生就是一對。
MongoDB 的安裝
推薦使用 Docker 來部署管理,一行命令就可以搞定,官方版本:
- docker run -d --name mongodb \
- -e MONGO_INITDB_ROOT_USERNAME=admin \
- -e MONGO_INITDB_ROOT_PASSWORD=admin \
- -v ~/data/mongo_dir:/data/db \
- -p 27017:27017 \
- mongo
官方版本的 Docker 啥都好,就是體積有點(diǎn)大。還有一個(gè)小體積的 alpine 版本,開發(fā)時(shí)使用很方便,不過不能配置賬戶和密碼。
- docker run -d --name mongo-lite \
- -p 27018:27017 \
- -v ~/data/mongo_lite:/data/db \
- mvertes/alpine-mongo
如果想嘗試 Mongo 的命令行 (Mongo Shell),直接進(jìn)到 Docker 里:
- $ docker exec -it mongo-lite mongo
- MongoDB shell version v4.0.6
- connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
- ...
- > use mydb
- switched to db mydb
- > db.User.insertOne({"name":"Toby",age:18})
- {
- "acknowledged" : true,
- "insertedId" : ObjectId("612c84c5d93795436ad27ebc")
- }
- > db.User.find()
- { "_id" : ObjectId("612c84c5d93795436ad27ebc"), "name" : "Toby", "age" : 18 }
Mongo Shell 官方文檔:https://docs.mongodb.com/manual/reference/mongo-shell/
PyMongo 五分鐘上手
安裝 PyMongo 可以通過 pip 搞定。
- pip install pymongo
以下內(nèi)容也可以參考官方文檔:https://pymongo.readthedocs.io/en/stable/
連接數(shù)據(jù)庫
常見方式如下:
- from pymongo import MongoClient
- # 連接有密碼的Mongo
- client = MongoClient('mongodb://admin:admin@localhost:27017/')
- # 連接沒密碼的Mongo
- client = MongoClient('mongodb://localhost:27018/')
- # 列出所有已經(jīng)存在的DB
- for db in client.list_databases():
- print(db)
- # 使用Mongo里的某個(gè)DB,這個(gè)DB可以不存在,后面寫數(shù)據(jù)時(shí)會(huì)被創(chuàng)建出來
- db = client.mydb
插入數(shù)據(jù)
插入的每條數(shù)據(jù)都是一個(gè) dict,一樣的字段允許類型不一樣,也允許每次插入的數(shù)據(jù)字段不一樣,可以理解成動(dòng)態(tài)類型數(shù)據(jù),你想放什么都行,唯一的約束就是他們會(huì)被放在同一個(gè) Document 里。
- # 插入一條數(shù)據(jù)
- def add_one_user():
- db.User.insert_one({
- 'name': 'Toby',
- 'age': 18
- })
- # 插入多條數(shù)據(jù)
- def add_many_users():
- db.User.insert_many([{
- 'name': 'Tom',
- 'age': 10
- }, {
- 'name': 'Toby',
- 'age': 'unknown',
- 'hobbies': ['write bugs', 'raise dogs']
- }])
這里的 User 約等于關(guān)系型數(shù)據(jù)庫的表,但它的名字叫 Document,每次數(shù)據(jù)插入完成后會(huì)返回一個(gè)_id,這是 Mongo 里最重要的東西了,它就是靠這個(gè)_id 來保證數(shù)據(jù)的一致性,后續(xù)的數(shù)據(jù)修改和刪除主要就是靠這個(gè)_id 來完成,所以一般針對某條特定的數(shù)據(jù)的處理,都是需要先查詢它的_id,然后再進(jìn)行后面的操作。
查詢數(shù)據(jù)
- # 查詢多個(gè)數(shù)據(jù)
- def show_users():
- # 一個(gè)表里所有數(shù)據(jù)
- for e in db.User.find():
- print(e)
- # 匹配條件的多條數(shù)據(jù)
- for e in db.User.find({'name': 'Toby'}):
- print(e)
- # 查詢單個(gè)數(shù)據(jù)
- def query_user(name):
- return db.User.find_one({'name': name})
- # 忽略大小寫
- def query_user_ignore_case(name):
- return db.User.find_one({'name': re.compile(name, re.IGNORECASE)})
- # 使用運(yùn)算符 https://docs.mongodb.com/manual/reference/operator/query/
- def query_teenager():
- return db.User.find_one({'age': {'$lt': 18}})
Mongo 的查詢主要還是依賴 DB 自己提供的運(yùn)算符,在 PyMongo 里要注意,這里不會(huì)拋出異常,如果找不到數(shù)據(jù),默認(rèn)返回 None。
通過運(yùn)算符查詢數(shù)據(jù):https://docs.mongodb.com/manual/reference/operator/query/
通過聚合查詢數(shù)據(jù):https://docs.mongodb.com/manual/aggregation/
修改數(shù)據(jù)
- # 修改一個(gè)數(shù)據(jù)
- def update_user(user, attributes: dict):
- user.update(attributes)
- result = db.User.replace_one({'_id': user['_id']}, user, upsert=True)
- return {'affected_count': result.modified_count}
- u = query_user_ignore_case('toby')
- result = update_user(u, {'code': 'python'})
- # 修改多個(gè)數(shù)據(jù),注意有坑,Replace 和 Update是不一樣的
- def update_many():
- todo = [
- UpdateOne({'age': 19}, {'$set': {'name': 'Toby'}}),
- ReplaceOne({'name': 'Tom'}, {'age': 19}), # name 會(huì)被吃掉
- ]
- result = db.User.bulk_write(todo)
- def delete_user(name):
- result = db.User.delete_one({'name': name})
- return {'affected_count': result.deleted_count}
- print(result.matched_count)
Replace 是替換,所以要帶上原有字段,這里有點(diǎn)坑。Update 不接受單獨(dú)的 dict,需要用 $set / $unset 來標(biāo)識(shí)修改的字段的方式。
- [
- { $set: { status: "Modified", comments: [ "$misc1", "$misc2" ] } },
- { $unset: [ "misc1", "misc2" ] }
- ]
刪除數(shù)據(jù)
- def delete_user(name):
- result = db.User.delete_one({'name': name})
- return {'affected_count': result.deleted_count}
刪除多個(gè)數(shù)據(jù):
- >>> db.test.count_documents({'x': 1})
- 3
- >>> result = db.test.delete_many({'x': 1})
- >>> result.deleted_count
- 3
- >>> db.test.count_documents({'x': 1})
- 0
常見問題
有什么辦法可以讓 Mongo 不自動(dòng)添加 _id 到我的數(shù)據(jù)里?
幾乎沒有,這是 MongoDB 的特性決定的,如果你的數(shù)據(jù)沒有 ID 的話,并且進(jìn)行高并發(fā)插入時(shí),大概率會(huì)遇到 BulkWriteError 這個(gè)錯(cuò)誤。
- >>> doc = {}
- >>> collection.insert_many(doc for _ in range(10))
- Traceback (most recent call last):
- ...
- pymongo.errors.BulkWriteError: batch op errors occurred
- >>> doc
- {'_id': ObjectId('560f171cfba52279f0b0da0c')}
- >>> docs = [{}]
- >>> collection.insert_many(docs * 10)
- Traceback (most recent call last):
- ...
- pymongo.errors.BulkWriteError: batch op errors occurred
- >>> docs
- [{'_id': ObjectId('560f1933fba52279f0b0da0e')}]
如果你不想要自動(dòng)生成的 ID,可以自己在插入數(shù)據(jù)前指定這個(gè)字段。
為啥我指定了_id 還是查詢不到我的數(shù)據(jù)?
比如我要查詢數(shù)據(jù)庫里的某個(gè) post:
- >>> post_id_as_str = str(post_id)
- >>> posts.find_one({"_id": post_id_as_str}) # No result
因?yàn)?pyMongo 里的這個(gè) ID 不是字符串類型,你需要做一下數(shù)據(jù)轉(zhuǎn)換。
- from bson.objectid import ObjectId
- # The web framework gets post_id from the URL and passes it as a string
- def get(post_id):
- # Convert from string to ObjectId:
- document = client.db.collection.find_one({'_id': ObjectId(post_id)})
用標(biāo)準(zhǔn)庫里的 json 模塊來序列化和反序列化 Mongo 的數(shù)據(jù)會(huì)有什么問題?
有一些數(shù)據(jù)類型在反序列后會(huì)得不到預(yù)期的結(jié)果,比如 ObjectId 和 DBRef,PyMongo 為了解決這個(gè)問題自己封裝了一個(gè)輔助類 json_util,可以很好的解決這些問題。
- from bson.json_util import loads
- from bson.json_util import dumps
總結(jié)
Mongo 屬于非關(guān)系型數(shù)據(jù)庫,使用 Mongo 作為 DB 的思維需要做比較大的轉(zhuǎn)變:
- 關(guān)系型數(shù)據(jù)庫一般讀寫容易,修改難,容易理解
- 非關(guān)系型數(shù)據(jù)庫一般是讀寫改容易,設(shè)計(jì)難(相對而言)
“關(guān)系型數(shù)據(jù)庫支持 ACID (Atomicity, Consistency, Isolation, Duration) 即原子性,一致性,隔離性和持續(xù)性。相對而言,NoSQL 采用更寬松的模型 BASE (Basically Available, Soft state, Eventual Consistency) 即基本可用,軟狀態(tài)和最終一致性。
NoSQL 在精心的設(shè)計(jì)下查詢性能會(huì)更高,數(shù)據(jù)結(jié)構(gòu)也十分有彈性,特別適合快速發(fā)展和屬性不確定的產(chǎn)品功能,但 Mongo 不支持事務(wù),如何確保數(shù)據(jù)一致性是個(gè)挺大的挑戰(zhàn)。
在選擇上可以考慮從以下角度去思考:
- 需要 ACID 還是 BASE
- 需要結(jié)構(gòu)化數(shù)據(jù)還是非結(jié)構(gòu)化數(shù)據(jù)
- 需要對數(shù)據(jù)進(jìn)行靈活擴(kuò)展
- 開發(fā)人員的經(jīng)驗(yàn)
很多情況只考慮最后一點(diǎn)就可以了。