sqlalchemy入門(上)

發表於2017-04-26

建議從這裡下載這篇文章對應的.ipynb檔案和相關資源。這樣你就能在Jupyter中邊閱讀,邊測試文中的程式碼。

說明

SQLAlchemy包含SQLAlchemy Core和SQLAlchemy ORM兩部分, 這個系列只包含SQLAlchemy Core的內容。

由於內容較多,教程被分成了上,下兩部分。 Select,Update,Delete本身內容較為豐富,放在sqlalchemy入門(下)行演示講解。

準備

安裝sqlalchemy

安裝postgresql資料庫

如果想執行文中的程式碼,請安裝postgresql資料庫並且建立相應的測試使用者和測試資料庫。

匯入helper.py

為了方便程式碼實現的進行,我編寫了helper.py,裡面提供的函式功能如下

  • reset_tables:在名為’test’的schema下重置users,addresses兩張表的資料
  • clear_tables:在名為’test’的schema下刪除users和addresses兩張表並返回兩張表對應的object
  • clear_schema:刪除名為’test’的schema
  • get_table:獲得名為’test’的schema中指定表的資料(DataFrame形式)
  • get_select:獲得名為’test’的schema中指定查詢語句得到資料(DataFrame形式)
  • print_sql:print sqlalchemy object編譯後對應的sql語句

讀者暫時先不必關心這些函式是怎麼實現的,等完成這份教程後自然有能力自己去實現同樣的功能。

匯入其它程式碼實驗要用到的庫

建立engine

SQLAlchemy通過engine和目標資料庫建立連線,它是後面所有的資料庫操作都需要使用的object。 我本機的使用的使用者名稱,資料庫名,密碼都是’test’,埠為’5432’。如果不一致請相應的對下面的程式碼做出修改。

測試資料

教程中用到的測試資料如下

users表和user是一一對應關係,它包含的測試資料是id為1,2的使用者的name和fullname

id name fullname
0 1 jack Jack Jones
1 2 wendy Wendy Williams

addresses表和user是一對多的關係,它包含的測試資料是id為1,2的使用者的email_addresses

user_id email_address
0 1 jack@yahoo.com
1 1 jack@msn.com
2 2 www@www.org
3 2 wendy@aol.com

SQLAlchemy Core初印象

SQLAlchemy Core提供了一套SQL Expression language,它提供了一套用Python construct(Python object)去表達SQL邏輯的體系。下面通過一些程式碼演示一下SQL Expression language的基本特徵。這裡讀者只需要有大致的感覺即可,如果有一些細節不理解不用在意,後面都會有詳細的解釋。

傳統的SQL語句是用文字方式編寫的。

在SQLAlchemy Core中是這樣表達的

 

上面的SQL邏輯可以看作是很多更基本的元件構成的,包括表,列,條件,join語句等等。整個Select邏輯和這些組成元件,對應的都是sqlalchemy object

由於因此使用SQLAlchemy Core表達SQL邏輯的時候,是一個從代表基本SQL邏輯模組的object逐步組裝成複雜object的過程。這樣做有幾個好處。

容易拆分

當SQL邏輯複雜的時候,可以分階段的構造。先構造簡單的SQL邏輯模組,再逐步組裝成複雜的SQL邏輯。相比一次性構造完整的複雜SQL邏輯相比,頭腦的負擔更低,也不容易出錯。

下面的例子裡,我們可以把前面例子中的要選擇的columns,join語句,having條件先構造好,然後再組裝成完整的SQL邏輯。每一個SQL邏輯模組構造好後我們都可以觀察一下對應的SQL語句是什麼。

容易複用

由於使用SQLAlchemy Core去表達SQL,本質上是使用python語言寫程式碼。 因此我們可以利用python提供的一切工具和手段將重複出現的SQL邏輯抽提成可複用的python程式碼。

例如我們在多個地方要根據fullname的長度,和首字母去篩選user。那麼可以用一個函式生成這個條件,以後直接呼叫這個函式即可。

處理資料庫差異

在用SQLAlchemy Core表達SQL邏輯的時候,只是表達了使用者的意圖,並未生成最終的SQL語句。

同樣的SQL邏輯,在不同的database中語法可能會有變化,因此對應的SQL語句是不同的。 而SQLAlchemy Core會根據database的種類,編譯出和這個database匹配的SQL語句。這樣使用者用SQLAlchemy Core組織一次SQL邏輯,就可以在多個資料庫中複用。

當然每個database都有一些自己獨有的功能,對於這部分差異SQLAlchemy是不能自動處理的。

SQLAlchemy Core使用詳解

檢視編譯後的語句

