python
sqlalchemy
最近收到sqlalchemy已經正式發佈2.0,似乎有不少改變,這影響flask-slqalchemy的使用,所以在這邊簡單整理一些使用上的資訊。當然,我給的始終是一個『我看過』覺得重要的資料,如果有辦法的話真心的建議你一定要快速的看過一次官方文件。
粗淺來看,SQLAlchemy有兩個組成,一個是Core,另一個是建立在Core上的ORM,這是一種把資料庫當做物件一般操作的概念,官方文件中特別提到,1.4跟2.0之間的ORM在使用上有著明顯的差異,一定要特別的注意。
首先讓我們利用create_engine
來建立一個資料庫的連線:
from sqlalchemy import create_engine
engine = create_engine('sqlite+pysqlite:///:memory:', echo=True)
上面定義的是一個記憶體資料庫,用完就丟了,只是測試。值得注意的是,sqlite+pysqlite
中間不能有空格。
有這個engine
之後就可以跟資料庫連接,如果是Core的話就是engine.connect
,如果是ORM的話就另外有個Session
來包裝它。
先看Core的情況,我們測試在一個上下文中執行select
:
from sqlalchemy import text
with engine.connect() as conn:
result = conn.execute(text('select "good job test"'))
print(result.all())
上面執行的就有點像是select xxx from dual
,而且因為我們在create_engine
的時候有設置echo=True
,所以執行之後會看到一些響應資訊:
2023-03-08 16:16:55,205 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-03-08 16:16:55,205 INFO sqlalchemy.engine.Engine select "good job test"
2023-03-08 16:16:55,205 INFO sqlalchemy.engine.Engine [generated in 0.00064s] ()
[('good job test',)]
2023-03-08 16:16:55,206 INFO sqlalchemy.engine.Engine ROLLBACK
可以看到最終出現的是ROLLBACK
,目前預設情況下都不會自動commit(印象中也無法設置自動commit)。也因此最後的所有行為都必需要自己手動加一個conn.commit()
來完成整個交易記錄。官方有特別提到,這種走到那commit
到那的風格稱之為commit as you go
如下官方給出的一個建立table,然後寫入一筆資料的範例:
with engine.connect() as conn:
conn.execute(text("CREATE TABLE some_table (x int, y int)"))
conn.execute(
text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
[{"x": 1, "y": 1}, {"x": 2, "y": 4}],
)
conn.commit()
執行之後得到的響應就會是:
2023-03-08 16:22:04,551 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2023-03-08 16:22:04,551 INFO sqlalchemy.engine.Engine CREATE TABLE some_table (x int, y int)
2023-03-08 16:22:04,551 INFO sqlalchemy.engine.Engine [generated in 0.00058s] ()
2023-03-08 16:22:04,554 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (?, ?)
2023-03-08 16:22:04,554 INFO sqlalchemy.engine.Engine [generated in 0.00074s] [(1, 1), (2, 4)]
2023-03-08 16:22:04,554 INFO sqlalchemy.engine.Engine COMMIT
另外有一種把conn context在一開始就定義為transaction block的方式:
with engine.begin() as conn:
conn.execute(
text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
[{"x": 6, "y": 8}, {"x": 9, "y": 10}],
)
看的出上下文的宣告差異,engine.begin() as conn
。因為已經定義成是交易記錄的區塊,所以執行之後沒異常就會自動commit,這樣的風格則稱之為begin once。
官方建議是採用begin once,明確的定義整個交易記錄的上下文,不過範例中的說明都會採用commit as you go,就是想要就commit一發。
這邊已經有一點概念了,利用create_engine
來定義一個engine
,然後在交易記錄上下文中利用conn.execute
搭配text
來執行資料庫語法。
如果是ORM的話也會有相對應的Session.execute()
的方法可以執行
利用剛剛寫入的資料來做一個基本的查詢吧:
with engine.connect() as conn:
result = conn.execute(text("SELECT x, y FROM some_table"))
for row in result:
print(f"x: {row.x} y: {row.y}")
其中result
是一個CursorResult
的物件。我們可以直接對這個回傳的物件做迭代取值的動作。當然你也可以選擇先用result.all()
把資料先丟去另一個變數再來看你要做什麼處理,又或者像我們搜尋的是兩個欄位,也可以直接用for x, y in result:
的迭代方式來處理,還是用索引的方式x = row[0]
、或是利用mappings()
把結果轉為dict也可以:
result = conn.execute(text("select x, y from some_table"))
for dict_row in result.mappings():
x = dict_row["x"]
y = dict_row["y"]
查詢如果有需要條件的話,在execute
的時候可以利用參數加入:
with engine.connect() as conn:
result = conn.execute(text("SELECT x, y FROM some_table WHERE y > :y"), {"y": 2})
for row in result:
print(f"x: {row.x} y: {row.y}")
任何資料庫的操作都請一定要考慮到sql injection的問題。
寫入的時候我們也可以利用參數來寫入資料:
with engine.connect() as conn:
conn.execute(
text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
[{"x": 11, "y": 12}, {"x": 13, "y": 14}],
)
conn.commit()
上面範例可以看的到利用list搭配dict來做多筆的寫入,這樣的作法稱為executemany
,會對單個sql語法做多次的執行,結果來看就是會執行兩次的寫入動作。
在ORM的情況下,同樣的範例可以這麼寫:
from sqlalchemy.orm import Session
stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y")
with Session(engine) as session:
result = session.execute(stmt, {"y": 6})
for row in result:
print(f"x: {row.x} y: {row.y}")
利用Session
建立一個上下文的區塊來執行我們所定義的資料庫語法stms
,在execute
的過程中給定參數y=6
。
利用ORM做多筆資料的更新:
with Session(engine) as session:
result = session.execute(
text("UPDATE some_table SET y=:y WHERE x=:x"),
[{"x": 9, "y": 11}, {"x": 13, "y": 15}],
)
session.commit()
在抽象觀念上跟採用Core沒有什麼不一樣。不過這邊真的只是一個ORM的概念說明,能這麼用,但比較不會這樣用。