β

记录一些坑

无知的 TonySeek 167 阅读

用动态语言编写一些简单的应用时,好的 ORM 往往能带来开发效率的提升 —— 尽管 ORM 为不少大型项目所不齿。Python 社区的 ORM 框架最著名的莫属 SQLAlchemy,它的特点是利用 Python 的元编程支持构造了一套既能照顾到面向对象开发习惯,又能向下支持复杂数据库查询操作的 DSL。这类框架在分类上,应该属于抽象层。

抽象层的一个问题,就是在应用本身的业务复杂度之外又增加了额外的“抽象复杂度”,加上去的复杂度是要花费开发者额外精力去理解的。我在使用 SQLAlchemy 的过程中,也因为对这层附加上去的复杂度理解不深,掉了许多次坑。掉坑的次数多了,我慢慢有点感觉有些“危险的路径”坑是比较多的,有些安全的路径是比较太平的。这种感觉还不成体系,暂时先写这么一篇博客记录下来,希望日后的开发学习中再求理解。

SQLAlchemy 的使用方式

SQLAlchemy 曾经的使用方式比较繁琐,需要自己定义 Engine (数据访问层)、 Metadata (数据库 Schema 定义)、 Table (表结构定义)、 Mapper (映射规则),然后用 MapperMetadata 映射到目标模型类中。需要使用模型类的时候,建立 Session (数据库访问会话)并用业务方法修改模型数据,然后提交会话,SQLAlchemy 的控制流一层层往回走,数据的前后变动被 Session 内部的[工作单元][0]析出,经过 Metadata 整理成查询 DSL,再由 Engine 转换成本地数据库驱动支持的 SQL 方言。

后来大概是被反复吐槽这样太麻烦了吧,SQLAlchemy 开发组就在新版本中支持了一种稍微简洁一些的用法,即用定义类的方式定义表的结构。通过描述符、元类等元编程手段,SQLAlchemy 自动生成上述构建。有了这一机制,SQLAlchemy 用起来就和 Django Model 很像了,如下。

from szulabs.extensions import db, bcrypt

class UserAccount(db.Model):
    user_id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, index=True)
    nickname = db.Column(db.Unicode(20), unique=True)
    hashed_password = db.Column(db.String(60))

    def change_password(self, new_password):
        self.hashed_password = bcrypt.generate_password_hash(new_password, 10)

    def check_password(self, input_value):
        return bcrypt.check_password_hash(self.hashed_password, input_value)

因为 Python 中元类有“在类的定义完成后执行某个操作”的功效,所以继承 db.ModelUserAccount 会在类的定义完成后生成 user_account 这个 Table 和对应的 Mapper

于是,这就是悲剧的开始。

Python 元类的蛋疼点

Python 中元类(Meta Class)是“类的类”,它定义了一个类的创建过程。可以把类看成实例,那么此时的这个实例的类就是元类。当然这个定义不会无线循环下去,因为元类也是类,所以元类的类还是元类。

利用元类可以实现一些非常强大的特性,比如在一个类被创建之后,立即将它注册到一个注册表中去,用元类的实现如下。

classes_register = {}

class SpamType(type):
    def __new__(meta, name, bases, members):
        cls = type.__new__(meta, name, bases, members)
        classes_register[name] = cls  # here
        return cls

class Spam(object):
    __metaclass__ = SpamType

assert "Spam" in classes_register
assert classes_register["Spam"] is Spam

SQLAlchemy 的新式用法就是利用元类的这一特性来工作的。甚至在定义 Column 的时候都不用为其传入字段名,预设的元类会将这个 Column 实例在类中的属性名(通过访问元类的 members 参数取得)作为其默认名字。这样许多“约定大于配置”的预设,让开发者确实方便了很多。

但元类有一个很大的蛋疼点,就是它对类的扩充是基于**继承**而非**组合**来实现的。

Python 的对象模型不支持 Ruby 风格的 mixin,但支持 Ruby 不支持的多重继承。于是多重继承常常被作为 Python 中实现 mixin 的一种手段。这种 mixin 实现在语义上体现为继承,逻辑上的含义却是组合。因为 mixin 的模块多数是职责独立的 partial class,类与类之间互不获知互不重叠,因此多重继承设计中陷阱重重的 MRO 查找顺序问题、钻石继承问题,在这种设计中都不会出现。但若想结合元类来使用基于多重继承的 mixin,那简直就是做梦。设想上述的 UserAccount 模型类,如果需要继承两三个 mixin 类来实现扩展功能,而这两三个 mixin 类都有各自的元类,那么最终 UserAccount 如何保证这两三个不同的元类都能正常工作呢?实际情况是,Python 会直接将这种继承视为错误。

>>> class Meta1(type): pass
>>> class Meta2(type): pass
>>> class PartialA(object):
...     __metaclass__ = Meta1
...
>>> class PartialB(object):
...     __metaclass__ = Meta2
...
>>> class Base(object): pass
>>> class UserAccount(Base, PartialA, PartialB): pass
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: Error when calling the metaclass bases
    metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

事实上 Python 除了告诉开发者 “metaclass conflict”,别的根本无能为力。当遇到多个基类携带了多个不同的元类时,Python 最多只能让其中一个可以工作,因为元类是基于继承而非基于组合的。虽然逻辑上可以让多个 mixin 模块使用同一个元类,然后让这个元类用 hook 的方式分调 mixin 各自的代码,但实际开发中多个 mixin 可能来自多个不同的模块,对元类的修改牵一发而动全身。

(未完)

[0]Unit of Work
作者:无知的 TonySeek
原文地址:记录一些坑, 感谢原作者分享。

发表评论