翻译:introduce to tornado - Databases

在这一章,我们将会给出几个关于tornado web应用如何使用数据库的例子。首先我们将会通过一个简单的RESTful API的例子,结合之前的练习Burt’s Book网站,一步一步去创建一个具有完整功能的Burt‘s Book 网站。 我们将会使用MongoDB作为数据库,pymongo作为python连接MongoDB的驱动。当然还有web应用还可以与许多数据库结合:Redis, CouchDB, MySQL是其中几个比较出名的选择。tornado自带的库中也封装了MySQL的驱动包。我们之所以选择MongoDB,只是因为它非常简单与方便,很容易安装并集成到Python的代码中,MongoDB具备NoSQL的特性,让我们可以直接使用原型不需要去预定义数据结构。
在这里我们假设你有一台可以运行MongoDB的电脑,这样你就可以很轻松地通过这个示例代码去连接MongoDB服务,假如你不想将MongoDB安装到你的电脑中,或者没有合适的操作系统安装MongoDB,我们建议你使用MongoHQ这个托管的MongoDB服务。 在第一个例子中,我们假设你已经将MongoDB安装到你的电脑中,当然我们也可以将使用MongoDB的代码调整成连接远程服务器(MongoHQ),这非常简单。 我们还假设你拥有一些数据库方面的经验,虽然不一定是与MongoDB相关的,在这里我们只需要了解MongoDB最基础的一些知识,你可以通过阅读MongoDB的文档获取更多信息。让我们开始学习吧! 

基本的MongoDB操作 

在我们编写web应用程序之前,需要学习如何在python中使用MongoDB,在这一小节中,你将会学习到如何通过PyMongo去连接MongoDB,如何通过PyMongo去创建、查找、更新MongoDB中的数据。 PyMongo是一个很轻量的Python库,封装了很多MongoDB 的API,你可以在这里下载它们:http://api.mongodb.org/python/current/。一旦你完成安装,就可以在python的交互命令行中继续下面的内容。 

建立连接 

首先,在你创建MongoDB数据库连接前,你需要导入PyMongoDB库。

Code    View Copy Print
  1. >>> import pymongo   
  2. >>> conn = pymongo.Connection(“localhost”, 27017)  

上面的代码将会连接到一个名为your_mongohq_db的主机上面,你需要输入你的用户名和密码,想要了解更多MongoDB 统一资源标识符(URI)的信息可以查看这里:

http://www.mongodb.org/display/DOCS/Connections 一个MongoDB服务器可以同时运行多个数据库,它也允许连接对象去连接服务器上面任意一个数据库读取数据。你可以得到一个具有特定数据库或对象属性的连接,如果数据库不存在,它将会自动创建一个。

Code    View Copy Print
  1. >>> db = conn.example or: db = conn['example']  

一个数据库可以有任意个集合,一个集合只需要存储到ige相关的文档中,大多数情况下,我们执行MongoDB的操作(查找、保存、删除)都只针对其中的一组对象,你可以通过数据库对象中collection_names方法去获取列表的信息。

  1. >>> db.collection_names()   
  2. []  

当然我们还没有添加任何数据到数据库中,所以这个集合是空的。当我们插入第一个文档时MongoDB会自动为我们创建一个集合。你就可以得到一个返回的对象,这个对象代表一个集合,我么可以通过它访问数据库上面对应的集合,然后通过调用对象的insert方法指定一个pyhon字典。例如在下面的代码中调用widgets插入一个文件到集合中,因为它之前没有创建,所以MongoDB将会在添加文档时自动创建出来。

Code    View Copy Print
  1. >>> widgets = db.widgets or: widgets = db['widgets'] (see below)   
  2. >>> widgets.insert({“foo”: ”bar”})   
  3. ObjectId(’4eada0b5136fc4aa41000000′)   
  4. >>> db.collection_names()   
  5. [u'widgets', u'system.indexes']  

system.indexes将会为MongoDB创建一个内部使用的索引,在本章中,你可以忽略掉他。 在这个简单的例子中可以看到,你可以通过集合去访问一个数据库对象的属性,也可以通过集合名作为关键字访问字典的形式去访问数据库对象。例如,如果DB是一个pymongo数据库对象,db.widgets和db['widgets']都是等价的集合。

