欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SQLAlchemy 数据建模过程的改进

程序员文章站 2022-07-15 13:10:40
...

SQLAlchemy是python里面最好的orm框架(注意, 没有"之一"两个字), 不过它定义orm的过程比较繁琐, 要分别定义table和model, 然后在两者之间弄个mapper. 纯手工的过程就是这样的, 一步步来, 有点体力活的感觉. 其实我没有实际写过这种代码, 因为我不喜欢干体力活.

 

#纯手工建模的代码我也没写过, 这里空缺
 

也许正是因为这个问题,很多人更喜欢django的orm, 尽管后者远远不如SQLAlchemy强大. 题外话: django的orm跟django的问题是一样的: 做些常见的东西还行, 稍微有点复杂或者稍微有点特殊的情况, 会让你很头疼. 这东西是报社开发的, 比较适合报社使用, 其它情况慎用. 不过django admin是比较牛的, 快速开发一个数据录入系统非常合适. django admin是我离开的django后唯一怀念的东西.

 

后来SQLAlchemy为了让纯手工的过程简化, 推出了一个Declarative扩展. 之所以是个扩展, 是因为它并没有增加实质的功能, 提供的价值就是帮你把建模的过程简化,实际的东西还是通过那套model, table, mapper的机制来完成的. 它可以让你只定义model, 然后table和mapper自动帮你搞定. 听起来还不错, 工作量至少减少了一半以上. 一个例子:

 

class Manufacturer(Base):
    __tablename__ = 'manufacturer'
    id = Column(Integer, primary_key = True)
    name = Column(String(30))
 

 

按道理能搞到这个程度, 比django的orm也复杂不到哪里去了, 我还是觉得有个地方不是很爽, 就是定义一对多关系的时候, 需要先定义一个外键, 然后再定义一个relationship. 总感觉有点重复: 既然我都定义了外键了, 那么当然我期望有一个relationship啊. SQLAlchemy这么设计我能理解, 它是为了更大的灵活性, 毕竟很多东西不应该一下子写死.

 

class Car(models.Model):
    __tablename__ = 'car'
    id = Column(Integer, primary_key = True)
    name = Column(String(30))

    manufacturer_id = Column(Integer, ForeignKey('manufacturer.id'))
    manufacturer = relationship('Manufacturer', backref = backref('cars', lazy = 'dynamic'))
 

 

