SQLAlchemy中的继承

SQLAlchemy中的继承

SQLAlchemy是Python下常用的ORM之一,虽然相对于Django ORM,Peewee等其他ORM而言,上手难度相对较高,但是它提供了相当丰富且灵活的高级特性,这里介绍一下模型继承方面的内容

本文代码以Flask-SQLAlchemy为例进行说明,原生SQLAlchemy差异不大,主要有:

  • db.Model约等于sqlalchemy.ext.declarative.declarative_base
  • Model.query约等于db.session.query(Model)
    {: .note}

单表继承

最简单的联表形式就是单表继承, 在这种继承关系下,无论继承多少子类,数据库层面始终只有一张表,使用一个polymorphic_identity字段来进行区分。 显而易见,这是查询效率最高的一种继承方式。但我们无法灵活地为每个子类添加自己专有的字段。适用于各子类数据结构相同,但业务逻辑不同的场合。

比如我们有一张用户表,普通用户需要进行权限检查,但管理员用户具有一切权限, 我们当然可以用if进行条件判断,但在条件复杂的时候这会严重降低代码的可读性和可维护性,我们可以用更面向对象的方式来解决这个问题,比如:

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_type = db.Column(db.String(10))

    __mapper_args__ = {
        'polymorphic_identity': 'normal',
        'polymorphic_on': user_type
    }

    def has_permission(self, permission):
        return some_permission_check_logic()


class AdminUser(User):
    __mapper_args__ = {
        'polymorphic_identity': 'admin',
    }

    def has_permission(self, permission):
        return True

这样当我们进行数据库查询的时候,ORM会自动根据polymorphic_identity使用不同的类映射对象,比如:

>>> User.query.all()
[<User: xxx@xxx.com>, <User: xxx2@xxx.com>, <AdminUser: xxx3@xxx.com>]

这样还带来一个额外的好处,当我们直接创建子类对象的时候,相应的polymorphic_identity会自动帮我们填好:

>>> admin_user = AdminUser()
>>> print(admin_user.user_type)
admin

自动初始化子类对象

那既然创建子类会自动填入polymorphic_identity,那如果我对父类传入相应的值,是否能自动创建子类对象呢?

很遗憾,答案是否定的:

>>> user = User(user_type='admin', email='xxx@xxx.com') 
>>> print(user)
<User: xxx@xxx.com>
>>> # 但是当我们重新从数据库取出对象的时候,可以正确的使用子类
>>> db.session.add(user)
>>> db.session.commit()
>>> user_id = user.id
>>> del user # 这里需要删除已经生成的对象, 否则sqlalchemy会自动复用已经生成的对象
>>> user = User.query.get(user_id)
>>> print(user)
<AdminUser: xxx@xxx.com>

