利用python实现ORM

Posted 文大侠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用python实现ORM相关的知识,希望对你有一定的参考价值。

做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 

以上是关于利用python实现ORM的主要内容,如果未能解决你的问题,请参考以下文章

python リスト内包表记

python 快译通のリスト内包表记

Python元类实现ORM框架

ORM批量添加

python 蟒蛇はリスト内包表记の中で正规表现が使えるとのこと。テキスト处理に使えそう。

Django进阶