2022年 11月 3日

利用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类如下即可完成全部工作:

  1. class UserModel(BaseModel):
  2. id = Field("bigint")
  3. name = Field("varchar(100)")
  4. age = Field("int")

在这个类定义中,我们指定了类属性到表字段的映射关系,这里认为类属性名和表字段名相同。现在类只有映射关系,还没有具体的属性,而且映射也不是我们想要的map形式,因此程序如下:

在BaseModel中继承内置字典dict来完成类属性的设置,如下

  1. class BaseModel(dict):
  2. __metaclass__ = ModelMetaClass
  3. def __init__(self, **kv):
  4. super(BaseModel, self).__init__(**kv)
  5. def __getattr__(self, key):
  6. try:
  7. return self[key]
  8. except KeyError:
  9. raise AttributeError("Model has not key %s" % key)
  10. def __setattr__(self, key, value):
  11. self[key] = value

有了类属性,需要将映射关系转成我们想要的map形式,具体映射关系在子类中指定,参考上一节,借助元类在BaseModel中我们通过指定元类hook类创建过程,如下

  1. class Field(object):
  2. def __init__(self, column_type):
  3. self.column_type = column_type
  4. class ModelMetaClass(type):
  5. def __new__(cls, name, bases, attr_dict):
  6. if name == 'BaseModel':
  7. return type.__new__(cls, name, bases, attr_dict)
  8. print("Creating Model:%s" % name)
  9. print(attr_dict)
  10. mapping = dict()
  11. for k,v in attr_dict.items():
  12. if isinstance(v, Field):
  13. print("Found Mapping:%s=>%s" % (k, v))
  14. setattr(v, 'name', k)
  15. mapping[k] = v
  16. attr_dict.pop(k)
  17. attr_dict['__mapping__'] = mapping
  18. attr_dict['__table__'] = name
  19. return type.__new__(cls, name, bases, attr_dict)

在类创建时,凡是指定Field类型的类属性都是我们定义的映射关系,在此取出来填充Field属性表示字段信息,并以属性名作为key来索引对应信息,对应的表名也映射为类的名称

到此整个映射关系建立完成,此时实现ORM就很简单了,基本上就是查表生成sql语句即可,如下

save保存一个记录,按照映射关系插入对应字段对应值即可,可在BaseModel中如下实现:

  1. def save(self):
  2. fields = []
  3. params = []
  4. args = []
  5. for k, v in self.__mapping__.items():
  6. fields.append(v.name)
  7. params.append('?')
  8. args.append(getattr(self, k, None))
  9. sql = 'insert into %s (%s) values (%s)' %(self.__table__, ','.join(fields), ','.join(params))
  10. print('SQL:%s' % sql)
  11. print('ARGS:%s' % str(args))

getone获取指定id的一条记录,查询得结果,按照映射关系指定到对应属性上即可,可在BaseModel中如下实现:

  1. @classmethod
  2. def getone(cls, id):
  3. # 简化,根据数据库查询生成对应数据表
  4. data = {"id":"2", "name":"wenwei", "age":"19"}
  5. a = cls()
  6. for k, v in a.__mapping__.items():
  7. a.__setattr__(k, data[v.name])
  8. return a

如下调用

  1. if __name__ == '__main__':
  2. u1 = UserModel(id='1', name='wenzhou', age='20')
  3. u1.save()
  4. u2 = UserModel.getone('2')
  5. print(u2, type(u2))

结果如下,即模拟了常见ORM的操作

  1. Creating Model:UserModel
  2. {'age': <__main__.Field object at 0x00000000031FF710>, '__module__': '__main__', 'id': <__main__.Field object at 0x00000000031FF668>, 'name': <__main__.Field object at 0x00000000031FF6D8>}
  3. Found Mapping:age=><__main__.Field object at 0x00000000031FF710>
  4. Found Mapping:id=><__main__.Field object at 0x00000000031FF668>
  5. Found Mapping:name=><__main__.Field object at 0x00000000031FF6D8>
  6. SQL:insert into UserModel (age,id,name) values (?,?,?)
  7. ARGS:['20', '1', 'wenzhou']
  8. ({'age': '19', 'id': '2', 'name': 'wenwei'}, <class '__main__.UserModel'>)

peewee源码分析

python下常见的ORM有django orm、SQLAlchemy和peewee,前两者太重,peewee相对来说比较轻量灵活,代码非常简洁,我们以此为例来分析下它的实现。

先看下简单的使用,如下:

  1. from peewee import *
  2. settings = {'host': 'localhost', 'password': '', 'port': 3306, 'user': 'root'}
  3. db = MySQLDatabase('test', **settings)
  4. class Person(Model):
  5. id = BigIntegerField()
  6. name = CharField(50)
  7. age = IntegerField()
  8. class Meta:
  9. database = db
  10. db_table = 'user_test'
  11. if __name__ == '__main__':
  12. db.connect()
  13. usernames = ['Bob', 'huey', 'mickey']
  14. for person in Person.select().where(Person.id >= 5):
  15. print(person.id, person.name, person.age)
  16. db.close()