处理文档

 MongoDB 集合将数据做为文档进行存储,这是一种相对松散的数据结构,MongoDB是一种结构化的数据库,通常文档在同一个集合中拥有相同的结构,但是MongoDB中并不强制设定结构,在MongoDB内部使用一种叫做BSON,类似于二进制JSON数据流的格式存储文档。Pymongo 允许我们像Python字典一样去写入和检索文档。 在集合中创建一个新的文档,需要用字典作为变量调用文档的一个insert方法:

Code    View Copy Print
  1. >>> widgets.insert({“name”: ”flibnip”, ”description”: ”grade-A industrial flibnip”, »   
  2. “quantity”: 3})   
  3. ObjectId(’4eada3a4136fc4aa41000001′)  

现在这个文档在数据库中,我们可以使用集合对象的 find_one 方法去检索它,你可以看到使用 find_one 来匹配你在这个表达式中的变量,并通过文档的字段名作为字典的关键字去调用一个特定的文档。例如,返回的文档字段名等价于filbnip(刚创建的文档),我们将会像下面这样调用 find_one 方法:

  1. >>> widgets.find_one({“name”: ”flibnip”})   
  2. {u’description’: u’grade-A industrial flibnip’,   
  3.  u’_id’: ObjectId(’4eada3a4136fc4aa41000001′),   
  4.  u’name’: u’flibnip’, u’quantity’: 3}  

请注意 _id 字段,MongoDB会自动给所有新建的文档添加这个字段,它的变量值是一个 ObjectID, 一种用于保证文档唯一性的特殊类型: BSON 对象。你可能已经注意到这个 ObjectID 的变量是通过 insert 方法成功创建一个新的文档后返回的值(你可以在创建它的时候,通过放入一个 _id 关键字到文档的中,去重写自动创建的内容) 这个返回值是通过 find_one 从一个简单的 python 字典获得的。你可以像操作其他 python 字典函数一样,通过关联对它进行迭代遍历或修改数值。

Code    View Copy Print
  1. >>> doc = db.widgets.find_one({“name”: ”flibnip”})   
  2. >>> type(doc)   
  3. <type ’dict‘>   
  4. >>> print doc['name']   
  5. flibnip   
  6. >>> doc['quantity'] = 4  

不过,pymongo不会自动将字典修改的数值保存到数据库中,如果你想将字典改动的内容保存,需要调用集合保存的方法,将修改的字典作为参数传递:

  1. >>> doc['quantity'] = 4   
  2. >>> db.widgets.save(doc)   
  3. >>> db.widgets.find_one({“name”: ”flibnip”})   
  4. {u’_id’: ObjectId(’4eb12f37136fc4b59d000000′),   
  5. u’description’: u’grade-A industrial flibnip’,   
  6. u’quantity’: 4, u’name’: u’flibnip’}  

让我们添加一些新的文档到集合中:

Code    View Copy Print
  1. >>> widgets.insert({“name”: ”smorkeg”, ”description”: ”for external use only”, »   
  2. “quantity”: 4})   
  3. ObjectId(’4eadaa5c136fc4aa41000002′)   
  4. >>> widgets.insert({“name”: ”clobbasker”, ”description”: »   
  5. “properties available on request”, ”quantity”: 2})   
  6. ObjectId(’4eadad79136fc4aa41000003′)  

我们还可以通过调用集合的 find 方法,将集合中的所有文档列出来。下面是迭代的结果:

Code    View Copy Print
  1. >>> for doc in widgets.find():   
  2. … print doc   
  3. …   
  4. {u’_id’: ObjectId(’4eada0b5136fc4aa41000000′), u’foo’: u’bar’}   
  5. {u’description’: u’grade-A industrial flibnip’,   
  6. u’_id’: ObjectId(’4eada3a4136fc4aa41000001′),   
  7. u’name’: u’flibnip’, u’quantity’: 4}   
  8. {u’description’: u’for external use only’,   
  9. u’_id’: ObjectId(’4eadaa5c136fc4aa41000002′),   
  10. u’name’: u’smorkeg’, u’quantity’: 4}   
  11. {u’description’: u’properties available on request’,   
  12. u’_id’: ObjectId(’4eadad79136fc4aa41000003′),   
  13. u’name’: u’clobbasker’,   
  14. u’quantity’: 2}  

