跟着廖大大學習的過程中,遇到的許多問題都得以解決,但是在實戰編寫ORM時給我的感覺就是知識出現了斷層,看來看去不知道哪裏有問題,又感覺都是問題,不得已去git上參考着源碼一行一行的敲,這樣的方式給了我很多的靈感,所以將此記錄分享,希望能夠幫助到各位博友。
經過學習,得出結論,我的斷層在面向對象高級編程中使用元類一篇:
此篇廖大大說:metaclass是Python面向對象裏最難理解,也是最難使用的魔術代碼。正常情況下,你
不會碰到需要使用metaclass的情況,所以,以下內容看不懂也沒關係,因爲基本上你不會用到。
所以當時並沒有仔細的去看,理解metaclass的概念在此實戰中幫助很大,建議先大概瞭解基礎概念後對照實際代碼具體理解。
下圖可以幫助理解,當一個類使用動態創建時基本的運行過程:
orm數據庫連接文件
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'orm數據庫連接'
__author__ = 'BWone'
import aiomysql, logging
from boto.compat import StandardError
from socks import log
logging.basicConfig(level=logging.INFO)
def log(sql, args=()):
' 打印sql '
logging.info('SQL: %s' % sql)
async def create_pool(loop, **kw):
' 創建全局的連接池 '
logging.info('create database connection pool...')
global __pool
# kw.get()的方式直接定義,kw['']的方式需要傳入相應的屬性
__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', 10), # 最小連接數量
loop = loop
)
async def select(sql, args, size=None):
' 執行Select '
log(sql, args)
global __pool
async with __pool.get() as conn:
# aiomysql.DictCursor將結果作爲字典返回
async with conn.cursor(aiomysql.DictCursor) as cur:
# 執行語句,第一個參數傳入sql語句並將語句中的?替換爲%s,第二個語句傳入參數
await cur.execute(sql.replace('?', '%s'), args or ())
# 如果size有值根據值獲取行數,沒有值時默認爲None查詢所有數據
if size:
# 指定一次要獲取的行數
rs = await cur.fetchmany(size)
else:
# 返回查詢結果集的所有行(查到的所有數據)
rs = await cur.fetchall()
logging.info('rows returned: %s' % len(rs))
return rs
async def execute(sql, args, autocommit=True):
' 執行Insert, Update, Delete '
log(sql)
async with __pool.get() as conn:
# 執行改變數據的語句時判斷是否自動提交,not True相當於False
if not autocommit:
await conn.begin()
try:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute(sql.replace('?', '%s'), args)
affected = cur.rowcount
if not autocommit:
await conn.commit()
except BaseException as e:
if not autocommit:
await conn.rollback()
raise
return affected
def create_args_string(num):
' 根據輸入的數字創建參數個數,例如:輸入3返回 ?, ?, ? '
L = []
for n in range(num):
L.append('?')
# join意爲用指定的字符連接生成一個新字符串
return ', '.join(L)
class Field(object):
' 構建屬性時的父類 '
# __init__只是用來將傳入的參數初始化給對象
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):
super().__init__(name, 'varchar', primary_key, default)
class BooleanField(Field):
def __init__(self, name=None, default=False):
super().__init__(name, 'boolean', False, default)
class IntegerField(Field):
def __init__(self, name=None, primary_key=False, default=0):
super().__init__(name, 'int', primary_key, default)
class FloatField(Field):
def __init__(self, name=None, primary_key=False, default=0.0):
# 在sql中float可以存儲爲4字節或8字節,而real和float近似,不同的是real存儲4字節
super().__init__(name, 'real', primary_key, default)
class TextField(Field):
def __init__(self, name=None, default=None):
# text比varchar存儲容量更大,text不允許有默認值,定義了也不生效,比如:text(200)
super().__init__(name, 'text', False, default)
# metaclass意爲元類,是類的模板,所以必須從'type'類型派生,一般用來動態的創建類
class ModelMetaclass(type):
' 根據metaclass創建實例 '
# __new__是在__init__之前被調用的特殊方法
# __new__是用來創建對象並返回的方法
# __new__()方法接收到的參數依次是:當前準備創建的類的對象;類的名字;類繼承的父類集合;類的方法集合(通過metaclass動態創建的類都會將類中定義的屬性以K,V形式傳入attrs,Key爲變量名,Value爲值)
def __new__(cls, name, bases, attrs):
# 排除Model類本身:
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 獲取table名稱,如果要創建的類中定義了__table__屬性,則取__table__屬性的值,如果沒有定義__table__屬性(爲None),則使用要創建類的類名
tableName = attrs.get('__table__', None) or name
logging.info('found model: %s (table: %s)' % (name, tableName))
# 獲取所有的Field和主鍵名
mappings = dict()
fields = []
primaryKey = None
# 使用items()對字典遍歷,接下來的語句操作都是爲了獲取鍵值後轉存至mappings,再根據鍵刪除類中同名屬性
for k, v in attrs.items():
# 判斷類型
if isinstance(v, Field):
logging.info(' found mapping: %s ==> %s' % (k, v))
mappings[k] = v
if v.primary_key:
# 找到主鍵
if primaryKey:
raise StandardError('Duplicate primary key for field: %s' % k)
primaryKey = k
else:
fields.append(k)
if not primaryKey:
raise StandardError('Primary key not found.')
# 使用keys()以列表形式返回一個字典所有的鍵
for k in mappings.keys():
# 根據鍵移除指定元素,相當於從類屬性中刪除該Field屬性,否則,容易造成運行時錯誤(實例的屬性會遮蓋類的同名屬性)
attrs.pop(k)
# map會將field的每一個元素傳入function,返回每次function返回值的新列表
escaped_fields = list(map(lambda f:'`%s`' % f, fields))
# 保存屬性和列的映射關係
attrs['__mappings__'] = mappings
# 表名
attrs['__table__'] = tableName
# 主鍵屬性名
attrs['__primary_key__'] = primaryKey
# 除主鍵外的屬性名
attrs['__fields__'] = fields
# 構造默認的SELECT, INSERT, UPDATE和DELETE語句,傳入的單個值使用`%s`,多個值使用%s:
# select語句操作時還需要拼接where條件
attrs['__select__'] = 'select `%s`, %s from `%s`' % (primaryKey, ', '.join(escaped_fields), tableName)
# insert語句操作時會調用create_args_string根據參數的數量拼接成(?, ?, ?)
attrs['__insert__'] = 'insert into `%s` (%s, `%s`) values (%s)' % (tableName, ', '.join(escaped_fields), primaryKey, create_args_string(len(escaped_fields) + 1))
# update語句操作時用join和map結合,先用map使每一次lambda表達式返回的值作爲新列表,再使用join連接成(值1=?, 值2=?, 值3=?)
attrs['__update__'] = 'update `%s` set %s where `%s`=?' % (tableName, ', '.join(map(lambda f: '`%s`=?' % (mappings.get(f).name or f), fields)), primaryKey)
# delete語句操作時只根據主鍵刪除
attrs['__delete__'] = 'delete from `%s` where `%s`=?' % (tableName, primaryKey)
return type.__new__(cls, name, bases, attrs)
# metaclass=ModelMetaclass指示使用ModelMetaclass來定製類
# 擴展dict
class Model(dict, metaclass=ModelMetaclass):
' 定製類 '
def __init__(self, **kw):
' 初始化 '
super(Model, self).__init__(**kw)
def __getattr__(self, key):
' 獲取值,如果取不到值拋出異常 '
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
' 根據Key,Value設置值 '
self[key] = value
def getValue(self, key):
' 根據Key獲取Value '
return getattr(self, key, None)
def getValueOrDefault(self, key):
' 獲取某個屬性的值,如果該對象的該屬性還沒有賦值,就去獲取它對應的列的默認值 '
value = getattr(self, key, None)
if value is None:
field = self.__mappings__[key]
if field.default is not None:
value = field.default() if callable(field.default) else field.default
logging.debug('using default value for %s: %s' % (key, str(value)))
setattr(self, key, value)
return value
# @classmethod表明該方法是類方法,類方法不需要實例化類就可以被類本身調用,第一個參數必須是cls,cls表示自身類,可以來調用類的屬性、類的方法、實例化對象等
# cls調用類方法時必須加括號,例如:cls().function()
# 不使用@classmethod也可以被類本身調用,前提是方法不傳遞默認self參數,例如:def function()
@classmethod
async def findAll(cls, where=None, args=None, **kw):
' 根據條件查詢 '
# 將sql裝配成一個列表,用於下列的拼接操作
sql = [cls.__select__]
if where:
sql.append('where')
sql.append(where)
# 將args裝配成一個空列表,用於下列的拼接操作(存放limit參數)
if args is None:
args = []
orderBy = kw.get('order by', None)
if orderBy:
sql.append('order by')
sql.append(orderBy)
limit = kw.get('limit', None)
if limit is not None:
sql.append('limit')
# limit接受一個或兩個數字參數,否則拋出異常
if isinstance(limit, int):
sql.append('?')
args.append(limit)
elif isinstance(limit, tuple) and len(limit) == 2:
sql.append('?, ?')
# extend也類似於拼接,用新列表追加到原來的列表後
args.extend(limit)
else:
raise ValueError('Invalid limit value: %s' % str(limit))
# 調用select方法並傳入拼接好的sql語句和參數,其中sql列表用空格間隔
rs = await select(' '.join(sql), args)
return [cls(**r) for r in rs]
@classmethod
async def findNumber(cls, selectField, where=None, args=None):
' 查詢數據條數 '
# 其中_num_是列名的代替名,返回一條數據時適用,如果返回多條數據建議去掉(同時去掉返回值中的['_num_'])
sql = ['select %s _num_ from `%s`' % (selectField, cls.__table__)]
# sql = ['select %s from `%s`' % (selectField, cls.__table__)]
if where:
sql.append('where')
sql.append(where)
# 因爲輸出的數據條數在一行顯示,所以傳入數值1
rs = await select(' '.join(sql), args, 1)
if len(rs) == 0:
return None
# rs[0]返回 列名:條數,例如:{'_num_': 15}
# rs[0]['_num_']返回 {'_num_': 15}中'_num_'的數據,運行結果爲15
return rs[0]['_num_']
# return rs[0]
@classmethod
async def find(cls, pk):
' 根據主鍵查詢 '
# 此處直接引用metaclass定義過的__select__語句拼接where條件語句
rs = await select('%s where `%s`=?' % (cls.__select__, cls.__primary_key__), [pk], 1)
if len(rs) == 0:
return None
return cls(**rs[0])
async def save(self):
' 新增 '
# 使用map將每個fields屬性傳入getValueOrDefault方法,獲取值後返回成列表
args = list(map(self.getValueOrDefault, self.__fields__))
# 單獨將主鍵傳入getValueOrDefault方法,獲取值後拼接
args.append(self.getValueOrDefault(self.__primary_key__))
# 傳入插入語句和參數並執行
rows = await execute(self.__insert__, args)
if rows == 0:
logging.warn('failed to update by primary key: affected rows: %s' % rows)
else:
logging.info('succeed to update by primary key: affected rows: %s' % rows)
async def update(self):
' 更新 '
args = list(map(self.getValue, self.__fields__))
args.append(self.getValue(self.__primary_key__))
rows = await execute(self.__update__, args)
if rows == 0:
logging.warn('failed to update by primary key: affected rows: %s' % rows)
else:
logging.info('succeed to update by primary key: affected rows: %s' % rows)
async def remove(self):
' 刪除 '
args = [self.getValue(self.__primary_key__)]
rows = await execute(self.__delete__, args)
if rows == 0:
logging.warn('failed to update by primary key: affected rows: %s' % rows)
else:
logging.info('succeed to update by primary key: affected rows: %s' % rows)
實體文件
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'實體'
__author__ = 'BWone'
import time, uuid
from www.orm import Model, StringField, BooleanField, FloatField, TextField, IntegerField
def next_id():
' 根據時間和uuid隨機生成主鍵,如果主鍵是自增不會用到 '
return '%015d%s000' % (int(time.time() * 1000), uuid.uuid4().hex)
class User(Model):
# 定義表名,要和數據庫表名一致
__table__ = 's_user'
# 設置字段對應的類型,字段名和類型需要和數據庫中數據類型一致
id = IntegerField(primary_key=True, default=None)
s_username = StringField()
s_pwd = StringField()
s_gender = IntegerField()
s_age = IntegerField()
class Comment(Model):
__table__ = 'comments'
id = IntegerField(primary_key=True, default=None)
user_id = IntegerField()
content = StringField()
create_time = FloatField(default=time.time())
測試文件
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'測試'
__author__ = 'BWone'
import asyncio
import www.orm as orm
from www.model import User, Comment
loop = asyncio.get_event_loop()
async def test():
# 數據庫參數
await orm.create_pool(user='root', password='123', db='test', loop=loop)
# 傳入事件循環
await orm.create_pool(loop=loop)
# 新增:當我們創建類時,Python解釋器首先在當前類User的定義中查找metaclass,如果沒有找到,就繼續在父類Model中查找metaclass,找到了,就使用Model中定義的metaclass的ModelMetaclass來創建User類
# u = User(s_username='test', s_pwd='test', s_gender=1, s_age=11)
# u.save()
# 查詢
# u = User()
# 條件查詢
# find1 = await u.findAll("s_username='test'")
# 多個關鍵字查詢:兩種方式
# find2 = await u.findAll("s_username='test' order by id desc limit 2")
# find3 = await u.findAll("s_username='test'", None, orderBy='id desc', limit=2)
# 數據數量查詢
# find4 = await u.findNumber('count(*)')
# 求和查詢:需要去掉_num_
# find5 = await u.findNumber('sum(s_age) age')
# 主鍵查詢:find方法通過cls.__primary_key__獲取,調用時只傳入參數即可
# find6 = await u.find(1)
# print(find2)
# 更新
# u = User(id=4, s_username='testUpdate', s_pwd='123', s_gender=0, s_age=0)
# update1 = await u.update()
# 刪除
# u = User(id=15)
# remove1 = await u.remove()
loop.run_until_complete(test())