鍍金池/ 教程/ PHP/ 數(shù)據(jù)庫(kù)
外部服務(wù)認(rèn)證
數(shù)據(jù)庫(kù)
引言
模板擴(kuò)展
編寫(xiě)安全應(yīng)用
部署Tornado
異步Web服務(wù)
表單和模

數(shù)據(jù)庫(kù)

在本章中,我們將給出幾個(gè)使用數(shù)據(jù)庫(kù)的Tornado Web應(yīng)用的例子。我們將從一個(gè)簡(jiǎn)單的RESTful API例子起步,然后創(chuàng)建3.1.2節(jié)中的Burt's Book網(wǎng)站的完整功能版本。

本章中的例子使用MongoDB作為數(shù)據(jù)庫(kù),并通過(guò)pymongo作為驅(qū)動(dòng)來(lái)連接MongoDB。當(dāng)然,還有很多數(shù)據(jù)庫(kù)系統(tǒng)可以用在Web應(yīng)用中:Redis、CouchDB和MySQL都是一些知名的選擇,并且Tornado自帶處理MySQL請(qǐng)求的庫(kù)。我們選擇使用MongoDB是因?yàn)樗暮?jiǎn)單性和便捷性:安裝簡(jiǎn)單,并且能夠和Python代碼很好地融合。它結(jié)構(gòu)自然,預(yù)定義數(shù)據(jù)結(jié)構(gòu)不是必需的,很適合原型開(kāi)發(fā)。

在本章中,我們假設(shè)你已經(jīng)在機(jī)器上安裝了MongoDB,能夠運(yùn)行示例代碼,不過(guò)也可以在遠(yuǎn)程服務(wù)器上使用MongoDB,相關(guān)的代碼調(diào)整也很容易。如果你不想在你的機(jī)器上安裝MongoDB,或者沒(méi)有一個(gè)適合你操作系統(tǒng)的MongoDB版本,你也可以選擇一些MongoDB主機(jī)服務(wù)。我們推薦使用MongoHQ。在我們最初的例子中,假設(shè)你已經(jīng)在你的機(jī)器上運(yùn)行了MongoDB,但使用遠(yuǎn)程服務(wù)器(包括MongoHQ)運(yùn)行的MongoDB時(shí),調(diào)整代碼也很簡(jiǎn)單。

我們同樣還假設(shè)你已經(jīng)有一些數(shù)據(jù)庫(kù)的經(jīng)驗(yàn)了,盡管并不一定是特定的MongoDB數(shù)據(jù)庫(kù)的經(jīng)驗(yàn)。當(dāng)然,我們只會(huì)使用MongoDB的一點(diǎn)皮毛;如果想獲得更多信息請(qǐng)查閱MongoDB文檔(http://www.mongodb.org/display/DOCS/Home)讓我們開(kāi)始吧!

4.1 使用PyMongo進(jìn)行MongoDB基礎(chǔ)操作

在我們使用MongoDB編寫(xiě)Web應(yīng)用之前,我們需要了解如何在Python中使用MongoDB。在這一節(jié),你將學(xué)會(huì)如何使用PyMongo連接MongoDB數(shù)據(jù)庫(kù),然后學(xué)習(xí)如何使用pymongo在MongoDB集合中創(chuàng)建、取出和更新文檔。

PyMongo是一個(gè)簡(jiǎn)單的包裝MongoDB客戶端API的Python庫(kù)。你可以在http://api.mongodb.org/python/current/下載獲得。一旦你安裝完成,打開(kāi)一個(gè)Python解釋器,然后跟隨下面的步驟。

4.1.1 創(chuàng)建連接

首先,你需要導(dǎo)入PyMongo庫(kù),并創(chuàng)建一個(gè)到MongoDB數(shù)據(jù)庫(kù)的連接。

>>> import pymongo</kbd>
>>> conn = pymongo.Connection("localhost", 27017)</kbd>

前面的代碼向我們展示了如何連接運(yùn)行在你本地機(jī)器上默認(rèn)端口(27017)上的MongoDB服務(wù)器。如果你正在使用一個(gè)遠(yuǎn)程MongoDB服務(wù)器,替換localhost27017為合適的值。你也可以使用MongoDB URI來(lái)連接MongoDB,就像下面這樣:

>>> conn = pymongo.Connection(</kbd>
... "mongodb://user:password@staff.mongohq.com:10066/your_mongohq_db")</kbd>

前面的代碼將連接MongoHQ主機(jī)上的一個(gè)名為your_mongohq_db的數(shù)據(jù)庫(kù),其中user為用戶名,password為密碼。你可以在http://www.mongodb.org/display/DOCS/Connections中了解更多關(guān)于MongoDB URI的信息。

一個(gè)MongoDB服務(wù)器可以包括任意數(shù)量的數(shù)據(jù)庫(kù),而Connection對(duì)象可以讓你訪問(wèn)你連接的服務(wù)器的任何一個(gè)數(shù)據(jù)庫(kù)。你可以通過(guò)對(duì)象屬性或像字典一樣使用對(duì)象來(lái)獲得代表一個(gè)特定數(shù)據(jù)庫(kù)的對(duì)象。如果數(shù)據(jù)庫(kù)不存在,則被自動(dòng)建立。

>>> db = conn.example</kbd> or: db = conn['example']</kbd>

一個(gè)數(shù)據(jù)庫(kù)可以擁有任意多個(gè)集合。一個(gè)集合就是放置一些相關(guān)文檔的地方。我們使用MongoDB執(zhí)行的大部分操作(查找文檔、保存文檔、刪除文檔)都是在一個(gè)集合對(duì)象上執(zhí)行的。你可以在數(shù)據(jù)庫(kù)對(duì)象上調(diào)用collection_names方法獲得數(shù)據(jù)庫(kù)中的集合列表。

>>> db.collection_names()</kbd>
[]

當(dāng)然,我們還沒(méi)有在我們的數(shù)據(jù)庫(kù)中添加任何集合,所以這個(gè)列表是空的。當(dāng)我們插入第一個(gè)文檔時(shí),MongoDB會(huì)自動(dòng)創(chuàng)建集合。你可以在數(shù)據(jù)庫(kù)對(duì)象上通過(guò)訪問(wèn)集合名字的屬性來(lái)獲得代表集合的對(duì)象,然后調(diào)用對(duì)象的insert方法指定一個(gè)Python字典來(lái)插入文檔。比如,在下面的代碼中,我們?cè)诩蟱idgets中插入了一個(gè)文檔。因?yàn)閣idgets集合并不存在,MongoDB會(huì)在文檔被添加時(shí)自動(dòng)創(chuàng)建。