如果你只是想获取一个只有子集的文档,可以像使用 find_one 方法一样将字典参数传递给 find 方法。例如我们想只查找 quantity 关键字等于4的子集:

Code    View Copy Print
  1. >>> for doc in widgets.find({“quantity”: 4}):   
  2. … print doc   
  3. …   
  4. {u’description’: u’grade-A industrial flibnip’,   
  5. u’_id’: ObjectId(’4eada3a4136fc4aa41000001′),   
  6. u’name’: u’flibnip’, u’quantity’: 4}   
  7. {u’description’: u’for external use only’,   
  8. u’_id’: ObjectId(’4eadaa5c136fc4aa41000002′),   
  9. u’name’: u’smorkeg’,   
  10. u’quantity’: 4}  

最后,我们可以使用集合的 remove 方法将文档从集合中删除,这个 remove 方法像 find_one 方法一样接受一个字典参数,然后将指定的文档删除。例如,删除所有关键字等于 flipnip 的文档:

Code    View Copy Print
  1. >>> widgets.remove({“name”: ”flibnip”})  

然后将集合中的所有文档打印出来,确认这个文档已经被成功移除:

Code    View Copy Print
  1. >>> for doc in widgets.find():   
  2. … print doc   
  3. …   
  4. {u’_id’: ObjectId(’4eada0b5136fc4aa41000000′),   
  5. u’foo’: u’bar’}   
  6. {u’description’: u’for external use only’,   
  7. u’_id’: ObjectId(’4eadaa5c136fc4aa41000002′),   
  8. u’name’: u’smorkeg’, u’quantity’: 4}   
  9. {u’description’: u’properties available on request’,   
  10. u’_id’: ObjectId(’4eadad79136fc4aa41000003′),   
  11. u’name’: u’clobbasker’,   
  12. u’quantity’: 2}  

MongoDB的文档和JSON 当我们的 web 应用工作的时候,你会频繁地使用python的字典并将它作为JSON对象进行序列化(例如,响应一个AJAX的请求),因为PyMongo只能将MongoDB的文档按照字典操作,你也许需要通过 json 模块的 dumps 函数将它转化成JSON,不过这里有一个问题:

Code    View Copy Print
  1. >>> doc = db.widgets.find_one({“name”: ”flibnip”})   
  2. >>> import json   
  3. >>> json.dumps(doc)   
  4. Traceback (most recent call last):   
  5. File ”“, line 1, in    
  6. [stack trace omitted]   
  7. TypeError: ObjectId(’4eb12f37136fc4b59d000000′) is not JSON serializable  

这个问题的原因是,Python 的 json 模块不知道如何将MongoDB的特定值 ObjectID 类型转换成 JSON,这里有几种方法进行处理。 最简单的方法(也是我们在本章将使用的方法)是在序列化之前,从文档中将 _id 这个关键字删除:

Code    View Copy Print
  1. >>> del doc["_id"]   
  2. >>> json.dumps(doc)   
  3. ‘{“description”: ”grade-A industrial flibnip”, ”quantity”: 4, ”name”: ”flibnip”}’  

更复杂的解决方案是在PyMongo中导入 json_util 库。这样也可以帮助我们将 MongoDB 中特定的类型转换成 JSON 序列。想要了解更多关于这个库的使用可以查看这里:

http://api.mongodb.org/python/current/api/bson/json_util.html 


一个简单的持久化 web 服务 

现在我们掌握了通过 MongoDB 数据库访问数据的方法,这已经能够帮助我们完成一个 web 服务了。首先我们需要编写一个只从 MongoDB 中读取数据的 web service 服务。然后我们将会编写一个能够读取和写入数据的。 

只读字典 

下面我们将会创建一个简单的基础 web 应用——字典。 你需要接收请求中的特定单词,然后返回一个单词的定义,这里是典型的样例:

Code    View Copy Print
  1. $ curl http://localhost:8000/oarlock   
  2. {definition: ”A device attached to a rowboat to hold the oars in place”,   
  3. “word”: ”oarlock”}  

