Web App里面有很多地方都要访问数据库。访问数据库需要创建数据库连接、游标对象,然后执行SQL语句,最后处理异常,清理资源。这些访问数据库的代码如果分散到各个函数中,势必无法维护,也不利于代码复用。
所以,我们要把常用的SELECT、INSERT、UPDATE和DELETE操作用函数封装起来,再
将一个数据库中的表映射成python中的类,类的实例就是数据库中的每一行数据
。这样,对这个对象的操作,可以保存到数据库中。同样,从数据库中提取出来的数据,可以转变为Python中的对象。
我们的ORM框架就是封装以上这些操作,只提供接口函数,让数据库操作变得更简单和方便。
在开始之前,首先要建立ORM框架和数据库的连接,这样框架才能够访问数据库。而且,由于Web框架使用了基于asyncio的aiohttp,这是基于协程的异步模型。所以我们数据库连接也要以异步形式来驱动。
1. 创建连接池
创建ORM框架和数据库的连接,使用
aiomysql
库,
aiomysql
是基于
asyncio
的异步库,使用方法和pymysql一样。ORM框架使用这个库即可异步访问MySQL数据库。
参考资料:aiomysql
我们需要创建一个
全局的连接池
,每个HTTP请求都可以从连接池中直接获取数据库连接。使用连接池的好处是不必频繁地打开和关闭数据库连接,而是能复用就尽量复用。
# 创建一个全局连接池
async def create_pool(loop, **kw):
logging.info('create database connection pool...')
global __pool # 将__pool变为全局变量
__pool = await aiomysql.create_pool(
host = kw.get('host', 'localhost'),
port = kw.get('port', 3306),
user = kw['user'],
password = kw['password'],
db = kw['db'],
charset = kw.get('charset', 'utf8'),
autocommit = kw.get('autocommit', True),
maxsize = kw.get('maxsize', 10),
minsize = kw.get('minsize', 1),
loop = loop)
2. 编写SQL语句执行函数
SQL语句不具备通用性,语句和参数都不唯一,需要传入sql执行函数。
要编写
Select
,
Update
,
Delete
,
Insert
四种sql操作的执行函数,我们将其分为两类:
2.1 Select
Select语句需要返回查找结果,我们用
select
函数执行,传入SQL语句和参数:
# 单独封装select
async def select(sql, args, size=None):
log(sql, args)
global __pool
# 以上下文方式打开conn连接,无需再调用conn.close()
# 或--> with await __pool as conn:
async with __pool.get() as conn:
# 创建一个DictCursor类指针,返回dict形式的结果集
# 以上下文方式创建cur指针,无需再调用cur.close()
async with conn.cursor(aiomysql.DictCursor) as cur:
# 替换占位符,SQL语句占位符为?,MySQL为%s。
await cur.execute(sql.replace('?', "%s"), args or ())
if size:
rs = await cur.fetchmany(size)
else:
rs = await cur.fetchall()
logging.info('rows returned: %s' % len(rs))
return rs
2.2 Update、Delete、Insert
执行这三种SQL语句,可以
统一用一个execute函数执行
,因为它们所需参数都一样,而且都只返回一个整数表示影响的行数。
# 封装insert,update,delete
async def execute(sql, args, autocommit=True):
log(sql, args)
with await __pool as conn:
# 如果不是自动提交,手动开始
if not autocommit:
await conn.begin()
try:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute(sql.replace('?', "%s"), args)
# rowcount返回整数表示受影响行数
affected = cur.rowcount
# 如果不是自动提交,手动提交
if not autocommit:
await conn.commit()
except BaseException as e:
if not autocommit:
await conn.rollback()
raise e
return affected
2.3 其他的辅助函数编写
1、在SQL语句执行函数中添加,可以输出SQL语句
# SQL语句反馈
def log(sql, args=()):
logging.info('SQL: %s, %s' % (sql, args))
2、构造SQL语句时用来创建参数的占位符' ? '
# 创建占位符,用于insert,updae,delete语句
def create_args_string(num):
L = []
for i in range(num):
L.append('?')
return ','.join(L)
3. 编写ORM
3.1 设计思路
有了SQL语句执行函数,就可以开始编写一个简单的ORM了。
之前提到过ORM的思路就是把一个
表映射成python中的类,行数据映射成类的实例
。
那么,首先构思如何以此来设计我们的ORM框架。
我们想这样来构造一个python类,让它映射成一个数据库中的表。
from orm import Model, StringField, IntegerField
class User(Model):
__table__ = 'users' # 表的名字
# 以下类的属性表示数据库中users表的列
# id, name 是列的名字,后面的值是列的类型
id = IntegerField(primary_key=True)
name = StringField()
而且,要注意这些属性是类的属性,当类生成实例时,需要通过
__init__
初始化,所以并不互相干扰。
那么,现在User就表示数据库中名叫users的表,它含有id和name两个列。
往表中加入数据可以创建一个类的实例:
创建实例:user = User(id=123, name='Michael')
3.2 编写Field字段
定义表需要定义列的类型,需要定义主键等信息。自己传入工作量太大,编写ORM的思路就是封装一切,只提供接口。
故我们需要编写一些
Field
字段供ORM调用,在定义类的时候直接调用即可。
# 定义Feild基类,负责保存表的字段名和字段类型
class Field(object):
def __init__(self, name, column_type, primary_key, default):
# 表的字段包含名字、类型、是否为主键和默认值
self.name = name
self.column_type = column_type
self.primary_key = primary_key
self.default = default
# 输出数据表的信息:类名,字段类型,名字
def __str__(self):
return '<%s, %s:%s>' % (self.__class__.__name__, self.column_type, self.name)
# 不同类型的字段类型,供不同类型的列调用,均继承自Field基类
# 字段信息均有默认值,可缺省定义也可定制
# 字符串类型
class StringField(Field):
def __init__(self, name=None, primary_key=False, default=None, ddl='varchar(255)'):
super(StringField, self).__init__(name, ddl, primary_key, default)
# 整数类型
class IntegerField(Field):
def __init__(self, name=None, primary_key=False, default=0):
super(IntegerField, self).__init__(name, 'bigint', primary_key, default)
# 布尔值类型
class BooleanField(Field):
def __init__(self, name=None, default=False):
super(BooleanField, self).__init__(name, 'bollean', False, default)
# 文本类型
class TextField(Field):
def __init__(self, name=None, default=None):
super(TextField, self).__init__(name, 'Text', False, default)
# 浮点数类型
class FloatField(Field):
def __init__(self, name=None, primary_key=False, default=None):
super(FloatField, self).__init__(name, 'real', primary_key, default)
3.3 Metaclass元类
定义了Field字段以后,User表已经具有了表和列的映射信息。
那么我们怎么提取这些映射呢?首先想到的是编写一个基类,提供一个方法来获取。让所有继承它的子类都能调用。但我们的数据库表不具唯一性,这样就只能在定义每一个类的时候去编写方法,那太繁琐,一点也不符合ORM框架只提供调用接口的设计前提。
并且,当类生成类的实例,实例的同名属性会和类的属性冲突。
所以,我们用到了元类
metaclass
,在创建类的时候,就扫描类中属性和字段的映射关系,并将其保存在自身的类属性中。
参考资料:深刻理解元类metaclass
#定义Metaclass元类
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
, # 排除对Model基类的改动,只作用于Model的子类(数据库表)
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
tableName = attrs.get('__table__', None) or name
logging.info('found model: %s (table: %s)' % (name, tableName))
# 保存当前类属性名和Field字段的映射关系
mappings = dict()
# 保存除主键外的属性名
fields = []
primarykey = None
for k, v in attrs.items():
# 找到Field类型字段
if isinstance(v, Field):
logging.info('found mapping: %s ==> %s' % (k, v))
mappings[k] = v
# 若字段primary_key为True
if v.primary_key:
# 判断主键是否已存在
if primarykey:
raise BaseException('Duplicate primary key for field: %s' % k)
primarykey = k
else:
fields.append(k)
if not primarykey:
raise BaseException('primary key not found')
# 删除类中的属性,因为会和类的实例同名属性冲突
for k in mappings.keys():
attrs.pop(k)
#保存除主键外的属性名为``(输出字符串)的列表形式
escaped_fields = list(map(lambda f: '`%s`' % f, fields))
# 映射关系,表名,字段名,主键名
# 将属性名和Field字段保存到类的__mappings__属性中
attrs['__mappings__'] = mappings
attrs['__table__'] = tableName
attrs['__fields__'] = fields
attrs['__primary_key__'] = primarykey
#构造默认的SQL语句
#反引号··功能同repr(),输出机器阅读语言
attrs['__select__'] = 'select `%s`, %s from `%s`' % (primarykey, ','.join(escaped_fields), tableName)
attrs['__insert__'] = 'insert into `%s` (%s, `%s`) values (%s)' % (tableName, ','.join(escaped_fields), primarykey, create_args_string(len(escaped_fields)+1))
attrs['__update__'] = 'update `%s` set %s where `%s`=?' % (tableName, ','.join(map(lambda f:'`%s`=?' % (mappings.get(f).name or f), fields)), primarykey)
attrs['__delete__'] = 'delete from `%s` where `%s`=?' % (tableName, primarykey)
return type.__new__(cls, name, bases, attrs)
现在,我们创建User类的时候,
ModelMetaclass
会自动扫描类中的映射信息,并保存到类属性当中
。
3.4 编写Model基类
然后编写一个Model基类,往Model类中添加class方法,就可以让所有子类调用class方法:
class Model(dict, metaclass = ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(self, **kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError('Model object has no attribute: %s' % key)
def __setattr__(self, key, value):
self[key] = value
def getValue(self, key):
# 继承父类dict的内建函数getattr()
return getattr(self, key, None)
def getValueOrDefault(self, key):
value = getattr(self, key, None)
if not value:
field = self.__mappings__[key]
if field.default is not None:
#default值可以设置为值或调用对象(无法设置值时,如default = time.time 插入当前时间)
value = field.default() if callable(field.default) else field.default
logging.debug('use default value for %s: %s' % (key, str(value)))
return value
# 类方法有类变量cls传入,从而可以用cls做一些相关的处理。
# 有子类继承时,调用该类方法时,传入的类变量cls是子类,而非父类。
@classmethod
async def findAll(cls, where=None, args=None, **kw):
if not args:
args = []
sql = [cls.__select__]
if where:
sql.append('where')
sql.append(where)
orderBy = kw.get('orderBy', None)
if orderBy:
sql.append('order by')
sql.append(orderBy)
limit = kw.get('limit', None)
if limit:
sql.append('limit')
if isinstance(limit, int):
sql.append('?')
args.append(limit)
elif isinstance(limit, tuple) and len(limit)==2:
sql.append('?, ?')
args.extend(limit)
else:
raise('Invalid limit value: %s' % str(limit))
rs = await select(' '.join(sql), args)
# 将返回的结果迭代生成类的实例,返回的都是实例对象, 而非仅仅是数据
return [cls(**r) for r in rs]
@classmethod
async def find(cls,primarykey):#根据主键查找数据库
sql = '%s where `%s`=?' % (cls.__select__, cls.__primary_key__)
rs = await select(sql, [primarykey], 1)
if len(rs) == 0:
return None
return cls(**rs[0])
@classmethod
async def findNumber(cls, selectField, where=None, args=None):
# 使用了SQL的聚集函数 count()
# select %s as __num__ from table ==> __num__表示列的别名
# 返回集合的列名会变成__num__
sql = ['select %s __num__ from `%s`' % (selectField, cls.__table__)]
if where:
sql.append('where')
sql.append(where)
rs = await select(' '.join(sql), args, 1)
if len(rs) == 0:
return None
# fetchmany()返回列表结果,用索引取出。又因为Dictcursor,值用key取出。
return rs[0]['__num__']
async def save(self):
args = list(map(self.getValueOrDefault, self.__fields__))
primarykey = self.getValueOrDefault(self.__primary_key__)
args.append(primarykey)
rows = await execute(self.__insert__, args)
if rows != 1:
logging.warn('failed to insert record: affected rows: %s' % rows)
async def update(self):
args = list(map(self.getValue, self.__fields__))
primarykey = self.getValue(self.__primary_key__)
args.append(primarykey)
rows = await execute(self.__update__, args)
if rows != 1:
logging.warn('failed to update record: affected rows: %s' % rows)
async def remove(self):
args = [self.getValue(self.__primary_key__)]
rows = await execute(self.__delete__, args)
if rows != 1:
logging.warn('failed to remove by primary key: affected rows: %s' % rows)