Day 3 - 编写ORM框架

Web App里面有很多地方都要访问数据库。访问数据库需要创建数据库连接、游标对象,然后执行SQL语句,最后处理异常,清理资源。这些访问数据库的代码如果分散到各个函数中,势必无法维护,也不利于代码复用。
所以,我们要把常用的SELECT、INSERT、UPDATE和DELETE操作用函数封装起来,再 将一个数据库中的表映射成python中的类,类的实例就是数据库中的每一行数据 。这样,对这个对象的操作,可以保存到数据库中。同样,从数据库中提取出来的数据,可以转变为Python中的对象。
我们的ORM框架就是封装以上这些操作,只提供接口函数,让数据库操作变得更简单和方便。
SQLAlchemy就是这样一种ORM框架。 参考资料: SQLAlchemy
在开始之前,首先要建立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元类
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) 








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值