这个 web 服务将会从MongoDB中抓取数据,需要明确的是,我们将要从文档中查看这些单词的属性,在我们开始查看 web 应用程序的源代码之前,先让我们将一些单词通过交互的命令行添加到数据库中。

Code    View Copy Print
  1. >>> import pymongo   
  2. >>> conn = pymongo.Connection(“localhost”, 27017)   
  3. >>> db = conn.example   
  4. >>> db.words.insert({“word”: ”oarlock”, ”definition”:»   
  5. “A device attached to a rowboat to hold the oars in place”})   
  6. ObjectId(’4eb1d1f8136fc4be90000000′)   
  7. >>> db.words.insert({“word”: ”seminomadic”, ”definition”: ”Only partially nomadic”})   
  8. ObjectId(’4eb1d356136fc4be90000001′)   
  9. >>> db.words.insert({“word”: ”perturb”, ”definition”: ”Bother, unsettle, modify”})   
  10. ObjectId(’4eb1d39d136fc4be90000002′)  

查看例子4-1 web 应用——字典的源代码,他将会查找我们刚才添加的单词,并将定义返回给客户端。 例子4-1 definitions_readonly.py

Code    View Copy Print
  1. import tornado.httpserver   
  2. import tornado.ioloop   
  3. import tornado.options   
  4. import tornado.web   
  5. import pymongo   
  6. from tornado.options import define, options   
  7. define(“port”, default=8000, help=”run on the given port”, type=int)   
  8.   
  9. class Application(tornado.web.Application):   
  10.     def __init__(self):   
  11.         handlers = [(r"/(\w+)", WordHandler)]   
  12.         conn = pymongo.Connection(“localhost”, 27017)   
  13.         self.db = conn["example"]   
  14.         tornado.web.Application.__init__(self, handlers, debug=True)   
  15.   
  16. class WordHandler(tornado.web.RequestHandler):   
  17.     def get(self, word):   
  18.     coll = self.application.db.words   
  19.     word_doc = coll.find_one({“word”: word})   
  20.     if word_doc:   
  21.         del word_doc["_id"]   
  22.         self.write(word_doc)   
  23.     else:   
  24.         self.set_status(404)   
  25.         self.write({“error”: ”word not found”})   
  26. if __name__ == ”__main__“:   
  27.     tornado.options.parse_command_line()   
  28.     http_server = tornado.httpserver.HTTPServer(Application())   
  29.     http_server.listen(options.port)   
  30.     tornado.ioloop.IOLoop.instance().start()  

在命令行中通过这条命令运行这个程序:

  1. $ python definitions_readonly.py  

现在我们可以通过 curl 命令或浏览器向应用提交请求。

  1. $ curl http://localhost:8000/perturb   
  2. {“definition”: ”Bother, unsettle, modify”, ”word”: ”perturb”}  

假如你提交的请求的单词没变添加到数据库中,我们将会得到一个404错误的响应信息。

  1. $ curl http://localhost:8000/snorkle   
  2. {“error”: ”word not found”}  

那么这个程序是如何工作的呢?让我们从一些关键的代码行开始讨论。首先我们需要在一开始将 pymongo 导入到我们的程序中。然后我们通过 tornado application 对象中的 __init__ 方法去初始化pymongo 的连接对象。 我们创建一个应用的数据库属性对象,它会指向 MongoDB 中的实例数据库。下面是相关的代码:

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

一旦我们将数据库属性添加到应用中,我们就可以通过RequestHandler 对象的 self.application.db 去访问我们的数据库。实际上,我们要做的就是通过 WordHandler 中的 get 方法,去检索 pymongo 集合中的 word 集合,下面是这个代码的 get 方法:

  1. def get(self, word):   
  2.     coll = self.application.db.words   
  3.     word_doc = coll.find_one({“word”: word})   
  4.     if word_doc:   
  5.         del word_doc["_id"]   
  6.         self.write(word_doc)   
  7.     else:   
  8.         self.set_status(404)   
  9.         self.write({“error”: ”word not found”})  

