peewee代码分析

2013-01-04 22:48

peewee是一个轻量级的python ORM. 由于其功能较简单, 因此效率比Django的ORM或SQLAlchemy要略高一些. 本文会对这个ORM进行简单的代码分析, 理清它的逻辑, 了解它的原理. 我手上的peewee的版本是2.0.5, 共计2078行. 大致分成下面几个部分:

  1. 0001-0060: 系统模块加载, 数据库模块载入等.
  2. 0062-0205: 查询逻辑变量及类的定义.
  3. 0208-0523: 数据库字段类的定义.
  4. 0526-1414: 数据库查询类的定义.
  5. 1417-2078: 数据库类的定义.

Model

我们先从一段文档中的例程开始:

import peewee

class Blog(peewee.Model):
    title = peewee.CharField()

    def __unicode__(self):
        return self.title

用过Django的同学对这种语法应该很熟悉. 这儿的定义和Django的模型定义基本是一致的. 我们来看下这个模型定义过程中发生了什么事情. Model这个类的定义如下:

class Model(object):
    __metaclass__ = BaseModel

    def __init__(self, *args, **kwargs):
        self._data = self._meta.get_default_dict()
        self._obj_cache = {} # cache of related objects

        for k, v in kwargs.items():
            setattr(self, k, v)

    @classmethod
    def select(cls, *selection):
        """ more logic here."""

由于这个类有__metaclass__这个属性, 因此我们需要用BaseModel来创建Model这个类. 此时, 传递给BaseModel的__new__方法的参数为: "Model", object, Model里定义的类似于select这样的类方法的一个字典. 为了把这个过程分析得更清楚一点儿, 我们砍掉BaseModel里在这个过程中无意义的代码:

class BaseModel(type):
    def __new__(cls, name, bases, attrs):
        # initialize the new class and set the magic attributes
        cls = super(BaseModel, cls).__new__(cls, name, bases, attrs)
        cls._meta = ModelOptions(cls)

        primary_key = PrimaryKeyField(primary_key=True)
        primary_key.add_to_class(cls, 'id')

        cls._meta.primary_key = primary_key
        cls._meta.auto_increment = isinstance(primary_key, PrimaryKeyField) or primary_key.sequence
        if not cls._meta.db_table:
            cls._meta.db_table = re.sub('[^\w]+', '_', cls.__name__.lower())

        exception_class = type('%sDoesNotExist' % cls.__name__, (DoesNotExist,), {})
        cls.DoesNotExist = exception_class
        cls._meta.prepared()

        return cls

这一段做的事情也不算复杂: 首先是父类的初始化, 然后给Model这个类添加了一个_meta属性, 这个属性是一个ModelOptions的实例. 接下来确定了主键和数据表的名字, 最后添加了一个异常. 我们可以注意到, 在定义某个Model的过程中, Model自动都会带上_meta这个属性, 这个属性是一个ModelOptions实例, 现在我们来具体看下ModelOptions的实例化函数:

class ModelOptions(object):
    def __init__(self, cls, database=None, db_table=None, indexes=None,
                 order_by=None, primary_key=None):
        self.model_class = cls
        self.name = cls.__name__.lower()
        self.fields = {}
        self.columns = {}
        self.defaults = {}

        self.database = database or default_database
        self.db_table = db_table
        self.indexes = indexes or []
        self.order_by = order_by
        self.primary_key = primary_key

        self.auto_increment = None
        self.rel = {}
        self.reverse_rel = {}

由字段的名字我们已可以看出这个类主要用来存放数据库数据表相关的信息, 包括数据表对应的类, 数据表的名字, 字段, 索引, 排序, 主键等信息.

看完__init__方法, 我们来看它的prepared方法, 这个方法会在__new__方法的结尾被执行:

    def prepared(self):
        for field in self.fields.values():
            if field.default is not None:
                self.defaults[field] = field.default

        if self.order_by:
            norm_order_by = []
            for clause in self.order_by:
                field = self.fields[clause.lstrip('-')]
                if clause.startswith('-'):
                    norm_order_by.append(field.desc())
                else:
                    norm_order_by.append(field.asc())
            self.order_by = norm_order_by

这段代码中第一个循环是用来设置某些字段的默认值, 后一个循环是用来设置字段的排序方式. 这些代码也都不算太复杂, 不深入分析了.

