python濃縮(21)

  • 數據庫 和 Python RDBMSs, ORMs, and Pythonhtml

  • Python 數據庫應用程序程序員接口(DB-API)前端

  • 關係數據庫 (RDBMSs)python

  • 對象-關係管理器(ORMs)mysql

  • 關係模塊linux

本章的主題是如何經過Python 訪問數據庫。程序員

21.1 介紹

21.1.1 持久存儲

在任何的應用程序中,都須要持久存儲。通常說來,有三種基本的存儲機制:文件、關係型數據庫或其它的一些變種,例如現有系統的API,ORM、文件管理器、電子表格、配置文件等等。前面研究了經過基於常規文件的 Python 和 DBM 接口來實現持久存儲, 好比*dbm, dbhas/bsddb 文件, shelve(pickle 和DBM 的結合). 這些接口都提供了相似字典的對象接口。sql

本章的主題是如何在中大型項目中使用關係型數據庫.(對這些項目而言, 那些接口力不從心)數據庫

21.1.2 基本的數據庫操做和SQL 語言

先簡單介紹一下基本的數據庫概念和結構化查詢語言(SQL)。編程

底層存儲小程序

數據庫的底層存儲一般使用文件系統, 它能夠是普通操做系統文件、專用操做系統文件,甚至有多是磁盤分區。

用戶界面

大部分的數據庫系統會提供一個命令行工具來執行SQL 命令和查詢,固然也有一些使用圖形界面的客戶端程序來幹一樣的事。

數據庫

關係型數據庫管理系統一般一般都支持多個數據庫,例如銷售庫等等. 若是你使用的關係數據庫管理系統是基於服務器的,這些數據庫就都在同一臺服務器上 (一些簡單的關係型數據庫沒有服務器, 如sqlite). 本章的例子中, MySQL 是一種基於服務器的關係數據庫管理系統(只要服務器在運行, 它就一直在等待運行指令),SQLite 和Gadfly 則是另外一種輕量型的基於文件的關係數據庫(它們沒有服務器)。

組件

能夠將數據庫存儲想像爲一個表格, 每行數據都有一個或多個字段對應着數據庫中的列. 每一個表每一個列及其數據類型的集合構成數據庫結構的定義. 數據庫可以被建立, 也能夠被刪除. 表也同樣. 這些操做一般做爲數據庫操做命令來提交. 從一個數據庫中請求符合條件的數據稱爲查詢(querying). 當你對一個數據庫進行查詢時, 你能夠一步取回全部符合條件的數據, 也能夠循環逐條取出每一行. 有些數據庫使用遊標的概念來表示 SQL 命令, 查詢, 取回結果集等等.

SQL

數據庫命令和查詢操做須要經過SQL 語句來執行. 不是全部的數據庫都使用SQL, 但全部主流的關係型數據庫都使用SQL. 絕大多數數據庫被配置爲大小寫不敏感,除了數據庫操做命令之外. 被廣爲接受的書寫SQL 的基本風格是關鍵字大寫. 絕大多數命令行程序要求用一個分號來結束一條SQL 語句.

建立數據庫

CREATE DATABASE test;

GRANT ALL ON test.* to user(s);

第二行將該數據庫的權限賦給具體的用戶(或者所有用戶)

選擇要使用的數據庫

USE test;

刪除數據庫

DROP DATABASE test;

它用來刪除數據庫(包括數據庫中全部的表及表中的數據).

建立表

CREATE TABLE users (login VARCHAR(8), uid INT, prid INT);

刪除表

DROP TABLE users;

刪除數據庫中的一個表和它的全部數據。

插入行

INSERT INTO users VALUES('leanna', 311, 1);

語句中必須指定要插入的表及該表中各個字段的值。字符串'leanna' 對應着 login 字段,311 和 1 分別對應着uid 和prid。

更新行

UPDATE users SET prid=4 WHERE prid=2;

UPDATE users SET prid=1 WHERE uid=311;

使用 SET 關鍵字來指定你要修改的字段及新值,能夠指定條件來篩選出須要更新的記錄.

刪除行

DELETE FROM users WHERE prid=%d;

DELETE FROM users;

若是未提供(可選的)篩選條件, 就象第二個例子同樣, 表中全部的數據都會被刪除.

21.1.3 數據庫 和 Python

Python 直接經過數據庫接口(Python 數據庫API)或 經過 ORM來訪問關係數據庫.

相似數據庫原理, 併發能力, 視圖, 原子性, 數據完整性, 數據可恢復性, 還有左鏈接, 觸發器, 查詢優化, 事務支持, 及存儲過程等等, 本章不討論, 咱們要了解在 python 框架下如何將數據保存到數據庫, 若是將數據從數據庫中取出來. 與數據庫協同工做已是幾乎全部應用程序的核心部分,下面介紹一下 Python DB-API。

要適應其它的數據庫也至關容易, 須要特別提到的是 Aaron Watter 的 Gadfly 數據庫, 一個徹底由Python 代碼寫成的數據庫系統。從python 中訪問數據庫須要接口程序. 接口程序是一個 python 模塊, 它提供數據庫客戶端庫(一般是C 語言寫成的)的接口供你訪問。全部 Python 接口程序都必定程度上遵照 Python DB-API 規範. 

圖21.1 演繹了 Python 數據庫應用程序的結構(包括使用和不使用 ORM). 你能夠看到 DB-API是數據庫客戶端 C 庫的接口.

圖 21-1 數據庫和應用程序之間的多層通信.第一個盒子通常是 C/C++ 程序, 你的程序經過DB-API 兼容接口程序訪問數據庫.ORM 經過程序處理數據庫細節來簡化數據庫開發.

21.2 Python 數據庫應用程序

程序員接口(DB-API)