我们将会通过 find_one 方法去获取用户从 HTTP 请求路径中传过来的特定单词,然后关联到集合对象的变量中,如果我们找到了这个单词,就可以将 _id 关键字从字典中删除 (这样就可以使用 python 的json库将其序列化),然后将它传递给 RequestHandler 的 write 方法,write 方法将会把其转换成 JSON 序列。 如果 find_one 方法没有查找到匹配的对象, 它就会返回一个 None 。在这种情况下,我们可以设置一个 404错误的状态响应和一个更短小的 JSON 将信息传递给用户:这个单词在指定的数据库中没有找到。

写入字典 

查找字典中的单词确实非常有趣,但是它有一个麻烦的地方,如果要添加单词,必须要提前在交互的命令行中进行添加。我们将会在下面的例子中让它可以通过HTTP给 web 服务提交创建和修改单词的请求。它是这样工作的: 发送一个 POST 请求将一个包括单词及其定义的请求,这个单词将会作为关键字进行查找,如果它不存在,就进行创建。例如,创建一个新的单词:

  1. $ curl -d definition=a+leg+shirt http://localhost:8000/pants   
  2. {“definition”: ”a leg shirt”, ”word”: ”pants”}  

当创建了这个单词之后,我们可以通过 get 请求进行验证:

  1. $ curl http://localhost:8000/pants   
  2. {“definition”: ”a leg shirt”, ”word”: ”pants”}  

我们可以通过 POST 请求将一个单词的定义进行修改(和我们定义新单词的参数一样)

  1. curl -d definition=a+boat+wizard http://localhost:8000/oarlock   
  2. {“definition”: ”a boat wizard”, ”word”: ”oarlock”}  

看一下例子4-2,一个可以读取和写入的字典 web 应用服务。 例子4-2 definistions_readwrite.py  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 ( )

这个代码除了WordHnadler中附加的post方法,其它代码与只读服务完全一致。让我们看看这个post方法更多的细节:

1
2
3
4
5
6
7
8
9
10
11
12
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 )

我们做的第一件事情是使用 get_argument 方法去获取 definition 变量,这个变量是通过 POST 传过来的请求中定义的。然后和 get 方法一样,我们尝试去加载数据库文档,使用 find_one 方法从数据库中查找 POST 入口获取到的 word 变量。如果查找到,我们就使用 POST 入口中获取的 definition 变量去更新文档中的单词,然后调用 collection 对象的 save 方法将改动写入到数据库中。如果没有找到文档,我们将会通过 insert 方法去创建一个新的并保存到数据库中。不论发生哪种情况,在对数据库进行取值操作之后,我们都应该将数据库文档的输出写入到响应中(请注意删除掉 _id )。

Burt’s Books

在第三章中,我们以 Burt’s Books 作为例子,展示了如何使用 Tornado 的模板工具去创建一个复杂的web应用。在这一个段落,我们将会向你展示一个使用 MongoDB 作为数据存储的 Burt’s Books 版本。(在你继续学习后面的内容之前,你应该去复习第三章的 Burt’s Books 例子)

从数据库中获取书籍列表

让我们通过这个简单的 Burt’s Books 例子开始吧,这是一个从数据库中获取书籍列表的版本。首先我们需要在我们的 MongoDB 服务器中创建一个数据库并将所有书籍信息汇集到 book 这个文档中,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> import pymongo
>>> conn = pymongo . Connection ( )
>>> db = conn [ "bookstore" ]
>>> db . books . insert ( {
. . .      "title" : "Programming Collective Intelligence" ,
. . .      "subtitle" : "Building Smart Web 2.0 Applications" ,
. . .      "image" : "/static/images/collective_intelligence.gif" ,
. . .      "author" : "Toby Segaran" ,
. . .      "date_added" : 1310248056 ,
. . .      "date_released" : "August 2007" ,
. . .      "isbn" : "978-0-596-52932-1" ,
. . .      "description" : "<p>
  [...]
  
</p>"
. . . } )
ObjectId ( '4eb6f1a6136fc42171000000' )
>>> db . books . insert ( {
. . .      "title" : "RESTful Web Services" ,
. . .      "subtitle" : "Web services for the real world" ,
. . .      "image" : "/static/images/restful_web_services.gif" ,
. . .      "author" : "Leonard Richardson, Sam Ruby" ,
. . .      "date_added" : 1311148056 ,
. . .      "date_released" : "May 2007" ,
. . .      "isbn" : "978-0-596-52926-0" ,
. . .      "description" :"
. . . } )
ObjectId ( '4eb6f1cb136fc42171000001' )

