做web开发的基本绕不过ORM,很多时候ORM可以大大减少开发工作量,那么ORM是怎样实现的呢?其实很简单,说穿了一文不值,本文先实现一个简单的ORM然后分析下现在流行的python ORM框架peewee源码。
ORM原理
ORM即对象关系映射(Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),简单来说就是把数据库的一个表映射成程序中的一个对象,表中的每个字段对应程序对象的一个属性,一个表记录对应一个对象实例。
这样的好处在于,数据库的操作变得透明,底层数据库被隔离,而且操作数据的过程中感觉不到数据库的操作,始终处理的只有程序对象,程序结构性非常好。
如下图,要建立一个ORM映射包括如下:
1.类名ClassName到表名TableName的映射,通常两者相同
2.类属性先建立到对应表字段的映射,通常属性和字段名相同,表字段类型在类属性初始化时指定
ORM简单实现
按照上述,我们希望定义一个ORM类如下即可完成全部工作:
- class UserModel(BaseModel):
- id = Field("bigint")
- name = Field("varchar(100)")
- age = Field("int")
在这个类定义中,我们指定了类属性到表字段的映射关系,这里认为类属性名和表字段名相同。现在类只有映射关系,还没有具体的属性,而且映射也不是我们想要的map形式,因此程序如下:
在BaseModel中继承内置字典dict来完成类属性的设置,如下
- class BaseModel(dict):
- __metaclass__ = ModelMetaClass
-
- def __init__(self, **kv):
- super(BaseModel, self).__init__(**kv)
-
- def __getattr__(self, key):
- try:
- return self[key]
- except KeyError:
- raise AttributeError("Model has not key %s" % key)
-
- def __setattr__(self, key, value):
- self[key] = value
有了类属性,需要将映射关系转成我们想要的map形式,具体映射关系在子类中指定,参考上一节,借助元类在BaseModel中我们通过指定元类hook类创建过程,如下
- class Field(object):
- def __init__(self, column_type):
- self.column_type = column_type
-
-
- class ModelMetaClass(type):
- def __new__(cls, name, bases, attr_dict):
- if name == 'BaseModel':
- return type.__new__(cls, name, bases, attr_dict)
-
- print("Creating Model:%s" % name)
- print(attr_dict)
-
- mapping = dict()
-
- for k,v in attr_dict.items():
- if isinstance(v, Field):
- print("Found Mapping:%s=>%s" % (k, v))
- setattr(v, 'name', k)
- mapping[k] = v
- attr_dict.pop(k)
-
- attr_dict['__mapping__'] = mapping
- attr_dict['__table__'] = name
-
- return type.__new__(cls, name, bases, attr_dict)
在类创建时,凡是指定Field类型的类属性都是我们定义的映射关系,在此取出来填充Field属性表示字段信息,并以属性名作为key来索引对应信息,对应的表名也映射为类的名称。
到此整个映射关系建立完成,此时实现ORM就很简单了,基本上就是查表生成sql语句即可,如下
save保存一个记录,按照映射关系插入对应字段对应值即可,可在BaseModel中如下实现:
- def save(self):
- fields = []
- params = []
- args = []
-
- for k, v in self.__mapping__.items():
- fields.append(v.name)
- params.append('?')
- args.append(getattr(self, k, None))
-
- sql = 'insert into %s (%s) values (%s)' %(self.__table__, ','.join(fields), ','.join(params))
- print('SQL:%s' % sql)
- print('ARGS:%s' % str(args))
getone获取指定id的一条记录,查询得结果,按照映射关系指定到对应属性上即可,可在BaseModel中如下实现:
- @classmethod
- def getone(cls, id):
- # 简化,根据数据库查询生成对应数据表
- data = {"id":"2", "name":"wenwei", "age":"19"}
-
- a = cls()
- for k, v in a.__mapping__.items():
- a.__setattr__(k, data[v.name])
-
- return a
如下调用
- if __name__ == '__main__':
- u1 = UserModel(id='1', name='wenzhou', age='20')
- u1.save()
-
- u2 = UserModel.getone('2')
- print(u2, type(u2))
结果如下,即模拟了常见ORM的操作
- Creating Model:UserModel
- {'age': <__main__.Field object at 0x00000000031FF710>, '__module__': '__main__', 'id': <__main__.Field object at 0x00000000031FF668>, 'name': <__main__.Field object at 0x00000000031FF6D8>}
- Found Mapping:age=><__main__.Field object at 0x00000000031FF710>
- Found Mapping:id=><__main__.Field object at 0x00000000031FF668>
- Found Mapping:name=><__main__.Field object at 0x00000000031FF6D8>
- SQL:insert into UserModel (age,id,name) values (?,?,?)
- ARGS:['20', '1', 'wenzhou']
- ({'age': '19', 'id': '2', 'name': 'wenwei'}, <class '__main__.UserModel'>)
peewee源码分析
python下常见的ORM有django orm、SQLAlchemy和peewee,前两者太重,peewee相对来说比较轻量灵活,代码非常简洁,我们以此为例来分析下它的实现。
先看下简单的使用,如下:
- from peewee import *
-
- settings = {'host': 'localhost', 'password': '', 'port': 3306, 'user': 'root'}
- db = MySQLDatabase('test', **settings)
-
-
- class Person(Model):
- id = BigIntegerField()
- name = CharField(50)
- age = IntegerField()
-
- class Meta:
- database = db
- db_table = 'user_test'
-
-
- if __name__ == '__main__':
- db.connect()
-
- usernames = ['Bob', 'huey', 'mickey']
- for person in Person.select().where(Person.id >= 5):
- print(person.id, person.name, person.age)
-
- db.close()
这里使用和我们自定义类类似,定义一个包含表字段的Model子类,然后创建连接使用即可。它这里把连接信息database和db_table当做类的元数据Meta传入。框架对数据库层操作做了封装,我们指定Person.select().where(Person.id >= 5)查询的时候,类似我们自定义操作,根据定义的字段信息拼接出对应的SQL语句执行查询,并填充对应的对象属性值。
因此我们主要看下,它的字段映射关系如何指定的,一般编辑器中Ctrl选中Model,查看定义如下:
- class Model(with_metaclass(ModelBase, Node)):
- def __init__(self, *args, **kwargs):
- ...
对应元类为ModelBase,查看定义如下:
- class ModelBase(type):
- ...
- def __new__(cls, name, bases, attrs):
- ...
- Meta = meta_options.get('model_metadata_class', Metadata)
- ...
-
- # Construct the new class.
- cls = super(ModelBase, cls).__new__(cls, name, bases, attrs)
- cls.__data__ = cls.__rel__ = None
-
- cls._meta = Meta(cls, **meta_options)
- cls._schema = Schema(cls, **sopts)
-
- fields = []
- for key, value in cls.__dict__.items():
- if isinstance(value, Field):
- if value.primary_key and pk:
- raise ValueError('over-determined primary key %s.' % name)
- elif value.primary_key:
- pk, pk_name = value, key
- else:
- fields.append((key, value))
- ...
- if pk is not False:
- cls._meta.set_primary_key(pk_name, pk)
-
- for name, field in fields:
- cls._meta.add_field(name, field)
- ...
- return cls
这里判断字段是Field实例的属性为数据库表对应字段,注意看这里add_field,展开如下:
- def add_field(self, field_name, field, set_attribute=True):
- if field_name in self.fields:
- self.remove_field(field_name)
- elif field_name in self.manytomany:
- self.remove_manytomany(self.manytomany[field_name])
-
- ...
- field.bind(self.model, field_name, set_attribute)
是不是很相似,先从自身移除属性,然后绑定属性名到新值,这个新值是什么,继续看bind展开:
- class Field(ColumnBase):
- ...
- accessor_class = FieldAccessor
- ...
-
- def bind(self, model, name, set_attribute=True):
- self.model = model
- self.name = name
- self.column_name = self.column_name or name
- if set_attribute:
- setattr(model, name, self.accessor_class(model, self, name))
-
- class FieldAccessor(object):
- def __init__(self, model, field, name):
- self.model = model
- self.field = field
- self.name = name
-
- def __get__(self, instance, instance_type=None):
- if instance is not None:
- return instance.__data__.get(self.name)
- return self.field
-
- def __set__(self, instance, value):
- instance.__data__[self.name] = value
- instance._dirty.add(self.name)
可以看到,这里设置的新值就是FiledAccesor,这是个属性描述符。
不妨print(Person.__dict__)如下:
- {
- 'name': < peewee.FieldAccessor object at 0x000000000280B6D8 > ,
- 'age': < peewee.FieldAccessor object at 0x0000000002839748 > ,
- 'id': < peewee.FieldAccessor object at 0x0000000002843198 > ,
- '_meta': < peewee.Metadata object at 0x0000000002791828 >
- ...
- }
可以看到,通过元类,每个属性已经被换成属性描述符,通过这种方式,统一每个字段的行为,保证Person.name返回的是对应字段的描述,通过person.name返回的是具体对象实例的值。打印一个print(Person.name.__dict__)如下:
- {
- 'column_name': 'name'
- 'primary_key': False,
- 'name': 'name',
- 'max_length': 50,
- 'unique': False,
- 'index': False,
- 'model': < Model: Person > ,
- ...
- }
即为对应的数据库表信息。
可以看到peewee整个ORM实现方式和我们的如出一辙,关键是利用好属性描述符和元类来完成,前者完成每个字段的类访问和实例访问的行为统一,后者实现拦截类创建过程实现替换属性建立类-数据库表映射,把握这两点就可以分析整个ORM框架了。
演示代码下载链接
原创,转载请注明来自http://blog.csdn.net/wenzhou1219