去 python.org 找到數據庫主題那一節, 會發現全部支持 DB-API 2.0 的各類數據庫模塊, 文檔, SIG 等等.  什麼是 DB-API ?DB-API 是一個規範. 它定義了一系列必須的對象和數據庫存取方式, 以便爲各類各樣的底層數據庫系統和多種多樣的數據庫接口程序提供一致的訪問接口。

DB-API 爲不一樣的數據庫提供了一致的訪問接口, 在不一樣的數據庫之間移植代碼成爲一件輕鬆的事情(通常來講, 只修要修改幾行代碼). 接下來你會看到這樣的例子.

21.2.1 模塊屬性

一個 DB-API 兼容的模塊必須定義下表 Table 21.1中定義的全部全局屬性.

數據屬性

apilevel

apilevel 這個字符串表示這個模塊所兼容的DB-API 最高版本號. 若是未定義, 則默認是 "1.0";

threadsafety

threadsafety 這是一個整數, 取值範圍以下:

  • 0:不支持線程安全, 多個線程不能共享此模塊

  • 1:初級線程安全支持: 線程能夠共享模塊, 但不能共享鏈接

  • 2:中級線程安全支持 線程能夠共享模塊和鏈接, 但不能共享遊標

  • 3:徹底線程安全支持 線程能夠共享模塊, 鏈接及遊標.

若是一個資源被共享, 就必需使用自旋鎖或者是信號量這樣的同步原語對其進行原子目標鎖定。對這個目標來講, 磁盤文件和全局變量都不可靠, 而且有可能妨礙 .

mutex(互斥量)的操做. 參閱 threading 模塊或第16 章(多線程編程)來了解如何使用鎖.

paramstyle

DB-API 支持多種方式的SQL 參數風格. 這個參數是一個字符串, 代表SQL 語句中字符串替代的方式. (參閱表21.2)

表21.2 數據庫參數風格

函數屬性

connect 方法生成一個connect 對象, 經過這個對象來訪問數據庫. 符合標準的模塊都會實現 connect 方法. 數據庫鏈接參數能夠以一個 DSN 字符串的形式提供, 也能夠以多個位置相關參數的形式提供(若是你明確知道參數的順序的話), 也能夠以關鍵字參數的形式提供:

connect(dsn='myhost:MYDB',user='guido',password='234$')

使用 DSN 字符串仍是獨立參數? 這要看你鏈接的是哪一種數據庫. 若是你使用相似ODBC 或 JDBC 的 API, 你就應該使用 DSN 字符串. 若是你直接訪問數據庫, 你就會更傾向於使用獨立參數. 另外一個使用獨立參數的緣由是, 不少數據庫接口程序還不支持 DSN 參數. 下面是一個非 DSN 的例子.

connect()調用. 注意不是全部的接口程序都是嚴格按照規範實現的. MySQLdb 就使用了 db 參數而不是規範推薦的 database 參數來表示要訪問的數據庫.

  • MySQLdb.connect(host='dbserv', db='inv', user='smith')

  • PgSQL.connect(database='sales')

  • psycopg.connect(database='template1', user='pgsql')

  • gadfly.dbapi20.connect('csrDB', '/usr/local/database')

  • sqlite3.connect('marketing/test')

異常

兼容標準的模塊也應該提供這些異常類. 見表 21.4

21.2.2 鏈接對象

要與數據庫進行通訊, 必須先和數據庫創建鏈接. 鏈接對象處理命令如何送往服務器, 以及如何從服務器接收數據等基礎功能. 鏈接成功(或一個鏈接池)後你就可以向數據庫服務器發送請求,獲得響應.

方法

鏈接對象沒有必須定義的數據屬性, 可是它至少應該定義表21.5 中的這些方法.一旦執行了 close() 方法, 再試圖使用鏈接對象的方法將會致使異常.

對不支持事務的數據庫或者雖然支持事務, 但設置了自動提交(auto-commit)的數據庫系統來講, commit()方法什麼也不作. 若是你確實須要, 能夠實現一個自定義方法來關閉自動提交行爲.因爲 DB-API 要求必須實現此方法, 對那些沒有事務概念的數據庫來講, 這個方法只須要有一條pass 語句就能夠了. 在提交commit()以前關閉數據庫鏈接將會自動調用rollback()方法.

對不支持遊標的數據庫來講, cursor()方法仍然會返回一個儘可能模仿遊標對象的對象. 這些是最低要求. 特定數據庫接口程序的開發者能夠任意爲他們的接口程序添加額外的屬性, 只要他們願意.

DB-API 規範建議但不強制接口程序的開發者爲全部數據庫接口模塊編寫異常類. 若是沒有提供異常類, 則假定該鏈接對象會引起一致的模塊級異常. 一旦你完成了數據庫鏈接, 而且關閉了遊標對象, 你應該執行 commit() 提交你的操做, 而後關閉這個鏈接.

21.2.3 遊標對象

創建鏈接以後, 就能夠與數據庫進行交互. 一個遊標容許用戶執行數據庫命令和獲得查詢結果. 一個 DB-API 遊標對象老是扮演遊標的角色, 不管數據庫是否真正支持遊標. 從這一點講, 數據庫接口程序必須實現遊標對象. 只有這樣, 才能保證不管使用何種後端數據庫你的代碼都不須要作任何改變.

建立遊標對象以後, 你就能夠執行查詢或其它命令(或者多個查詢和多個命令), 也能夠從結果集中取出一條或多條記錄. 表 21.6 列舉了遊標對象擁有的屬性和方法.

遊標對象最重要的屬性是 execute*() 和 fetch*() 方法. 全部對數據庫服務器的請求都由它們來完成.對fetchmany()方法來講, 設置一個合理的arraysize 屬性會頗有用. 固然, 在不須要時關掉遊標對象也是個好主意. 若是你的數據庫支持存儲過程, 你就可使用callproc() 方法.

21.2.4 類型對象和構造器