>>> widgets = db.widgets</kbd> or: widgets = db['widgets']</kbd> (see below)
>>> widgets.insert({"foo": "bar"})</kbd>
ObjectId('4eada0b5136fc4aa41000000')
>>> db.collection_names()</kbd>
[u'widgets', u'system.indexes']

(system.indexes集合是MongoDB內(nèi)部使用的。處于本章的目的,你可以忽略它。)

在之前展示的代碼中,你既可以使用數(shù)據(jù)庫(kù)對(duì)象的屬性訪問(wèn)集合,也可以把數(shù)據(jù)庫(kù)對(duì)象看作一個(gè)字典然后把集合名稱作為鍵來(lái)訪問(wèn)。比如,如果db是一個(gè)pymongo數(shù)據(jù)庫(kù)對(duì)象,那么db.widgets和db['widgets']同樣都可以訪問(wèn)這個(gè)集合。

4.1.2 處理文檔

MongoDB以文檔的形式存儲(chǔ)數(shù)據(jù),這種形式有著相對(duì)自由的數(shù)據(jù)結(jié)構(gòu)。MongoDB是一個(gè)"無(wú)模式"數(shù)據(jù)庫(kù):同一個(gè)集合中的文檔通常擁有相同的結(jié)構(gòu),但是MongoDB中并不強(qiáng)制要求使用相同結(jié)構(gòu)。在內(nèi)部,MongoDB以一種稱為BSON的類似JSON的二進(jìn)制形式存儲(chǔ)文檔。PyMongo允許我們以Python字典的形式寫(xiě)和取出文檔。

為了在集合中 創(chuàng)建一個(gè)新的文檔,我們可以使用字典作為參數(shù)調(diào)用文檔的insert方法。

>>> widgets.insert({"name": "flibnip", "description": "grade-A industrial flibnip", "quantity": 3})</kbd>
ObjectId('4eada3a4136fc4aa41000001')

既然文檔在數(shù)據(jù)庫(kù)中,我們可以使用集合對(duì)象的findone方法來(lái)取出文檔。你可以通過(guò)傳遞一個(gè)鍵為文檔名、值為你想要匹配的表達(dá)式的字典來(lái)告訴findone找到 一個(gè)特定的文檔。比如,我們想要返回文檔名域name的值等于flibnip的文檔(即,我們剛剛創(chuàng)建的文檔),可以像下面這樣調(diào)用find_oen方法:

>>> widgets.find_one({"name": "flibnip"})</kbd>
{u'description': u'grade-A industrial flibnip',
 u'_id': ObjectId('4eada3a4136fc4aa41000001'),
 u'name': u'flibnip', u'quantity': 3}

請(qǐng)注意id域。當(dāng)你創(chuàng)建任何文檔時(shí),MongoDB都會(huì)自動(dòng)添加這個(gè)域。它的值是一個(gè)ObjectID,一種保證文檔唯一的BSON對(duì)象。你可能已經(jīng)注意到,當(dāng)我們使用insert方法成功創(chuàng)建一個(gè)新的文檔時(shí),這個(gè)ObjectID同樣被返回了。(當(dāng)你創(chuàng)建文檔時(shí),可以通過(guò)給id鍵賦值來(lái)覆寫(xiě)自動(dòng)創(chuàng)建的ObjectID值。)

find_one方法返回的值是一個(gè)簡(jiǎn)單的Python字典。你可以從中訪問(wèn)獨(dú)立的項(xiàng),迭代它的鍵值對(duì),或者就像使用其他Python字典那樣修改值。

>>> doc = db.widgets.find_one({"name": "flibnip"})</kbd>
>>> type(doc)</kbd>
<type 'dict'>
>>> print doc['name']</kbd>
flibnip
>>> doc['quantity'] = 4</kbd>

