Python下ORM的一個設計舉例

輕舟輕發表於2016-12-07
ORM是物件-關係管理器,是對資料庫進行操作的API介面。選擇MySQL作為網站的後臺資料庫;執行SQL語句進行操作,並將常用的SELECT、INSERT等語句進行函式封裝;在非同步框架的基礎上,採用aiomysql作為資料庫的非同步IO驅動;將資料庫中表的操作,對映成一個類的操作,也就是資料庫表的一行對映成一個物件(ORM)。整個ORM也是非同步操作。
預備知識:Python協程和非同步IO(yield from的使用)、SQL資料庫操作、元類、物件導向知識、Python語法
思路
  
如何定義一個user類,這個類和資料庫中的表User構成對映關係,二者應該關聯起來,user可以操作表User    
  通過Field類將user類的屬性對映到User表的列中,其中每一列的欄位又有自己的一些屬性,包括資料型別,列名,主鍵和預設值

python的版本是Python 3.5.2

orm.py
# -*-coding:utf-8 -*-
import asyncio, logging
import aiomysql
 
# 列印SQL查詢語句
def log(sql, args=()):
    logging.info('SQL: %s'%(sql))
 
# 建立一個全域性的連線池,每個HTTP請求都從池中獲得資料庫連線
@asyncio.coroutine
def create_pool(loop, **kw):
    logging.info('create database connection pool...')
    # 全域性變數__pool用於儲存整個連線池
    global __pool
    __pool = yield from aiomysql.create_pool(
            # **kw引數可以包含所有連線需要用到的關鍵字引數
            # 預設本機IP
            host = kw.get('host','localhost'),
            user = kw['user'],
            password = kw['password'],
            db = kw['db'],
            port = kw.get('port',3306),
            charset = kw.get('charset','utf8'),
            autocommit = kw.get('autocommit', True),
            # 預設最大連線數為10
            maxsize = kw.get('maxsize', 10),
            minsize = kw.get('minisize', 1),            
            # 接收一個event_loop例項
            loop = loop
            )
 
# 封裝SQL SELECT語句為select函式
def select(sql, args, size=None):
    log(sql, args)
    global __pool    
    # -*- yield from 將會呼叫一個子協程,並直接返回撥用的結果
    # yield from從連線池中返回一個連線
    with(yield from __pool) as conn:
        # DictCursor is a cursor which returns results as a dictionary
        cur = yield from conn.cursor(aiomysql.DictCursor)        
        # 執行SQL語句
        # SQL語句的佔位符為?,MySQL的佔位符為%s
        yield from cur.execute(sql.replace('?','%s'), args or ())            
        # 根據指定返回的size,返回查詢的結果
        ifsize:
            # 返回size條查詢結果
            rs = fetchmany(size)
        else:
            # 返回所有查詢結果
            rs = fetchall()
        yield from cur.close()
        logging.info('rows return: %s'%(len(rs)))
        returnrs
 
# 封裝INSERT, UPDATE, DELETE
# 語句操作引數一樣,所以定義一個通用的執行函式
# 返回操作影響的行號
@asyncio.coroutine
def execute(sql, args):
    log(sql, args)
    global __pool
    with(yield from __pool) as conn:
        try:
            # execute型別的SQL操作返回的結果只有行號,所以不需要用DictCursor
            cur = yield from conn.cursor()
            cur.execute(sql.replace('?','%s'), args)
            affectedLine = cur.rowcount
            yield from cur.close()
        except BaseException as e:
            raise
        returnaffectedLine
 
# 根據輸入的引數生成佔位符列表
def create_args_string(num):
    L = []
    fornin range(num):
        L.append('?')    
    # 以','為分隔符,將列表合成字串
    return(','.join(L))
 
# 定義Field類,負責儲存(資料庫)表的欄位名和欄位型別
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, column_type='varchar(100)'):
        super().__init__(name, column_type, primary_key,default)
class BooleanField(Field):
    def __init__(self, name=None,default=None):
        super().__init__(name,'boolean', False,default)
class IntegerField(Field):
    def __init__(self, name=None, primary_key=False, default=0):
        super().__init__(name,'bigint', primary_key,default)
