SQLAlchemy Model 实例数据转 Dict 数据结构

在最近的移动端后台项目里, 基于 SQLAlchemy 这个组件封装了自己的业务模块,
也是第一次对 Python Web 开发 Model 有一个初步的印象和理解. 基于它做
业务开发, 使用中需要有将实例数据转换为 Python Dict 的操作(或者是序列化为
Json 数据, 返回给客户端接口). 简单的做法是, 对具体的每个业务 Model 定义了
Column 之后, 定义成员函数 def to_dict(self), 根据具体 Model 的数据属性
来写对应的转换方式, 工作量相对繁琐, 是否有更合适的方式实现, 且少写代码, 也就
开始实践和寻找更好的实现的方式.

本文提供四种实现方式, 方案1, 2 为 stackoverflow 提供的解答,
方案3,4 是我在项目中先后使用的方案(代码基于 Python3实现).
方案4为推荐实现, 并且分别提供原生的SQLAlchemy 和 Flask-SQLAlchemy BaseModel 实现例子:)

方案1:

Convert sqlalchemy row object to python dict
代码片段:

return {c.name: getattr(self, c.name) for c in self.__table__.columns}
其思路是 Model 成员函数 to_dict() 中,通过 getattr 获取 column 的字段名.
完整实现样例:

缺陷:
如果 column 中定义 :

user_email = Column(’email’, VARCHAR(128), unique=True)
那么 c.name 获取不到 user_email, 调试时会发现,
其实对应的是字段名 ’email’, 即代码中定义的 column 名称不一定与
数据库字段名一样.

方案2:

其实原理和方案1一样

方案3:

实际项目开发的过程中, 经过调试后, 发现实例中 _sa_instance_state 的属性,
可以利用(这里必须给 Pycharm 点赞). 即 _sa_instance_state.attrs.items()
的迭代器来遍历属性.

实现上参考了 sqlalchemy 的 __init__.py __go 函数 httpagentparser 对 __mro__ 使用的写法.

每次通过迭代器生成临时的 list 并不划算. 我们可以对其做优化, 在 Model 定义完成后,
通过 import 模块时, 在运行时修改 to_dict 的行为(即重新生成 to_dict 函数), 动态地
赋值的方式来减少这个不必要的计算:

当我们在业务代码里 from models.user import UserModel 时, 其实也默认提前将
models/__init__.py 引入, 因此在正式使用 UserModel 处理数据时, 已经重新定义了
to_dict 成员函数的行为.

方案4:

方案3在项目中已经使用了近3个月, 当时基于 tornado + SQLAlchemy, 现在
新项目在使用 Flask 框架和其插件(当然包括了 flask-sqlalchemy), 我们每增加
一个新的Model, 需要在 models/__init__.py import 新的 Model, 相当于
显示地调用 _register_func,为新的 Model 在运行时重新定义 to_dict 成员函数,
这样的缺陷是在团队合作时,队友新添加一个 Model 后却忘记了在 __init__.py 里引入,
导致在开发调试运行时才通过异常发现问题. 这需要新的改进方式, 在 Python Class 定义时就自动地
绑定 to_dict 行为. 因此我们需要了解 Python metaclass 的概念.

通常情况下, 如果我们没有指定元类, 那么 Python Class 的默认元类是 type.
即当我们创建一个 Class 时(注意, 不是Class 实例), 相当于:

“”” 创建类A “””

A = type(‘A’, bases=(), dict={})
等价于:
class A(object): pass

我们通常会通过 __new__ 来影响类实例的生成过程, 那么我们定义了元类 metaclass,
来影响 class 本身生成的过程.

我们以 sqlalchemy 组件(非 flask-sqlalchemy)为例子,
sqlalchemy 提供生成 BaseModel 的例子里使用 declarative_base 函数,
函数的 metaclass 参数默认是组件提供的 DeclarativeMeta 类.
那么我们在调用 declarative_base 时传入自定义的元类(继承自 DeclarativeMeta),
由于业务Model 皆继承于 BaseModel, 它们在定义的执行过程之后, 会自动地在元类的
__new__ 方法里被施加影响, to_dict 成员函数也不必被重新定义, 因为
_column_name_sets 在 class 被创造出来时就已经有了实际的内容.

代码例子:

基于Flask-SQLAlchemy 定义的业务Model 可以继承基类 BaseModel, 参见例子:

总结:
* 方案4出现距离方案3有3个月的时间了, 这三个月里阅读
了几本关于编程思考的书籍, 对方案4的思路有直接的影响,
在此特别推荐书籍:
《代码之髓》科普编程语言的关键字,语言特性诞生的背后思路
《代码的未来》与 《松本行弘的程序世界》Ruby之父对IT技术的思考,
Ruby这门语言的设计过程和思路(其中提到元编程).
* 对 Python 的使用不应该仅仅停留在接口调用, 这并没有极大地利用
其语言本身的优势, 另外是通过阅读开源的Python库里的代码和官方的
模块文档, 总会发现巧妙的使用技巧, 加上模仿和实践, 对项目本身的
代码维护有更多的益处. less code, more features.

原创文章,转载请注明: 转载自kaka_ace's blog

本文链接地址: SQLAlchemy Model 实例数据转 Dict 数据结构

6 thoughts on “SQLAlchemy Model 实例数据转 Dict 数据结构

  1. 请问我在方案4的BaseModel上建立的模型,在调用其to_dict方法时,为何总是报

    Traceback (most recent call last):
    File “”, line 1, in
    File “base.py”, line 30, in to_dict
    for column_name in self._column_name_sets
    TypeError: ‘NotImplementedType’ object is not iterable

    • kaka_ace says:

      代码样例可以给下吗, 如果比较长, 则在 github 上的gist区贴一段代码, 给一个链接给我.

      • 无言 says:

        您好,您有现成的例子吗?这个我内嵌进我的代码,报TypeError: Error when calling the metaclass bases
        unbound method modelmeta__new__() must be called with ModelMeta instance as first argument (got type instance instead)错误,我刚入手python不到2月所以求教。

        • kaka_ace says:

          不建议用这个了, 这个是去年写的, review了下代码, 可能是我写的有误或者版本不匹配.

          从设计上来说是个失败的案例, 转 dict 不应该用 Python metaclass 的黑魔法.
          显示转 dict 或者把各种 model 组合到一个 DAO 实例中反而更易于维护和使用.

          理想的设计:
          1) 一个 DAO 类里组合多个 model
          2) 该 DAO 是业务自己可控, 读写收拢
          3) 请求返回数据时打包的对象是 DAO, 通过 DAO 转 dict 转 json/其他格式.

发表评论

电子邮件地址不会被公开。 必填项已用*标注