接下来, 是Blog这个类继承了Model这个类, 则Blog这个类仍有__metaclass__这个属性, 我们仍要执行__new__, 此时传递给它的参数为: "Blog", peewee.Model, 第三个字典中包括了一个CharField和一个__unicode__方法. 之前在Model中定义的那些方法不是Blog这个类的属性, 因此不会被放进attr里传给__new__. 为了简化, 我们只关注在这个过程中和刚才不一样的逻辑:

class BaseModel(type):
    inheritable_options = ['database', 'indexes', 'order_by', 'primary_key']

    def __new__(cls, name, bases, attrs):
        meta_options = {}

        # inherit any field descriptors by deep copying the underlying field obj
        # into the attrs of the new model, additionally see if the bases define
        # inheritable model options and swipe them
        for b in bases:
            base_meta = getattr(b, '_meta')
            for (k, v) in base_meta.__dict__.items():
                if k in cls.inheritable_options and k not in meta_options:
                    meta_options[k] = v

            for (k, v) in b.__dict__.items():
                if isinstance(v, FieldDescriptor) and k not in attrs:
                    if not v.field.primary_key:
                        attrs[k] = deepcopy(v.field)

        cls = super(BaseModel, cls).__new__(cls, name, bases, attrs)
        for name, attr in cls.__dict__.items():
            cls._meta.indexes = list(cls._meta.indexes)
            if isinstance(attr, Field):
                attr.add_to_class(cls, name)
                if attr.primary_key:
                    primary_key = attr

        # create a repr and error class before finalizing
        if hasattr(cls, '__unicode__'):
            setattr(cls, '__repr__', lambda self: '<%s: %r>' % (
                cls.__name__, self.__unicode__()))

        return cls

这儿bases里面只有Model这一个类, 这个类是有_meta这个属性的, 这儿开头先设置了继承关系. 例如, 由于Model已经被设置了一个数据库, 这儿继承Model的类如果未加指定则认定Model的数据库就是自己的数据库. 所有这些该继承的属性都是放在inheritable_options这个列表里面. 接下来的一个循环是将父类中的键继承过来, 也作为这个类的键. 接下来终于把我们在例程中定义的title字段加了进来. 最后, 由于我们定义了__unicode__方法, 因此这儿给Blog这个类设置了__repr__.

到这儿, 我们前面的Blog类已经定义完成了.

字段和字段描述器

为了给用户提供一个清晰的API, 我们需要能够实现类似下面的语法:

class Blog(peewee.Model):
    title = peewee.CharField()

blog = Blog()
blog.title = "test"
blog.save()

而这一语法本身, 就是有些不合乎语法的: 一开始类定义中的title是一个类属性, 指到一个CharField, 后面我们直接对title这个字段赋值, 而且还期望后续的save操作中能够将"test"这个字符串自动保存到数据库. 具体这个字段和数据库中的表的对应关系是在哪儿保存的呢?

我们回头看BaseModel的__new__方法, 其中有这么一段:

class BaseModel(type):
    def __new__(cls, name, bases, attrs):
        for b in bases:
            for (k, v) in b.__dict__.items():
                if isinstance(v, FieldDescriptor) and k not in attrs:
                    if not v.field.primary_key:
                        attrs[k] = deepcopy(v.field)
        cls = super(BaseModel, cls).__new__(cls, name, bases, attrs)
        """ more logic here. """
        return cls

当Blog类被定义时, 我们交给__new__的参数中的bases是一个列表, 其中只有一个元素, 即Model. 所以在Blog被定义时, 这儿循环里的b就是Model这个类. 这个类里是FieldDescriptor实例的只有一个默认加上的id字段. 不过等等, 这个FieldDescriptor是从哪儿来的? 仔细跟下, 我们能够发现, FieldDescriptor这个名字是在给数据模型添加字段属性时自动加上的:

    def __new__(cls, name, bases, attrs):
        """ more logic here. """
        for name, attr in cls.__dict__.items():
            cls._meta.indexes = list(cls._meta.indexes)
            if isinstance(attr, Field):
                attr.add_to_class(cls, name)
        """ more logic here. """

这儿attr是某个字段, 例如Blog里的CharField, cls是这个数据模型(这儿是Blog类), name是这个字段的名字(这儿是'title'). 有了这两个参数, 我们去看add_to_class的逻辑:

class Field(Leaf):
    def add_to_class(self, model_class, name):
        self.name = name
        self.model_class = model_class
        self.db_column = self.db_column or self.name
        self.verbose_name = self.verbose_name or re.sub('_+', ' ', name).title()

        model_class._meta.fields[self.name] = self
        model_class._meta.columns[self.db_column] = self
        setattr(model_class, name, FieldDescriptor(self))