其实这还不算太糟糕啦. 关键是, 如果Menufacturer生产了不只Car一种产品呢, 比如韩国三星, 好多产品都做, 什么电冰箱,洗衣机,显示器,内存条,光驱...etc. 在你为这些所有的东西建模的时候, 你都要为它们加上外键, 并且再加上一个relationship, 就跟上面的类似的两行代码. 这给人的感觉是明显违背了DRY(Don't Repat Yourself)原则. 像我这种软件设计的小鸟都看出问题来了, 不用说SQLAlchemy开发者也早就看出问题了, 于是他们推出了Mixin的方式来减少重复. 

 

再来段题外话. Mixin似乎是Ruby的专利(当然我没有去考究过是不是Ruby第一个有这概念的). Ruby没有多继承, 却有Mixin. Mixin能够实现多继承的大部分功能, 但是却更加简单. Ruby的作者Matz为此颇为得意. 而Python中是有多继承的, 但是却没有Ruby中Mixin的机制. 于是可以用多继承的方式来实现类似Mixin的效果, 来简化SQLAlchemy中创建一对多关系的过程.

 

class BaseMixin:
     @declared_attr
     def __tablename__(cls):
          return cls.__name__.lower()

     id =  Column(Integer, primary_key = True)

class ManufacturerMixin:
     @declared_attr
     def manufacturer_id(self):
          return Column(Integer, ForeignKey('manufacturer.id'))

     @declared_attr
     def manufacturer(cls):
          return relationship('Manufacturer', backref = backref(cls.__name__.lower() + 's', lazy = 'dynamic'))
 

 

可以看到, 只要继承了BaseMixin, 就自动会声明tablename为小写的类名, 并且自动添加了一个Integer类型的主键. 只要继承Manufacturer, 就会动拥有到menufacturer表的外键及相应的关系. 通过这种继承Mixin的方式, 极大提供了代码的可重用性, 减少了代码量:

 

class Car(Base, BaseMixin, ManufacturerMixin)
     name = Column(String(30))
 

 

非常棒! 现在Car这个类里面只剩下了一行代码了! 上述代码中一个地方不是很容易理解, 就是@declared_attr. 这是个function decorator, 看过它的源码, 实际上是个类(decorator本身可以是个类, 事实上只要是callable就可以了). 根据文档: Mark a class-level method as representing the definition of a mapped property or special declarative member name. @declared_attr is more often than not applicable to mixins, to define relationships that are to be applied to different implementors of the class. 我的理解大致就是说, 把相关属性附加到目标类上面. 毕竟Mixin是为了让别的类重用, 改变别的类才是它最终的目的. 这里就不去深究了, 再没有深入了解SQLAlchemy其它的部分的基础知识的前提下, 这个问题恐怕很难彻底搞清楚.

 

不过大家有没有一种感觉, 就是通过继承来实现Mixin的方式是可以进一步改善的? 比如上面的那个ManufactureerMixin类, 里面的代码虽然不多, 但写得很拖沓, 明明是两个字段而已, 却写成了两个方法, 并且这两个方法都加上了@declared_attr这个样的一个decorator.(并且这个decorator的真实含义不太容易弄明白, 就算你看了文档, 也只能说大致了解, 知其然不知其所以然). 于是我开始思考能不能在Python里面用另外一种方式来实现Mixin? 我想到了Python2.6中新出现的class decorator.(有人可能会问python2.5以及之前的版本怎么版呢? 我只能说凉拌, 你还用上面的通过继承来Mixin的方式就行了. Python2.6在今天已经是很古老的版本了, python2.7和python3.2稳定版都出来很久了, 谁还有心思去兼顾python2.5啊). 先上代码吧:

 

def manufacturer_mixin(cls):
          cls.manufacturer_id = Column(Integer, ForeignKey('manufacturer.id'))
          cls.manufacturer = relationship('Manufacturer', backref = backref(cls.__name__.lower() + 's', lazy = 'dynamic'))

@manufacturer_mixin
class Car(Base, BaseMixin)
          name = Column(String(30))
 

 

上面的代码是不是更加好了呢? 首先代码行短了很多, 其次添加decorator的方式似乎也比多继承要优雅些. 最让人高兴的是, 那个难以理解的"@declared_attr"不见了. 剩下的都是可以理解的代码.

 

不过现在还是不够, 因为每定义一个外键关系就得定一个相应的class decorator. 每个class decorator之中的代码都是类似的: 一个外键和一个relationship. 这显然还是不符合DRY原则的. 可不可以只定义一个class decorator就搞定所有的外键? 如果可以的话, 这个decorator可以放到通用库中, 供以后所有的需要定义数据库外键的项目中使用, 极大地提高代码的复用程度.

 

def foreign_key(table):
        """Class decorator, add a foreign key to a SQLAlchemy model.
        Parameter table is the destination table, in a one-to-many relationship, table is the "one" side.
        """
        def ref_table(cls):
            setattr(cls, '{0}_id'.format(table), Column(Integer, ForeignKey('{0}.id'.format(table))))
            setattr(cls, table, relationship(table.capitalize(), backref = backref(cls.__name__.lower() + 's', lazy = 'dynamic')))
            return cls
        return ref_table
 

 

上面的代码没有任何跟具体模型相关的代码, 所以它的通用性是非常强的. 理论上讲在所有的需要定义sqlalchemy模型的地方都可以使用, 不用一遍遍地重复代码了. 只要定义像上面的一个foreign_key class decorator, 就可以一行代码搞定一个外键关系, 比如:

 

@foreign_key('another_foreign_key')
@foreign_key('manufacturer')
class Car(Base, BaseMixin)
    name = Column(String(30))
 

 

到此为止吧. 编程有个原则, 叫做"不要过度优化"(Avoid Premature Optimization). 这个原则很多时候特指性能方面的优化. 在这里也勉强适用: 优化代码也得适可而止, 太过分的优化就是浪费时间.