class FloatField(Field):
    def __init__(self, name=None, primary_key=False, default=0.0):
        super().__init__(name,'real', primary_key,default)        
class TextField(Field):
    def __init__(self, name=None,default=None):
        super().__init__(name,'Text', False,default)
 
# -*-定義Model的元類
# 所有的元類都繼承自type
# ModelMetaclass元類定義了所有Model基類(繼承ModelMetaclass)的子類實現的操作
# -*-ModelMetaclass的工作主要是為一個資料庫表對映成一個封裝的類做準備:
# ***讀取具體子類(user)的對映資訊
# 創造類的時候,排除對Model類的修改
# 在當前類中查詢所有的類屬性(attrs),如果找到Field屬性,就將其儲存到__mappings__的dict中,同時從類屬性中刪除Field(防止例項屬性遮住類的同名屬性)
# 將資料庫表名儲存到__table__中
# 完成這些工作就可以在Model中定義各種資料庫的操作方法
class ModelMetaclass(type):      
    # __new__控制__init__的執行,所以在其執行之前
    # cls:代表要__init__的類,此引數在例項化時由Python直譯器自動提供(例如下文的User和Model)
    # bases:代表繼承父類的集合
    # attrs:類的方法集合
    def __new__(cls, name, bases, attrs):        
        # 排除Model
        ifname =='Model':
            returntype.__new__(cls, name, bases, attrs)        
        # 獲取table名詞
        tableName = attrs.get('__table__', None) or name
        logging.info('found model: %s (table: %s)'%(name, tableName))        
        # 獲取Field和主鍵名
        mappings = dict()
        fields = []
        primaryKey = None      
        fork,vin attrs.items():
            # Field 屬性
            ifisinstance(v, Field):
                # 此處列印的k是類的一個屬性,v是這個屬性在資料庫中對應的Field列表屬性
                logging.info('  found mapping: %s --> %s'%(k, v))
                mappings[k] = v                
                # 找到了主鍵
                ifv.primary_key:                    
                    # 如果此時類例項的以存在主鍵,說明主鍵重複了               
                    ifprimaryKey:
                        raise StandardError('Duplicate primary key for field: %s'%k)
                    # 否則將此列設為列表的主鍵
                    primaryKey = k
                else:
                    fields.append(k)
        # end for
 
        ifnot primaryKey:
            raise StandardError('Primary key is nor founnd')        
        # 從類屬性中刪除Field屬性
        forkin mappings.keys():
            attrs.pop(k)
 
        # 儲存除主鍵外的屬性名為``(運算出字串)列表形式
        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語句
        # ``反引號功能同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)            
        returntype.__new__(cls, name, bases, attrs)
 
