環境:Ubuntu 15.10 64-bit
SQLAlchemy是Python的ORM框架,它的理念是:資料庫的量級和效能重要於物件集合,而物件集合的抽象又重要於表和行。
安裝
直接通過pip安裝:
1 |
$ pip install sqlalchemy |
開啟Python,測試是否安裝成功:
1 2 3 |
>>> import sqlalchemy >>> sqlalchemy.__version__ '1.0.9' |
建立引擎
SQLite
首先以SQLite為例,因為它比較簡單。
1 2 3 4 |
from sqlalchemy import create_engine, MetaData engine = create_engine('sqlite:///foo.db', echo=True) metadata = MetaData(engine) |
引數 sqlite:///foo.db 解釋為:
1 |
sqlite://<nohostname>/<path> |
其中foo.db是相對路徑。也可寫成:
1 |
sqlite:///./foo.db |
SQLAlchemy預設使用Python內建的sqlite3模組來連線或建立SQLite資料庫。執行完create_engine後,可以發現當前目錄多了foo.db檔案,不妨用sqlite開啟看看。
1 2 3 4 |
$ sqlite3 foo.db SQLite version 3.8.11.1 2015-07-29 20:00:57 Enter ".help" for usage hints. sqlite> .tables |
注意這裡用的是sqlite3而非sqlite,因為foo.db是經由Python內建的sqlite3模組建立的。
MySQL
再來看看連線MySQL時怎麼建立引擎。
本文後續示例全部基於MySQL,這是與官方文件不同的地方。
先在MySQL裡建立一個測試資料庫:sa_test,後續示例都將基於這個資料庫。
1 |
mysql> CREATE DATABASE sa_test DEFAULT CHARACTER SET UTF8; |
1 2 3 4 |
from sqlalchemy import create_engine, MetaData engine = create_engine('mysql+mysqldb://root:******@localhost/sa_test', echo=True) metadata = MetaData(engine) |
這裡的引數看上去就比較複雜了,完整的格式為:
1 |
dialect+driver://username:password@host:port/database |
這裡driver用了mysqldb,詳見:MySQLdb:Python操作MySQL資料庫
引擎配置的詳細資訊可參考官方文件:Engine Configuration
MetaData
前面在建立MetaData時繫結了引擎:
1 |
metadata = MetaData(engine) |
當然也可以不繫結。繫結的好處是,後續很多呼叫 (比如 MetaData.create_all(),Table.create(),等等)就不用指定引擎了。
建立表
建立兩張表,user和address,address表裡有一個user id的外來鍵。
注意:表名沒有像官方文件及很多人推薦的那樣使用複數形式,個人偏好而已,詳細討論請見StackOverflow的這個問題:Table Naming Dilemma: Singular vs. Plural Names (中文版)
1 2 3 4 5 |
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey engine = create_engine('mysql+mysqldb://root:******@localhost/sa_test', echo=True) metadata = MetaData(engine) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
user_table = Table('user', metadata, Column('id', Integer, primary_key=True), Column('name', String(50)), Column('fullname', String(100)) ) address_table = Table('address', metadata, Column('id', Integer, primary_key=True), Column('user_id', None, ForeignKey('user.id')), Column('email', String(128), nullable=False) ) metadata.create_all() |
執行完metadata.create_all()這一句,兩張表就建立好了,可以在MySQL裡立即檢視。
MetaData.create_all()可以多次呼叫,不會報錯,它在內部會檢查表是否已經建立。
因為MetaData建立時已經繫結了引擎,所以此處create_all()就不必再指定了,否則得寫成:
1 |
metadata.create_all(engine) |
建立引擎時,echo引數為True,程式執行時便有很多除錯資訊列印出來。在這些除錯資訊中,可以看到如下兩條MySQL的CREATE TABLE語句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
CREATE TABLE user ( id INTEGER NOT NULL AUTO_INCREMENT, name VARCHAR(50), fullname VARCHAR(100), PRIMARY KEY (id) ) CREATE TABLE address ( id INTEGER NOT NULL AUTO_INCREMENT, user_id INTEGER, email VARCHAR(128) NOT NULL, PRIMARY KEY (id), FOREIGN KEY(user_id) REFERENCES user (id) ) |
除了metadata.create_all(),Table自己也有create方法:
1 |
create(bind=None, checkfirst=False) |
引數bind一般就是指引擎。
引數checkfirst表示是否檢查表已經存在。為True時,若表已經存在,不報錯,只是什麼也不做;為False時,若表已經存在,則將引發異常。
使用這個方法來建立這兩張表:
1 2 |
user_table.create(checkfirst=True) address_table.create(checkfirst=True) |
這裡忽略了bind引數,因為建立MetaData物件時已經繫結了引擎,而建立表物件時又傳入了metadata,所以順藤摸瓜,表自己是知道引擎的。
如果調整一下表的建立順序,就會報錯,因為address表裡有一個user表的外來鍵,而這時候user表還沒建立呢。所以,還是建議使用MetaData.create_all()吧,畢竟它也會檢查表是否已經存在。
表的反射 Table Reflection
表建立好了,一般也就不動了。所以實際應用時,往往表都已經存在,並不需要建立,只需把它們”匯入”進來即可,這時就得使用autoload引數。
1 2 3 4 5 6 7 8 9 10 11 12 |
from sqlalchemy import create_engine, MetaData, Table engine = create_engine('mysql+mysqldb://root:******@localhost/sa_test', echo=False) metadata = MetaData(engine) user_table = Table('user', metadata, autoload=True) print 'user' in metadata.tables print [c.name for c in user_table.columns] address_table = Table('address', metadata, autoload=True) print 'address' in metadata.tables |
輸出:
1 2 3 |
True ['id', 'name', 'fullname'] True |
如果MetaData沒有繫結引擎,則另需指定autoload_with引數:
1 |
user_table = Table('user', metadata, autoload=True, autoload_with=engine) |
如果被反射的表外來鍵引用了另一個表,那麼被引用的表也會一併被反射。比如只反射address表,user表也一併被反射了。
1 2 3 4 5 6 7 8 9 |
from sqlalchemy import create_engine, MetaData, Table engine = create_engine('mysql+mysqldb://root:******@localhost/sa_test', echo=False) metadata = MetaData(engine) address_table = Table('address', metadata, autoload=True) print 'user' in metadata.tables print 'address' in metadata.tables |
輸出:
1 2 |
True True |
插入資料
插入資料之前,必須要有表物件,不管是新建立的,還是通過反射匯入的。
Insert物件
要往表裡插資料,先建立一個Insert物件:
1 2 |
ins = user_table.insert() print ins |
列印這個Insert物件,可以看到它所對應的SQL語句:
1 |
INSERT INTO user (id, name, fullname) VALUES (%s, %s, %s) |
如果連線的資料庫不是MySQL而是SQLite,那輸出可能就是下面這樣:
1 |
INSERT INTO user (id, name, fullname) VALUES (?, ?, ?) |
可見SQLAlchemy幫我們封裝了不同資料庫之間語法的差異。
如果MetaData建立時沒有繫結引擎,那麼輸出會略有不同:
1 |
INSERT INTO "user" (id, name, fullname) VALUES (:id, :name, :fullname) |
這時SQLAlchemy還不知道具體的資料庫語法,表名加了引號(”user”),列名也改用為:id之類一般性的格式。
此外,這條INSERT語句列出了user表裡的每一列,而id在插入時一般是不需要指定的,可以通過Insert.values()方法加以限制:
1 2 |
ins = ins.values(name='adam', fullname='Adam Gu') print ins |
限制後,id列已經沒有了:
1 |
INSERT INTO user (name, fullname) VALUES (%s, %s) |
可見values()方法限制了INSERT語句所包含的列。但是我們指定的name和fullname的值並沒有列印出來,這兩個值儲存在Insert物件裡,只有等到執行時才會用到。
執行
我們一直在說的引擎,可以理解成一個資料庫連線物件的倉庫,通過連線物件可以往資料庫傳送具體的SQL語句。呼叫引擎的connect()方法可以獲取一個連線:
1 |
conn = engine.connect() |
現在把前面的Insert物件丟給它來執行:
1 |
result = conn.execute(ins) |
由除錯資訊可見具體的INSERT語句:
1 2 3 |
INSERT INTO user (name, fullname) VALUES (%s, %s) ('adam', 'Adam Gu') COMMIT |
返回值result是一個ResultProxy物件,ResultProxy是對DB-API中cursor的封裝。插入語句的結果並不常用,但是查詢語句肯定是要用到它的。
不妨在MySQL裡看一下剛插入的資料。
1 2 3 4 5 6 7 |
mysql> select * from user; +----+------+----------+ | id | name | fullname | +----+------+----------+ | 1 | adam | Adam Gu | +----+------+----------+ 1 row in set (0.00 sec) |
執行多條語句
還記得前面的Insert物件使用values()方法來限制列嗎?
1 |
ins = ins.values(name='adam', fullname='Adam Gu') |
這種方式其實不利於Insert物件的複用,更好的做法是把引數通過execute()方法傳進去:
1 2 |
ins = user_table.insert() conn.execute(ins, name='adam', fullname='Adam Gu') |
Insert物件本身還是會包含所有列,最終INSERT語句裡的列由execute()的引數決定。由除錯資訊可見具體的INSERT語句:
1 2 3 |
INSERT INTO user (name, fullname) VALUES (%s, %s) ('adam', 'Adam Gu') COMMIT |
一次插入多條記錄也很簡單,只要傳一個字典列表(每個字典的鍵必須一致)給execute()即可。
1 2 3 4 |
conn.execute(address_table.insert(), [ { 'user_id': 1, 'email': 'sprinfall@gmail.com' }, { 'user_id': 1, 'email': 'sprinfall@hotmail.com' }, ]) |
除錯資訊裡具體的INSERT語句:
1 2 3 |
INSERT INTO address (user_id, email) VALUES (%s, %s) ((1, 'sprinfall@gmail.com'), (1, 'sprinfall@hotmail.com')) COMMIT |
在MySQL裡看一下插入的地址:
1 2 3 4 5 6 7 8 |
mysql> select * from address; +----+---------+-----------------------+ | id | user_id | email | +----+---------+-----------------------+ | 1 | 1 | sprinfall@gmail.com | | 2 | 1 | sprinfall@hotmail.com | +----+---------+-----------------------+ 2 rows in set (0.00 sec) |
第一部分到此結束。