然而,字典的改變并不會(huì)自動(dòng)保存到數(shù)據(jù)庫(kù)中。如果你希望把字典的改變保存,需要調(diào)用集合的save方法,并將修改后的字典作為參數(shù)進(jìn)行傳遞:

>>> doc['quantity'] = 4</kbd>
>>> db.widgets.save(doc)</kbd>
>>> db.widgets.find_one({"name": "flibnip"})</kbd>
{u'_id': ObjectId('4eb12f37136fc4b59d000000'),
 u'description': u'grade-A industrial flibnip',
 u'quantity': 4, u'name': u'flibnip'}

讓我們?cè)诩现刑砑痈嗟奈臋n:

>>> widgets.insert({"name": "smorkeg", "description": "for external use only", "quantity": 4})</kbd>
ObjectId('4eadaa5c136fc4aa41000002')
>>> widgets.insert({"name": "clobbasker", "description": "properties available on request", "quantity": 2})</kbd>
ObjectId('4eadad79136fc4aa41000003')

我們可以通過(guò)調(diào)用集合的find方法來(lái)獲得集合中所有文檔的列表,然后迭代其結(jié)果:

>>> for doc in widgets.find():</kbd>
...     print doc</kbd>
...
{u'_id': ObjectId('4eada0b5136fc4aa41000000'), u'foo': u'bar'}
{u'description': u'grade-A industrial flibnip',
 u'_id': ObjectId('4eada3a4136fc4aa41000001'),
 u'name': u'flibnip', u'quantity': 4}
{u'description': u'for external use only',
 u'_id': ObjectId('4eadaa5c136fc4aa41000002'),
 u'name': u'smorkeg', u'quantity': 4}
{u'description': u'properties available on request',
 u'_id': ObjectId('4eadad79136fc4aa41000003'),
 u'name': u'clobbasker',
 u'quantity': 2}

如果我們希望獲得文檔的一個(gè)子集,我們可以在find方法中傳遞一個(gè)字典參數(shù),就像我們?cè)趂ind_one中那樣。比如,找到那些quantity鍵的值為4的集合:

>>> for doc in widgets.find({"quantity": 4}):</kbd>
...     print doc</kbd>
...
{u'description': u'grade-A industrial flibnip',
 u'_id': ObjectId('4eada3a4136fc4aa41000001'),
 u'name': u'flibnip', u'quantity': 4}
{u'description': u'for external use only',
 u'_id': ObjectId('4eadaa5c136fc4aa41000002'),
 u'name': u'smorkeg',
 u'quantity': 4}

最后,我們可以使用集合的remove方法從集合中刪除一個(gè)文檔。remove方法和find、find_one一樣,也可以使用一個(gè)字典參數(shù)來(lái)指定哪個(gè)文檔需要被刪除。比如,要?jiǎng)h除所有name鍵的值為flipnip的文檔,輸入:

>>> widgets.remove({"name": "flibnip"})</kbd>

列出集合中的所有文檔來(lái)確認(rèn)上面的文檔已經(jīng)被刪除:

>>> for doc in widgets.find():</kbd>
...     print doc</kbd>
...
{u'_id': ObjectId('4eada0b5136fc4aa41000000'),
 u'foo': u'bar'}
{u'description': u'for external use only',
 u'_id': ObjectId('4eadaa5c136fc4aa41000002'),
 u'name': u'smorkeg', u'quantity': 4}
{u'description': u'properties available on request',
 u'_id': ObjectId('4eadad79136fc4aa41000003'),
 u'name': u'clobbasker',
 u'quantity': 2}

4.1.3 MongoDB文檔和JSON

使用Web應(yīng)用時(shí),你經(jīng)常會(huì)想采用Python字典并將其序列化為一個(gè)JSON對(duì)象(比如,作為一個(gè)AJAX請(qǐng)求的響應(yīng))。由于你使用PyMongo從MongoDB中取出的文檔是一個(gè)簡(jiǎn)單的字典,你可能會(huì)認(rèn)為你可以使用json模塊的dumps函數(shù)就可以簡(jiǎn)單地將其轉(zhuǎn)換為JSON。但,這還有一個(gè)障礙:

>>> doc = db.widgets.find_one({"name": "flibnip"})</kbd>
>>> import json</kbd>
>>> json.dumps(doc)</kbd>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    [stack trace omitted]
TypeError: ObjectId('4eb12f37136fc4b59d000000') is not JSON serializable

這里的問(wèn)題是Python的json模塊并不知道如何轉(zhuǎn)換MongoDB的ObjectID類型到JSON。有很多方法可以處理這個(gè)問(wèn)題。其中最簡(jiǎn)單的方法(也是我們?cè)诒菊轮胁捎玫姆椒ǎ┦窃谖覀冃蛄谢皬淖值淅锖?jiǎn)單地刪除_id鍵。

>>> del doc["_id"]</kbd>
>>> json.dumps(doc)</kbd>
'{"description": "grade-A industrial flibnip", "quantity": 4, "name": "flibnip"}'

一個(gè)更復(fù)雜的方法是使用PyMongo的json_util庫(kù),它同樣可以幫你序列化其他MongoDB特定數(shù)據(jù)類型到JSON。我們可以在http://api.mongodb.org/python/current/api/bson/json_util.html了解更多關(guān)于這個(gè)庫(kù)的信息。