可见, 这儿给Blog加上的属性是一个FieldDescriptor而不是Field本身. 我们去看看这个FieldDescriptor的逻辑:

class FieldDescriptor(object):
    def __init__(self, field):
        self.field = field
        self.att_name = self.field.name

    def __get__(self, instance, instance_type=None):
        if instance:
            return instance._data.get(self.att_name)
        return self.field

    def __set__(self, instance, value):
        instance._data[self.att_name] = value

好吧这儿的逻辑很简单, 就是用了描述器来实现前面提到的语法. 当Blog去取title这个属性时, 执行的是__get__里的逻辑, 这个时候没有instance这个东西, 返回的是self.field即CharField. 而当Blog的某个实例blog去取这个属性时, 存在instance, 返回的是blog._data里的内容. 而我们给blog这个实例设置值的时候, 执行的是__set__的逻辑, 给blog._data这个字典里title这个键设置值.

数据表创建

接下来, 我们讨论数据表的创建. 要有数据表首先要有数据库. 默认的数据库是在peewee的源代码中指定的, 是一个sqlite数据库, 我们不打算更改这一点. 数据表的创建是通过执行Model的create_table方法来实现的:

    @classmethod
    def create_table(cls, fail_silently=False):
        if fail_silently and cls.table_exists():
            return

        db = cls._meta.database
        pk = cls._meta.primary_key
        if db.sequences and pk.sequence and not db.sequence_exists(pk.sequence):
            db.create_sequence(pk.sequence)

        db.create_table(cls)

        for field_name, field_obj in cls._meta.fields.items():
            if isinstance(field_obj, ForeignKeyField):
                db.create_foreign_key(cls, field_obj)
            elif field_obj.index or field_obj.unique:
                db.create_index(cls, [field_obj], field_obj.unique)

        if cls._meta.indexes:
            for fields, unique in cls._meta.indexes:
                db.create_index(cls, fields, unique)

其中, 真正建表是通过调用对应的database的create_table方法来实现的, 对于目前peewee所支持的三种数据库(sqlite/mysql/postgres), 都没做任何自定义, 而是调用了Database这个类的对应方法:

    def create_table(self, model_class):
        qc = self.get_compiler()
        return self.execute_sql(qc.create_table(model_class))

这儿涉及到了数据库的查询, 我们一会儿再讨论QueryCompiler的使用. 目前我们先回到刚才Model的create_table方法. 一开始是对表存在的检查, 然后是为主键创建序列. 接下来是刚才提到的建表, 然后是创建外键关系和索引.

数据表查询

我们先看看刚才悬而未决的建表查询. 首先要看看这个compiler是什么东西:

class Database(object):
    compiler_class = QueryCompiler

    def get_compiler(self):
        return self.compiler_class(
            self.quote_char, self.interpolation, self.field_overrides,
            self.op_overrides)

可见此处的compiler就是一个QueryCompiler的实例. 为了适配多个后端数据库, 初始化这个实例的参数在Database这个类中有默认值, 而当这个函数真正被调用时, 这儿的数据库都是Database的子类, 例如SqliteDatabase这样的东西, 这些子类能够覆盖父类中的这些默认值.

我们接下来看看QueryCompiler的实例化函数:

class QueryCompiler(object):
    def __init__(self, quote_char='"', interpolation='?', field_overrides=None,
                 op_overrides=None):
        self.quote_char = quote_char
        self.interpolation = interpolation
        self._field_map = dict_update(self.field_map, field_overrides or {})
        self._op_map = dict_update(self.op_map, op_overrides or {})

可见也没做什么实际的事情, 都是在继承实例化的参数. 我们于是开始看QueryCompiler的create_table方法:

    def parse_create_table(self, model_class, safe=False):
        parts = ['CREATE TABLE']
        if safe:
            parts.append('IF NOT EXISTS')
        parts.append(self.quote(model_class._meta.db_table))
        columns = ', '.join(self.field_sql(f) for f in model_class._meta.get_fields())
        parts.append('(%s)' % columns)
        return parts

    def create_table(self, model_class, safe=False):
        return ' '.join(self.parse_create_table(model_class, safe))