一般兩個不一樣系統的接口要求的參數類型是不一致的,譬如python調用C函數時Python對象和C類型之間就須要數據格式的轉換, 反之亦然. 相似的, 在Python 對象和原生數據庫對象之間也是如此. 對於 Python DB-API 的開發者來講, 你傳遞給數據庫的參數是字符串形式的, 但數據庫會根據須要將它轉換爲多種不一樣的形式. 以確保每次查詢能被正確執行.

舉例來講, 一個 Python 字符串可能被轉換爲一個 VARCHAR, 或一個TEXT, 或一個BLOB, 或一個原生 BINARY 對象, 或一個DATE 或TIME 對象. 一個字符串到底會被轉換成什麼類型? 必須當心的儘量以數據庫指望的數據類型來提供輸入, 所以另外一個DB-API 的需求是建立一個構造器以生成特殊的對象, 以便可以方便的將Python 對象轉換爲合適的數據庫對象. 表21.7 描述了能夠用於此目的的類. SQL 的 NULL 值被映射爲 Pyhton 的 NULL 對象, 也就是 None.

DB-API 版本變動

有幾個重要的變動發生在 DB-API 從1.0(1996)升級到2.0(1999)時:

  • 從 API 中移除了原來必須的 dbi 模塊

  • 更新了類型對象

  • 增長了新的屬性以提供更易用的數據庫綁定

  • 變動了 callproc() 的語義及重定義了 execute() 的返回值

  • 基於異常的錯誤處理

自從 DB-API 2.0 發佈以來, 曾經在2002 年加入了一些可選的 DB- API 擴展, 但一直沒有什麼重大的變動. 在DB-SIG 郵件列表中一直在討論DB-API 的將來版本 -- 暫時命名爲 DB-API 3.0. 它將包括如下特性:

當有一個新的結果集時nextset()會有一個更合適的返回值

  • float 變動爲 Decimal

  • 支持更靈活的參數風格

  • 預備語句或語句緩存

  • 優化事務模型

  • 肯定 DB-API 可移值性的角色

  • 增長單元測試

若是你對這些API 特別感興趣, 歡迎積極參與. 下面有一些手邊的資源.

  • http://python.org/topics/database

  • http://www.linuxjournal.com/article/2605

  • http://wiki.python.org/moin/DbApi3

21.2.5 關係數據庫

在Pyhton 裏可使用哪一種數據庫接口? 換言之,Python 支持哪些平臺? 是幾乎全部的平臺. 下面是一個不怎麼完整的數據庫支持列表:

商業關係數據庫管理系統

􀁺 Informix

􀁺 Sybase

􀁺 Oracle

􀁺 MS SQL Server

􀁺 DB/2

􀁺 SAP

􀁺 Interbase

􀁺 Ingres

開源關係數據庫管理系統

􀁺 MySQL

􀁺 PostgreSQL

􀁺 SQLite

􀁺 Gadfly

數據庫APIs

􀁺 JDBC

􀁺 ODBC

要知道 Python 支持哪些數據庫, 請參閱下面網址:

http://python.org/topics/database/modules.html

21.2.6 數據庫和Python:接口程序

某些數據庫, 好比 Sybase, SAP, Oracle 和SQLServer, 都有兩個或更多個接口程序可供選擇. 你要作的就是挑選一個最能知足你需求的接口程序. 你挑選接口程序的標準能夠是: 性能如何? 文檔或WEB 站點的質量如何? 是否有一個活躍的用戶或開發社區? 接口程序的質量和穩定性如何? 等等等等. 記住絕大多數接口程序只提供基本的鏈接功能, 你可能須要一些額外的特性. 高級應用代碼如線程和線程管理以及數據庫鏈接池的管理等等, 須要你本身來完成.

若是你不想處理這些, 比方你不喜歡本身寫SQL, 也不想參與數據庫管理的細節--那麼本章後面講到的 ORM 應該能夠知足你的要求. 如今來看一些使用接口程序訪問數據庫的例子, 關鍵之處在於設置數據庫鏈接.在創建鏈接以後, 無論後端是何種數據庫, 對 DB-API 對象的屬性和方法進行操做都是同樣的.

21.2.7 使用數據庫接口程序舉例

建立數據庫, 建立表, 使用表. 分別提供了使用 MySQL,PostgreSQL, SQLite 的例子.

MySQL

使用惟一的 MySQL 接口程序: MySQLdb, 又名MySQL-python. 