(为了节省空间,我们省略了更多书籍的详细信息),一旦我们的数据库中有了这些文档,我们就可以开始重构。例子4-3 展示了 Burt’s Books web应用的代码是怎么被修改的。 让我们看看 burts_books_db.py 例子4-3 burts_books_db.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import os.path
import tornado . auth
import tornado . escape
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 ( )

正如你看到的,这个程序与第三章中 Burt’s Books web应用的原始版本代码基本一致,只有两个地方不一样.

第一:我们添加了 db 这个属性到我们的 Application 应用去链接 MongoDB 服务器:

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

第二:我们使用 connection 的 find 方法去获取数据库文档中的书籍列表信息,并且通过 RecommendedHandler 的 get 方法中的 rendering 把书籍列表的信息填充到 recommended.html 返回给用户。这些是相关的代码:

1
2
3
4
5
6
7
8
9
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
         )

在前面的代码中,可以看到书籍列表的所有信息都可以通过 get 方法查询到。然而因为我们在 MongoDB 中添加的文档使用的是同一个域的字典,这个模板的代码没有任何修改数据的功能。 通过下面的命令,运行这个应用:

1
< code class = "shell" > $ python burts \ _books \ _db . py < / code >

然后在浏览器中访问http://localhost:8000/recommended/。你会发现这个页面的内容与第三章那一个版本的 Burt’s Books 图3-6的内容完全一致。

添加与修改书籍信息

接下来需要在数据库中创建接口用于添加或修改书籍信息,要实现这个功能,我们需要创建一个给用户填写书籍信息的表单,这个表单由一个 handler 服务接收,并且由 handler 将表单中的内容全部写入到数据库中。 Burt’s Books 这个版本与上一个版本的代码基本一致,额外添加了上面讨论的功能,这个关联的程序是 burts_books_rwdb.py。你可以按照书中这种不断完善代码的方式去构建应用。

返回编辑的表单

这是 BookEditHandler 的代码,它实现了两个功能:
1. 通过 handler 的 get 请求返回给用户一个 html 表单(在模板 book_edit.html),它可能包含已经存在的书籍信息。
2. 通过 handler 的 post 请求获取表单中提交的数据,根据数据库的支持,对数据库中已有的数据进行更新或者添加新记录操作。

下面是这个 handler 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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/" )

在讲解代码的细节之前,让我们再讨论一下怎么样去设置 application 类的地址映射,这里是 application 的 init 方法中的实现细节:

1
2
3
4
5
6
7
8
< code = "python" >
handlers = [
     ( r "/" , MainHandler ) ,
     ( r "/recommended/" , RecommendedHandler ) ,
     ( r "/edit/([0-9Xx\-]+)" , BookEditHandler ) ,
     ( r "/add" , BookEditHandler )
]
< / code >

正如你看到的, BookEditHandler 有两个对应不同地址映射变量的请求。一个是\/add, 在数据库不存在任何表单提交的信息时生效,所以你可以通过它去添加一个新的书籍信息。另外一个 /edit/([0-9Xx-]+),根据书籍的ISBN信息,返回对应书籍的信息的表单。

从数据库中检索书籍信息

来看看 BookEditHandler 中的 get 方法是如何工作的:

1
2
3
4
5
6
7
8
9
10
11
< code = "python" >
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 )
< / code >

如果这个方法是由 /add 请求调用的, tornado 调用的 get 方法将不会包括第二个变量(请求路径没有匹配的正则表达式)。在这个例子中,默认情况下,将会把一个空的 book 字典 传给 book_edit.html 模板。

如果这个方法是由类似于 /edit/0-123-456这样的请求调用的。这个isbn变量将会被设置成 0-123-456。在这个例子中,我们的应用将会从数据库中检索匹配这个 isbn 号的书籍信息,并且存储在 books 字典中,然后我们会将这个 books 字典的请求传到 template 中返回给用户。
这里是相关的 template 模板(book_edit.html):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
< code = "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 % }
< / code >