这里使用和我们自定义类类似,定义一个包含表字段的Model子类,然后创建连接使用即可。它这里把连接信息database和db_table当做类的元数据Meta传入。框架对数据库层操作做了封装,我们指定Person.select().where(Person.id >= 5)查询的时候,类似我们自定义操作,根据定义的字段信息拼接出对应的SQL语句执行查询,并填充对应的对象属性值。

因此我们主要看下,它的字段映射关系如何指定的,一般编辑器中Ctrl选中Model,查看定义如下:

  1. class Model(with_metaclass(ModelBase, Node)):
  2. def __init__(self, *args, **kwargs):
  3. ...

对应元类为ModelBase,查看定义如下:

  1. class ModelBase(type):
  2. ...
  3. def __new__(cls, name, bases, attrs):
  4. ...
  5. Meta = meta_options.get('model_metadata_class', Metadata)
  6. ...
  7. # Construct the new class.
  8. cls = super(ModelBase, cls).__new__(cls, name, bases, attrs)
  9. cls.__data__ = cls.__rel__ = None
  10. cls._meta = Meta(cls, **meta_options)
  11. cls._schema = Schema(cls, **sopts)
  12. fields = []
  13. for key, value in cls.__dict__.items():
  14. if isinstance(value, Field):
  15. if value.primary_key and pk:
  16. raise ValueError('over-determined primary key %s.' % name)
  17. elif value.primary_key:
  18. pk, pk_name = value, key
  19. else:
  20. fields.append((key, value))
  21. ...
  22. if pk is not False:
  23. cls._meta.set_primary_key(pk_name, pk)
  24. for name, field in fields:
  25. cls._meta.add_field(name, field)
  26. ...
  27. return cls

这里判断字段是Field实例的属性为数据库表对应字段,注意看这里add_field,展开如下:

  1. def add_field(self, field_name, field, set_attribute=True):
  2. if field_name in self.fields:
  3. self.remove_field(field_name)
  4. elif field_name in self.manytomany:
  5. self.remove_manytomany(self.manytomany[field_name])
  6. ...
  7. field.bind(self.model, field_name, set_attribute)

是不是很相似,先从自身移除属性,然后绑定属性名到新值,这个新值是什么,继续看bind展开:

  1. class Field(ColumnBase):
  2. ...
  3. accessor_class = FieldAccessor
  4. ...
  5. def bind(self, model, name, set_attribute=True):
  6. self.model = model
  7. self.name = name
  8. self.column_name = self.column_name or name
  9. if set_attribute:
  10. setattr(model, name, self.accessor_class(model, self, name))
  11. class FieldAccessor(object):
  12. def __init__(self, model, field, name):
  13. self.model = model
  14. self.field = field
  15. self.name = name
  16. def __get__(self, instance, instance_type=None):
  17. if instance is not None:
  18. return instance.__data__.get(self.name)
  19. return self.field
  20. def __set__(self, instance, value):
  21. instance.__data__[self.name] = value
  22. instance._dirty.add(self.name)

可以看到,这里设置的新值就是FiledAccesor,这是个属性描述符。

不妨print(Person.__dict__)如下:

  1. {
  2. 'name': < peewee.FieldAccessor object at 0x000000000280B6D8 > ,
  3. 'age': < peewee.FieldAccessor object at 0x0000000002839748 > ,
  4. 'id': < peewee.FieldAccessor object at 0x0000000002843198 > ,
  5. '_meta': < peewee.Metadata object at 0x0000000002791828 >
  6. ...
  7. }

可以看到,通过元类,每个属性已经被换成属性描述符,通过这种方式,统一每个字段的行为,保证Person.name返回的是对应字段的描述,通过person.name返回的是具体对象实例的值。打印一个print(Person.name.__dict__)如下:

  1. {
  2. 'column_name': 'name'
  3. 'primary_key': False,
  4. 'name': 'name',
  5. 'max_length': 50,
  6. 'unique': False,
  7. 'index': False,
  8. 'model': < Model: Person > ,
  9. ...
  10. }

即为对应的数据库表信息。

可以看到peewee整个ORM实现方式和我们的如出一辙,关键是利用好属性描述符和元类来完成,前者完成每个字段的类访问和实例访问的行为统一,后者实现拦截类创建过程实现替换属性建立类-数据库表映射,把握这两点就可以分析整个ORM框架了。

 

演示代码下载链接

原创,转载请注明来自http://blog.csdn.net/wenzhou1219