ORM: SQLAlchemy 初識

ORM: Object Relational Mapper.

目前Python有很多ORM工具可以將數據庫映像爲Python的Objects對象。
其中比較知名的有Django的ORM,SQLAlchemy, PostgreSQL等。
SQLAlchemy有更多的人維護,功能也比較齊全。所以一般是我們的首選項。

對於SQLAlchemy的使用者來說,只要你一開始連接上數據庫,不管是Sqlite,MySQL還是什麼,後面的處理方式完全一樣。這種便利性也是它受歡迎的原因。

拋棄了傳統的自己編織SQL語句、製作模型、連接數據庫方式,SQLAlchemy直接把這些東西全包在黑盒裏面,讓我們完全不需要去管。連SQL-Injection注入這種東西也被它幫忙防範了。這樣一來,可以說在連接數據庫方面,幫我們節省了最少一半以上的代碼。

甚至連數據查詢,SQLAlchemy也代替了SQL語句,而使用了專門的類似MongoDB的Object.query.filter_by(name='Jason').all()這種方法。

安裝:

# 安裝sqlalchemy
$ pip install sqlalchemy

安裝Drivers:

# Sqlite
# 不需要,Python自帶

# MySQL
$ pip install pymysql

# Postgresql
$ pip install psycopg2

SQLAlchemy自身不帶數據庫driver,需要我們自己安裝,並在連接時候指定。
而這些driver,實際上就是我們曾經手動連接數據庫所用的包。而SQLAlchemy只是代替我們使用這些同樣的包。

連接數據庫

創建一個sqlite的ORM引擎:

from sqlalchemy import create_engine

# 連接格式爲:sqlite://<Hostname>/<path>
engine  = create_engine('sqlite:///foo.db', echo=True)

創建一個MySQL的ORM引擎:

from sqlalchemy import create_engine

# 連接格式爲:dialect+driver://username:password@host:port/database
engine  = create_engine('mysql+pymysql://root:password123@localhost/db_test_01', echo=True)

數據庫的位置(三斜槓爲相對路徑,四斜槓爲絕對路徑):

# 使用絕對路徑的數據庫文件(////),如/tmp/mydatabase.db
engine  = create_engine('sqlite:////tmp/mydatabase.db')

# 使用當前「執行位置」數據庫文件(///或///./)
engine  = create_engine('sqlite:///mydatabase.db')

# 使用當前「執行位置」父級目錄(///../)的數據庫文件
engine  = create_engine('sqlite:///../mydatabase.db')

# 使用當前「腳本位置」的數據庫文件
import os
cwd = os.path.split(os.path.realpath(__file__))[0]
engine  = create_engine('sqlite:///{}/mydatabase.db'.format(cwd))

Create Tables 創建表

注意:不同於SQL語句,SQLAlchemy中的表名是完全區分大小寫的

創建一個Schema表(指單純表,不包含ORM對象):

from sqlalchemy import create_engine, MetaData
from sqlalchemy import Table, Column
from sqlalchemy import Integer, String, ForeignKey

engine  = create_engine('mysql+pymysql://root:password123@localhost/db_test_01', echo=True)
metadata = MetaData(engine)

# 創建一個表
user_table = Table( 'tb_user', metadata,
        Column('id', Integer, primary_key=True),
        Column('name', String(50)),
        Column('fullname', String(100))
)

# 讓改動生效
metadata.create_all()

創建一個ORM對象(包括表):

# 導入表格創建引擎
from sqlalchemy import create_engine
# 導入列格式
from sqlalchemy import Column, Integer, String, ForeignKey
# 導入創建ORM模型相關
from sqlalchemy.ext.declarative import declarative_base

Base  = declarative_base()

class User(Base):
    __tablename__ = 'tb_Person'

    id = Column('id', Integer, primary_key=True)
    username = Column('username', String, unique=True)

engine = create_engine('sqlite:///test.sqlite', echo=True)
Base.metadata.create_all(bind=engine)

用普通表Table和ORM對象創建的表有什麼不同?
他們在數據庫中創建的,是完全相同的表!唯一區別是,Table創建的不包含ORM對象,也就是不提供讓你直接操作Python對象的功能。
這麼做的好處是,有很多隻是關聯作用的表,沒有必要生成ORM對象。

刪除數據庫中的表

# engine = ...
# Base = ...

# 逐個ORM對象刪除對應的表,如User類
User.__table__.drop(engine)

# 刪除全部表
Base.metadata.drop_all(engine)

設計或調試過程中,我們經常要頻繁改動表格,所以有必要在創建表格前把測試數據庫中的表都清除掉,再創建新的定義。

Insertion 插入數據

將數據添加到數據庫:

# ...
# 導入session相關(用於添加數據)
from sqlalchemy.orm import sessionmaker, relationship

user = User()
user.id = 1
user.username = 'Jason'

Session = sessionmaker(bind=engine)
session = Session()
session.add(user)
session.commit()
session.close()

注意:這裏的session和網站上的session概念有點不一樣。這裏是用來commit提交數據庫變動的工具。

批量添加數據(向add_all()傳入列表):

