返回介绍

15.2 简化模型的映射

发布于 2024-01-21 17:11:03 字数 10879 浏览 0 评论 0 收藏 0

近年来,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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文