forked from alioth310/itt2zh
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ch4.html
596 lines (555 loc) · 44.4 KB
/
ch4.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
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>第四章:数据库 - Introduction to Tornado 中文翻译</title>
<meta name="author" content="你像从前一样">
<link href="./static/css/styles.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="sidebar">
<div class="indextable">
<ul class="index-chapter">
<li><a href="./index.html">索引页</a></li>
<li><a href="./ch1.html">第一章:引言</a></li>
<li><a href="./ch2.html">第二章:表单和模板</a></li>
<li><a href="./ch3.html">第三章:模板扩展</a></li>
<li><a href="./ch4.html" class="current-chapter">第四章:数据库</a></li>
<ul class="index-section">
<li><a href="#ch4-1">4.1 使用PyMongo进行MongoDB基础操作</a></li>
<ul class="index-section">
<li><a href="#ch4-1-1">4.1.1 创建连接</a></li>
<li><a href="#ch4-1-2">4.1.2 处理文档</a></li>
<li><a href="#ch4-1-3">4.1.3 MongoDB文档和JSON</a></li>
</ul>
<li><a href="#ch4-2">4.2 一个简单的持久化Web服务</a></li>
<ul class="index-section">
<li><a href="#ch4-2-1">4.2.1 只读字典</a></li>
<li><a href="#ch4-2-2">4.2.2 写字典</a></li>
</ul>
<li><a href="#ch4-3">4.3 Burt's Books</a></li>
<ul class="index-section">
<li><a href="#ch4-3-1">4.3.1 读取书籍(从数据库)</a></li>
<li><a href="#ch4-3-2">4.3.2 编辑和添加书籍</a></li>
<ul class="index-section">
<li><a href="#ch4-3-2-1">4.3.2.1 渲染编辑表单</a></li>
<li><a href="#ch4-3-2-2">4.3.2.2 从数据库中取出书籍信息</a></li>
<li><a href="#ch4-3-2-3">4.3.2.3 保存到数据库中</a></li>
</ul>
</ul>
<li><a href="#ch4-4">4.4 MongoDB:下一步</a></li>
</ul>
<li><a href="./ch5.html">第五章:异步Web服务</a></li>
<li><a href="./ch6.html">第六章:编写安全应用</a></li>
<li><a href="./ch7.html">第七章:外部服务认证</a></li>
<li><a href="./ch8.html">第八章:部署Tornado</a></li>
</ul>
</div>
</div>
<div class="article">
<h1>第四章:数据库<a class="headerlink" id="ch4" href="#ch4">¶</a></h1>
<p>在本章中,我们将给出几个使用数据库的Tornado Web应用的例子。我们将从一个简单的RESTful API例子起步,然后创建3.1.2节中的Burt's Book网站的完整功能版本。</p>
<p>本章中的例子使用MongoDB作为数据库,并通过pymongo作为驱动来连接MongoDB。当然,还有很多数据库系统可以用在Web应用中:Redis、CouchDB和MySQL都是一些知名的选择,并且Tornado自带处理MySQL请求的库。我们选择使用MongoDB是因为它的简单性和便捷性:安装简单,并且能够和Python代码很好地融合。它结构自然,预定义数据结构不是必需的,很适合原型开发。</p>
<p>在本章中,我们假设你已经在机器上安装了MongoDB,能够运行示例代码,不过也可以在远程服务器上使用MongoDB,相关的代码调整也很容易。如果你不想在你的机器上安装MongoDB,或者没有一个适合你操作系统的MongoDB版本,你也可以选择一些MongoDB主机服务。我们推荐使用<a href="http://www.mongohq.com">MongoHQ</a>。在我们最初的例子中,假设你已经在你的机器上运行了MongoDB,但使用远程服务器(包括MongoHQ)运行的MongoDB时,调整代码也很简单。</p>
<p>我们同样还假设你已经有一些数据库的经验了,尽管并不一定是特定的MongoDB数据库的经验。当然,我们只会使用MongoDB的一点皮毛;如果想获得更多信息请查阅MongoDB文档(<a href="http://www.mongodb.org/display/DOCS/Home">http://www.mongodb.org/display/DOCS/Home</a>)让我们开始吧!</p>
<h2>4.1 使用PyMongo进行MongoDB基础操作<a class="headerlink" id="ch4-1" href="#ch4-1">¶</a></h2>
<p>在我们使用MongoDB编写Web应用之前,我们需要了解如何在Python中使用MongoDB。在这一节,你将学会如何使用PyMongo连接MongoDB数据库,然后学习如何使用pymongo在MongoDB集合中创建、取出和更新文档。</p>
<p>PyMongo是一个简单的包装MongoDB客户端API的Python库。你可以在<a href="http://api.mongodb.org/python/current/">http://api.mongodb.org/python/current/</a>下载获得。一旦你安装完成,打开一个Python解释器,然后跟随下面的步骤。</p>
<h3>4.1.1 创建连接<a class="headerlink" id="ch4-1-1" href="#ch4-1-1">¶</a></h3>
<p>首先,你需要导入PyMongo库,并创建一个到MongoDB数据库的连接。</p>
<pre class="shell">>>> <kbd>import pymongo</kbd>
>>> <kbd>conn = pymongo.Connection("localhost", 27017)</kbd></pre>
<p>前面的代码向我们展示了如何连接运行在你本地机器上默认端口(27017)上的MongoDB服务器。如果你正在使用一个远程MongoDB服务器,替换<var>localhost</var>和<var>27017</var>为合适的值。你也可以使用MongoDB URI来连接MongoDB,就像下面这样:</p>
<pre class="shell">>>> <kbd>conn = pymongo.Connection(</kbd>
... <kbd>"mongodb://user:[email protected]:10066/your_mongohq_db")</kbd></pre>
<p>前面的代码将连接MongoHQ主机上的一个名为<var>your_mongohq_db</var>的数据库,其中<var>user</var>为用户名,<var>password</var>为密码。你可以在<a href="http://www.mongodb.org/display/DOCS/Connections">http://www.mongodb.org/display/DOCS/Connections</a>中了解更多关于MongoDB URI的信息。</p>
<p>一个MongoDB服务器可以包括任意数量的数据库,而<var>Connection</var>对象可以让你访问你连接的服务器的任何一个数据库。你可以通过对象属性或像字典一样使用对象来获得代表一个特定数据库的对象。如果数据库不存在,则被自动建立。</p>
<pre class="shell">>>> <kbd>db = conn.example</kbd> or: <kbd>db = conn['example']</kbd></pre>
<p>一个数据库可以拥有任意多个<span class="term">集合</span>。一个集合就是放置一些相关文档的地方。我们使用MongoDB执行的大部分操作(查找文档、保存文档、删除文档)都是在一个集合对象上执行的。你可以在数据库对象上调用<var>collection_names</var>方法获得数据库中的集合列表。</p>
<pre class="shell">>>> <kbd>db.collection_names()</kbd>
[]</pre>
<p>当然,我们还没有在我们的数据库中添加任何集合,所以这个列表是空的。当我们插入第一个文档时,MongoDB会自动创建集合。你可以在数据库对象上通过访问集合名字的属性来获得代表集合的对象,然后调用对象的<var>insert</var>方法指定一个Python字典来插入文档。比如,在下面的代码中,我们在集合<var>widgets</var>中插入了一个文档。因为<var>widgets</var>集合并不存在,MongoDB会在文档被添加时自动创建。</p>
<pre class="shell">>>> <kbd>widgets = db.widgets</kbd> or: <kbd>widgets = db['widgets']</kbd> (see below)
>>> <kbd>widgets.insert({"foo": "bar"})</kbd>
ObjectId('4eada0b5136fc4aa41000000')
>>> <kbd>db.collection_names()</kbd>
[u'widgets', u'system.indexes']</pre>
<p>(<var>system.indexes</var>集合是MongoDB内部使用的。处于本章的目的,你可以忽略它。)</p>
<p>在之前展示的代码中,你既可以使用数据库对象的属性访问集合,也可以把数据库对象看作一个字典然后把集合名称作为键来访问。比如,如果<var>db</var>是一个pymongo数据库对象,那么<var>db.widgets</var>和<var>db['widgets']</var>同样都可以访问这个集合。</p>
<h3>4.1.2 处理文档<a class="headerlink" id="ch4-1-2" href="#ch4-1-2">¶</a></h3>
<p>MongoDB以<span class="term">文档</span>的形式存储数据,这种形式有着相对自由的数据结构。MongoDB是一个"无模式"数据库:同一个集合中的文档通常拥有相同的结构,但是MongoDB中并不强制要求使用相同结构。在内部,MongoDB以一种称为<span class="term">BSON</span>的类似JSON的二进制形式存储文档。PyMongo允许我们以Python字典的形式写和取出文档。</p>
<p>为了在集合中 创建一个新的文档,我们可以使用字典作为参数调用文档的<var>insert</var>方法。</p>
<pre class="shell">>>> <kbd>widgets.insert({"name": "flibnip", "description": "grade-A industrial flibnip", "quantity": 3})</kbd>
ObjectId('4eada3a4136fc4aa41000001')</pre>
<p>既然文档在数据库中,我们可以使用集合对象的<var>find_one</var>方法来取出文档。你可以通过传递一个键为文档名、值为你想要匹配的表达式的字典来告诉<var>find_one</var>找到 一个特定的文档。比如,我们想要返回文档名域<var>name</var>的值等于<var>flibnip</var>的文档(即,我们刚刚创建的文档),可以像下面这样调用<var>find_oen</var>方法:</p>
<pre class="shell">>>> <kbd>widgets.find_one({"name": "flibnip"})</kbd>
{u'description': u'grade-A industrial flibnip',
u'_id': ObjectId('4eada3a4136fc4aa41000001'),
u'name': u'flibnip', u'quantity': 3}</pre>
<p>请注意<var>_id</var>域。当你创建任何文档时,MongoDB都会自动添加这个域。它的值是一个<var>ObjectID</var>,一种保证文档唯一的BSON对象。你可能已经注意到,当我们使用<var>insert</var>方法成功创建一个新的文档时,这个<var>ObjectID</var>同样被返回了。(当你创建文档时,可以通过给<var>_id</var>键赋值来覆写自动创建的<var>ObjectID</var>值。)</p>
<p><var>find_one</var>方法返回的值是一个简单的Python字典。你可以从中访问独立的项,迭代它的键值对,或者就像使用其他Python字典那样修改值。</p>
<pre class="shell">>>> <kbd>doc = db.widgets.find_one({"name": "flibnip"})</kbd>
>>> <kbd>type(doc)</kbd>
<type 'dict'>
>>> <kbd>print doc['name']</kbd>
flibnip
>>> <kbd>doc['quantity'] = 4</kbd></pre>
<p>然而,字典的改变并不会自动保存到数据库中。如果你希望把字典的改变保存,需要调用集合的<var>save</var>方法,并将修改后的字典作为参数进行传递:</p>
<pre class="shell">>>> <kbd>doc['quantity'] = 4</kbd>
>>> <kbd>db.widgets.save(doc)</kbd>
>>> <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'}</pre>
<p>让我们在集合中添加更多的文档:</p>
<pre class="shell">>>> <kbd>widgets.insert({"name": "smorkeg", "description": "for external use only", "quantity": 4})</kbd>
ObjectId('4eadaa5c136fc4aa41000002')
>>> <kbd>widgets.insert({"name": "clobbasker", "description": "properties available on request", "quantity": 2})</kbd>
ObjectId('4eadad79136fc4aa41000003')</pre>
<p>我们可以通过调用集合的<var>find</var>方法来获得集合中所有文档的列表,然后迭代其结果:</p>
<pre class="shell">>>> <kbd>for doc in widgets.find():</kbd>
... <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}</pre>
<p>如果我们希望获得文档的一个子集,我们可以在<var>find</var>方法中传递一个字典参数,就像我们在<var>find_one</var>中那样。比如,找到那些<var>quantity</var>键的值为4的集合:</p>
<pre class="shell">>>> <kbd>for doc in widgets.find({"quantity": 4}):</kbd>
... <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}</pre>
<p>最后,我们可以使用集合的<var>remove</var>方法从集合中删除一个文档。<var>remove</var>方法和<var>find</var>、<var>find_one</var>一样,也可以使用一个字典参数来指定哪个文档需要被删除。比如,要删除所有<var>name</var>键的值为<var>flipnip</var>的文档,输入:</p>
<pre class="shell">>>> <kbd>widgets.remove({"name": "flibnip"})</kbd></pre>
<p>列出集合中的所有文档来确认上面的文档已经被删除:</p>
<pre class="shell">>>> <kbd>for doc in widgets.find():</kbd>
... <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}</pre>
<h3>4.1.3 MongoDB文档和JSON<a class="headerlink" id="ch4-1-3" href="#ch4-1-3">¶</a></h3>
<p>使用Web应用时,你经常会想采用Python字典并将其序列化为一个JSON对象(比如,作为一个AJAX请求的响应)。由于你使用PyMongo从MongoDB中取出的文档是一个简单的字典,你可能会认为你可以使用<var>json</var>模块的<var>dumps</var>函数就可以简单地将其转换为JSON。但,这还有一个障碍:</p>
<pre class="shell">>>> <kbd>doc = db.widgets.find_one({"name": "flibnip"})</kbd>
>>> <kbd>import json</kbd>
>>> <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</pre>
<p>这里的问题是Python的<var>json</var>模块并不知道如何转换MongoDB的<var>ObjectID</var>类型到JSON。有很多方法可以处理这个问题。其中最简单的方法(也是我们在本章中采用的方法)是在我们序列化之前从字典里简单地删除<var>_id</var>键。</p>
<pre class="shell">>>> <kbd>del doc["_id"]</kbd>
>>> <kbd>json.dumps(doc)</kbd>
'{"description": "grade-A industrial flibnip", "quantity": 4, "name": "flibnip"}'</pre>
<p>一个更复杂的方法是使用PyMongo的<var>json_util</var>库,它同样可以帮你序列化其他MongoDB特定数据类型到JSON。我们可以在<a href="http://api.mongodb.org/python/current/api/bson/json_util.html">http://api.mongodb.org/python/current/api/bson/json_util.html</a>了解更多关于这个库的信息。</p>
<h2>4.2 一个简单的持久化Web服务<a class="headerlink" id="ch4-2" href="#ch4-2">¶</a></h2>
<p>现在我们知道编写一个Web服务,可以访问MongoDB数据库中的数据。首先,我们要编写一个只从MongoDB读取数据的Web服务。然后,我们写一个可以读写数据的服务。</p>
<h3>4.2.1 只读字典<a class="headerlink" id="ch4-2-1" href="#ch4-2-1">¶</a></h3>
<p>我们将要创建的应用是一个基于Web的简单字典。你发送一个指定单词的请求,然后返回这个单词的定义。一个典型的交互看起来是下面这样的:</p>
<pre class="shell">$ <kbd>curl http://localhost:8000/oarlock</kbd>
{definition: "A device attached to a rowboat to hold the oars in place",
"word": "oarlock"}</pre>
<p>这个Web服务将从MongoDB数据库中取得数据。具体来说,我们将根据<var>word</var>属性查询文档。在我们查看Web应用本身的源码之前,先让我们从Python解释器中向数据库添加一些单词。</p>
<pre class="shell">>>> <kbd>import pymongo</kbd>
>>> <kbd>conn = pymongo.Connection("localhost", 27017)</kbd>
>>> <kbd>db = conn.example</kbd>
>>> <kbd>db.words.insert({"word": "oarlock", "definition": "A device attached to a rowboat to hold the oars in place"})</kbd>
ObjectId('4eb1d1f8136fc4be90000000')
>>> <kbd>db.words.insert({"word": "seminomadic", "definition": "Only partial
ly nomadic"})</kbd>
ObjectId('4eb1d356136fc4be90000001')
>>> <kbd>db.words.insert({"word": "perturb", "definition": "Bother, unsettle
, modify"})</kbd>
ObjectId('4eb1d39d136fc4be90000002')</pre>
<p>代码清单4-1是我们这个词典Web服务的源码,在这个代码中我们查询刚才添加的单词然后使用其定义作为响应。</p>
<div class="codelist">
<div class="codelist-title">代码清单4-1 一个词典Web服务:definitions_readonly.py</div>
<pre class="codelist-code">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()</pre>
</div>
<p>在命令行中像下面这样运行这个程序:</p>
<pre class="shell">$ <kbd>python definitions_readonly.py</kbd></pre>
<p>现在使用curl或者你的浏览器来向应用发送一个请求。</p>
<pre class="shell">$ <kbd>curl http://localhost:8000/perturb</kbd>
{"definition": "Bother, unsettle, modify", "word": "perturb"}</pre>
<p>如果我们请求一个数据库中没有添加的单词,会得到一个404错误以及一个错误信息:</p>
<pre class="shell">$ <kbd>curl http://localhost:8000/snorkle</kbd>
{"error": "word not found"}</pre>
<p>那么这个程序是如何工作的呢?让我们看看这个程序的主线。开始,我们在程序的最上面导入了<var>import pymongo</var>库。然后我们在我们的Tornado<var>Application</var>对象的<var>__init__</var>方法中实例化了一个pymongo连接对象。我们在<var>Application</var>对象中创建了一个<var>db</var>属性,指向MongoDB的<var>example</var>数据库。下面是相关的代码:</p>
<pre class="codelist-code">conn = pymongo.Connection("localhost", 27017)
self.db = conn["example"]</pre>
<p>一旦我们在<var>Application</var>对象中添加了<var>db</var>属性,我们就可以在任何<var>RequestHandler</var>对象中使用<var>self.application.db</var>访问它。实际上,这正是我们为了取出pymongo的<var>words</var>集合对象而在<var>WordHandler</var>中<var>get</var>方法所做的事情。</p>
<pre class="codelist-code">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"})</pre>
<p>在我们将集合对象指定给变量<var>coll</var>后,我们使用用户在HTTP路径中请求的单词调用<var>find_one</var>方法。如果我们发现这个单词,则从字典中删除<var>_id</var>键(以便Python的<var>json</var>库可以将其序列化),然后将其传递给RequestHandler的<var>write</var>方法。<var>write</var>方法将会自动序列化字典为JSON格式。</p>
<p>如果<var>find_one</var>方法没有匹配任何对象,则返回<var>None</var>。在这种情况下,我们将响应状态设置为404,并且写一个简短的JSON来提示用户这个单词在数据库中没有找到。</p>
<h3>4.2.2 写字典<a class="headerlink" id="ch4-2-2" href="#ch4-2-2">¶</a></h3>
<p>从字典里查询单词很有趣,但是在交互解释器中添加单词的过程却很麻烦。我们例子的下一步是使HTTP请求网站服务时能够创建和修改单词。</p>
<p>它的工作流程是:发出一个特定单词的<dfn>POST</dfn>请求,将根据请求中给出的定义修改已经存在的定义。如果这个单词并不存在,则创建它。例如,创建一个新的单词:</p>
<pre class="shell">$ <kbd>curl -d definition=a+leg+shirt http://localhost:8000/pants</kbd>
{"definition": "a leg shirt", "word": "pants"}</pre>
<p>我们可以使用一个<dfn>GET</dfn>请求来获得已创建单词的定义:</p>
<pre class="shell">$ <kbd>curl http://localhost:8000/pants</kbd>
{"definition": "a leg shirt", "word": "pants"}</pre>
<p>我们可以发出一个带有一个单词定义域的<dfn>POST</dfn>请求来修改一个已经存在的单词(就和我们创建一个新单词时使用的参数一样):</p>
<pre class="shell">$ <kbd>curl -d definition=a+boat+wizard http://localhost:8000/oarlock</kbd>
{"definition": "a boat wizard", "word": "oarlock"}</pre>
<p>代码清单4-2是我们的词典Web服务的读写版本的源代码。</p>
<div class="codelist">
<div class="codelist-title">代码清单4-2 一个读写字典服务:definitions_readwrite.py</div>
<pre class="codelist-code">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()</pre>
</div>
<p>除了在<var>WordHandler</var>中添加了一个<var>post</var>方法之外,这个源代码和只读服务的版本完全一样。让我们详细看看这个方法吧:</p>
<pre class="codelist-code">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)</pre>
<p>我们首先做的事情是使用<var>get_argument</var>方法取得<dfn>POST</dfn>请求中传递的<var>definition</var>参数。然后,就像在<var>get</var>方法一样,我们尝试使用<var>find_one</var>方法从数据库中加载给定单词的文档。如果发现这个单词的文档,我们将<var>definition</var>条目的值设置为从<dfn>POST</dfn>参数中取得的值,然后调用集合对象的<var>save</var>方法将改变写到数据库中。如果没有发现文档,则创建一个新文档,并使用<var>insert</var>方法将其保存到数据库中。无论上述哪种情况,在数据库操作执行之后,我们在响应中写文档(注意首先要删掉<var>_id</var>属性)。</p>
<h2>4.3 Burt's Books<a class="headerlink" id="ch4-3" href="#ch4-3">¶</a></h2>
<p>在<a href="./ch3.html">第三章</a>中,我们提出了Burt's Book作为使用Tornado模板工具构建复杂Web应用的例子。在本节中,我们将展示使用MongoDB作为数据存储的Burt's Books示例版本呢。</p>
<h3>4.3.1 读取书籍(从数据库)<a class="headerlink" id="ch4-3-1" href="#ch4-3-1">¶</a></h3>
<p>让我们从一些简单的版本开始:一个从数据库中读取书籍列表的Burt's Books。首先,我们需要在我们的MongoDB服务器上创建一个数据库和一个集合,然后用书籍文档填充它,就像下面这样:</p>
<pre class="shell">>>> <kbd>import pymongo</kbd>
>>> <kbd>conn = pymongo.Connection()</kbd>
>>> <kbd>db = conn["bookstore"]</kbd>
>>> <kbd>db.books.insert({</kbd>
... <kbd>"title":"Programming Collective Intelligence",</kbd>
... <kbd>"subtitle": "Building Smart Web 2.0 Applications",</kbd>
... <kbd>"image":"/static/images/collective_intelligence.gif",</kbd>
... <kbd>"author": "Toby Segaran",</kbd>
... <kbd>"date_added":1310248056,</kbd>
... <kbd>"date_released": "August 2007",</kbd>
... <kbd>"isbn":"978-0-596-52932-1",</kbd>
... <kbd>"description":"<p>[...]</p>"</kbd>
... </kbd>})</kbd>
ObjectId('4eb6f1a6136fc42171000000')
>>> <kbd>db.books.insert({</kbd>
... <kbd>"title":"RESTful Web Services",</kbd>
... <kbd>"subtitle": "Web services for the real world",</kbd>
... <kbd>"image":"/static/images/restful_web_services.gif",</kbd>
... <kbd>"author": "Leonard Richardson, Sam Ruby",</kbd>
... <kbd>"date_added":1311148056,</kbd>
... <kbd>"date_released": "May 2007",</kbd>
... <kbd>"isbn":"978-0-596-52926-0",</kbd>
... <kbd>"description":"<p>[...]>/p>"</kbd>
... </kbd>})</kbd>
ObjectId('4eb6f1cb136fc42171000001')</pre>
<p>(我们为了节省空间已经忽略了这些书籍的详细描述。)一旦我们在数据库中有了这些文档,我们就准备好了。代码清单4-3展示了Burt's Books Web应用修改版本的源代码<span class="filename">burts_books_db.py</span>。</p>
<div class="codelist">
<div class="codelist-title">代码清单4-3 读取数据库:burts_books_db.py</div>
<pre class="codelist-code">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()</pre>
<p>正如你看到的,这个程序和<a href="./ch3.html">第三章</a>中Burt's Books Web应用的原始版本几乎完全相同。它们之间只有两个不同点。其一,我们在我们的<var>Application</var>中添加了一个<var>db</var>属性来连接MongoDB服务器:</p>
<pre class="codelist-code">conn = pymongo.Connection("localhost", 27017)
self.db = conn["bookstore"]</pre>
<p>其二,我们使用连接的<var>find</var>方法来从数据库中取得书籍文档的列表,然后在渲染<span class="filename">recommended.html</span>时将这个列表传递给<var>RecommendedHandler</var>的<var>get</var>方法。下面是相关的代码:</p>
<pre class="codelist-code">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
)</pre>
<p>此前,书籍列表是被硬编码在<var>get</var>方法中的。但是,因为我们在MongoDB中添加的文档和原始的硬编码字典拥有相同的域,所以我们之前写的模板代码并不需要修改。</p>
<p>像下面这样运行应用:</p>
<pre class="shell">$ <kbd>python burts_books_db.py</kbd></pre>
<p>然后让你的浏览器指向<a href="http://localhost:8000/recommended/">http://localhost:8000/recommended/</a>。这次,页面和硬编码版本的Burt's Books看起来几乎一样(参见图3-6)。</p>
<h3>4.3.2 编辑和添加书籍<a class="headerlink" id="ch4-3-2" href="#ch4-3-2">¶</a></h3>
<p>我们的下一步是添加一个接口用来编辑已经存在于数据库的书籍以及添加新书籍到数据库中。为此,我们需要一个让用户填写书籍信息的表单,一个服务表单的处理程序,以及一个处理表单结果并将其存入数据库的处理函数。</p>
<p>这个版本的Burt's Books和之前给出的代码几乎是一样的,只是增加了下面我们要讨论的一些内容。你可以跟随本书附带的完整代码阅读下面部分,相关的程序名为<span class="filename">burts_books_rwdb.py</span>。
<h4>4.3.2.1 渲染编辑表单<a class="headerlink" id="ch4-3-2-1" href="#ch4-3-2-1">¶</a></h4>
<p>下面是<var>BookEditHandler</var>的源代码,它完成了两件事情:</p>
<ol>
<li><dfn>GET</dfn>请求渲染一个显示已存在书籍数据的HTML表单(在模板<span class="filename">book_edit.html</span>中)。</li>
<li><dfn>POST</dfn>请求从表单中取得数据,更新数据库中已存在的书籍记录或依赖提供的数据添加一个新的书籍。</li>
</ol>
<p>下面是处理程序的源代码:</p>
<pre class="codelist-code">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/")</pre>
<p>我们将在稍后对其进行详细讲解,不过现在先让我们看看如何在<var>Application</var>类中建立请求到处理程序的路由。下面是<var>Application</var>的<var>__init__</var>方法的相关代码部分:</p>
<pre class="codelist-code">handlers = [
(r"/", MainHandler),
(r"/recommended/", RecommendedHandler),
(r"/edit/([0-9Xx\-]+)", BookEditHandler),
(r"/add", BookEditHandler)
]</pre>
<p>正如你所看到的,<var>BookEditHandler</var>处理了两个<em>不同</em>路径模式的请求。其中一个是<var>/add</var>,提供不存在信息的编辑表单,因此你可以向数据库中添加一本新的书籍;另一个<var>/edit/([0-9Xx\-]+)</var>,根据书籍的ISBN渲染一个已存在书籍的表单。</p>
<h4>4.3.2.2 从数据库中取出书籍信息<a class="headerlink" id="ch4-3-2-2" href="#ch4-3-2-2">¶</a></h4>
<p>让我们看看<var>BookEditHandler</var>的<var>get</var>方法是如何工作的:</p>
<pre class="codelist-code">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)</pre>
<p>如果该方法作为到<var>/add</var>请求的结果被调用,Tornado将调用一个没有第二个参数的<var>get</var>方法(因为路径中没有正则表达式的匹配组)。在这种情况下,默认将一个空的<var>book</var>字典传递给<span class="filename">book_edit.html</span>模板。</p>
<p>如果该方法作为到类似于<var>/edit/0-123-456</var>请求的结果被调用,那么</var>isdb</var>参数被设置为<var>0-123-456</var>。在这种情况下,我们从<var>Application</var>实例中取得<var>books</var>集合,并用它查询ISBN匹配的书籍。然后我们传递结果<var>book</var>字典给模板。</p>
<p>下面是模板(<span class="filename">book_edit.html</span>)的代码:</p>
<pre class="codelist-code">{% 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 %}</pre>
<p>这是一个相当常规的HTML表单。如果请求处理函数传进来了<var>book</var>字典,那么我们用它预填充带有已存在书籍数据的表单;如果键不在字典中,我们使用Python字典对象的<var>get</var>方法为其提供默认值。记住<var>input</var>标签的<var>name</var>属性被设置为<var>book</var>字典的对应键;这使得与来自带有我们期望放入数据库数据的表单关联变得简单。</p>
<p>同样还需要记住的是,因为<var>form</var>标签没有<var>action</var>属性,因此表单的<dfn>POST</dfn>将会定向到当前URL,这正是我们想要的(即,如果页面以<var>/edit/0-123-456</var>加载,<dfn>POST</dfn>请求将转向<var>/edit/0-123-456</var>;如果页面以<var>/add</var>加载,则<dfn>POST</dfn>将转向<dfn>/add</dfn>)。图4-1所示为该页面渲染后的样子。</p>
<div class="figure">
<img src="./static/images/Figure4-1.jpg" alt="图4-1" />
<p>图4-1 Burt's Books:添加新书的表单</p>
</div>
<h4>4.3.2.3 保存到数据库中<a class="headerlink" id="ch4-3-2-3" href="#ch4-3-2-3">¶</a></h4>
<p>让我们看看<var>BookEditHandler</var>的<var>post</var>方法。这个方法处理书籍编辑表单的请求。下面是源代码:</p>
<pre class="codelist-code">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/")</pre>
<p>和<var>get</var>方法一样,<var>post</var>方法也有两个任务:处理编辑已存在文档的请求以及添加新文档的请求。如果有<var>isbn</var>参数(即,路径的请求类似于<var>/edit/0-123-456</var>),我们假定为编辑给定ISBN的文档。如果这个参数没有被提供,则假定为添加一个新文档。</p>
<p>我们先设置一个空的字典变量<var>book</var>。如果我们正在编辑一个已存在的书籍,我们使用<var>book</var>集合的<var>find_one</var>方法从数据库中加载和传入的ISBN值对应的文档。无论哪种情况,<var>book_fields</var>列表指定哪些域应该出现在书籍文档中。我们迭代这个列表,使用<var>RequestHandler</var>对象的<var>get_argument</var>方法从<dfn>POST</dfn>请求中抓取对应的值。</p>
<p>此时,我们准备好更新数据库了。如果我们有一个ISBN码,那么我们调用集合的<var>save</var>方法来更新数据库中的书籍文档。如果没有的话,我们调用集合的<var>insert</var>方法,此时要注意首先要为<var>date_added</var>键添加一个值。(我们没有将其包含在我们的域列表中获取传入的请求,因为在图书被添加到数据库之后<var>date_added</var>值不应该再被改变。)当我们完成时,使用<var>RequestHandler</var>类的<var>redirect</var>方法给用户返回推荐页面。我们所做的任何改变可以立刻显现。图4-2所示为更新后的推荐页面。</p>
<div class="figure">
<img src="./static/images/Figure4-2.jpg" alt="图4-2" />
<p>图4-2 Burt's Books:带有新添加书籍的推荐列表</p>
</div>
<p>你还将注意到我们给每个图书条目添加了一个"Edit"链接,用于链接到列表中每个书籍的编辑表单。下面是修改后的图书模块的源代码:</p>
<pre class="codelist-code"><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></pre>
<p>其中最重要的一行是:</p>
<pre class="codelist-code"><p><a href="/edit/{{ book['isbn'] }}">Edit</a></p></pre>
<p>编辑页面的链接是把图书的<var>isbn</var>键的值添加到字符串<var>/edit/</var>后面组成的。这个链接将会带你进入这本图书的编辑表单。你可以从图4-3中看到结果。</p>
<div class="figure">
<img src="./static/images/Figure4-3.jpg" alt="图4-3" />
<p>图4-3 Burt's Books:带有编辑链接的推荐列表</p>
</div>
<h2>4.4 MongoDB:下一步<a class="headerlink" id="ch4-4" href="#ch4-4">¶</a></h2>
<p>我们在这里只覆盖了MongoDB的一些基础知识--仅仅够实现本章中的示例Web应用。如果你对于学习更多更用的PyMongo和MongoDB知识感兴趣的话,PyMongo教程(<a href="http://api.mongodb.org/python/2.0.1/tutorial.html">http://api.mongodb.org/python/2.0.1/tutorial.html</a>)和MongoDB教程(<a href="http://www.mongodb.org/display/DOCS/Tutorial">http://www.mongodb.org/display/DOCS/Tutorial</a>)是不错的起点。</p>
<p>如果你对使用Tornado创建在扩展性方面表现更好的MongoDB应用感兴趣的话,你可以自学asyncmongo(<a href="https://github.com/bitly/asyncmongo">https://github.com/bitly/asyncmongo</a>),这是一种异步执行MongoDB请求的类似PyMongo的库。我们将在<a href="./ch5.html">第5章</a>中讨论什么是异步请求,以及为什么它在Web应用中扩展性更好。</p>
</div>
<div class="footer">
<small>© 本文由<a href="http://www.pythoner.com">你像从前一样</a>翻译,转载请注明出处。本书版权由原作者拥有。</small>
</div>
</div>
</body>
</html>