使用SQLAlchemy Core一個基本的需求是檢視sqlalchemy object編譯後的SQL語句是什麼樣的。這個可以用object提供的compile方法實現。

預設情況下編譯後的SQL語句是帶引數的形式,並沒有把’jack’代入name_1。可以通過呼叫params屬性檢視對應的數值是多少。

如果希望編譯後的SQL語句是非引數化的形式,可以新增compile_kwargs={"literal_binds": True}選項。

schema操作

建立schema

如果建立已經存在的schema,會導致異常。例如,剛才已經建立了名為’test’的schema,如果再建立一遍的話,會提示schema “test” already exists

注意

有些sqlalchemy object,例如這個例子中的CreateSchema(schema_name),結果為None。

對於這類object,compile的時候新增compile_kwargs={"literal_binds": True}會導致異常。

預設情況print_sql函式會新增"literal_binds": True, 可以將第三個引數設定成False關閉這個設定。

刪除schema

和新建schema類似。不過如果這個schema下有一些依賴於這個schema存在的資源,比如tables,那麼只有先刪除了這些資源後才能刪除這個schema,否則會異常。

這裡有一個有用的引數cascade,設定成True的話會自動刪除所有依賴於這個schema的資源。

同樣, 如果刪除已經不存在的schema,會報ProgrammingError

同樣,如果刪除並不存在的schema,會報異常,這個不演示了。

table操作

定義table

定義SQLAlchemy可理解的table資料結構,主要引數是table名,schema名以及相關的column的名稱,型別,是否是primary_key等資訊。

定義table是進行新建表,構建select語句等操作的基礎。

如果是資料庫中已經存在的表,可以直接使用autoload功能從資料庫中讀取表的列資訊,可以免去很多麻煩。下面reset_db確保test.users表存在後用autoload自動讀取users表的資訊。

可以看到users中自動包含了column的定義資訊。

注意

如果table中定義了foreign key資訊,SQLAlchemy Core構建join語句的時候能夠自動將foreign key作為join的條件。 但是autoload得到的table會失去這個便利,暫時沒找到解決方法。(見join章節的演示)

新建table

再定義了table後,可以在資料庫中新建這張表。

先清空資料庫

新建表

SQLAlchemy會根據資料庫的型別,將String等列型別資訊轉化成資料庫中對應的資訊,例如Oracle中的VARCHAR2。

注意,不同的資料庫對於configs的要求會不同。例如,postgresql只需要寫String,不需要指定長度;而Oracle在定義時,必須指定長度,得改成類似下面的設定才會生效。

同樣, 如果嘗試新建已經存在的表,會出錯,這個不演示了。

drop table

drop table的處理方法和create table類似。不過在定義

不過執行的話會報錯

這是由於在定義addresses表的時候,定義了addresses的user_id是users表的foreign key,因此foreign key依賴於users表,只有Drop時指定CASCADE選項才能順利的刪除這張表。(它會刪除所有依賴於users表的foreign_key),遺憾的是,我並沒有在sqlalchemy中找到相關的選項啟動CASCADE。

不過SQLAlchemy的一個好處是,它完全可以接受原生的SQL語句去對資料庫進行操作。我們在語句中加上CASCADE和IF EXISTS來進行drop table的操作。

提示

SQLAlchemy的優勢更多的是體現在構造和複用複雜的SQL邏輯上。在刪除table的這個例子裡。SQLAlchemy Core實際上並不如原生的SQL語句好用。我們完全可以針對自己的場景,選擇適合的工作去完成任務。

插入資料

插入單行資料

可以用result.insered_primary_key很方便的找到插入記錄的id

驗證一下插入資料後的結果

id name fullname
0 1 jack Jack Jones
1 2 wendy Wendy Williams
2 3 Junjie Junjie Cai

注意也可以在engine.execute中傳入資料

id name fullname
0 1 jack Jack Jones
1 2 wendy Wendy Williams
2 3 jack Jack Jones

插入多行資料

如果是插入部分列的話,可以用list of dict的結構。

id name fullname
0 1 jack Jack Jones
1 2 wendy Wendy Williams
2 3 jack Jack Jones
3 4 Junjie CaiJunjie
4 5 Xu ZhangXu

注意如果要插入dict list,sqlalchemy會以list中第一條記錄的key為準

id name fullname
0 1 jack Jack Jones
1 2 wendy Wendy Williams
2 3 jack Jack Jones
3 4 Junjie CaiJunjie
4 5 Xu ZhangXu
5 6 Name1 None
6 7 Name2 None

如果第一行包含了所有的key,後面的記錄key缺失的話,會直接報錯。

如果插入資料時會使用所有的列,那麼可以簡化成直接用tuple list插入資料。但是這是就不能利用自動編號id,而是要傳入id。