# 定義ORM所有對映的基類:Model
# Model類的任意子類可以對映一個資料庫表
# Model類可以看作是對所有資料庫表操作的基本定義的對映
# 基於字典查詢形式
# Model從dict繼承,擁有字典的所有功能,同時實現特殊方法__getattr__和__setattr__,能夠實現屬性操作
# 實現資料庫操作的所有方法,定義為class方法,所有繼承自Model都具有資料庫操作方法
class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kw):
        super(Model, self).__init__(**kw)
    def __getattr__(self, key):
        try:
            returnself[key]
        except KeyError:
            raise AttributeError(r'"Model" object has no attribute:%s'%(key))
    def __setattr__(self, key, value):
        self[key] = value
    def getValue(self, key):
        # 內建函式getattr會自動處理
        returngetattr(self, key, None)
    def getValueOrDefault(self, key):
        value = getattr(self, key, None)
        ifnot value:
            field = self.__mappings__[key]
            iffield.defaultis not None:
                value = field.default()ifcallable(field.default)elsefield.default
                logging.debug('using default value for %s: %s'%(key, str(value)))
                setattr(self, key, value)
        returnvalue
 
      @classmethod
    # 類方法有類變數cls傳入,從而可以用cls做一些相關的處理。並且有子類繼承時,呼叫該類方法時,傳入的類變數cls是子類,而非父類。
    @asyncio.coroutine
    def findAll(cls, where=None, args=None, **kw):
        '''find objects by where clause'''
        sql = [cls.__select__]        
        ifwhere:
            sql.append('where')
            sql.append(where)        
        ifargs is None:
            args = []        
        orderBy = kw.get('orderBy', None)
        iforderBy:
            sql.append('order by')
            sql.append(orderBy)
        limit = kw.get('limit', None)
        iflimit is not None:
            sql.append('limit')
            ifisinstance(limit, int):
                sql.append('?')
                args.append(limit)
            elif isinstance(limit, tuple) and len(limit) == 2:
                sql.append('?,?')
                args.extend(limit)
            else:
                raise ValueError('Invalid limit value: %s'%str(limit))
        rs = yield from select(' '.join(sql), args)
        return[cls(**r)for r inrs] 
 
    @classmethod
    @asyncio.coroutine
    def findNumber(cls, selectField, where=None, args=None):
        '''find number by select and where.'''
        sql = ['select %s __num__ from `%s`'%(selectField, cls.__table__)]
        ifwhere:
            sql.append('where')
            sql.append(where)
        rs = yield from select(' '.join(sql), args, 1)
        iflen(rs) == 0:
            returnNone
        returnrs[0]['__num__']

    @classmethod
    @asyncio.coroutine
    def find(cls, primarykey):
        '''find object by primary key'''
        rs = yield from select('%s where `%s`=?'%(cls.__select__, cls__primary_key__), [primarykey], 1)
        iflen(rs) == 0:
            returnNone
        returncls(**rs[0])
 
    @asyncio.coroutine
    def save(self):
        args = list(map(self.getValueOrDefault, self.__fields__))
        args.append(self.getValueOrDefault(self.__primary_key__))
        rows = yield from execute(self.__insert__, args)
        ifrows != 1:
            logging.warn('failed to insert record: affected rows: %s'%rows)
 
    @asyncio.coroutine
    def update(self):
        args = list(map(self.getValue, self.__fields__))
        args.append(self.getValue(self.__primary_key__))
        rows = yield from execute(self.__updata__, args)
        ifrows != 1:
            logging.warn('failed to update by primary key: affected rows: %s'%rows)
 
    @asyncio.coroutine
    def remove(self):
        args = [self.getValue(self.__primary_key__)]
        rows = yield from execute(self.__updata__, args)
        ifrows != 1:
            logging.warn('failed to remove by primary key: affected rows: %s'%rows)
 
if __name__ == '__main__':    
    class User(Model):
        # 定義類的屬性到列的對映:
        id = IntegerField('id',primary_key=True)
        name = StringField('username')
        email = StringField('email')
        password = StringField('password')
    # 建立一個例項:
    u = User(id=12345, name='peic', email='peic@python.org', password='password')
    print(u)
    # 儲存到資料庫:
    u.save()
    print(u)



有了ORM,我們就可以把Web App需要的3個表用Model表示出來:
model.py
import time
import uuid

from orm import Model, StringField, BooleanField, FloatField, TextField

# 用當前時間與隨機生成的uuid合成作為id
def next_id():
    # uuid4()以隨機方式生成uuid,hex屬性將uuid轉為32位的16進位制數
    return '%015d%s000' % (int(time.time() * 1000), uuid.uuid4().hex)

# ORM對映,將User對映到資料庫users表
class User(Model):
    __table__ = 'users'    # __table__的值將在建立類時被對映為表名

    # 定義各屬性的域,以及是否主鍵,將在建立類時被對映為資料庫表的列
    # 此處default用於儲存每個用於獨有的id,next_id將在insert的時候被呼叫
    id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
    email = StringField(ddl='varchar(50)')
    passwd = StringField(ddl='varchar(50)')
    admin = BooleanField()
    name = StringField(ddl='varchar(50)')
    image = StringField(ddl='varchar(500)')
    # 時間和日期用float型別儲存在資料庫中,不用datetime型別的好處在於不必關心資料庫的時區和時區轉換,顯示時只需要做一個float到str的轉換
    created_at = FloatField(default=time.time)