如果要实现这个效果,我们可以使用元类(见参考#2),但在这里个人不建议使用这种黑魔法,因为一是修改SQLAlchemy中已经被深度定制化的元类可能带来一些神奇的问题,二是你初始化一个类,结果初始化出来另一个类的实例,这本身也是有点奇葩的,因此个人还是使用更直接一点的classmethod的方式实现:

class User(db.Model):
    # ...

    @classmethod
    def init_for(cls, user_type, **kwargs):
        """根据user_type初始化不同子类的实例"""
        children_classes = {
            x.polymorphic_identity: x.class_
            for x in cls.__mapper__.self_and_descendants
            }
        return children_classes[user_type](**kwargs)

注意: 单表继承不应为自类定义 __tablename__

联表继承

很多时候我们需要处理更灵活一点的情况, 子类从父类继承一些属性, 但也需要定义部分自己的属性, 联表继承可以帮我们处理这种状况, 但相应的会带来一些性能开销

顾名思义, 联表继承在hit数据库时会进行一个JOIN查询, 数据结构上的定义实际上是子类的主键作为外键, 一对一关联到父类表的主键, 子类独有的数据单独存放在一张表上

看代码:

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    user_type = db.Column(db.String(10))

    __mapper_args__ = {
        'polymorphic_identity': 'normal',
        'polymorphic_on': user_type
    }

class WechatUser(User):
    __tablename__ = 'user_wechat'

    # 定义一个到父类的外键
    id = db.Column(db.Integer, db.ForeignKey('user.id', primary_key=True))
    open_id = db.Column(db.String(40))
    __mapper_args__ = {
        'polymorphic_identity': 'wechat',
    }

查询行为

我们来看下查询SQL

>>> print(User.query)
SELECT "user".id AS user_id, "user".user_type AS user_user_type
FROM "user"
>>> print(WechatUser.query)
SELECT user_wechat.id AS user_wechat_id, "user".id AS user_id, "user".user_type AS user_user_type, user_wechat.open_id AS user_wechat_open_id
FROM "user" JOIN user_wechat ON "user".id = user_wechat.id

可以看到,只查询父类的时候,不会查询子类的表,反之则会拼出一个JOIN查询, 且默认为 INNER JOIN, 亦即只查询子类的时候, 是不会列出父类的相关数据的。

>>> db.session.add(WechatUser())
>>> db.session.add(User())
>>> db.session.commit()
>>> 
>>> print(User.query.all())
>>> print(WechatUser.query.all())
[<__main__.WechatUser object at 0x1016a3e10>, <__main__.User object at 0x1016a3ed0>]
[<__main__.WechatUser object at 0x10170b510>]

使用with_polymorphic对查询进行一些控制

在默认情况下,如果是从父类查出一个子类对象,在获取子类属性的时候会额外发起一次查询, 相当于我们正常定义的relationshiplazy=True时的行为, 例如:

代码:(测试数据中User表的第一个对象为WechatUser)

user = User.query.first()
print(user.open_id)

查询:

SELECT user.id AS user_id, user.user_type AS user_user_type
FROM user
 LIMIT 1 OFFSET 0;
SELECT user_wechat.open_id AS user_wechat_open_id;

当我们获取子类属性次数较少的时候,这样的查询是完全可以接受的,但是当我们预计会频繁调用子类属性的时候,这么做会带来大量的查询,这时候将所有表JOIN进来会是更好的选择

幸运的是,SQLAlchemy同样提供了这种支持:

>>> mixed = db.with_polymorphic(User, [User, WechatUser])
>>> # 也可以写作 mixed = db.with_polymorphic(User, '*') join所有的子表
>>> query = db.session.query(mixed)
>>> print(query)
SELECT "user".id AS user_id, "user".user_type AS user_user_type, user_wechat.id AS user_wechat_id, user_wechat.open_id AS user_wechat_open_id
FROM "user" LEFT OUTER JOIN user_wechat ON "user".id = user_wechat.id
>>> query.all()
[<__main__.WechatUser object at 0x1016a3e10>, <__main__.User object at 0x1016a3ed0>]

with_polymorphic 同样提供了对过滤的支持,

>>> print(db.session.query(mixed).filter(mixed.WechatUser.open_id.is_(None)))
SELECT "user".id AS user_id, "user".user_type AS user_user_type, user_wechat.id AS user_wechat_id, user_wechat.open_id AS user_wechat_open_id
FROM "user" LEFT OUTER JOIN user_wechat ON "user".id = user_wechat.id
WHERE user_wechat.open_id IS NULL

文档中还提供了一些更精细化的控制查询的手段,比如 of_type, 自定义的 Join, 还有 .options()joinedload 策略, 本文在此暂不展开, 有需要请参考文档

实体继承

SQLAlchemy还提供了一种继承方法,称为concrete 继承, 姑且翻译为实体继承

在这种继承关系中,父类及其每个子类都具有一张完整的表, 表与表之间在数据库层面可以没有任何关系, 甚至我们在python中每个子类都必须完整地定义每个字段,这些字段并不会从父类继承

因为每个子类都有一张完全独立的表, 因此并不像前两种继承关系一样,需要一个polymorphic_identity来区分每个子类, 也避免了JOIN和遇到查询字段时需要额外产生一个请求的问题

但是代价就是,因为每个类都是一张独立的表,因此当我们想要从父类查询子类的时候,就必须UNION所有的表,所以子类不宜过多,否则性能开销是很恐怖的

(所以说实话,个人并不是非常理解这种继承关系存在的意义。)

类定义如下:

from sqlalchemy.ext.declarative import ConcreteBase

class User(ConcreteBase, db.Model):  # 注意: 此处应继承ConcreteBase
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))

class WechatUser(User):
    __tablename__ = 'user_wechat'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))  # 注意: 子类必须重新定义字段
    open_id = db.Column(db.String(40))

    __mapper_args__ = {
        'concrete': True
    }

查询:

print(User.query)
SELECT pjoin.id AS pjoin_id, pjoin.name AS pjoin_name, pjoin.type AS pjoin_type, pjoin.open_id AS pjoin_open_id
FROM (SELECT "user".id AS id, "user".name AS name, CAST(NULL AS VARCHAR(40)) AS open_id, 'normal' AS type
FROM "user" UNION ALL SELECT user_wechat.id AS id, user_wechat.name AS name, user_wechat.open_id AS open_id, 'wechat' AS type
FROM user_wechat) AS pjoin
print(WechatUser.query)
SELECT pjoin.id AS pjoin_id, pjoin.name AS pjoin_name, pjoin.open_id AS pjoin_open_id, pjoin.type AS pjoin_type
FROM (SELECT user_wechat.id AS id, user_wechat.name AS name, user_wechat.open_id AS open_id, 'wechat' AS type
FROM user_wechat) AS pjoin

可以看到,这里的查询动用了 UNION CAST, 形成了比较复杂的查询, 而且随着子类的增多,查询上的复杂性会快速增加,个人没有在生产环境中用过这种继承类型(不敢, 而且感觉没有必要)

如果谁有这种继承关系适用的场合希望能够指出,一起学习。

代码

本文相关代码可参考 gist

参考

  1. Mapping Class Inheritance Hierarchies
  2. Add child classes in SQLAlchemy session using parent constructor
comments powered by Disqus