Python中ORM案例詳細解釋

跟着廖大大學習的過程中,遇到的許多問題都得以解決,但是在實戰編寫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())
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章