session.add_all( [user1, user2, user3] )

添加每條數據的時候自動flush():

session = sessionmaker(bind=engine, autoflush=True)

autoflush是在每次session.add()自動執行session.flush(),即在插入數據庫之前就在內存中生成所有對象的動態數據(如主鍵ID等)。一般默認是選false,因爲會影響效率。最好是需要的時候,才手動執行session.flush()

具體緣由,看下一節“數據生效”。

Take effect 數據生效

SQLAlchemy中的create_all()session.commit()都是直接讓python文件中定義的對象在數據庫中生效的語句。在此之前,無論怎麼定義,數據都是在內存中,而沒有在數據庫中的。

注意區分:

  • create_all只是讓創建表格結構生效,無關insert的數據條目
  • session.commit()只是讓添加的數據生效,而不負責任何表格結構。

這兩個的順序,當然是先創建表格,再插入數據。

只是,如果我們知道了這個原理,在編碼中才能比較運用自由。比如,連create_engine()創建引擎,我們都可以在後面定義,而沒必要非得寫在文件頭,即所有的ORM定義之前。
create_engine只要定義在所有ORM類和Schema表之後即可。

此後,我們再開始進行數據插入工作,也就利用到了session。

session過程中呢,我們也會遇到互相引用主鍵外鍵ID的情況。但是注意,這時候因爲還沒有使用最終的session.commit()真正提交數據到數據庫中,這些ID是沒有值的。
解決辦法就是利用內置的方法session.flush(),將session中已添加的所有對象填充好數據,但是這時候還沒有提交到數據庫,只是我們內部可以正常訪問各種ID了。

更新/刪除數據

更新:

# Get a row of data
me = session.query(User).filter_by(username='Jason').first()

# Method 1:
me.age += 1
session.commit()

# Method 2:
session.query().filter(
    User.username == 'Jason'
).update(
    {"age": (User.age +1)}
)
session.commit()

# Method 3:
setattr(user, 'age', user.age+1)
session.commit()

Get Primary Key Value 獲取主鍵值

#sqlalchemy can't get primary key , #sqlalchemy 如何獲得主鍵的值

這個問題花了我很多時間探索查詢,不得其解,才明白原來是很顯然的事。

參考思否:SQLAlchemy中返回新插入數據的id?

雖然在沒有用session或engine插入數據之前,我們可以直接瀏覽從ORM創建的對象中的屬性值。
但是這個時候無論如何都獲取不到primar_key主鍵列的值。

因爲這時候主鍵還沒有插入數據庫,作爲動態的值,在數據庫沒生效之前也就爲None。

爲什麼需要獲取value of primary_key?考慮如下這些場景:

  • 子表中的foreign key外鍵需要引用主表的id
  • ??

那麼該怎麼獲取主鍵ID呢?

再參考Stackoverflow:sqlalchemy flush() and get inserted id?
再參考:sqlalchemy獲取插入的id
再參考:Sqlalchemy;將主鍵設置爲預先存在的數據庫表(不使用sqlite)

如果要想在插入數據之前就獲取主鍵等動態列的值,那麼有這幾種方法:

  • 直接利用SQLAlchemy建立類直接的內部關聯,而不直接使用ID
  • 主表插入數據,另session生效後,再用query獲取相應對象,來得到它的ID。
  • (*) 主表先用session.add(..),再session.flush(),然後就可以獲取ID,最後再session.commit()
  • 不使用primary key主鍵,自己手動創建ID,這樣來隨便獲取。

推薦做法如下:
image

即每次新創建對象後,立刻session.add(..),然後立刻session.flush(),全部都添加好的文末,再session.commit().

Query 查詢

注意:query是通過session進行的,也就是必須在session.commit()之後才能進行查詢,否則會報錯。

這裏將的query查詢,指的都是在插入到數據庫生效之後。理解這個很重要,因爲在對象未插入到數據庫之前,很多主鍵、外鍵等內容都是不存在的,也就無法查詢到。

參考:pythonsheets - Object Relational basic query

查詢數據:

session.commit()
# ...

users = session.query(User).all()
# 返回的是多個User類的對象:>>> [ <User 1>, <User 2>, .... ]

for u in users:
    print(u.id, u.username)

常用查詢方法:

# 獲取某ORM中數據 .query(ORM類名)
>>> session.query( User ).all()     # All rows of data
>>> session.query( User ).first()    # First row of data as an object

# 查詢結果排序 .order_by(類名.列名)
>>> session.query(User).order_by( User.birth ).all()

# 篩選結果 .filter( True/False 表達式 )
>>> session.query(User).filter( User.name != 'Jason' ).all()
>>> session.query(User).filter( User.name.like('%ed%') ).all()    # Fuzzy search
>>> session.query(User).filter( User.id in [1, 2, 3] ).all()    # IN
>>> session.query(User).filter( ~ User.id in [4, 5, 6] ).all()   # NOT IN
>>> session.query(User).filter( User.school == 'MIT', User.age < 24 ).first()   # AND
>>> session.query(User).filter( _or(User.school == 'MIT', User.age < 24) ).first()   # OR
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章