这是一个非常传统的 html 表单,我们将会使用这个表单。将数据库中对应请求的 ISBN 对应的书籍信息整理到一起返回给用户,如果有的话。
如果 ISBN 值不存在,我们也可以使用 python 字典对象的 get 方法填充一个缺省值到 book 字典中,请记住,这个 book 字典的关键字将被设置为 ISBN 的值,这会让我们在后续操作中将表单的数据录入到数据库的操作变得更加容易。
同样要注意,因为表单标签缺少一个 action 属性, POST 会根据对应的URL地址将值准确地传递到我们希望的地址(如果这个表单在 /edit/0-123-456 被加载,POST 将会把我们的请求送到 /edit/0-123-456;如果这个表单在 /add 被加载, POST 将会把我们的请求送到 /add)。图4-1 展示了返回的页面信息。

保存到数据库中

让我们来讨论一下 BookEditHandler 中 post 方法的细节,这个方法获得的变量来自于 book edit 表单。这里是相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
< code = "python" >
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/" )
 
< / code >

和 get 方法一样, post 方法有两个职责,编辑已经存在的文档或添加新的文档。如果存在 isbn 变量(例如,请求的路径类似于 /edit/0-123-456) 我们将会把这个 ISBN 关联的信息填充到返回给用户的编辑页面中,如果不存在关联的信息,我们将会将返回给用户添加新文档的页面。

![图4-1](form for adding a new book)

我们从调用 book 中的空字典变量开始,如果我们要编辑一个已经存在的书籍信息, book 会通过 find_one 方法查找到数据库中对应输入的 ISBN 信息,并将存在的信息加载到页面中。不论发生什么情况, 文档中应该存在 book_fields 列表的值。我们遍历这个列表,使用 RequestHandler 对象中的 get_argument 方法从 POST 请求中获取对应的值。

在这里,如果数据库中存在 ISBN 对应的信息,我们将会调用 save 方法将 book 文档的数据更新到数据库中。如果不存在,我们将会调用 insert 方法将新数据插入到数据库中,请注意额外添加的是 date_added 键值。(这个键值在book添加到数据库之后产生的,我们传入的请求中没有包括这条信息,它只是便于我们对数据进行唯一识别,没有实际意义)当我们完成之后,会使用 RequestHandler 类中的 redirect 方法将书籍信息的页面推送给用户,我们进行的任何改动都会立即推送给用户。图4-2 展示了这个推送的页面包含的信息。

你应该还注意到,我们在每一本书籍的页面入口添加了一个“edit”链接,点击链接将会返回每一本书籍对应的编辑页面,下面是改动后的 Book 模块的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
< code = "html" >
< 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 >
 
< / code >

这里面最重要的一行是:

1
2
3
< code = "html" >
< p > < a href = "/edit/{{ book['isbn'] }}" > Edit < / a > < / p >
< / code >

这一行将每一本书籍对应的 isbn 键值 传送到生成的 /edit 超链接中,让我们能够连接到对应的编辑页面中,如果想知道链接到编辑页面的效果,你可以查看图4-3的执行结果。

图4-2
图4-3

MongoDB 下一步

在这个章节中,我们仅仅介绍了能够满足实现web应用的例子一些MongoDB接口,没有办法在这短短的篇幅覆盖所有 MongoDB 的功能。如果你想要学习更多 PyMongo 和 MongoDB 的知识, 这份PyMongo 手册 和 MongoDB 手册将会是非常好的入门文档。
如果你想要将更多 MongoDB 应用的功能应用到 tornado 平台中,你应该掌握 asyncmogo,一个与 PyMongo 类似的库,它能够更完美地支持异步的MongoDB 请求。我们将会在第五章的异步请求中花费更多篇幅讨论它。


这一次翻译的时间跨度比较大,加上博主对mongdb特性还有诸多不了解的地方,翻译的内容未必能够将作者的原文完整翻译出来,本博文求校正,求指导,如有问题请私信博主。

原创翻译,首发:http://blog.xihuan.de/tech/web/tornado/tornado_database_mongodb.htm

上一篇:翻译:introduce to tornado - Extending Templates

下一篇:翻译:introduce to tornado - Asynchronous Web Services

转载于:https://my.oschina.net/f1eming/blog/123791

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值