>>> import MySQLdb
>>> cxn = MySQLdb.connect(user='root')
>>> cxn.query('DROP DATABASE test') Traceback (most recent call last):
File "<stdin>", line 1, in ?
_mysql_exceptions.OperationalError: (1008, "Can't drop database 'test'; database
doesn't exist")
>>> cxn.query('CREATE DATABASE test')
>>> cxn.query("GRANT ALL ON test.* to ''@'localhost'")
>>> cxn.commit()
>>> cxn.close()

沒有使用cursor 對象。某些(但不是全部的)接口程序擁有鏈接對象,這些鏈接對象擁有query()方法, 能夠執行SQL 查詢。咱們建議你不要使用這個方法, 或者事先檢查該方法在當前接口程序當中是否可用。以後咱們以普通用戶身份再次鏈接這個新數據, 建立表,而後經過Python 執行SQL 查詢和命令, 來完成咱們的工做。此次咱們使用遊標對象(cursors)和它們的execute()方法, 下一個交互集演示了建立表。

>>> cxn = MySQLdb.connect(db='test')
>>> cur = cxn.cursor()
>>> cur.execute('CREATE TABLE users(login VARCHAR(8), uid INT)')
0L
>>> cur.execute("INSERT INTO users VALUES('john', 7000)")
1L
>>> cur.execute("INSERT INTO users VALUES('jane', 7001)")
1L
>>> cur.execute("INSERT INTO users VALUES('bob', 7200)")
1L
>>> cur.execute("SELECT * FROM users WHERE login LIKE 'j%'")
2L
>>> for data in cur.fetchall():
... print '%s\t%s' % data
...
john 7000
jane 7001

最後一個特性是更新表, 包括更新或刪除數據.

>>> cur.execute("UPDATE users SET uid=7100 WHERE uid=7001")
1L
>>> cur.execute("SELECT * FROM users")
3L
>>> for data in cur.fetchall():
... print '%s\t%s' % data
...
john 7000
jane 7100
bob 7200
>>> cur.execute('DELETE FROM users WHERE login="bob"')
1L
>>> cur.execute('DROP TABLE users')
0L
>>> cur.close()
>>> cxn.commit()
>>> cxn.close()

不過Python 標準庫中並無集成這個接口程序, 這是一個第三方包, 你須要單獨下載並安裝它. 

PostgreSQL

與 MySQL 不一樣, 有至少 3 個 Python 接口程序能夠訪問 PosgreSQL: psycopg, PyPgSQL 和 PyGreSQL. 多虧他們都支持 DB-API, 因此他們的接口基本一致, 你只須要寫一個應用程序, 而後分別測試這三個接口的性能(若是性能對你的程序很重要的化). 下面我給出這三個接口的鏈接代碼:

psycopg
>>> import psycopg
>>> cxn = psycopg.connect(user='pgsql')
PyPgSQL
>>> from pyPgSQL import PgSQL
>>> cxn = PgSQL.connect(user='pgsql')
PyGreSQL
>>> import pgdb
>>> cxn = pgdb.connect(user='pgsql')

好, 下面的代碼就可以在全部接口程序下工做了.

>>> cur = cxn.cursor()
>>> cur.execute('SELECT * FROM pg_database')
>>> rows = cur.fetchall()
>>> for i in rows:
... print i
>>> cur.close()
>>> cxn.commit()
>>> cxn.close()

最後, 你會發現他們的輸出有一點點輕微的不一樣.

PyPgSQL
sales
template1
template0
psycopg
('sales', 1, 0, 0, 1, 17140, '140626', '3221366099','', None, None)
('template1', 1, 0, 1, 1, 17140, '462', '462', '', None,'{pgsql=C*T*/pgsql}')
('template0', 1, 0, 1, 0, 17140, '462', '462', '', None,'{pgsql=C*T*/pgsql}')
PyGreSQL
['sales', 1, 0, False, True, 17140L, '140626','3221366099', '', None, None]
['template1', 1, 0, True, True, 17140L, '462', '462','', None, '{pgsql=C*T*/pgsql}']
['template0', 1, 0, True, False, 17140L, '462','462', '', None, '{pgsql=C*T*/pgsql}']

SQLite

對很是簡單的應用來講, 使用文件進行持久存儲一般就足夠了. 但對於絕大多數數據驅動的應用程序必須使用全功能的關係數據庫. SQLite 介於兩者之間, 它定位於中小規模的應用.它是至關輕量級的全功能關係型數據庫, 速度很快, 幾乎不用配置, 而且不須要服務器.SQLite 正在快速的流行. 而且在各個平臺上都能用. 在 python2.5 集成了前面介紹的pysqlite 數據庫接口程序, 做爲 python2.5 的 sqlite3 模塊. 這是 Python 標準庫第一次將一個數據庫接口程序歸入標準庫, 也許這標誌着一個新的開始.

它被打包到 Python 當中並非由於他比其它的數據庫接口程序更優秀, 而是由於他足夠簡單,使用文件(或內存)做爲它的後端存儲, 就象 DBM 模塊作的那樣, 不須要服務器, 並且也不存在受權問題. 它是Python 中其它的持久存儲解決方案的一個替代品, 一個擁有 SQL 訪問界面的優秀替代品. 在標準庫中有這麼一個模塊, 就能方便用戶使用Python 和 SQLite 進行軟件開發, 等到軟件產品正式上市發佈時, 只要須要, 就可以很容易的將產品使用的數據庫後端變動爲一個全功能的,更強大的相似 MySQL, PostgreSQL, Oracle 或 SQL Server 那樣的數據庫. 

>>> import sqlite3
>>> cxn = sqlite3.connect('sqlite_test/test')
>>> cur = cxn.cursor()
>>> cur.execute('CREATE TABLE users(login VARCHAR(8), uid INTEGER)')
>>> cur.execute('INSERT INTO users VALUES("john", 100)')
>>> cur.execute('INSERT INTO users VALUES("jane", 110)')
>>> cur.execute('SELECT * FROM users')
>>> for eachUser in cur.fetchall():
... print eachUser
...
(u'john', 100)
(u'jane', 110)
>>> cur.execute('DROP TABLE users')
<sqlite3.Cursor object at 0x3d4320>
>>> cur.close()
>>> cxn.commit()
>>> cxn.close()

接下來, 咱們來看一個小程序, 它相似前面使用 MySQL 的例子。

  • 建立數據庫(若是成功)

  • 建立表

  • 從表中增長行

  • 從表中修改行

  • 從表中刪除行

  • 刪除表

這個例子中用到的另外一個數據庫是 Gadfly, 一個基本兼容 SQL 的純 Python 寫成的關係數據庫. (某些關鍵的數據庫結構有一個C 模塊, 不過 Gadfly 沒有它也同樣能夠運行[固然, 會慢很多]).

SQLite 和Gadfly 須要用戶指定保存數據庫文件的位置(Mysql有一個默認區域保存數據, 在使用Mysql 數據庫時無需指定這個). 另外, Gadfly 目前的版本還不兼容 DB-API 2.0, 也就是說, 它缺失一些功能, 尤爲是缺乏咱們例子中用到的 cursor 屬性rowcount.

數據庫接口程序應用程序舉例

下面演示瞭如何訪問數據庫. 事實上, 咱們的程序支持三種不一樣的數據庫系統: Gadfly, SQLite 和 MySQL. 

#!/usr/bin/env python

import os
from random import randrange as rrange

COLSIZ = 10
RDBMSs = {'s': 'sqlite', 'm': 'mysql', 'g': 'gadfly'}
# 值得留意的是 DB_EXC 常量, 它表明數據庫異常. 他最終的值由用戶最終選擇使用的數據庫決定. 
DB_EXC = None

def setup():
    return RDBMSs[raw_input('''
Choose a database system:

(M)ySQL
(G)adfly
(S)QLite

Enter choice: ''').strip().lower()[0]]

def connect(db, dbName):
    global DB_EXC
    dbDir = '%s_%s' % (db, dbName)

    if db == 'sqlite':
		# 首先嚐試載入標準庫模塊 sqlite3(Python2.5 及更高版本支持), 若是載入失敗, 就會去尋找第三方 pysqlite2 包.
        try:
            import sqlite3
        except ImportError, e:
            try:
                from pysqlite2 import dbapi2 as sqlite3
            except ImportError, e:
                return None

        DB_EXC = sqlite3
		# 確認一下數據庫文件所在的目錄是否存在.(固然, 你也能夠選擇在內存裏建立一個數據庫)
        if not os.path.isdir(dbDir):
            os.mkdir(dbDir)
        cxn = sqlite3.connect(os.path.join(dbDir, dbName))

    elif db == 'mysql':
		# 數據文件會存保在默認的數據存儲區域, 因此不須要用戶指定存儲位置
        try:
            import MySQLdb
            import _mysql_exceptions as DB_EXC 
        except ImportError, e:
            return None

        try:
            cxn = MySQLdb.connect(db=dbName)
        except DB_EXC.OperationalError, e:
            cxn = MySQLdb.connect(user='root')
            try:
                cxn.query('DROP DATABASE %s' % dbName)
            except DB_EXC.OperationalError, e:
                pass
            cxn.query('CREATE DATABASE %s' % dbName)
            cxn.query("GRANT ALL ON %s.* to ''@'localhost'" % dbName)
            cxn.commit()
            cxn.close()
            cxn = MySQLdb.connect(db=dbName)

    elif db == 'gadfly':
		# 使用相似SQLite 的啓動機制: 它的啓動目錄是數據文件所在的目錄
        try:
            from gadfly import gadfly
            DB_EXC = gadfly
        except ImportError, e:
            return None
		# 若是數據文件在那兒, OK, 若是那兒沒有數據文件,你必須從新啓動一個新的數據庫
        try:
            cxn = gadfly(dbName, dbDir)
        except IOError, e:
            cxn = gadfly()
            if not os.path.isdir(dbDir):
                os.mkdir(dbDir)
            cxn.startup(dbName, dbDir)
    else:
        return None
    return cxn

def create(cur):
	# 這個代碼有一個缺陷, 就是當重建表仍然失敗的話, 你將陷入死循環, 直至內存耗盡
    try:
        cur.execute('''
            CREATE TABLE users (
                login VARCHAR(8),
                uid INTEGER,
                prid INTEGER)
        ''')
    except DB_EXC.OperationalError, e:
        drop(cur)
        create(cur)

drop = lambda cur: cur.execute('DROP TABLE users')

# 它由一組固定用戶名及ID 值的集合及一個生成器函數 randName() 構成. 
# NAMES 常量是一個元組, 由於咱們在randName()這個生成器裏須要改變它的值, 因此咱們必須在 randName()裏先將它轉換爲一個列表. 
# 咱們一次隨機的移除一個名字, 直到列表爲空爲止. 若是 NAMES 自己是一個列表, 咱們只能使用它一次(它就被消耗光了). 
# 咱們將它設計成爲一個元組, 這樣咱們就能夠屢次從這個元組生成一個列表供生成器使用.
NAMES = (
    ('aaron', 8312), ('angela', 7603), ('dave', 7306),
    ('davina',7902), ('elliot', 7911), ('ernie', 7410),
    ('jess', 7912), ('jim', 7512), ('larry', 7311),
    ('leslie', 7808), ('melissa', 8602), ('pat', 7711),
    ('serena', 7003), ('stan', 7607), ('faye', 6812),
    ('amy', 7209),
)

def randName():
    pick = list(NAMES)
    while len(pick) > 0:
        yield pick.pop(rrange(len(pick)))

def insert(cur, db):
	# insert()函數裏的代碼是依賴具體數據庫的. 舉例來講,SQLite 和 MySQL 的接口程序都是 DB-API 兼容的, 因此它們的遊標對象都擁有 executemany()方法, 
	# 但是是 Gadfly 沒有這個方法, 所以它只能一次插入一行.另外一個不一樣之處在於 SQLite 和 Gadfly 的參數風格是 qmark, 而MySQL 的參數風格是format.
    if db == 'sqlite':
        cur.executemany("INSERT INTO users VALUES(?, ?, ?)",
        [(who, uid, rrange(1,5)) for who, uid in randName()])
    elif db == 'gadfly':
        for who, uid in randName():
            cur.execute("INSERT INTO users VALUES(?, ?, ?)",
            (who, uid, rrange(1,5)))
    elif db == 'mysql':
        cur.executemany("INSERT INTO users VALUES(%s, %s, %s)",
        [(who, uid, rrange(1,5)) for who, uid in randName()])

getRC = lambda cur: cur.rowcount if hasattr(cur, 'rowcount') else -1

def update(cur):
    fr = rrange(1,5)
    to = rrange(1,5)
    cur.execute(
        "UPDATE users SET prid=%d WHERE prid=%d" % (to, fr))
    return fr, to, getRC(cur)

def delete(cur):
    rm = rrange(1,5)
    cur.execute('DELETE FROM users WHERE prid=%d' % rm)
    return rm, getRC(cur)

def dbDump(cur):
	# 首先, 經過 fetchall() 方法讀取數據, 而後迭代遍歷每一個用戶, 將三列數據(login, uid,prid)轉換爲字符串(若是它們還不是的話), 
	# 並將姓和名的首字母大寫, 再格式化整個字符爲左對齊的COLSIZ 列.( 右邊留白) .
	# 由代碼生成的字符串是一個列表( 經過列表解析, listcomprehension), 咱們須要將它們轉換成一個元組以支持 % 運算符.
    cur.execute('SELECT * FROM users')
    print '\n%s%s%s' % ('LOGIN'.ljust(COLSIZ),
        'USERID'.ljust(COLSIZ), 'PROJ#'.ljust(COLSIZ))
    for data in cur.fetchall():
        print '%s%s%s' % tuple([str(s).title().ljust(COLSIZ) \
            for s in data])

def main():
    db = setup()
    print '*** Connecting to %r database' % db
    cxn = connect(db, 'test')
    if not cxn:
        print '\nERROR: %r not supported, exiting' % db
        return
    cur = cxn.cursor()

    print '\n*** Creating users table'
    create(cur)

    print '\n*** Inserting names into table'
    insert(cur, db)
    dbDump(cur)

    print '\n*** Randomly moving folks',
    fr, to, num = update(cur)
    print 'from one group (%d) to another (%d)' % (fr, to)
    print '\t(%d users moved)' % num
    dbDump(cur)

    print '\n*** Randomly choosing group',
    rm, num = delete(cur)
    print '(%d) to delete' % rm
    print '\t(%d users removed)' % num
    dbDump(cur)

    print '\n*** Dropping users table'
    drop(cur)
    cur.close()
    cxn.commit()
    cxn.close()

if __name__ == '__main__':
    main()

在數據庫鏈接創建之後, 其他的代碼對數據庫和接口程序來講都是透明的(不區分哪一種數據庫,哪一種接口程序, 代碼均可以工做). 有一個惟一的例外, 就是腳本的 insert()函數. 在全部三個小節的這段代碼裏, 數據庫鏈接成功後會返回一個鏈接對象 cxn.

它返回最後一步操做所影響的行數, 若是遊標對象不支持這個屬性(也就是說這個接口程序不兼容 DB-API)的話, 它返回 -1. python 2.5 中新增了條件表達式, 若是你使用的是python 2.4.x 或更老版本, 你可能須要將它轉換爲老風格的方式, 以下:

getRC = lambda cur: (hasattr(cur, 'rowcount') \

and [cur.rowcount] or [-1])[0]

(假定它們沒有由於找不到數據庫接口程序或者不能獲得有效鏈接對象而中途退出[第143-145 行]). 

21.3 對象-關係管理器(ORMs)

若是你是一個喜歡折騰 Python 對象卻討厭 SQL 查詢的傢伙, 又想使用關係型數據庫作爲你的數據存儲的後端, 你就徹底具有成爲一個 ORM 用戶的天資.

21.3.1 考慮對象,而不是SQL

這些系統的建立者將絕大多數純 SQL 層功能抽象爲Python 對象, 這樣你就無需編寫SQL 也可以完成一樣的任務. 數據庫的表被轉換爲 Python 類, 它具備列屬性和操做數據庫的方法. 因爲大部分工做由 ORM 代爲處理, 相比直接使用接口程序來講, 一些事情可能實際須要更多的代碼. 

21.3.2 Python 和 ORM

現在最知名的 Python ORM 模塊是 SQLAlchemy 和 SQLObject. 會分別給出 SQLAlchemy 和 SQLObject 的例子. 其它的 Python ORM 包括 PyDO/PyDO2, PDO, Dejavu, Durus, QLime 和 ForgetSQL. 一些大型的Web 開發工具/框架也能夠有本身的 ORM 組件, 如 WebWare MiddleKit 和 Django 的數據庫 API.

21.3.3 僱員數據庫舉例

將 shuffle 應用程序 ushuffle_db.py 改造爲使用 SQLAlchemy 和 SQLObject 實現. 數據庫後端仍然是 MySQL.  兩個例子都使用了 ushuffle_db.py 中的 NAMES 集合和 隨機名字選擇函數. 

SQLAlchemy

與SQLObject 相比, SQLAlchemy 的接口在某種程度上更接近 SQL, 因此咱們先從 SQLAlchemy開始.SQLAlchemy 的抽象層確實至關完美, 並且在你必須使用SQL 完成某些功能時, 它提供了足夠的靈活性. 你會發現這兩個ORM 模塊在設置及存取數據時使用的術語很是類似, 代碼長度也很接近,都比ushuffle_db.py 少. 

倡導首先導入Python 標準庫模塊, 而後再導入第三方或擴展模塊, 最後導入本地模塊這種風格. 這些常量都是自解釋的, 因此無需廢話.

第12-31 行

是類的構造器, 相似 ushuffle_db.connect(). 它確保數據庫可用並返回一個有效鏈接(第18-31 行). 這也是惟一能看到原始 SQL 的地方. 這是一種典型的操做任務, 不是面向應用的任務.

第33-44 行

這個 try-except 子句(第33-40 行)用來從新載入一個已有的表, 或者在表不存在的狀況下建立一個新表. 最終咱們獲得一個合適的對象實例.

例子21.2 SQLAlchemy ORM 例子

這個 user shuffle 程序的主角是 SQLAlchemy 前端 和 MySQL 數據庫後端

1 #!/usr/bin/env python

2

3 import os

4 from random import randrange as rrange

5 from sqlalchemy import *

6 from ushuffle_db import NAMES, randName

7

8 FIELDS = ('login', 'uid', 'prid')

9 DBNAME = 'test'

10 COLSIZ = 10

11

12 class MySQLAlchemy(object):

13 def __init__(self, db, dbName):

14 import MySQLdb

15 import _mysql_exceptions

16 MySQLdb = pool.manage(MySQLdb)

17 url = 'mysql://db=%s' % DBNAME

18 eng = create_engine(url)

19 try:

20 cxn = eng.connection()

21 except _mysql_exceptions.OperationalError, e:

22 eng1 = create_engine('mysql://user=root')

23 try:

24 eng1.execute('DROP DATABASE %s' % DBNAME)

25 except _mysql_exceptions.OperationalError, e:

26 pass

27 eng1.execute('CREATE DATABASE %s' % DBNAME)

28 eng1.execute(

29 "GRANT ALL ON %s.* TO ''@'localhost'" % DBNAME)

30 eng1.commit()

31 cxn = eng.connection()

32

33 try:

Edit By Vheavens

Edit By Vheavens

34 users = Table('users', eng, autoload=True)

35 except exceptions.SQLError, e:

36 users = Table('users', eng,

37 Column('login', String(8)),

38 Column('uid', Integer),

39 Column('prid', Integer),

40 redefine=True)

41

42 self.eng = eng

43 self.cxn = cxn

44 self.users = users

45

46 def create(self):

47 users = self.users

48 try:

49 users.drop()

50 except exceptions.SQLError, e:

51 pass

52 users.create()

53

54 def insert(self):

55 d = [dict(zip(FIELDS,

56 [who, uid, rrange(1,5)])) for who,uid in randName()]

57 return self.users.insert().execute(*d).rowcount

58

59 def update(self):

60 users = self.users

61 fr = rrange(1,5)

62 to = rrange(1,5)

63 return fr, to, \

64 users.update(users.c.prid==fr).execute(prid=to).rowcount

65

66 def delete(self):

67 users = self.users

68 rm = rrange(1,5)

69 return rm, \

70 users.delete(users.c.prid==rm).execute().rowcount

71

72 def dbDump(self):

73 res = self.users.select().execute()

74 print '\n%s%s%s' % ('LOGIN'.ljust(COLSIZ),

75 'USERID'.ljust(COLSIZ), 'PROJ#'.ljust(COLSIZ))

76 for data in res.fetchall():

77 print '%s%s%s' % tuple([str(s).title().ljust

(COLSIZ) for s in data])

78

79 def __getattr__(self, attr):

80 return getattr(self.users, attr)

81

82 def finish(self):

83 self.cxn.commit()

84 self.eng.commit()

85

86 def main():

87 print '*** Connecting to %r database' % DBNAME

88 orm = MySQLAlchemy('mysql', DBNAME)

89

90 print '\n*** Creating users table'

91 orm.create()

92

93 print '\n*** Inserting names into table'

94 orm.insert()

95 orm.dbDump()

96

97 print '\n*** Randomly moving folks',

98 fr, to, num = orm.update()

99 print 'from one group (%d) to another (%d)' % (fr, to)

100 print '\t(%d users moved)' % num

101 orm.dbDump()

102

103 print '\n*** Randomly choosing group',

104 rm, num = orm.delete()

105 print '(%d) to delete' % rm

106 print '\t(%d users removed)' % num

107 orm.dbDump()

108

109 print '\n*** Dropping users table'

110 orm.drop()

111 orm.finish()

112

113 if __name__ == '__main__':

114 main()

例子21.2 SQLAlchemy ORM 示例(ushuffle_sa.py)

第46-70 行

這四個方法處理數據庫核心功能: 建立表(46-52 行), 插入數據(54-57 行), 更新數據(59-64行), 刪除數據(66-70 行). 咱們也有一個方法用來刪除表.

def drop(self):

self.users.drop()

or

drop = lambda self: self.users.drop()

不過, 咱們仍是決定提供另外一種委託處理方式(曾在第13 章, 面向對象編程中介紹). 委託處理就是指一個方法調用不存在時, 轉交給另外一個擁有此方法的對象去處理. 參見第79-80 行的解釋.

第72-77 行

輸出內容由dbDump()方法完成. 它從數據庫中獲得數據, 就象 ushuffle_db.py 中那樣對數據進行美化, 事實上, 這部分代碼幾乎徹底相同.

Lines 79–80

應該儘可能避免爲一個表建立一個 drop() 方法, 由於這老是會調用 table 自身的 drop() 方法. 一樣, 既然沒有新增功能, 那咱們有什麼必要要建立另外一個函數?不管屬性查找是否成功特殊方法 __getattr__() 老是會被調用. 若是調用 orm.drop() 卻發現這個對象並無 drop() 方法,getattr(orm, 'drop')就會被調用. 發生這種狀況時,__getattr__() 被調用, 以後將這個屬性名委託給 self.users. 解釋器會發現 self.users 有一個 drop 屬性並執行.

Example 21.3 SQLObject ORM Example (ushuffle_so.py)

這個 user shuffle 應用程序的主角前端是 SQLObject, 後端是MySQL 數據庫.

1 #!/usr/bin/env python

2

3 import os

4 from random import randrange as rrange

5 from sqlobject import *

6 from ushuffle_db import NAMES, randName

7

8 DBNAME = 'test'

9 COLSIZ = 10

Edit By Vheavens

Edit By Vheavens

10 FIELDS = ('login', 'uid', 'prid')

11

12 class MySQLObject(object):

13 def __init__(self, db, dbName):

14 import MySQLdb

15 import _mysql_exceptions

16 url = 'mysql://localhost/%s' % DBNAME

17

18 while True:

19 cxn = connectionForURI(url)

20 sqlhub.processConnection = cxn

21 #cxn.debug = True

22 try:

23 class Users(SQLObject):

24 class sqlmeta:

25 fromDatabase = True

26 login = StringCol(length=8)

27 uid = IntCol()

28 prid = IntCol()

29 break

30 except _mysql_exceptions.ProgrammingError, e:

31 class Users(SQLObject):

32 login = StringCol(length=8)

33 uid = IntCol()

34 prid = IntCol()

35 break

36 except _mysql_exceptions.OperationalError, e:

37 cxn1 = sqlhub.processConnection=

connectionForURI('mysql://root@localhost ')

38 cxn1.query("CREATE DATABASE %s" % DBNAME)

39 cxn1.query("GRANT ALL ON %s.* TO ''@'

localhost'" % DBNAME)

40 cxn1.close()

41 self.users = Users

42 self.cxn = cxn

43

44 def create(self):

45 Users = self.users

46 Users.dropTable(True)

47 Users.createTable()

48

49 def insert(self):

50 for who, uid in randName():

51 self.users(**dict(zip(FIELDS,

52 [who, uid, rrange(1,5)])))

53

54 def update(self):

55 fr = rrange(1,5)

56 to = rrange(1,5)

57 users = self.users.selectBy(prid=fr)

58 for i, user in enumerate(users):

59 user.prid = to

60 return fr, to, i+1

61

62 def delete(self):

63 rm = rrange(1,5)

64 users = self.users.selectBy(prid=rm)

65 for i, user in enumerate(users):

66 user.destroySelf()

67 return rm, i+1

68

69 def dbDump(self):

70 print '\n%s%s%s' % ('LOGIN'.ljust(COLSIZ),

71 'USERID'.ljust(COLSIZ), 'PROJ#'.ljust(COLSIZ))

72 for usr in self.users.select():

73 print '%s%s%s' % (tuple([str(getattr(usr,

74 field)).title().ljust(COLSIZ) \

75 for field in FIELDS]))

76

77 drop = lambda self: self.users.dropTable()

78 finish = lambda self: self.cxn.close()

79

80 def main():

81 print '*** Connecting to %r database' % DBNAME

82 orm = MySQLObject('mysql', DBNAME)

83

84 print '\n*** Creating users table'

85 orm.create()

86

87 print '\n*** Inserting names into table'

88 orm.insert()

89 orm.dbDump()

90

91 print '\n*** Randomly moving folks',

92 fr, to, num = orm.update()

93 print 'from one group (%d) to another (%d)' % (fr, to)

94 print '\t(%d users moved)' % num

95 orm.dbDump()

96

97 print '\n*** Randomly choosing group',

98 rm, num = orm.delete()

99 print '(%d) to delete' % rm

100 print '\t(%d users removed)' % num

101 orm.dbDump()

102

103 print '\n*** Dropping users table'

104 orm.drop()

105 orm.finish()

106

107 if __name__ == '__main__':

108 main()

Lines 82–84

最後一個方法是 finish, 它來提交整個事務.

第86-114 行

main() 函數是整個應用程序的入口, 它建立了一個 MySQLAlchemy 對象並經過它完成全部的數據庫操做. 這段腳本和 ushuffle_db.py 功能同樣. 你會注意到數據庫參數 db 是可選的,並且在 ushuffle_sa.py 和即將碰到 的ushuffle_so.py 中, 它不起任何做用. 它只是一個佔位符以方便你對這個應用程序添加其它的數據庫支持.(參見本章後面的習題)

運行這段腳本, 你會看到相似下面的輸出:

$ ushuffle_sa.py

*** Connecting to 'test' database

*** Creating users table

*** Inserting names into table

逐行解釋

第1-10 行

除了咱們使用的是 SQLObject 而不是 SQLAlchemy 之外, 導入模塊和常量聲明幾乎與ushuffle_sa.py 相同.

12-42 行

相似咱們的SQLAlchemy 例子, 類的構造器作大量工做以確保有一個數據庫可用, 而後返回一個鏈接. 一樣的, 這也是你能在程序裏看到SQL 語句的惟一位置. 咱們這個程序, 若是由於某種緣由形成SQLObject 沒法成功建立用戶表, 就會陷入無限循環當中.

咱們嘗試可以聰明的處理錯誤, 解決掉這個重建表的問題. 由於 SQLObject 使用元類, 咱們知道類的建立幕後發生特殊事件, 因此咱們不得不定義兩個不一樣的類, 一個用於表已經存在的狀況,

一個用於表不存的狀況. 代碼工做原理以下:

1. 嘗試創建一個鏈接到一個已經存在的表. 若是正常工做, OK. (第23-29 行)

2.若是第一步不成功, 則從零開始爲這個表建立一個類, 若是成功, OK. (第31-36 行)

3. 若是第二步仍不成功, 咱們的數據庫可能遇到麻煩, 那就從新建立一個新的數據庫(第

37-40 行)

4. 從新開始新的循環.

但願程序最終能在第一步或第二步成功完成. 當循環結束時, 相似ushuffle_sa.py, 咱們獲得合適的對象實例.

第44-67 行, 77-78 行

這些行處理數據庫操做. 咱們在 44-47 行建立了表, 並在77 行刪掉了表. 在49-52 行插入數據,在54-60 行更新數據, 在62-67 行刪除了數據. 78 行調用了finish()方法來關閉數據庫鏈接. 咱們不能象SQLAlchemy 那樣使用刪表代理, 由於SQLObject 的刪表代理名爲 dropTable()而不是drop().

第69-75 行

使用dbDump()方法, 咱們從數據庫中獲得數據, 並將它顯示在屏幕上.

第80-108 行

又到了 main() 函數. 它工做的方式很是相似 ushuffle_sa.py . 一樣的, 構造器的 db 參數僅僅是一個佔位符, 用以支持其它的數據庫系統(參閱本章最後的習題)

當你運行這段腳本時, 你的輸出可能相似這樣:

21.3.4 總結

關於如何在python 中使用關係型數據庫, 但願咱們前面介紹的東西對你有用. 當你應用程序的需求超出純文本或相似DBM 等特殊文件的能力時, 有多種數據庫能夠選擇, 別忘了還有一個徹底由Python 實現的真正的免安裝維護和管理的真實數據庫系統. 你能在下面找到多種Python 數據庫接口程序和 ORM 系統. 咱們也建議你研究一下互聯網上的 DB-SIG 的網頁和郵件列表. 相似其它的軟件開發領域, 只不過 Python 更簡單易學, 用戶體驗更好.

相關文章
相關標籤/搜索