4.2 一個(gè)簡(jiǎn)單的持久化Web服務(wù)

現(xiàn)在我們知道編寫(xiě)一個(gè)Web服務(wù),可以訪問(wèn)MongoDB數(shù)據(jù)庫(kù)中的數(shù)據(jù)。首先,我們要編寫(xiě)一個(gè)只從MongoDB讀取數(shù)據(jù)的Web服務(wù)。然后,我們寫(xiě)一個(gè)可以讀寫(xiě)數(shù)據(jù)的服務(wù)。

4.2.1 只讀字典

我們將要?jiǎng)?chuàng)建的應(yīng)用是一個(gè)基于Web的簡(jiǎn)單字典。你發(fā)送一個(gè)指定單詞的請(qǐng)求,然后返回這個(gè)單詞的定義。一個(gè)典型的交互看起來(lái)是下面這樣的:

$ curl http://localhost:8000/oarlock</kbd>
{definition: "A device attached to a rowboat to hold the oars in place",
"word": "oarlock"}

這個(gè)Web服務(wù)將從MongoDB數(shù)據(jù)庫(kù)中取得數(shù)據(jù)。具體來(lái)說(shuō),我們將根據(jù)word屬性查詢文檔。在我們查看Web應(yīng)用本身的源碼之前,先讓我們從Python解釋器中向數(shù)據(jù)庫(kù)添加一些單詞。

>>> import pymongo</kbd>
>>> conn = pymongo.Connection("localhost", 27017)</kbd>
>>> db = conn.example</kbd>
>>> db.words.insert({"word": "oarlock", "definition": "A device attached to a rowboat to hold the oars in place"})</kbd>
ObjectId('4eb1d1f8136fc4be90000000')
>>> db.words.insert({"word": "seminomadic", "definition": "Only partial
ly nomadic"})</kbd>
ObjectId('4eb1d356136fc4be90000001')
>>> db.words.insert({"word": "perturb", "definition": "Bother, unsettle
, modify"})</kbd>
ObjectId('4eb1d39d136fc4be90000002')

代碼清單4-1是我們這個(gè)詞典Web服務(wù)的源碼,在這個(gè)代碼中我們查詢剛才添加的單詞然后使用其定義作為響應(yīng)。

代碼清單4-1 一個(gè)詞典Web服務(wù):definitions_readonly.py

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

import pymongo

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [(r"/(\w+)", WordHandler)]
        conn = pymongo.Connection("localhost", 27017)
        self.db = conn["example"]
        tornado.web.Application.__init__(self, handlers, debug=True)

class WordHandler(tornado.web.RequestHandler):
    def get(self, word):
        coll = self.application.db.words
        word_doc = coll.find_one({"word": word})
        if word_doc:
            del word_doc["_id"]
            self.write(word_doc)
        else:
            self.set_status(404)
            self.write({"error": "word not found"})

if __name__ == "__main__":
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

在命令行中像下面這樣運(yùn)行這個(gè)程序:

$ python definitions_readonly.py</kbd>

現(xiàn)在使用curl或者你的瀏覽器來(lái)向應(yīng)用發(fā)送一個(gè)請(qǐng)求。

$ curl http://localhost:8000/perturb</kbd>
{"definition": "Bother, unsettle, modify", "word": "perturb"}

如果我們請(qǐng)求一個(gè)數(shù)據(jù)庫(kù)中沒(méi)有添加的單詞,會(huì)得到一個(gè)404錯(cuò)誤以及一個(gè)錯(cuò)誤信息:

$ curl http://localhost:8000/snorkle</kbd>
{"error": "word not found"}

那么這個(gè)程序是如何工作的呢?讓我們看看這個(gè)程序的主線。開(kāi)始,我們?cè)诔绦虻淖钌厦鎸?dǎo)入了import pymongo庫(kù)。然后我們?cè)谖覀兊腡ornadoApplication對(duì)象的init方法中實(shí)例化了一個(gè)pymongo連接對(duì)象。我們?cè)贏pplication對(duì)象中創(chuàng)建了一個(gè)db屬性,指向MongoDB的example數(shù)據(jù)庫(kù)。下面是相關(guān)的代碼:

conn = pymongo.Connection("localhost", 27017)
self.db = conn["example"]

一旦我們?cè)贏pplication對(duì)象中添加了db屬性,我們就可以在任何RequestHandler對(duì)象中使用self.application.db訪問(wèn)它。實(shí)際上,這正是我們?yōu)榱巳〕鰌ymongo的words集合對(duì)象而在WordHandler中g(shù)et方法所做的事情。

def get(self, word):
    coll = self.application.db.words
    word_doc = coll.find_one({"word": word})
    if word_doc:
        del word_doc["_id"]
        self.write(word_doc)
    else:
        self.set_status(404)
        self.write({"error": "word not found"})

在我們將集合對(duì)象指定給變量coll后,我們使用用戶在HTTP路徑中請(qǐng)求的單詞調(diào)用findone方法。如果我們發(fā)現(xiàn)這個(gè)單詞,則從字典中刪除id鍵(以便Python的json庫(kù)可以將其序列化),然后將其傳遞給RequestHandler的write方法。write方法將會(huì)自動(dòng)序列化字典為JSON格式。

