PyMongo 常見問題

白夜發表於2017-12-24

這是一篇翻譯文章,原連結在這裡。翻譯可能不準確,歡迎指出文章中存在的問題。

PyMongo是執行緒安全的嗎

PyMongo是執行緒安全的,並且為多執行緒應用提供了內建的連線池

PyMongo是程式安全的嗎

PyMongo不是程式安全的,如果你在fork()中使用MongoClient例項,必須小心。具體來說,MongoClient例項不能從父程式複製到子程式,父程式和每個子程式必須建立屬於自己的MongoClient例項。由於本身的不相容性,在子程式中使用從父程式複製的MonogoClient例項很有可能發生死鎖。PyMongo會在有可能引起死鎖的情況下發出警告。

MongoClient產生多個執行緒來執行後臺任務,如監視連線伺服器。這些執行緒共享受Lock例項(程式不安全)保護的狀態,所以,MongoClient受到與其他使用鎖(互斥)的多執行緒程式一樣的限制,其中一個限制是在使用fork()後鎖失效。在fork過程中,所有鎖都會被複制到子程式中,並且與父程式保持相同的狀態:如果父程式中是鎖定的,子程式複製的鎖也是鎖定的。由fork()建立的子程式只有一個執行緒,所以在這個子執行緒中任何從父程式中任何子執行緒中取出的鎖都不會被釋放,當這個子執行緒嘗試獲取其中一個鎖時,會發生死鎖。

有關在fork()使用多執行緒上下文中的Python鎖的問題,請參閱bugs.python.org/issue6721

連線池在PyMongo中是如何工作的

每個MongoClient例項在每個MongoDb伺服器都有一個內建的連線池,這些連線池會立即開啟socket,用來支援多執行緒應用所需的併發操作MongoDB數量。這些socket沒有執行緒相關性。

每個連線池的大小被限制在maxPoolSize,預設值為100.如果存在maxPoolSize個到伺服器的連線並且這些連線全部在使用中,那麼到該伺服器的下一個請求會一直等待,直到其中一個連線可用。

客戶端例項在MongoDB叢集中的每個伺服器上額外開啟一個socket來監視伺服器的狀態。

例如:一個連線到3個節點主從複製伺服器的客戶端將開啟3個監視socket。它還可以根據需要開啟更多個socket(最多maxPoolSize)來支援每個伺服器上多執行緒應用的併發操作。在maxPoolSize為100的情況下,如果應用只使用主連線,則只有主連線池的連線數增加(最多103)。如果應用使用ReadPreference查詢輔助資料庫,則它的連線池的連線數也會增加,總連線數可以達到303.

可以通過使用minPoolSize(預設0)引數來設定每個伺服器的最小併發連線數。連線池將初始化minPoolZise個socket。如果由於網路原因導致socket關閉,導致socket的連線數(使用中和空閒中)下降到最小值以下,則會開啟新的socket,直到socket的數量達到最小值。

可以使用maxIdleTime引數來設定一個連線在連線池中保持空閒的最大毫秒數,之後它將被刪除或者替換,預設值為None(沒有限制)。

MongoClient的預設配置適用於大多數應用:

client = MongoClient(host, port)
複製程式碼

為每個程式建立一次客戶端,並將其重用於所有操作。為每個請求建立一個新的客戶端是一個常見的錯誤,因為這樣非常低效。

要在一個程式中支援極高數量的併發MongoDB操作,需增加maxPoolSize:

client = MongoClient(host, port, maxPoolSize=200)
複製程式碼

或者使其沒有限制:

client = MongoClient(host, port, maxPoolSize=None)
複製程式碼

預設情況下,允許任意數量的執行緒等待socket可用,並且可以等待任意長的時間。可以設定waitQueueMultiple引數來限制等待執行緒的數量。例如:限制等待數量不大於500:

client = MongoClient(host, port, maxPoolSize=50, waitQueueMultiple=10)
複製程式碼

當已經由500個執行緒正在等待socket時,第501個需要socket的執行緒將丟擲ExceededMaxWaiters。使用waitQueueMultiple可以現在載入峰值期間應用中排隊的數量,但是會引起額外的異常。

一旦連線池達到最大值,另外的執行緒可以無限等待socket可用,除非你設定了waitQueueTimeoutMS:

client = MongoClient(host, port, waitQueueTimeoutMS=100)
複製程式碼

