用动态语言编写一些简单的应用时,好的 ORM 往往能带来开发效率的提升 —— 尽管 ORM 为不少大型项目所不齿。Python 社区的 ORM 框架最著名的莫属 SQLAlchemy,它的特点是利用 Python 的元编程支持构造了一套既能照顾到面向对象开发习惯,又能向下支持复杂数据库查询操作的 DSL。这类框架在分类上,应该属于抽象层。
抽象层的一个问题,就是在应用本身的业务复杂度之外又增加了额外的“抽象复杂度”,加上去的复杂度是要花费开发者额外精力去理解的。我在使用 SQLAlchemy 的过程中,也因为对这层附加上去的复杂度理解不深,掉了许多次坑。掉坑的次数多了,我慢慢有点感觉有些“危险的路径”坑是比较多的,有些安全的路径是比较太平的。这种感觉还不成体系,暂时先写这么一篇博客记录下来,希望日后的开发学习中再求理解。
SQLAlchemy 的使用方式
SQLAlchemy 曾经的使用方式比较繁琐,需要自己定义 Engine (数据访问层)、 Metadata (数据库 Schema 定义)、 Table (表结构定义)、 Mapper (映射规则),然后用 Mapper 将 Metadata 映射到目标模型类中。需要使用模型类的时候,建立 Session (数据库访问会话)并用业务方法修改模型数据,然后提交会话,SQLAlchemy 的控制流一层层往回走,数据的前后变动被 Session 内部的[工作单元][0]析出,经过 Metadata 整理成查询 DSL,再由 Engine 转换成本地数据库驱动支持的 SQL 方言。
后来大概是被反复吐槽这样太麻烦了吧,SQLAlchemy 开发组就在新版本中支持了一种稍微简洁一些的用法,即用定义类的方式定义表的结构。通过描述符、元类等元编程手段,SQLAlchemy 自动生成上述构建。有了这一机制,SQLAlchemy 用起来就和 Django Model 很像了,如下。
因为 Python 中元类有“在类的定义完成后执行某个操作”的功效,所以继承 db.Model 的 UserAccount 会在类的定义完成后生成 user_account 这个 Table 和对应的 Mapper 。
于是,这就是悲剧的开始。
Python 元类的蛋疼点
Python 中元类(Meta Class)是“类的类”,它定义了一个类的创建过程。可以把类看成实例,那么此时的这个实例的类就是元类。当然这个定义不会无线循环下去,因为元类也是类,所以元类的类还是元类。
利用元类可以实现一些非常强大的特性,比如在一个类被创建之后,立即将它注册到一个注册表中去,用元类的实现如下。
SQLAlchemy 的新式用法就是利用元类的这一特性来工作的。甚至在定义 Column 的时候都不用为其传入字段名,预设的元类会将这个 Column 实例在类中的属性名(通过访问元类的 members 参数取得)作为其默认名字。这样许多“约定大于配置”的预设,让开发者确实方便了很多。
但元类有一个很大的蛋疼点,就是它对类的扩充是基于**继承**而非**组合**来实现的。
Python 的对象模型不支持 Ruby 风格的 mixin,但支持 Ruby 不支持的多重继承。于是多重继承常常被作为 Python 中实现 mixin 的一种手段。这种 mixin 实现在语义上体现为继承,逻辑上的含义却是组合。因为 mixin 的模块多数是职责独立的 partial class,类与类之间互不获知互不重叠,因此多重继承设计中陷阱重重的 MRO 查找顺序问题、钻石继承问题,在这种设计中都不会出现。但若想结合元类来使用基于多重继承的 mixin,那简直就是做梦。设想上述的 UserAccount 模型类,如果需要继承两三个 mixin 类来实现扩展功能,而这两三个 mixin 类都有各自的元类,那么最终 UserAccount 如何保证这两三个不同的元类都能正常工作呢?实际情况是,Python 会直接将这种继承视为错误。
事实上 Python 除了告诉开发者 “metaclass conflict”,别的根本无能为力。当遇到多个基类携带了多个不同的元类时,Python 最多只能让其中一个可以工作,因为元类是基于继承而非基于组合的。虽然逻辑上可以让多个 mixin 模块使用同一个元类,然后让这个元类用 hook 的方式分调 mixin 各自的代码,但实际开发中多个 mixin 可能来自多个不同的模块,对元类的修改牵一发而动全身。
(未完)
[0] | Unit of Work |