id name fullname
0 1 jack Jack Jones
1 2 wendy Wendy Williams
2 3 jack Jack Jones
3 4 Junjie CaiJunjie
4 5 Xu ZhangXu
5 6 Name1 None
6 7 Name2 None
7 8 Cai Junjie
8 9 Zhang Xu

但是用這種方式傳入資料的話,自動id的狀態並不會做出相應的調整,而是繼續從上次終止的地方開始,不會跳過用上面方式插入的id。 如果再利用dict list插入資料,生成id就可能和以後的重複,導致異常。

例如下面的例子裡,最後一次自動id是7,繼續使用自動id的話,會從8開始。可以上面再用tuple list插入資料的時候已經把8佔用了,於是導致異常。

從DataFrame插入資料

pandas DataFrame是資料工作者經常使用的資料結構。

fullname name
0 ZhangXu Xu
1 CaiJunjie Junjie

可以利用to_dict()方法很方便的把dataframe轉成dict list

注意

儘管list(df.to_records())轉成的結果看上去是tuple list

但是直接插入這個資料的話會導致異常

原因是list中的資料型別是numpy.record,不是tuple。

即使修復了這個問題

也依然會因為資料結構型別不一致導致異常

因此建議直接使用to_dict(orient = ‘record’)方式轉化資料。

Select, Update, Delete

這部門內容比較豐富,這裡只演示最基本的應用。更詳細的說明放在下一期的的文章講解。

基本的select結構

id name
0 1 jack

其中select_from相當於SQL中的FROM。 如果不會產生歧義,select_from部分可以省略不寫。SQLAlchemy會自動補齊相關的FROM語句。

id name
0 1 jack

帶引數的SQL邏輯

如果希望生成的SQL邏輯支援引數,有兩種實現方式。

函式生成方式

用函式生成SQL邏輯,用函式的引數去實現SQL邏輯引數可變的效果。例如我們構造一個針對user.id的條件。

上面這種方式每次執行函式的時候都會構建新的SQLAlchemy object。

用bindparam指定引數

另一種方式是構建SQLAlchemy object時,用bindparam指定引數部分。 然後用.params繫結數值。

實際上,在SQLAlchemy中使用常數的時候,只是把定義引數和繫結資料兩步一起做而已。

如果定義了引數後沒有通過params繫結數值,那麼在execute階段傳入數值也是可以的。

上面這種方式, obj生成一次後可以反覆被利用,不必重複的生成object。

型別提示

有些場景下,需要指定變數型別,幫助sqlalchemy正確的編譯語句。下面的例子裡,即使後面繫結了string型別的資料,+依然沒能正確的編譯成字串的連線符。應該是”||”。

這時候,需要主動在bindparam中通過type_指定資料型別,幫助SQLAlchemy正確的編譯

id name fullname
0 1 jack Jack Jones
1 3 jack Jack Jones

用text定義sqlalchemy object

除了用純粹的sqlalchemy object去定義SQL邏輯的各種元件,有時候我們希望將文字形式的sql直接轉化成sqlalchemy object。例如下面兩種場景。

  • 已經存在現成的sql程式碼片段,不想用SQLAlchemy重寫
  • 遇到SQLAlchemy無法表達,只有原生的SQL能表達的場景

例如下面這樣包含待定引數的SQL語句,:id是名為id的引數。在傳入實際的數值前,這個語句是不完整的,如果直接傳入engine.execute的話,會出錯。

這時可以用text處理並且用bindparams函式繫結資料

繫結引數呼叫的方法是bindparams,不是params,也不是bindparam! 注意區分!

也可以不繫結引數,而是在execute階段傳入資料

除了用文字定義大段的SQL邏輯外,也可以用文字SQL的片段去定義部分的SQL元件。

注意上面例子中s構造的時候,用到了text生成的帶引數的SQL邏輯元件,但是本身的資料型別是sqlalchemy.sql.selectable.Select,因此繫結資料的時候呼叫的方法是params,而不是bindparam

如果用文字定義的SQL片段是table,和column, 可以用literal_column, table代替text去處理文字SQL。

注意schema不能在構造table時以字串傳入,否則生成的語句執行時會錯誤。儘管構造出來的SQL看上去是完全正確的。

用literal_column和table相比text,構造出的object能夠更好的被SQLAlchemy支援。看下面的例子。

儘管編譯出的語句是一樣的,但是觀察SQLAlchemy識別出的column names,發現SQLAlchemy無法識別text函式構造出的列。

因此應該優先考慮使用literal_column, table等更確切具體的構造方式以獲得SQLAlchemy更好的支援。

相關文章