在這個例子中,一個執行緒如果等待socket的時間超過100ms,它將丟擲ConnectionFailure錯誤。waitQueueTimeoutMS適用於在載入峰值期間限制操作的持續時間比完成每個操作更重要的情景。

當任何執行緒呼叫close()時, 所有閒置的socket都會被關閉,所有正在使用的socket將在它返回連線池時被關閉。

PyMongo支援Python3 嗎?

PyMongo支援CPython3.4+和PyPy3。詳情請參閱Python3 FAQ

PyMongo是否支援Gevent,asyncio,Tornado或Twisted等非同步框架?

PyMongo完全支援Gevent

要將MongoDB與asyncioTornado一起使用,請參閱Motor專案。

對於Twisted,請參閱TxMongo

為什麼PyMongo將一個_id欄位新增到我所有的文件中?

當使用insert_one(),insert_many()或者bulk_write()向MongoDB中插入一個文件時,如果文件沒有_id欄位,PyMongo將自動加上_id欄位,其值為ObjectId的一個例項。例如:

>>> my_doc = {'x': 1}
>>> collection.insert_one(my_doc)
<pymongo.results.InsertOneResult object at 0x7f3fc25bd640>
>>> my_doc
{'x': 1, '_id': ObjectId('560db337fba522189f171720')}
複製程式碼

當呼叫insert_many()向單個文件插入一個引用列表時,經常會引起BulkWriteError錯誤。這是幾個Python習慣引起的:

>>> 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')}]
複製程式碼

PyMongo以這種方式新增_id欄位有以下幾個原因:

  • 所有的MongoDB文件都必須由一個_id欄位。
  • 如果PyMongo插入補個不帶有_id欄位的文件,MongoDB會自己新增,並且不會返回_id欄位給PyMongo。
  • 在新增_id欄位之前複製要插入的文件對於大多數高寫入的應用而言代價是極其昂貴的。

如果你不希望PyMongo向文件中新增_id欄位,則只能插入已有_id欄位的文件。

副本中的鍵順序-為什麼查詢在shell是有序的,在PyMongo中無序?

BSON文件中的鍵值對可以是任何順序(除了_id始終是第一個)。在讀寫資料是,mongo shell按鍵保持順序。下面的例子中請注意在插入是'b'在'a'前面,查詢時也一樣:

> // mongo shell.
> db.collection.insert( { "_id" : 1, "subdocument" : { "b" : 1, "a" : 1 } } )
WriteResult({ "nInserted" : 1 })
> db.collection.find()
{ "_id" : 1, "subdocument" : { "b" : 1, "a" : 1 } }
複製程式碼

PyMongo在預設情況下將BSON文件表示為Python字典,並且沒有字典中鍵的順序。也就是說,宣告Python字典時,'a'在前面或者'b'在前面是一樣的。

>>> print({'a': 1.0, 'b': 1.0})
{'a': 1.0, 'b': 1.0}
>>> print({'b': 1.0, 'a': 1.0})
{'a': 1.0, 'b': 1.0}
複製程式碼

因此,Python的字典不能保證按照他們在BSON中的順序顯示鍵值對。下面的例子中,'a'顯示在'b'前面:

>>> print(collection.find_one())
{u'_id': 1.0, u'subdocument': {u'a': 1.0, u'b': 1.0}}
複製程式碼

使用SON類可以在讀取BSON時保持順序,它是一個記住了鍵順序的字典。首先,獲取集合的控制程式碼,通過配置使用SON代替字典:

>>> from bson import CodecOptions, SON
>>> opts = CodecOptions(document_class=SON)
>>> opts
CodecOptions(document_class=<class 'bson.son.SON'>,
             tz_aware=False,
             uuid_representation=PYTHON_LEGACY,
             unicode_decode_error_handler='strict',
             tzinfo=None)
>>> collection_son = collection.with_options(codec_options=opts)
複製程式碼

現在,查詢結果中的文件和副本都用SON物件表示:

>>> print(collection_son.find_one())
SON([(u'_id', 1.0), (u'subdocument', SON([(u'b', 1.0), (u'a', 1.0)]))])
複製程式碼

副本中鍵順序與實際儲存的一致:'b'在'a'前面。

