读书人

SQLAlchemy 数据建模历程的改进

发布时间: 2012-07-03 13:37:43 作者: rapoo

SQLAlchemy 数据建模过程的改进

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_mixinclass 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). 这个原则很多时候特指性能方面的优化. 在这里也勉强适用: 优化代码也得适可而止, 太过分的优化就是浪费时间.

?

?

?

读书人网 >SQL Server

热点推荐