如果find_one方法沒(méi)有匹配任何對(duì)象,則返回None。在這種情況下,我們將響應(yīng)狀態(tài)設(shè)置為404,并且寫(xiě)一個(gè)簡(jiǎn)短的JSON來(lái)提示用戶這個(gè)單詞在數(shù)據(jù)庫(kù)中沒(méi)有找到。

4.2.2 寫(xiě)字典

從字典里查詢單詞很有趣,但是在交互解釋器中添加單詞的過(guò)程卻很麻煩。我們例子的下一步是使HTTP請(qǐng)求網(wǎng)站服務(wù)時(shí)能夠創(chuàng)建和修改單詞。

它的工作流程是:發(fā)出一個(gè)特定單詞的POST請(qǐng)求,將根據(jù)請(qǐng)求中給出的定義修改已經(jīng)存在的定義。如果這個(gè)單詞并不存在,則創(chuàng)建它。例如,創(chuàng)建一個(gè)新的單詞:

$ curl -d definition=a+leg+shirt http://localhost:8000/pants</kbd>
{"definition": "a leg shirt", "word": "pants"}

我們可以使用一個(gè)GET請(qǐng)求來(lái)獲得已創(chuàng)建單詞的定義:

$ curl http://localhost:8000/pants</kbd>
{"definition": "a leg shirt", "word": "pants"}

我們可以發(fā)出一個(gè)帶有一個(gè)單詞定義域的POST請(qǐng)求來(lái)修改一個(gè)已經(jīng)存在的單詞(就和我們創(chuàng)建一個(gè)新單詞時(shí)使用的參數(shù)一樣):

$ curl -d definition=a+boat+wizard http://localhost:8000/oarlock</kbd>
{"definition": "a boat wizard", "word": "oarlock"}

代碼清單4-2是我們的詞典Web服務(wù)的讀寫(xiě)版本的源代碼。

代碼清單4-2 一個(gè)讀寫(xiě)字典服務(wù):definitions_readwrite.py

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

import pymongo

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [(r"/(\w+)", WordHandler)]
        conn = pymongo.Connection("localhost", 27017)
        self.db = conn["definitions"]
        tornado.web.Application.__init__(self, handlers, debug=True)

class WordHandler(tornado.web.RequestHandler):
    def get(self, word):
        coll = self.application.db.words
        word_doc = coll.find_one({"word": word})
        if word_doc:
            del word_doc["_id"]
            self.write(word_doc)
        else:
            self.set_status(404)
    def post(self, word):
        definition = self.get_argument("definition")
        coll = self.application.db.words
        word_doc = coll.find_one({"word": word})
        if word_doc:
            word_doc['definition'] = definition
            coll.save(word_doc)
        else:
            word_doc = {'word': word, 'definition': definition}
            coll.insert(word_doc)
        del word_doc["_id"]
        self.write(word_doc)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

除了在WordHandler中添加了一個(gè)post方法之外,這個(gè)源代碼和只讀服務(wù)的版本完全一樣。讓我們?cè)敿?xì)看看這個(gè)方法吧:

def post(self, word):
    definition = self.get_argument("definition")
    coll = self.application.db.words
    word_doc = coll.find_one({"word": word})
    if word_doc:
        word_doc['definition'] = definition
        coll.save(word_doc)
    else:
        word_doc = {'word': word, 'definition': definition}
        coll.insert(word_doc)
    del word_doc["_id"]
    self.write(word_doc)

我們首先做的事情是使用getargument方法取得POST請(qǐng)求中傳遞的definition參數(shù)。然后,就像在get方法一樣,我們嘗試使用findone方法從數(shù)據(jù)庫(kù)中加載給定單詞的文檔。如果發(fā)現(xiàn)這個(gè)單詞的文檔,我們將definition條目的值設(shè)置為從POST參數(shù)中取得的值,然后調(diào)用集合對(duì)象的save方法將改變寫(xiě)到數(shù)據(jù)庫(kù)中。如果沒(méi)有發(fā)現(xiàn)文檔,則創(chuàng)建一個(gè)新文檔,并使用insert方法將其保存到數(shù)據(jù)庫(kù)中。無(wú)論上述哪種情況,在數(shù)據(jù)庫(kù)操作執(zhí)行之后,我們?cè)陧憫?yīng)中寫(xiě)文檔(注意首先要?jiǎng)h掉_id屬性)。

4.3 Burt's Books

模板擴(kuò)展中,我們提出了Burt's Book作為使用Tornado模板工具構(gòu)建復(fù)雜Web應(yīng)用的例子。在本節(jié)中,我們將展示使用MongoDB作為數(shù)據(jù)存儲(chǔ)的Burt's Books示例版本呢。

4.3.1 讀取書(shū)籍(從數(shù)據(jù)庫(kù))

讓我們從一些簡(jiǎn)單的版本開(kāi)始:一個(gè)從數(shù)據(jù)庫(kù)中讀取書(shū)籍列表的Burt's Books。首先,我們需要在我們的MongoDB服務(wù)器上創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)和一個(gè)集合,然后用書(shū)籍文檔填充它,就像下面這樣:

>>> import pymongo</kbd>
>>> conn = pymongo.Connection()</kbd>
>>> db = conn["bookstore"]</kbd>
>>> db.books.insert({</kbd>
...     "title":"Programming Collective Intelligence",</kbd>
...     "subtitle": "Building Smart Web 2.0 Applications",</kbd>
...     "image":"/static/images/collective_intelligence.gif",</kbd>
...     "author": "Toby Segaran",</kbd>
...     "date_added":1310248056,</kbd>
...     "date_released": "August 2007",</kbd>
...     "isbn":"978-0-596-52932-1",</kbd>
...     "description":"<p>[...]</p>"</kbd>
... })
ObjectId('4eb6f1a6136fc42171000000')
>>> db.books.insert({</kbd>
...     "title":"RESTful Web Services",</kbd>
...     "subtitle": "Web services for the real world",</kbd>
...     "image":"/static/images/restful_web_services.gif",</kbd>
...     "author": "Leonard Richardson, Sam Ruby",</kbd>
...     "date_added":1311148056,</kbd>
...     "date_released": "May 2007",</kbd>
...     "isbn":"978-0-596-52926-0",</kbd>
...     "description":"<p>[...]>/p>"</kbd>
... })
ObjectId('4eb6f1cb136fc42171000001')

(我們?yōu)榱斯?jié)省空間已經(jīng)忽略了這些書(shū)籍的詳細(xì)描述。)一旦我們?cè)跀?shù)據(jù)庫(kù)中有了這些文檔,我們就準(zhǔn)備好了。代碼清單4-3展示了Burt's Books Web應(yīng)用修改版本的源代碼burtsbooksdb.py。

代碼清單4-3 讀取數(shù)據(jù)庫(kù):burts_books_db.py

import os.path
import tornado.locale
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options
import pymongo

define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", MainHandler),
            (r"/recommended/", RecommendedHandler),
        ]
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            static_path=os.path.join(os.path.dirname(__file__), "static"),
            ui_modules={"Book": BookModule},
            debug=True,
        )
        conn = pymongo.Connection("localhost", 27017)
        self.db = conn["bookstore"]
        tornado.web.Application.__init__(self, handlers, **settings)

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render(
            "index.html",
            page_title = "Burt's Books | Home",
            header_text = "Welcome to Burt's Books!",
        )

class RecommendedHandler(tornado.web.RequestHandler):
    def get(self):
        coll = self.application.db.books
        books = coll.find()
        self.render(
            "recommended.html",
            page_title = "Burt's Books | Recommended Reading",
            header_text = "Recommended Reading",
            books = books
        )

class BookModule(tornado.web.UIModule):
    def render(self, book):
        return self.render_string(
            "modules/book.html",
            book=book,
        )
    def css_files(self):
        return "/static/css/recommended.css"
    def javascript_files(self):
        return "/static/js/recommended.js"

if __name__ == "__main__":
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

正如你看到的,這個(gè)程序和模板擴(kuò)展中Burt's Books Web應(yīng)用的原始版本幾乎完全相同。它們之間只有兩個(gè)不同點(diǎn)。其一,我們?cè)谖覀兊?var>Application中添加了一個(gè)db屬性來(lái)連接MongoDB服務(wù)器:

conn = pymongo.Connection("localhost", 27017)
self.db = conn["bookstore"]

其二,我們使用連接的find方法來(lái)從數(shù)據(jù)庫(kù)中取得書(shū)籍文檔的列表,然后在渲染recommended.html時(shí)將這個(gè)列表傳遞給RecommendedHandler的get方法。下面是相關(guān)的代碼:

def get(self):
    coll = self.application.db.books
    books = coll.find()
    self.render(
        "recommended.html",
        page_title = "Burt's Books | Recommended Reading",
        header_text = "Recommended Reading",
        books = books
    )

此前,書(shū)籍列表是被硬編碼在get方法中的。但是,因?yàn)槲覀冊(cè)贛ongoDB中添加的文檔和原始的硬編碼字典擁有相同的域,所以我們之前寫(xiě)的模板代碼并不需要修改。

像下面這樣運(yùn)行應(yīng)用:

$ python burts_books_db.py</kbd>

然后讓你的瀏覽器指向http://localhost:8000/recommended/。這次,頁(yè)面和硬編碼版本的Burt's Books看起來(lái)幾乎一樣(參見(jiàn)圖3-6)。

4.3.2 編輯和添加書(shū)籍

我們的下一步是添加一個(gè)接口用來(lái)編輯已經(jīng)存在于數(shù)據(jù)庫(kù)的書(shū)籍以及添加新書(shū)籍到數(shù)據(jù)庫(kù)中。為此,我們需要一個(gè)讓用戶填寫(xiě)書(shū)籍信息的表單,一個(gè)服務(wù)表單的處理程序,以及一個(gè)處理表單結(jié)果并將其存入數(shù)據(jù)庫(kù)的處理函數(shù)。

這個(gè)版本的Burt's Books和之前給出的代碼幾乎是一樣的,只是增加了下面我們要討論的一些內(nèi)容。你可以跟隨本書(shū)附帶的完整代碼閱讀下面部分,相關(guān)的程序名為burtsbooksrwdb.py。

4.3.2.1 渲染編輯表單

下面是BookEditHandler的源代碼,它完成了兩件事情:

  1. GET
  2. POST

