peewee代码分析
2013-01-05 01:48
peewee是一个轻量级的python ORM. 由于其功能较简单, 因此效率比Django的ORM或SQLAlchemy要略高一些. 本文会对这个ORM进行简单的代码分析, 理清它的逻辑, 了解它的原理. 我手上的peewee的版本是2.0.5, 共计2078行. 大致分成下面几个部分:
- 0001-0060: 系统模块加载, 数据库模块载入等.
- 0062-0205: 查询逻辑变量及类的定义.
- 0208-0523: 数据库字段类的定义.
- 0526-1414: 数据库查询类的定义.
- 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
. 各个数据库的差异性和一致性都在这儿体现.