class Blog(Model):
    __table__ = 'blogs'

    id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
    user_id = StringField(ddl='varchar(50)')
    user_name = StringField(ddl='varchar(50)')
    user_image = StringField(ddl='varchar(500)')
    name = StringField(ddl='varchar(50)')
    summary = StringField(ddl='varchar(200)')
    content = TextField()
    created_at = FloatField(default=time.time)


class Comment(Model):
    __table__ = 'comments'

    id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
    blog_id = StringField(ddl='varchar(50)')
    user_id = StringField(ddl='varchar(50)')
    user_name = StringField(ddl='varchar(50)')
    user_image = StringField(ddl='varchar(500)')
    content = TextField
    created_at = FloatField(default=time.time)

初始化資料庫表

如果表的數量很少,可以手寫建立表的SQL指令碼:

schema.sql

drop database if exists awesome;

create database awesome;

use awesome;

grant select, insert, update, delete on awesome.* to 'www-data'@'localhost' identified by 'www-data';

create table users (
    `id` varchar(50) not null,
    `email` varchar(50) not null,
    `passwd` varchar(50) not null,
    `admin` bool not null,
    `name` varchar(50) not null,
    `image` varchar(500) not null,
    `created_at` real not null,
    unique key `idx_email` (`email`),
    key `idx_created_at` (`created_at`),
    primary key (`id`)
) engine=innodb default charset=utf8;

create table blogs (
    `id` varchar(50) not null,
    `user_id` varchar(50) not null,
    `user_name` varchar(50) not null,
    `user_image` varchar(500) not null,
    `name` varchar(50) not null,
    `summary` varchar(200) not null,
    `content` mediumtext not null,
    `created_at` real not null,
    key `idx_created_at` (`created_at`),
    primary key (`id`)
) engine=innodb default charset=utf8;

create table comments (
    `id` varchar(50) not null,
    `blog_id` varchar(50) not null,
    `user_id` varchar(50) not null,
    `user_name` varchar(50) not null,
    `user_image` varchar(500) not null,
    `content` mediumtext not null,
    `created_at` real not null,
    key `idx_created_at` (`created_at`),
    primary key (`id`)
) engine=innodb default charset=utf8;

如果表的數量很多,可以從Model物件直接通過指令碼自動生成SQL指令碼,使用更簡單。

把SQL指令碼放到MySQL命令列裡執行:

$ mysql -u root -p < schema.sql
我們就完成了資料庫表的初始化。

編寫資料訪問程式碼

接下來,就可以真正開始編寫程式碼操作物件了。比如,對於User物件,我們就可以做如下操作:#!/usr/bin/env python3

mysql_test.py

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

from orm import create_pool, destroy_pool
import asyncio
from models import User
import logging

async def test1(loop):
	await create_pool(loop=loop, host='localhost', port=3306, user='root', password='123456', db='awesome')
	
	#測試countRows語句        資料庫有多少條資訊
	raws = await User.countRows()
	logging.info('row is %s' % raws)

	#測試insert into語句
	if raws < 3:
		for idx in range(5):
			u = User(
				name='test%s' % idx,
				email='test%s@org.com' % idx,
				passwd='123456%s' % idx,
				image='about:blank'
			)
			row = await User.countRows(where='email = ?', args=[u.email])
			if row == 0:
				await u.save()
			else:
				print('the email is already reguster...')

	#測試select語句
	users = await User.findAll(orderBy='created_at')
	for user in users:
		logging.info('name: %s, password: %s, created_at: %s' % (user.name, user.passwd, user.created_at))

	#測試update語句
	user = users[1]
	user.email = 'guest@orm.com'
	user.name = 'guest'
	await user.update()

	#測試查詢指定使用者
	test_user = await User.find(user.id)
	logging.info('name: %s, email: %s' % (test_user.name, test_user.email))

	#測試delete語句
	users = await User.findAll(orderBy='created_at', limit=(0, 3))
	for user in users:
		logging.info('delete user: %s' % user.name)
		await user.remove()

	#先銷燬連線池
	await destroy_pool()
	print('test OK')

if __name__ == '__main__':
	loop = asyncio.get_event_loop()
	loop.run_until_complete(test1(loop))
	loop.close()

 

相關文章