Python下ORM的一個設計舉例
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()
相關文章
- 單一責任SRP設計舉例 - macerubMac
- 設計模式應用舉例設計模式
- jdbc執行DML程式設計舉例JDBC程式設計
- 話說C#程式設計師人手一個ORMC#程式設計師ORM
- Python qutip用法(舉例介紹)Python
- 舉例解釋一下explain各欄位的含義AI
- # Python設計模式 單例模式Python設計模式單例
- Python設計模式——單例模式Python設計模式單例
- python設計模式-單例模式Python設計模式單例
- 說一下泛型原理,並舉例說明泛型
- 玩轉用例設計:XMind2TestCase一個高效的測試用例設計解決方案
- 每天一個設計模式之單例模式設計模式單例
- Java Stream六個使用舉例Java
- 舉例說明寫一個button的按鈕的方法有哪些?
- ORM 例項教程ORM
- 使用列舉來寫出更優雅的單例設計模式單例設計模式
- 超細!細說Zookeeper選舉的一個案例(下)
- 一天一個設計模式(二) - 單例模式(Singleton)設計模式單例
- Python字串講解舉例—工資字串!Python字串
- 說說Vue 3.0中Treeshaking特性?舉例說明一下?Vue
- Java併發程式設計中的設計模式解析(二)一個單例的七種寫法Java程式設計設計模式單例
- 重走JAVA程式設計之路(一)列舉Java程式設計
- VC++視覺化程式設計第一個程式設計例項出錯C++視覺化程式設計
- 設計模式(一)_單例模式設計模式單例
- 設計模式一(單例模式)設計模式單例
- 舉個例子來理解一下Koa的洋蔥模型模型
- Python經典程式設計習題100例:第3例Python程式設計
- 發現 ORM 方法 MorphToMany 的一個 BugORM
- 題目:設計一個類,我們只能生成該類的一個例項。 程式碼如下
- 舉例說明物件導向程式設計有什麼缺點?物件程式設計
- 手把手教你寫一個java的orm(一)JavaORM
- 一種自頂而下的Python裝飾器設計方法Python
- 用Python實現設計模式——單例模式Python設計模式單例
- 來個大佬看看介面用例設計
- javascript設計模式一: 單例模式JavaScript設計模式單例
- 設計模式(一)之單例模式設計模式單例
- Python如何設計物件導向的類(下)Python物件
- JAVA反射舉例Java反射
- 死鎖-舉例