- 引言
- 本书涉及的内容
- 第 1 部分 Python 开发入门
- 第 1 章 Python 入门
- 第 2 章 开发 Web 应用
- 第 3 章 Python 项目的结构与包的创建
- 第 4 章 面向团队开发的工具
- 第 5 章 项目管理与审查
- 第 6 章 用 Mercurial 管理源码
- 第 7 章 完备文档的基础
- 第 8 章 模块分割设计与单元测试
- 第 9 章 Python 封装及其运用
- 第 10 章 用 Jenkins 持续集成
- 第 11 章 环境搭建与部署的自动化
- 第 12 章 应用的性能改善
- 第 13 章 让测试为我们服务
- 第 14 章 轻松使用 Django
- 第 15 章 方便好用的 Python 模块
- 附录 A VirtualBox 的设置
- 附录 B OS(Ubuntu)的设置
15.2 简化模型的映射
近年来,Web 系统为保证服务器与客户端、服务器与服务器之间的协作,越来越多地开始提供 JSON、XML 等格式的 API。在这类 API 的内部处理中,O/R 映射工具生成的对象要序列化成 JSON 或 XML 格式。
开发 API 时,API 提供的 JSON 数据的结构必须与 O/R 映射工具生成的模型对象的结构一致,否则就会出现问题。这种问题称为阻抗失配(Impedance Missmatch)。这种时候,如果模型层级结构比较复杂,那么模型的重复利用、代码的可读性、维护成本等方面都会遇到困难。
这里我们学习一个能有效解决阻抗失配的模块——bpmappers。
bpmappers
http://bpmappers.readthedocs.io/en/latest/ (日文)
https://pypi.python.org/pypi/bpmappers
15.2.1 模型映射的必要性
在实际开发系统的过程中,API 规定的键名与值的对应关系很少能与数据模型的结构一致。接下来,我们以使用 JSON 格式返回响应的 API 为例进行学习。现在假设系统中使用了如 LIST 15.8 所示的 User 类的数据模型。
LIST 15.8 User 类
class User(object): def __init__(self, id, password, nickname, age): self.id = id # 用户ID self.password = password # 密码 self.nickname = nickname # 昵称 self.age = age # 年龄
这个数据模型拥有“用户 ID”“密码”“昵称”“年龄”这 4 个值。而在我们生成的 API 中,只将“用户 ID”和“昵称”两个值包含到响应之中。该 API 通过如下 JSON 格式的响应公开了 User 类的数据。
{"user_id": " 用户ID", "user_nickname": " 昵称"}
接下来写一个函数,使用该函数可以获取一个 User 类的对象,并将其转换为 JSON 格式(LIST 15.9)。
LIST 15.9 将 User 类对象转换为 JSON 格式的函数
import json def convert_user_to_json(user): """ 获取一个User 对象并返回JSON """ # 生成用于转换格式的字典对象 user_dict = { 'user_id': user.id, # 使用名为user_id 的键 'user_nickname': user.nickname, # 使用名为user_nickname 的键 } return json.dumps(user_dict) # 转换为JSON
这个函数通过 user_dict 变量生成字典对象,它实质上是给模型类的值与字典对象做了映射。像上面这样,我们用 API 提供数据模型的值时,必须给键和值做好映射。
数据模型与 API 响应数据的结构一致时,可以通过给数据模型添加元信息的方式简化映射的描述。使用 O/R 映射工具的数据模型大多含有元信息,因此映射更加简单一些。但正如例子所示,我们很少能遇到数据结构一致的模型,所以描述映射操作是必不可少的一步。
15.2.2 映射规则的结构化与重复利用
在需要返回多种响应的 API 时,意义相同部分的映射代码要保持一致,以便重复利用。
LIST 15.10 是一个返回简单的用户数据以及留言数据(包含用户和文本的数据)的 API。为便于理解,这里不采用 Web API 的形式,而是直接在控制台调用并显示结果。另外,本例中没有使用数据库。
LIST 15.10 mapping_model.py
# coding: utf-8 import json class User(object): def __init__(self, id, password, nickname, age): self.id = id # 用户ID self.password = password # 密码 self.nickname = nickname # 昵称 self.age = age # 年龄 class Comment(object): def __init__(self, id, user, text): self.id = id # 留言ID self.user = user # 用户ID self.text = text # 留言内容 def get_user(user_id): """ 返回用户对象的函数 """ # 实际开发时应该访问数据库 user = User(id=user_id, password='hoge', nickname='tokibito', age=26) return user def get_comment(comment_id): """ 返回留言对象的函数 """ # 实际开发时应该访问数据库 comment = Comment(id=comment_id, user=get_user('bp12345'), text=u'Hello, world!') return comment def mapping_user(user): """User 模型与API 的映射 """ return {'user_id': user.id, 'user_nickname': user.nickname} def mapping_user_2(user): """User 模型与API 的映射2 """ return {'user_id': user.id, 'user_nickname': user.nickname, 'user_age': user.age} def mapping_comment(comment): """Comment 模型与API 的映射 """ return {'user': mapping_user(comment.user), 'text': comment.text} def api_user_json(user_id): """ 以JSON 格式返回用户数据的API """ user = get_user(user_id) # 获取User 对象 user_dict = mapping_user(user) # 映射到字典 return json.dumps(user_dict, indent=2) # 以JSON 格式返回 def api_user_detail_json(user_id): """ 以JSON 格式返回用户详细数据的API """ user = get_user(user_id) # 获取User 对象 user_dict = mapping_user_2(user) # 映射到字典 return json.dumps(user_dict, indent=2) # 以JSON 格式返回 def api_comment_json(comment_id): """ 以JSON 格式返回留言数据的API """ comment = get_comment(comment_id) # 获取Comment 对象 comment_dict = mapping_comment(comment) # 映射到字典 return json.dumps(comment_dict, indent=2) # 以JSON 格式返回 def main(): # 获取用户数据的JSON 并显示 print "--- api_user_json ---" print api_user_json('bp12345') # 获取用户数据(详细)的JSON 并显示 print "--- api_user_detail_json ---" print api_user_detail_json('bp12345') # 获取留言数据的JSON 并显示 print "--- api_comment_json ---" print api_comment_json('cm54321') if __name__ == '__main__': main()
在这段代码中,实现 API 功能的函数有 api_user_json、api_user_detail_json、api_comment_json。其执行结果如 LIST 15.11 所示。
LIST 15.11 执行结果
$ python mapping_model.py --- api_user_json --- { "user_id": "bp12345", "user_nickname": "tokibito" } --- api_user_detail_json --- { "user_id": "bp12345", "user_nickname": "tokibito", "user_age": 26 } --- api_comment_json --- { "text": "Hello, world!", "user": { "user_id": "bp12345", "user_nickname": "tokibito" } }
在 api_comment_json 的响应中,user 部分的数据结构要与 api_user_json 保持一致,因此使用了相同的映射函数。相对地,虽然 api_user_detail_json 与 api_user_json 的结构大致相同,但它们具有差异的部分使得它们用了不同的映射函数。
像上面这样,由于每个 API 之间都只存在细微的差异,使得映射函数成了一个俄罗斯套娃般的结构。随着这种函数增多,代码的可读性会越来越差。另外,因 API 的需求变更而导致函数传值参数增加时,需要一次性修正多个地方。
这些问题可以通过导入 bpmappers 来解决。
15.2.3 导入bpmappers
bpmappers 能帮助我们将对象或字典的数据映射到其他字典上。bpmappers 通过 pip 命令进行安装,代码如 LIST 15.12 所示。本书使用的 bpmappers 版本是 0.8。
LIST 15.12 用 pip 命令安装 bpmappers
$ pip install bpmappers
bpmappers 主要由 Mapper 类和 Field 类构成。Mapper 类相当于映射函数,Field 类相当于映射字典的键值对。我们通过 Python shell 执行 bpmappers,做一个简单的映射(LIST 15.13)。
LIST 15.13 用 bpmappers 做映射
>>> from bpmappers import Mapper, RawField >>> class SpamMapper(Mapper): ... spam = RawField('foo') ... egg = RawField('bar') ... >>> >>> SpamMapper(dict(foo=123, bar='abc')).as_dict() {'egg': 'abc', 'spam': 123}
例子中定义了继承 Mapper 类的 SpamMapper 类,其属性包含 spam 和 egg 两个 RawField 对象。生成 SpamMapper 类的实例时,传值参数中指定了用做映射对象的字典。映射后的字典可以通过执行 Mapper 类的 as_dict 方法来获取。SpamMapper 类将 foo 键(或属性)的值映射到了 spam 键,将 bar 键(或属性)的值映射到了 egg 键。
接下来我们对前面那个返回用户数据和留言数据的 API(mapping_model.py)的源码作一下修改,对其导入 bpmappers。类和函数的重复部分在此省略。
LIST 15.14 bpmappers_mapping_model.py
# coding: utf-8 import json from bpmappers import Mapper, RawField, DelegateField class User(object): " 省略" class Comment(object): " 省略" def get_user(user_id): " 省略" def get_comment(comment_id): " 省略" class UserMapper(Mapper): """User 模型与API 的映射 """ user_id = RawField('id') user_nickname = RawField('nickname') class UserMapper2(UserMapper): """User 模型与API 的映射2 """ user_age = RawField('age') class CommentMapper(Mapper): """Comment 模型与API 的映射 """ user = DelegateField(UserMapper) text = RawField() def api_user_json(user_id): """ 以JSON 格式返回用户数据的API """ user = get_user(user_id) # 获取User 对象 user_dict = UserMapper(user).as_dict() # 映射到字典 return json.dumps(user_dict, indent=2) # 以JSON 格式返回 def api_user_detail_json(user_id): """ 以JSON 格式返回用户详细数据的API """ user = get_user(user_id) # 获取User 对象 user_dict = UserMapper2(user).as_dict() # 映射到字典 return json.dumps(user_dict, indent=2) # 以JSON 格式返回 def api_comment_json(comment_id): """ 以JSON 格式返回留言数据的API """ comment = get_comment(comment_id) # 获取Comment 对象 comment_dict = CommentMapper(comment).as_dict() # 映射到字典 return json.dumps(comment_dict, indent=2) # 以JSON 格式返回 def main(): " 省略" if __name__ == '__main__': main()
LIST 15.14 的执行结果没有变化。api_user_json 使用了 UserMapper。api_user_detail_json 使用的是继承 UserMapper 且添加了 age 映射的 UserMapper2 类。可以看到,bpmappers 的 Mapper 类能够利用继承的结构来添加不同的映射。另外,api_comment_json 的 user 部分与 UserMapper 的数据结构相同,所以我们直接通过 DelegateField 指定了 UserMapper。这种俄罗斯套娃式的映射结构同样可以用其他类来实现。
另外,列表内元素的套娃式映射可以用 ListDelegateField 来完成。LIST 15.15 中,我们通过 Python shell 执行了一个用 ListDelegateField 实现的映射。
LIST 15.15 用 ListDelegateField 实现的映射
>>> from bpmappers import Mapper, RawField, ListDelegateField >>> class SpamMapper(Mapper): ... spam = RawField('foo') ... >>> class ListSpamMapper(Mapper): ... spam_list = ListDelegateField(SpamMapper) ... >>> ListSpamMapper({'spam_list': [{'foo': 123}, {'foo': 456}]}).as_dict() {'spam_list': [{'spam': 123}, {'spam': 456}]}
ListDelegateField 中指定了继承 Mapper 类的 SpamMapper 类。ListDelegateField 可以以指定的类映射列表中的各个元素。通过上述例子我们可以看到,用 bpmappers 能够简化映射定义,同时方便映射的重复利用。
15.2.4 与 Django 联动
bpmappers 的一些功能可以为 Django 框架的模型对象映射提供辅助。使用 bpmappers.djangomodel.ModelMapper 可以轻松地根据 Django 的模型类生成用于映射的类。
下面我们用 ModelMapper 类来给简单的 Django 模型类作一个映射。请注意,这里我们不创建 Django 工程,所以需要在源码内初始化 Django(LIST 15.16)。
LIST 15.16 django_and_bpmappers.py
# coding: utf-8 # 初始化Django from django.conf import settings settings.configure() from django.db import models from bpmappers.djangomodel import ModelMapper class Person(models.Model): """ 表示人的数据模型 """ name = models.CharField(u' 名字', max_length=20) age = models.IntegerField(u' 年龄') class Meta: # 指定app_label,防止应用名解析时出错 app_label = '' class PersonMapper(ModelMapper): """ 让Person 模型映射到字典时需要用到的类 """ class Meta: model = Person def main(): # 生成Person 对象 person = Person(id=123, name=u'okano', age=26) # 映射到字典 person_dict = PersonMapper(person).as_dict() # 输出到屏幕上 print person_dict if __name__ == '__main__': main()
为了让 Person 模型映射到字典,我们定义了一个继承 ModelMapper 类的 PersonMapper 类。ModelMapper 类内部定义了内部类 Meta,model 指定了 Person 模型。这样描述之后,ModelMapper 就会自动地根据 Person 模型拥有的字段生成映射。
在安装了 bpmappers 和 Django 的计算机上运行上述代码将得到如 LIST 15.17 所示的结果。
LIST 15.17 执行结果
$ python django_and_bpmappers.py {'id': 123, 'name': u'okano', 'age': 26}
15.2.5 编写JSON API
接下来我们在导入 bpmappers 的前提下实际编写一个返回 JSON 格式响应的 API。首先,我们以第 2 章中开发的留言板应用为例编写代码,实现在用户提交信息时以 JSON 格式返回响应。具体代码如下。
from flask import jsonify from bpmappers import Mapper, RawField, ListDelegateField class GreetingMapper(Mapper): name = RawField() comment = RawField() class GreetingListMapper(Mapper): greeting_list = ListDelegateField(GreetingMapper) @application.route('/api/') def api_index(): """ 留言 """ # 读取提交的数据 greeting_list = load_data() result_dict = GreetingListMapper( {'greeting_list': greeting_list}).as_dict() # 以JSON 格式返回响应 return jsonify(**result_dict)
将这段代码添加到 guestbook.py 的 if __name__ … 之前。JSON 的响应会以 greeting_list 为键,通过数组的形式返回各次提交的姓名以及留言内容。要返回的数据通过已有的 load_data 函数获取,然后以 GreetingListMapper 类进行映射。GreetingListMapper 类使用了 ListDelegateField 类,从而实现以 GreetingMapper 类对列表内的值进行映射。
接下来保存修改,执行源码并启动服务器。在添加几条数据之后访问 http://127.0.0.1:5000/api/ ,我们会得到 JSON 格式的响应。下面是用 urllib 访问时的例子。
$ python -m urllib http://127.0.0.1:5000/api/ { "greeting_list": [ { "comment": "\u65e5\u672c\u8a9e\u306e\u6587\u5b57\u5217", "name": "tokibito" }, { "comment": "Hello, world!", "name": "tokibito" } ] }
导入 bpmappers 能提高映射的重复利用率,还能让我们在需求变更时更灵活地加以应对,因此即便是很简单的 API,也建议用 bpmappers 来实现。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论