# Day 2 - 編寫數據庫模塊
在一個Web App中,所有數據,包括用戶信息、發布的日志、評論等,都存儲在數據庫中。在awesome-python-app中,我們選擇MySQL作為數據庫。
Web App里面有很多地方都要訪問數據庫。訪問數據庫需要創建數據庫連接、游標對象,然后執行SQL語句,最后處理異常,清理資源。這些訪問數據庫的代碼如果分散到各個函數中,勢必無法維護,也不利于代碼復用。
此外,在一個Web App中,有多個用戶會同時訪問,系統以多進程或多線程模式來處理每個用戶的請求。假設以多線程為例,每個線程在訪問數據庫時,都必須創建僅屬于自身的連接,對別的線程不可見,否則,就會造成數據庫操作混亂。
所以,我們還要創建一個簡單可靠的數據庫訪問模型,在一個線程中,能既安全又簡單地操作數據庫。
為什么不選擇SQLAlchemy?SQLAlchemy太龐大,過度地面向對象設計導致API太復雜。
所以我們決定自己設計一個封裝基本的SELECT、INSERT、UPDATE和DELETE操作的db模塊:`transwarp.db`。
### 設計db接口
設計底層模塊的原則是,根據上層調用者設計簡單易用的API接口,然后,實現模塊內部代碼。
假設`transwarp.db`模塊已經編寫完畢,我們希望以這樣的方式來調用它:
首先,初始化數據庫連接信息,通過`create_engine()`函數:
```
from transwarp import db
db.create_engine(user='root', password='password', database='test', host='127.0.0.1', port=3306)
```
然后,就可以直接操作SQL了。
如果需要做一個查詢,可以直接調用`select()`方法,返回的是list,每一個元素是用dict表示的對應的行:
```
users = db.select('select * from user')
# users =>
# [
# { "id": 1, "name": "Michael"},
# { "id": 2, "name": "Bob"},
# { "id": 3, "name": "Adam"}
# ]
```
如果要執行INSERT、UPDATE或DELETE操作,執行`update()`方法,返回受影響的行數:
```
n = db.update('insert into user(id, name) values(?, ?)', 4, 'Jack')
```
`update()`函數簽名為:
```
update(sql, *args)
```
統一用`?`作為占位符,并傳入可變參數來綁定,從根本上避免[SQL注入攻擊](http://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)。
每個`select()`或`update()`調用,都隱含地自動打開并關閉了數據庫連接,這樣,上層調用者就完全不必關心數據庫底層連接。
但是,如果要在一個數據庫連接里執行多個SQL語句怎么辦?我們用一個with語句實現:
```
with db.connection():
db.select('...')
db.update('...')
db.update('...')
```
如果要在一個數據庫事務中執行多個SQL語句怎么辦?我們還是用一個with語句實現:
```
with db.transaction():
db.select('...')
db.update('...')
db.update('...')
```
### 實現db模塊
由于模塊是全局對象,模塊變量是全局唯一變量,所以,有兩個重要的模塊變量:
```
# db.py
# 數據庫引擎對象:
class _Engine(object):
def __init__(self, connect):
self._connect = connect
def connect(self):
return self._connect()
engine = None
# 持有數據庫連接的上下文對象:
class _DbCtx(threading.local):
def __init__(self):
self.connection = None
self.transactions = 0
def is_init(self):
return not self.connection is None
def init(self):
self.connection = _LasyConnection()
self.transactions = 0
def cleanup(self):
self.connection.cleanup()
self.connection = None
def cursor(self):
return self.connection.cursor()
_db_ctx = _DbCtx()
```
由于`_db_ctx`是`threadlocal`對象,所以,它持有的數據庫連接對于每個線程看到的都是不一樣的。任何一個線程都無法訪問到其他線程持有的數據庫連接。
有了這兩個全局變量,我們繼續實現數據庫連接的上下文,目的是自動獲取和釋放連接:
```
class _ConnectionCtx(object):
def __enter__(self):
global _db_ctx
self.should_cleanup = False
if not _db_ctx.is_init():
_db_ctx.init()
self.should_cleanup = True
return self
def __exit__(self, exctype, excvalue, traceback):
global _db_ctx
if self.should_cleanup:
_db_ctx.cleanup()
def connection():
return _ConnectionCtx()
```
定義了`__enter__()`和`__exit__()`的對象可以用于with語句,確保任何情況下`__exit__()`方法可以被調用。
把`_ConnectionCtx`的作用域作用到一個函數調用上,可以這么寫:
```
with connection():
do_some_db_operation()
```
但是更簡單的寫法是寫個@decorator:
```
@with_connection
def do_some_db_operation():
pass
```
這樣,我們實現`select()`、`update()`方法就更簡單了:
```
@with_connection
def select(sql, *args):
pass
@with_connection
def update(sql, *args):
pass
```
注意到Connection對象是存儲在`_DbCtx`這個`threadlocal`對象里的,因此,嵌套使用`with connection()`也沒有問題。`_DbCtx`永遠檢測當前是否已存在Connection,如果存在,直接使用,如果不存在,則打開一個新的Connection。
對于transaction也是類似的,`with transaction()`定義了一個數據庫事務:
```
with db.transaction():
db.select('...')
db.update('...')
db.update('...')
```
函數作用域的事務也有一個簡化的@decorator:
```
@with_transaction
def do_in_transaction():
pass
```
事務也可以嵌套,內層事務會自動合并到外層事務中,這種事務模型足夠滿足99%的需求。
事務嵌套比Connection嵌套復雜一點,因為事務嵌套需要計數,每遇到一層嵌套就+1,離開一層嵌套就-1,最后到0時提交事務:
```
class _TransactionCtx(object):
def __enter__(self):
global _db_ctx
self.should_close_conn = False
if not _db_ctx.is_init():
_db_ctx.init()
self.should_close_conn = True
_db_ctx.transactions = _db_ctx.transactions + 1
return self
def __exit__(self, exctype, excvalue, traceback):
global _db_ctx
_db_ctx.transactions = _db_ctx.transactions - 1
try:
if _db_ctx.transactions==0:
if exctype is None:
self.commit()
else:
self.rollback()
finally:
if self.should_close_conn:
_db_ctx.cleanup()
def commit(self):
global _db_ctx
try:
_db_ctx.connection.commit()
except:
_db_ctx.connection.rollback()
raise
def rollback(self):
global _db_ctx
_db_ctx.connection.rollback()
```
最后,把`select()`和`update()`方法實現了,db模塊就完成了。
- JavaScript教程
- JavaScript簡介
- 快速入門
- 基本語法
- 數據類型和變量
- 字符串
- 數組
- 對象
- 條件判斷
- 循環
- Map和Set
- iterable
- 函數
- 函數定義和調用
- 變量作用域
- 方法
- 高階函數
- map/reduce
- filter
- sort
- 閉包
- 箭頭函數
- generator
- 標準對象
- Date
- RegExp
- JSON
- 面向對象編程
- 創建對象
- 原型繼承
- 瀏覽器
- 瀏覽器對象
- 操作DOM
- 更新DOM
- 插入DOM
- 刪除DOM
- 操作表單
- 操作文件
- AJAX
- Promise
- Canvas
- jQuery
- 選擇器
- 層級選擇器
- 查找和過濾
- 操作DOM
- 修改DOM結構
- 事件
- 動畫
- 擴展
- underscore
- Collections
- Arrays
- Functions
- Objects
- Chaining
- Node.js
- 安裝Node.js和npm
- 第一個Node程序
- 模塊
- 基本模塊
- fs
- stream
- http
- buffer
- Web開發
- koa
- mysql
- swig
- 自動化工具
- 期末總結
- Python 2.7教程
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷和循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 函數式編程
- 高階函數
- map/reduce
- filter
- sorted
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 使用__future__
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 面向對象高級編程
- 使用__slots__
- 使用@property
- 多重繼承
- 定制類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- 常用第三方模塊
- PIL
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 協程
- gevent
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫數據庫模塊
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 添加配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- 期末總結
- Python3教程
- Python簡介
- 安裝Python
- Python解釋器
- 第一個Python程序
- 使用文本編輯器
- Python代碼運行助手
- 輸入和輸出
- Python基礎
- 數據類型和變量
- 字符串和編碼
- 使用list和tuple
- 條件判斷
- 循環
- 使用dict和set
- 函數
- 調用函數
- 定義函數
- 函數的參數
- 遞歸函數
- 高級特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函數式編程
- 高階函數
- map/reduce
- filter
- sorted
- 返回函數
- 匿名函數
- 裝飾器
- 偏函數
- 模塊
- 使用模塊
- 安裝第三方模塊
- 面向對象編程
- 類和實例
- 訪問限制
- 繼承和多態
- 獲取對象信息
- 實例屬性和類屬性
- 面向對象高級編程
- 使用__slots__
- 使用@property
- 多重繼承
- 定制類
- 使用枚舉類
- 使用元類
- 錯誤、調試和測試
- 錯誤處理
- 調試
- 單元測試
- 文檔測試
- IO編程
- 文件讀寫
- StringIO和BytesIO
- 操作文件和目錄
- 序列化
- 進程和線程
- 多進程
- 多線程
- ThreadLocal
- 進程 vs. 線程
- 分布式進程
- 正則表達式
- 常用內建模塊
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模塊
- PIL
- virtualenv
- 圖形界面
- 網絡編程
- TCP/IP簡介
- TCP編程
- UDP編程
- 電子郵件
- SMTP發送郵件
- POP3收取郵件
- 訪問數據庫
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web開發
- HTTP協議簡介
- HTML簡介
- WSGI接口
- 使用Web框架
- 使用模板
- 異步IO
- 協程
- asyncio
- async/await
- aiohttp
- 實戰
- Day 1 - 搭建開發環境
- Day 2 - 編寫Web App骨架
- Day 3 - 編寫ORM
- Day 4 - 編寫Model
- Day 5 - 編寫Web框架
- Day 6 - 編寫配置文件
- Day 7 - 編寫MVC
- Day 8 - 構建前端
- Day 9 - 編寫API
- Day 10 - 用戶注冊和登錄
- Day 11 - 編寫日志創建頁
- Day 12 - 編寫日志列表頁
- Day 13 - 提升開發效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 編寫移動App
- FAQ
- 期末總結
- Git教程
- Git簡介
- Git的誕生
- 集中式vs分布式
- 安裝Git
- 創建版本庫
- 時光機穿梭
- 版本回退
- 工作區和暫存區
- 管理修改
- 撤銷修改
- 刪除文件
- 遠程倉庫
- 添加遠程庫
- 從遠程庫克隆
- 分支管理
- 創建與合并分支
- 解決沖突
- 分支管理策略
- Bug分支
- Feature分支
- 多人協作
- 標簽管理
- 創建標簽
- 操作標簽
- 使用GitHub
- 自定義Git
- 忽略特殊文件
- 配置別名
- 搭建Git服務器
- 期末總結