由於字典中的鍵順序沒有定義,所以你無法預測它如何序列化到BSON。但MongoDB認為副本只有在他們的鍵具有相同的順序時才是相同的。所以,使用字典查詢副本可能沒有結果:

>>> collection.find_one({'subdocument': {'a': 1.0, 'b': 1.0}}) is None
True
複製程式碼

在查詢中交換鍵順序沒有任何區別:

>>> collection.find_one({'subdocument': {'b': 1.0, 'a': 1.0}}) is None
True
複製程式碼

正如我們上面看到的,Python認為這兩個字典是相同的。

由兩個解決方法。第一個方法是按欄位匹配副本:

>>> collection.find_one({'subdocument.a': 1.0,
...                      'subdocument.b': 1.0})
{u'_id': 1.0, u'subdocument': {u'a': 1.0, u'b': 1.0}}
複製程式碼

上面的查詢匹配任何'a'為1.0和'b'為1.0的副本,無論你在Python中指定它們的順序如何或它們儲存在BSON中的順序如何。 此外,此查詢現在可以將副本中與'a'和'b'之外的其他鍵相匹配,而之前的查詢需要完全匹配。

第二個方法是使用SON來指定鍵的順序:

>>> query = {'subdocument': SON([('b', 1.0), ('a', 1.0)])}
>>> collection.find_one(query)
{u'_id': 1.0, u'subdocument': {u'a': 1.0, u'b': 1.0}}
複製程式碼

查詢時,在建立SON時使用的鍵順序在被序列化為BSON時會被保留。因此,您可以建立一個完全匹配集合中的副本的副本。

更多資訊,請參閱 MongoDB Manual entry on subdocument matching

CursorNotFound 遊標id無效在服務端是什麼意思?

如果MongoDB中的遊標已經開啟了很長時間而沒有對它們執行任何操作,他們會在伺服器上超時。這可能會導致在迭代遊標時引發CursorNotFound異常。

如何更改遊標的超時時間?

MongoDB不支援遊標自定義超時時間,但可以完全關閉。在find()時傳入no_cursor_timeout=True

如何儲存decimal.Decimal例項?

PyMongo >= 3.4 支援引入Decimal128 BSON型別。詳情請參閱Decimal12

MongoDB <= 3.2 僅支援IEEE 754 浮點數-與Python浮點型別相同。PyMongo可以在這些版本的MongoDB中儲存Decimal例項的唯一方法是將他們轉換成這個標準,所以不管如何你只可以儲存浮點數。我們強迫使用者明確的做這個轉換來告知它們轉換正在發生。

我儲存了9.99,但是查詢是確變成了9.9900000000000002,這是怎麼回事?

資料庫將9.99表示為IEEE浮點數(這是MongoDB和Python以及大多數其他現代語言通用的)。問題是9.99不能用雙精度浮點數來表示,在Python的某些版本中也是如此:

>>> 9.99
9.9900000000000002
複製程式碼

使用PyMongo儲存9.99時得到的結果與使用JavaScript shell或任何其他語言儲存的結果完全相同(以及將9.99輸入到 Python程式的一樣)。

你們能新增對文件屬性方式的取值嗎?

通過.獲取文件的值,而不僅僅是現在只能用Python字典的方式獲取

這個請求已經出現了很多次,但我們決定不實現任何這樣的方式。相關的jria case有關於這個決定的一些資訊,這裡是一個簡短的總結:

  1. 這將汙染文件的屬性名稱空間,因此當使用與字典方法相同的名稱的鍵時可能導致細微的bugs/混淆錯誤。
  2. 我們使用SON物件而不是常規字典的唯一原因是維護鍵排序,因為伺服器需要這些準確的操作。因此我們在是否需要複雜的SON的必要性上猶豫不決(某種程度上我們希望恢復單獨使用字典,而不會破壞每個人的向後相容性)
  3. 因為文件的表現像字典,所以新使用者很容易(和Pythonic)的處理文件。如果我們開始改變這點將會為新受增加一個障礙-另外的學習成本。

PyMongo中處理時區的正確方法是什麼?

有關如何正確處理datetime物件的示例,請參閱Datetime and Timezones

如何儲存一個datetime.date例項?

PyMongo不支援儲存datetime.date例項,因為BSON沒有型別來儲存(沒有時間的日期(yyyy-MM-dd))。PyMongo並沒有強制將datetime.date轉換為datetime.datetime的約定,所以你需要在程式碼中執行轉換。