具体调用的是create_table, 而实际上做拼字符串的逻辑是parse_create_table这个方法处理的. 具体逻辑也不复杂, 将相应SQL拼出来而已. 例如对于刚才的Blog类, create_table方法返回的字符串为(使用了SqliteDatabase):

CREATE TABLE "blog" ("id" INTEGER NOT NULL PRIMARY KEY, "title" VARCHAR(255) NOT NULL)

最后我们该看下Database的execute_sql方法:

    def execute_sql(self, sql, params=None, require_commit=True):
        cursor = self.get_cursor()
        res = cursor.execute(sql, params or ())
        if require_commit and self.get_autocommit():
            self.commit()
        logger.debug((sql, params))
        return cursor

这段就是直接和数据库进行交互的部分了. 我们费心费力地写ORM很多时候就是为了避免直接掺和到这种程度的细节中来. 具体这儿也没啥好说的, 做过数据库开发的同学应该都很熟悉了.

接下来我们以创建Blog实例, 保存为例来分析这个过程中的函数调用:

blog = Blog(title="test")
blog.save()

b = Blog.get(Blog.title == 'test')
b.title = "测试"
b.save()

一开始自然是看Model的__init__方法了:

class Model(object):
    def __init__(self, *args, **kwargs):
        self._data = self._meta.get_default_dict()
        self._obj_cache = {} # cache of related objects

        for k, v in kwargs.items():
            setattr(self, k, v)

这个时候, _data中放了这个类的默认值, 然后将kwargs中的内容全部设为这个实例的属性. 此处kwargs中只有一对键值, 因此我们实际上只是设置了:

self.title = "test"

之前我们在讲FieldDescriptor的时候已经知道, "test"这个字符串实际上保存到了_data里面. 我们可以期望保存的时候是从_data里把值拿出来:

    def save(self, force_insert=False):
        field_dict = dict(self._data)
        pk = self._meta.primary_key
        if self.get_id() is not None and not force_insert:
            field_dict.pop(pk.name)
            update = self.update(
                **field_dict
            ).where(pk == self.get_id())
            update.execute()
        else:
            if self._meta.auto_increment:
                field_dict.pop(pk.name, None)
            insert = self.insert(**field_dict)
            new_pk = insert.execute()
            if self._meta.auto_increment:
                self.set_id(new_pk)

后面的部分从python角度而言基本是食之无肉弃之可惜. 主要逻辑是调用Model中的insert/update这样的方法, 而这样的方法会生成一个Query对象, 这个对象的execute方法会调用对应的Database的execute方法, 进而写到数据库. 细节就从略了.

回顾

我们最后回头看看这个ORM的结构:

      /-------------------- ORM ----------------------\
      |  /-BaseModel---\  /-Fields\  /-QueryCompiler\ | /-sqlite
User -+--+-Model-------+--+-Field-+--+-Query--------+-+-MySQL
      |  \-ModelOptions/  \-Leaf--/  \-Database-----/ | \-Postgres
      \-----------------------------------------------/

首先, 我们希望能够用声明式的语法, 创建数据表时能够有一个方便的界面, 类似我们在文章开头给出的例程一样. 因此, 给用户的这个Model应有尽量简明的API, 设好合理的默认值, 并有一定的可扩展性来实现高级功能. 我们还要利用元类编程的方法实现声明式语法. 这是我们需要有Model(和BaseModel!)的原因. 此外, 为了避免将很多属性都堆放在Model里面, peewee和其他一些ORM一样定义了一个ModelOptions类, 专门用来放这些属性.

另一个显然需要的类是字段(Field), 我们需要各式各样的字段来记录现实时间中的数据, 这个字段应该和数据库实现中的字段能够有对应关系, 但是这儿的字段不需要严格和数据库中的实现一致. 我们应该注意到各个数据库实现中的字段属性可能略有差异, 这些差异逻辑不应该放在字段这一层梳理, 而是应该放在一个类似数据库适配器的东西中统一处理. 在做字段的过程中, 我们显然应该将字段的共性提取出来做一个公用的基类(Field), 其余的各种字段(例如CharField)应该继承这个基类. 最后, peewee里将一些和查询直接相关的属性和方法放在了Leaf这个类中.

接下来是处理各种查询逻辑的地方, 这儿应该负责将Model/Field这一层的查询语句转换成一个通用的查询调用, 还应该通过缓存/预处理等方式适当优化性能. 这一层的逻辑就开始比较琐碎了. 最后是负责和数据库对接的部分, Database. 各个数据库的差异性和一致性都在这儿体现.