下面是處理程序的源代碼:

class BookEditHandler(tornado.web.RequestHandler):
    def get(self, isbn=None):
        book = dict()
        if isbn:
            coll = self.application.db.books
            book = coll.find_one({"isbn": isbn})
        self.render("book_edit.html",
            page_title="Burt's Books",
            header_text="Edit book",
            book=book)

    def post(self, isbn=None):
        import time
        book_fields = ['isbn', 'title', 'subtitle', 'image', 'author',
            'date_released', 'description']
        coll = self.application.db.books
        book = dict()
        if isbn:
            book = coll.find_one({"isbn": isbn})
        for key in book_fields:
            book[key] = self.get_argument(key, None)

        if isbn:
            coll.save(book)
        else:
            book['date_added'] = int(time.time())
            coll.insert(book)
        self.redirect("/recommended/")

我們將在稍后對(duì)其進(jìn)行詳細(xì)講解,不過(guò)現(xiàn)在先讓我們看看如何在Application類中建立請(qǐng)求到處理程序的路由。下面是Application的init方法的相關(guān)代碼部分:

handlers = [
    (r"/", MainHandler),
    (r"/recommended/", RecommendedHandler),
    (r"/edit/([0-9Xx\-]+)", BookEditHandler),
    (r"/add", BookEditHandler)
]

正如你所看到的,BookEditHandler處理了兩個(gè)不同路徑模式的請(qǐng)求。其中一個(gè)是/add,提供不存在信息的編輯表單,因此你可以向數(shù)據(jù)庫(kù)中添加一本新的書(shū)籍;另一個(gè)/edit/([0-9Xx-]+),根據(jù)書(shū)籍的ISBN渲染一個(gè)已存在書(shū)籍的表單。

4.3.2.2 從數(shù)據(jù)庫(kù)中取出書(shū)籍信息

讓我們看看BookEditHandlerget方法是如何工作的:

def get(self, isbn=None):
    book = dict()
    if isbn:
        coll = self.application.db.books
        book = coll.find_one({"isbn": isbn})
    self.render("book_edit.html",
        page_title="Burt's Books",
        header_text="Edit book",
        book=book)

如果該方法作為到/add請(qǐng)求的結(jié)果被調(diào)用,Tornado將調(diào)用一個(gè)沒(méi)有第二個(gè)參數(shù)的get方法(因?yàn)槁窂街袥](méi)有正則表達(dá)式的匹配組)。在這種情況下,默認(rèn)將一個(gè)空的book字典傳遞給book_edit.html模板。

如果該方法作為到類似于/edit/0-123-456請(qǐng)求的結(jié)果被調(diào)用,那么isdb參數(shù)被設(shè)置為0-123-456。在這種情況下,我們從Application實(shí)例中取得books集合,并用它查詢ISBN匹配的書(shū)籍。然后我們傳遞結(jié)果book字典給模板。

下面是模板(book_edit.html)的代碼:

{% extends "main.html" %}
{% autoescape None %}

{% block body %}
<form method="POST">
    ISBN <input type="text" name="isbn"
        value="{{ book.get('isbn', '') }}"><br>
    Title <input type="text" name="title"
        value="{{ book.get('title', '') }}"><br>
    Subtitle <input type="text" name="subtitle"
        value="{{ book.get('subtitle', '') }}"><br>
    Image <input type="text" name="image"
        value="{{ book.get('image', '') }}"><br>
    Author <input type="text" name="author"
        value="{{ book.get('author', '') }}"><br>
    Date released <input type="text" name="date_released"
        value="{{ book.get('date_released', '') }}"><br>
    Description<br>
    <textarea name="description" rows="5"
        cols="40">{% raw book.get('description', '')%}</textarea><br>
    <input type="submit" value="Save">
</form>
{% end %}

這是一個(gè)相當(dāng)常規(guī)的HTML表單。如果請(qǐng)求處理函數(shù)傳進(jìn)來(lái)了book字典,那么我們用它預(yù)填充帶有已存在書(shū)籍?dāng)?shù)據(jù)的表單;如果鍵不在字典中,我們使用Python字典對(duì)象的get方法為其提供默認(rèn)值。記住input標(biāo)簽的name屬性被設(shè)置為book字典的對(duì)應(yīng)鍵;這使得與來(lái)自帶有我們期望放入數(shù)據(jù)庫(kù)數(shù)據(jù)的表單關(guān)聯(lián)變得簡(jiǎn)單。

同樣還需要記住的是,因?yàn)閒orm標(biāo)簽沒(méi)有action屬性,因此表單的POST將會(huì)定向到當(dāng)前URL,這正是我們想要的(即,如果頁(yè)面以/edit/0-123-456加載,POST請(qǐng)求將轉(zhuǎn)向/edit/0-123-456;如果頁(yè)面以/add加載,則POST將轉(zhuǎn)向/add)。圖4-1所示為該頁(yè)面渲染后的樣子。

http://wiki.jikexueyuan.com/project/introduction-tornado/images/Figure4-1.jpg" alt="圖4-1" />

圖4-1 Burt's Books:添加新書(shū)的表單

4.3.2.3 保存到數(shù)據(jù)庫(kù)中

讓我們看看BookEditHandler的post方法。這個(gè)方法處理書(shū)籍編輯表單的請(qǐng)求。下面是源代碼:

def post(self, isbn=None):
    import time
    book_fields = ['isbn', 'title', 'subtitle', 'image', 'author',
        'date_released', 'description']
    coll = self.application.db.books
    book = dict()
    if isbn:
        book = coll.find_one({"isbn": isbn})
    for key in book_fields:
        book[key] = self.get_argument(key, None)

    if isbn:
        coll.save(book)
    else:
        book['date_added'] = int(time.time())
        coll.insert(book)
    self.redirect("/recommended/")

和get方法一樣,post方法也有兩個(gè)任務(wù):處理編輯已存在文檔的請(qǐng)求以及添加新文檔的請(qǐng)求。如果有isbn參數(shù)(即,路徑的請(qǐng)求類似于/edit/0-123-456),我們假定為編輯給定ISBN的文檔。如果這個(gè)參數(shù)沒(méi)有被提供,則假定為添加一個(gè)新文檔。

我們先設(shè)置一個(gè)空的字典變量book。如果我們正在編輯一個(gè)已存在的書(shū)籍,我們使用book集合的findone方法從數(shù)據(jù)庫(kù)中加載和傳入的ISBN值對(duì)應(yīng)的文檔。無(wú)論哪種情況,bookfields列表指定哪些域應(yīng)該出現(xiàn)在書(shū)籍文檔中。我們迭代這個(gè)列表,使用RequestHandler對(duì)象的get_argument方法從POST請(qǐng)求中抓取對(duì)應(yīng)的值。

此時(shí),我們準(zhǔn)備好更新數(shù)據(jù)庫(kù)了。如果我們有一個(gè)ISBN碼,那么我們調(diào)用集合的save方法來(lái)更新數(shù)據(jù)庫(kù)中的書(shū)籍文檔。如果沒(méi)有的話,我們調(diào)用集合的insert方法,此時(shí)要注意首先要為dateadded鍵添加一個(gè)值。(我們沒(méi)有將其包含在我們的域列表中獲取傳入的請(qǐng)求,因?yàn)樵趫D書(shū)被添加到數(shù)據(jù)庫(kù)之后dateadded值不應(yīng)該再被改變。)當(dāng)我們完成時(shí),使用RequestHandler類的redirect方法給用戶返回推薦頁(yè)面。我們所做的任何改變可以立刻顯現(xiàn)。圖4-2所示為更新后的推薦頁(yè)面。

http://wiki.jikexueyuan.com/project/introduction-tornado/images/Figure4-2.jpg" alt="圖4-2" />

圖4-2 Burt's Books:帶有新添加書(shū)籍的推薦列表

你還將注意到我們給每個(gè)圖書(shū)條目添加了一個(gè)"Edit"鏈接,用于鏈接到列表中每個(gè)書(shū)籍的編輯表單。下面是修改后的圖書(shū)模塊的源代碼:

<div class="book" style="overflow: auto">
    <h3 class="book_title">{{ book["title"] }}</h3>
    {% if book["subtitle"] != "" %}
        <h4 class="book_subtitle">{{ book["subtitle"] }}</h4>
    {% end %}
    <img src="{{ book["image"] }}" class="book_image"/>
    <div class="book_details">
        <div class="book_date_released">Released: {{ book["date_released"]}}</div>
        <div class="book_date_added">Added: {{ locale.format_date(book["date_added"],
relative=False) }}</div>
        <h5>Description:</h5>
        <div class="book_body">{% raw book["description"] %}</div>
        <p><a href="/edit/{{ book['isbn'] }}">Edit</a></p>
    </div>
</div>

其中最重要的一行是:

<p><a href="/edit/{{ book['isbn'] }}">Edit</a></p>

編輯頁(yè)面的鏈接是把圖書(shū)的isbn鍵的值添加到字符串/edit/后面組成的。這個(gè)鏈接將會(huì)帶你進(jìn)入這本圖書(shū)的編輯表單。你可以從圖4-3中看到結(jié)果。

http://wiki.jikexueyuan.com/project/introduction-tornado/images/Figure4-3.jpg" alt="圖4-3" />

圖4-3 Burt's Books:帶有編輯鏈接的推薦列表

4.4 MongoDB:下一步

我們?cè)谶@里只覆蓋了MongoDB的一些基礎(chǔ)知識(shí)--僅僅夠?qū)崿F(xiàn)本章中的示例Web應(yīng)用。如果你對(duì)于學(xué)習(xí)更多更用的PyMongo和MongoDB知識(shí)感興趣的話,PyMongo教程(http://api.mongodb.org/python/2.0.1/tutorial.html)和MongoDB教程(http://www.mongodb.org/display/DOCS/Tutorial)是不錯(cuò)的起點(diǎn)。

如果你對(duì)使用Tornado創(chuàng)建在擴(kuò)展性方面表現(xiàn)更好的MongoDB應(yīng)用感興趣的話,你可以自學(xué)asyncmongo(https://github.com/bitly/asyncmongo),這是一種異步執(zhí)行MongoDB請(qǐng)求的類似PyMongo的庫(kù)。我們將在異步Web服務(wù)中討論什么是異步請(qǐng)求,以及為什么它在Web應(yīng)用中擴(kuò)展性更好。