Python第三方库开发应用实战
上QQ阅读APP看书,第一时间看更新

1.4 数据库操作

在下面的内容中,假设已经在计算机上安装了MongoDB,并通过PyMongo作为驱动来连接MongoDB。本节将介绍在Tornado框架中实现数据库操作的知识。

1.4.1 实现持久化Web服务

假设我们需要编写一个只从MongoDB读取数据的Web服务,然后编写一个可以读写数据的服务。例如,将要创建的应用是一个基于Web的简单字典,在发送一个指定单词的请求后返回这个单词的定义。一个典型的交互如下。

$ curl http://localhost:8000/oarlock
{definition: "连接到一个设备",
"word": "oarlock"}

这个Web服务将从MongoDB数据库中取得数据。具体来说,将根据word属性查询文档。在查看Web应用本身的源码之前,先从Python解释器向数据库中添加一些单词。例如,通过如下文件001.py,可以向MongoDB数据库中添加指定的单词。

源码路径:daima\1\1-4\001.py

import pymongo
conn = pymongo.MongoClient("localhost", 27017)
db = conn.example
db.words.insert({"word": "oarlock", "definition": "A device attached to a rowboat to hold the oars in
    place"})
db.words.insert({"word": "seminomadic", "definition": "Only partially nomadic"})
db.words.insert({"word": "perturb", "definition": "Bother, unsettle, modify"})

通过如下命令开启MongoDB服务:

mongod --dbpath "h:\data"

在上述命令中,“h:\data”是一个保存MongoDB数据库数据的目录,读者可以随意在本地计算机硬盘中创建,并且还可以自定义目录名字。然后运行文件001.py,执行后会向MongoDB数据库中添加指定的单词。

为了验证上述添加的单词,我们编写如下所示的文件definitions_readonly.py,在Tornado框架中实现对MongoDB数据库的访问。

源码路径:daima\1\1-4\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.MongoClient("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()

运行上述实例文件definitions_readonly.py,然后在浏览器中输入“http://localhost:8000/perturb”后会显示:

{"word": "perturb", "definition": "Bother, unsettle, modify"}

这说明在Tornado框架中实现了对MongoDB数据库数据的访问功能。如果在浏览器中请求一个数据库中没有添加的单词,会得到一个404错误以及一条错误消息。

{"error": "word not found"}

那么,这个程序是如何工作的呢?让我们看看这个程序的主线。开始,我们在程序的最上面导入了pymongo库。然后在TornadoApplication对象的init方法中实例化了一个pymongo连接对象。我们在Application对象中创建了一个db属性,指向MongoDB的example数据库。下面是相关的代码。

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

一旦我们在Application对象中添加了db属性,就可以在任何RequestHandler对象中使用self.application.db访问它。其实这正是我们为了取出pymongo的words集合对象而在WordHandler的get方法中所做的事情。

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"})

在我们将集合对象指定给变量coll后,我们使用用户在HTTP路径中请求的单词调用find_one方法。如果我们发现这个单词,则从字典中删除_id键(以便Python的JSON库可以将其序列化),然后将其传递给RequestHandler的write方法。write方法将会自动序列化字典为JSON格式。

如果find_one方法没有匹配任何对象,则返回None。这时将响应状态设置为404,并且写一个简短的JSON来提示用户这个单词在数据库中没有找到。

在上述实例中,虽然可以很简单地在字典中查询单词,但是在交互解释器中添加单词的过程会非常麻烦。其实我们完全可以使HTTP在请求网站服务时创建和修改单词:首先发出一个特定单词的POST请求,然后根据请求中给出的定义修改已经存在的定义。如果这个单词并不存在,则创建它。例如,通过如下过程创建一个新的单词“pants”。

http://localhost:8000/pants
{"definition": "a leg shirt", "word": "pants"}

下面的实例文件definitions_readwrite.py演示了实现一个可读写Web服务的过程。

源码路径:daima\1\1-4\definitions_readwrite.py

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.MongoClient("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()

在上述代码中,使用get_argument()函数获取了POST请求中传递的definition参数,然后使用find_one()函数从数据库中加载给定单词的文档。如果发现这个单词的文档,将definition条目的值设置为从POST参数中取得的值,然后调用集合对象的save()函数将改变写到数据库中。如果没有发现文档则创建一个新的,并使用insert()函数将文档保存到数据库中。无论上述哪种情况,都要在数据库操作执行之后在响应中写文档(注意,首先要删掉_id属性)。

1.4.2 图书管理系统

接下来,通过一个图书管理系统的实现过程,介绍在Tornado框架中使用MongoDB数据库实现动态Web的过程。

源码路径:daima\1\1-4\BookManger

(1)在MongoDB服务器中创建一个数据库和集合,并用图书内容进行填充。例如,下面的演示过程。

>>> import pymongo
>>> conn = pymongo.MongoClient ()
>>> db = conn["bookstore"]
>>> db.books.insert({
...    "title":"Python开发从入门到精通",
...    "subtitle": "Python",
...    "image":"123.gif",
...    "author": "浪潮",
...    "date_added":20171231,
...    "date_released": "August 2007",
...    "isbn":"978-7-596-52932-1",
...    "description":"<p>[...]</p>"
... })
ObjectId('4eb6f1a6136fc42171000000')
>>> db.books.insert({
...    "title":"PHP从入门到精通",
...    "subtitle": "Web服务",
...    "image":"345.gif",
...    "author": "学习PHP",
...    "date_added":20171231,
...    "date_released": "May 2007",
...    "isbn":"978-7-534-52926-0",
...    "description":"<p>[...]>/p>"
... })
ObjectId('4eb6f1cb136fc42171000001')

(2)编写Python程序文件burts_books_db.py。首先在程序中添加一个db属性来连接MongoDB服务器,然后使用连接的find()函数从数据库中取得图书文档的列表,并在渲染recommended.html时将这个列表传递给RecommendedHandler的get方法。文件burts_books_db.py的具体实现代码如下。

#!/usr/bin/env python
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=8001, help="请运行在给定的端口", 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.MongoClient("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 = "图书管理| 主页",
                   header_text = "欢迎使用图书管理系统!",
            )
class RecommendedHandler(tornado.web.RequestHandler):
      def get(self):
            coll = self.application.db.books
            books = coll.find()
            self.render(
                   "recommended.html",
                   page_title = "图书系统 | 图书信息",
                   header_text = "图书信息",
                   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 "css/recommended.css"
      def javascript_files(self):
            return "js/recommended.js"
def main():
     tornado.options.parse_command_line()
     http_server = tornado.httpserver.HTTPServer(Application())
     http_server.listen(options.port)
     tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
      main()

如果此时在浏览器中输入http://localhost:8001/recommended/,会读取并显示数据库中的图书信息。执行效果如图1-14所示。

图1-14 执行效果

(3)编写Python文件burts_books_rwdb.py实现图书添加和修改两个功能。具体实现代码如下。

define("port", default=8001, help="请运行在给定的端口", type=int)
class Application(tornado.web.Application):
      def __init__(self):
            handlers = [
                 (r"/", MainHandler),
                 (r"/recommended/", RecommendedHandler),
                 (r"/edit/([0-9Xx\-]+)", BookEditHandler),
                 (r"/add", BookEditHandler)
            ]
            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.MongoClient("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 = "图书管理 | 主页",
                   header_text = "欢迎使用图书管理系统!",
          )
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/")
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 "css/recommended.css"
      def javascript_files(self):
            return "js/recommended.js"
def main():
      tornado.options.parse_command line()
      http server = tornado.httpserver.HTTPServer(Application())
      http server.listen(options.port)
      tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
      main()

在上述代码中,BookEditHandler主要完成如下两个功能。

•GET请求渲染显示一个已存在图书数据的HTML表单(在模板book_edit.html中)。

•POST请求从表单中取得数据,更新数据库中已存在的图书记录或根据提供的数据添加一本新的图书。

BookEditHandler实现了两个不同路径模式的请求:其中一个是实现图书添加功能的/add,用于提供不存在信息的编辑表单,因此你可以向数据库中添加一本新的图书;另一个是实现图书修改功能的/edit/([0-9Xx-]+),用于根据图书的ISBN参数修改已存在图书的信息。

函数get()的功能是从数据库中取出图书信息,如果该函数作为/add请求的结果被调用,Tornado将调用一个没有第二个参数的get方法(因为路径中没有正则表达式的匹配组)。在这种情况下,默认将一个空的book字典传递给book_edit.html模板。如果该方法作为/edit/0-123-456请求的结果被调用,那么isbn参数被设置为0-123-456。在这种情况下,我们从程序实例中取得books集合,并用它查询isbn匹配的图书,然后传递book字典给模板。

函数post()的功能是将表单中的数据保存到数据库中,具体来说有两个功能:处理修改已存在图书信息的请求以及添加新图书信息的请求。如果有isbn参数(即路径的请求类似于/edit/0-123-4567),则编辑给定isbn。如果这个参数没有提供,则添加新图书信息。先设置一个空的字典变量book,如果正在编辑已存在的图书信息,则使用book集合的find_one()函数从数据库中加载和传入与isbn值对应的文档。无论是哪一种情况,book_fields列表都指定哪些域应该出现在图书文档中,再迭代这个列表,使用RequestHandler对象的get_argument方法从POST请求中抓取对应的值。

图书修改功能的模板文件是book_edit.html。具体实现代码如下所示。

{% extends "main.html" %}
{% autoescape None %}
{% block body %}
<form method="POST">
     ISBN <input type="text" name="isbn"
          value="{{ book.get('isbn', '') }}"><br>
     书名  <input type="text" name="title"
          value="{{ book.get('title', '') }}"><br>
     标题  <input type="text" name="subtitle"
          value="{{ book.get('subtitle', '') }}"><br>
     图片  <input type="text" name="image"
          value="{{ book.get('image', '') }}"><br>
     作者  <input type="text" name="author"
          value="{{ book.get('author', '') }}"><br>
     出版时间 <input type="text" name="date_released"
          value="{{ book.get('date_released', '') }}"><br>
     内容简介<br>
     <textarea name="description" rows="5"
           cols="40">{% raw book.get('description', '')%}</textarea><br>
     <input type="submit" value="Save">
</form>
{% end %}

上述代码实现了一个基本的HTML表单,如果请求处理函数传进来了book字典,那么将用它预填充带有已存在图书数据的表单中。如果键不在字典中,则使用Python字典对象的get方法为其提供默认值,标签input中的name属性被设置为book字典的对应键。因为form标签没有action属性,所以表单中的POST将会定向到当前URL。如果页面以/edit/0-123-4567进行加载,POST请求将转向/edit/0-123-4567;如果页面以/add进行加载,则POST将转向/add。

添加新图书的界面如图1-15所示。

图1-15 添加新图书的界面

单击图1-14中的“编辑”链接后会弹出图书修改界面,在此界面中显示修改此图书信息的界面。执行效果如图1-16所示。

图1-16 图书修改界面