在web程式中使用ObjectId查詢文件時,沒有得到任何結果?

在Web應用程式中,通常在URL中會對文件的ObjectId進行編碼,如:

"/posts/50b3bda58a02fb9a84d8991e"
複製程式碼

web框架將ObjectId作為url字串的一部分傳遞給後臺,因此在她們呢傳遞給find_one()之前,必須轉換為ObjectId。忘記這個轉換是一個常見的錯誤。下面的例子是在Flask中正確的執行操作(其他Web框架類似):

rom pymongo import MongoClient
from bson.objectid import ObjectId

from flask import Flask, render_template

client = MongoClient()
app = Flask(__name__)

@app.route("/posts/<_id>")
def show_post(_id):
   # NOTE!: converting _id from string to ObjectId before passing to find_one
   post = client.db.posts.find_one({'_id': ObjectId(_id)})
   return render_template('post.html', post=post)

if __name__ == "__main__":
    app.run()
複製程式碼

更多內容,請參閱Querying By ObjectId

如何在Django中使用PyMongo?

Django是一個流行的Python Web框架。 Django包含一個ORM,django.db。 目前,Django沒有官方的MongoDB庫。

django-mongodb-engine是一個非官方的MongoDB庫,支援Django聚合,(原子)更新,嵌入物件,Map / Reduce和GridFS。它允許您使用Django的大部分內建功能,包括ORM,admin,authentication,site 和會話框架以及快取。

但是,在Django中不使用Django庫也很容易使用MongoDB和PyMongo。除了某些需要django.db(管理,認證和會話)的Django的功能不能使用MongoDB外,Django提供的大部分功能仍然可以使用。

有一個讓Django和MongoDB容易使用的專案是mango,mango是為Django會話和認證開發的一系列MongoDB庫(完全繞過django.db)。

PyMongo是否可以使用mod_wsgi?

可以。詳情請參閱PyMongo and mod_wsgi

如何使用Python的json模組來將我的文件編碼為JSON?

json_util是PyMongo內建的工具庫,可以靈活與Python的json模組與BSON文件和 MongoDB Extended JSON一起使用。由於json模組不支援一些PyMongo的特殊型別(比如ObjectId和DBRef),所以不能支援所有documents。

python-bsonjs是建立在libbson之上的將BSON快速轉換為 MongoDB Extended JSON的轉換器。python-bsonjs不依賴於PyMongo,可以提供比json_util更好的效能。python-bsonjs在使用RawBSONDocument時與PyMongo最適合。

在解碼另一種語言儲存的日期時為什麼會出現OverflowError?

PyMongo將BSON日期時間值解碼為Python的datetime.datetime例項。datetime.datetime的例項被限制在datetime.MINYEAR(通常為1)和datetime.MAXYEAR(通常為9999)之間。某些MongoDB驅動程式(例如PHP驅動程式)可以儲存遠遠超出datetime.datetime支援的年份值的BSON日期時間。

有幾種方法可以解決此問題。 一種選擇是過濾掉datetime.datetime支援的範圍以外的值的文件:

>>> from datetime import datetime
>>> coll = client.test.dates
>>> cur = coll.find({'dt': {'$gte': datetime.min, '$lte': datetime.max}})
複製程式碼

另一個方法是在你不需要日期時間欄位時過濾掉這個欄位:

>>> cur = coll.find({}, projection={'dt': False})
複製程式碼

在多程式中使用PyMongo

在Unix系統上,多程式模組使用fork()生成程式。在fork()中使用MongoClient例項時必須小心:MongoClient的例項不能從父程式複製到子程式,父程式和每個子程式必須建立他們自己的MongoClient例項。例如:

# Each process creates its own instance of MongoClient.
def func():
    db = pymongo.MongoClient().mydb
    # Do something with db.

proc = multiprocessing.Process(target=func)
proc.start()
複製程式碼

永遠不要這樣做:

client = pymongo.MongoClient()

# Each child process attempts to copy a global MongoClient
# created in the parent process. Never do this.
def func():
  db = client.mydb
  # Do something with db.

proc = multiprocessing.Process(target=func)
proc.start()
複製程式碼

由於fork()、執行緒和鎖之間固有的不相容性,從父程式複製的MongoClient例項在子程式中死鎖的可能性很高。