Python SQLAlchemy 高級教程——使用SQLAlchemy創(chuàng)建/反射表結(jié)構(gòu)瞬测、增刪改數(shù)據(jù)

建表

連接一個數(shù)據(jù)庫

>>> from sqlalchemy import create_engine
>>> engine = create_engine('sqlite:///:memory:', echo=True)
這個 Engine 是任何SQLAlchemy應(yīng)用程序的起點(diǎn)横媚。

Dialect 定義特定數(shù)據(jù)庫的DBAPI行為纠炮,能為不同的數(shù)據(jù)庫轉(zhuǎn)換SQL語法。
Pool 是連接池灯蝴,當(dāng)生成的session實(shí)例操作數(shù)據(jù)庫(或者多個實(shí)例并發(fā)簽出)的時候把session放進(jìn)這個池統(tǒng)一管理恢口,如果session實(shí)例綁定了bind=engine,則該session實(shí)例在使用時默認(rèn)放到由這個engine管理的連接池中穷躁。

把表映射到自己的類

聲明ORM基類

>>> from sqlalchemy.ext.declarative import declarative_base
>>> Base = declarative_base()#這是一個關(guān)系映射基類

User 將是我們映射此表的類耕肩。在類中,我們定義了要映射到的表的詳細(xì)信息问潭,主要是表名以及列的名稱和數(shù)據(jù)類型:

>>> from sqlalchemy import Column, Integer, String
>>> class User(Base):
...     __tablename__ = 'users'
...
...     id = Column(Integer, primary_key=True)
...     name = Column(String)
...     fullname = Column(String)
...     nickname = Column(String)
...
...     def __repr__(self):
...        return "<User(name='%s', fullname='%s', nickname='%s')>" % (
...                             self.name, self.fullname, self.nickname)

查看剛才創(chuàng)建的表結(jié)構(gòu)

>>> User.__table__ 
Table('users', MetaData(bind=None),
            Column('id', Integer(), table=<users>, primary_key=True, nullable=False),
            Column('name', String(), table=<users>),
            Column('fullname', String(), table=<users>),
            Column('nickname', String(), table=<users>), schema=None)

這個表結(jié)構(gòu)被登記在Base.metadata.tables中猿诸,如果有多個子類繼承了這個Base,那么這個Base.metadata就記錄了所有剛剛定義的子類(表結(jié)構(gòu))狡忙。

在數(shù)據(jù)庫中創(chuàng)建表

現(xiàn)在用 MetaData 向數(shù)據(jù)庫發(fā)出create table語句:

>>> Base.metadata.create_all(engine)

如果Base.metadata存在多個子類梳虽,則會全部創(chuàng)建。

向表中插入數(shù)據(jù)

每一個映射類對應(yīng)一張表灾茁,我們要插入的每一條記錄都是映射類的實(shí)例窜觉,要插入數(shù)據(jù),就是向這張表中添加映射類的實(shí)例北专。

創(chuàng)建會話

會話(session)是一個與數(shù)據(jù)庫對話的控制器禀挫,能向Engine申請連接池(Pool)資源。

session常用操作:

session.query():查詢拓颓,從數(shù)據(jù)庫中定位數(shù)據(jù)
session.flush():預(yù)提交语婴,提交到數(shù)據(jù)庫文件,還未寫入數(shù)據(jù)庫文件中
session.commit():提交了一個事務(wù)录粱,把緩存的數(shù)據(jù)直接寫入數(shù)據(jù)庫
session.rollback():回滾
session.close():關(guān)閉

關(guān)于報(bào)錯 “Can’t reconnect until invalid transaction is rolled back”

session事務(wù)失敗腻格,占用連接池資源,無法提交新的事務(wù)啥繁。需要rollback()菜职,或者close()來打破這種狀態(tài)(也可以通過設(shè)置engine的autocommit為True來取消事務(wù)提交,比較野蠻旗闽,不推薦)酬核。

創(chuàng)建會話的方式一(為每個Session工廠分配engine)

>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=engine)# 生成Session工廠,與數(shù)據(jù)庫連接綁定
>>> session = Session()

創(chuàng)建會話的方式二(為每個session分配engine)

>>> Session = sessionmaker()
>>> session = Session(bind=engine)# 生成session實(shí)例适室,與數(shù)據(jù)庫連接綁定

創(chuàng)建會話的方式三(延遲綁定)

>>> Session = sessionmaker()
>>> Session.configure(bind=engine)
>>> session = Session()

創(chuàng)建會話的方式四(不綁定)

>>> Session = sessionmaker()
>>> session = Session()

注意:如果是Session = sessionmaker(bind=engine)嫡意,由這個Session實(shí)例化的session只能操作engine能訪問的視圖或表,關(guān)聯(lián)Engine是為了確保這個session可以使用該engine的連接資源捣辆。
如果不用Session.configure(bind=engine)綁定engine蔬螟,則這個會話實(shí)例可以操作任意的engine,由于我們在下面的程序中反射表的時候用了綁定到engine的metadata汽畴,使用上面這個沒有綁定engine的session查詢的時候會找到表對應(yīng)的metadata綁定的engine來進(jìn)行坎怪,但是這個session無法操作metadata沒有綁定到engine的表。

創(chuàng)建會話的方式五(線程安全)

>>> from sqlalchemy.orm import scoped_session
>>> from sqlalchemy.orm import sessionmaker
>>> session = scoped_session(sessionmaker(bind=engine))

更多關(guān)于session的問題廓握,請看我的另一篇文章:http://www.reibang.com/p/2524784c89c6

添加和更新對象

向之前創(chuàng)建的表中添加數(shù)據(jù):

>>> ed_user = User(name='ed', fullname='Ed Jones', nickname='edsnickname')
>>> session.add(ed_user)

此時搅窿,這個session處于Pending狀態(tài),還沒有發(fā)出SQL隙券,對象還沒有在數(shù)據(jù)庫中表示為一行是尔。這個session將使用一個刷新的方法殉了,在需要時立即發(fā)出SQL來持久化數(shù)據(jù)。
下面恩溅,我們創(chuàng)建一個新的query對象隔箍,查看User表中的名為‘ed’的對象:

>>> our_user = session.query(User).filter_by(name='ed').first() 
>>> our_user
<User(name='ed', fullname='Ed Jones', nickname='edsnickname')>

這時候會話已經(jīng)識別出返回的行與在其內(nèi)部對象映射中表示的行相同,所以我們實(shí)際上得到了與我們剛剛添加的相同的實(shí)例脚乡。

>>> ed_user is our_user
True

這里使用的ORM概念稱為標(biāo)識映射蜒滩,它確保會話中對特定行進(jìn)行的所有操作都對同一組數(shù)據(jù)進(jìn)行操作。一旦會話中出現(xiàn)具有特定主鍵的對象奶稠,該會話上的所有SQL查詢將始終返回該特定主鍵的相同Python對象俯艰;如果試圖在會話中放置具有相同主鍵的第二個已經(jīng)持久化的對象,也會引發(fā)錯誤锌订。
我們可以使用add_all()一次性添加更多的User對象:

>>> session.add_all([
...     User(name='wendy', fullname='Wendy Williams', nickname='windy'),
...     User(name='mary', fullname='Mary Contrary', nickname='mary'),
...     User(name='fred', fullname='Fred Flintstone', nickname='freddy')])

更改一個人的昵稱:

>>> ed_user.nickname = 'eddie'

這個更改出現(xiàn)在session.dirty屬性中:

>>> session.dirty
IdentitySet([<User(name='ed', fullname='Ed Jones', nickname='eddie')>])

三個新的用戶對象正在等待處理的掛起(pending)態(tài):

>>> session.new  
IdentitySet([<User(name='wendy', fullname='Wendy Williams', nickname='windy')>,
<User(name='mary', fullname='Mary Contrary', nickname='mary')>,
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')>])

我們告訴session竹握,我們希望向數(shù)據(jù)庫發(fā)出所有剩余的更改,并提交正在進(jìn)行中的事務(wù)辆飘。我們通過session.commit()來做這件事啦辐。會話發(fā)出針對“ed”上的昵稱更改的UPDATE語句,以及我們添加的三個新用戶對象的INSERT語句:

>>>  session.commit()

commit()將剩余的更改刷新到數(shù)據(jù)庫蜈项,并提交事務(wù)芹关。會話引用的連接資源現(xiàn)在返回到連接池。此會話的后續(xù)操作將在一個新事務(wù)中發(fā)生紧卒,該事務(wù)將在首次需要時再次重新獲取連接資源侥衬。
如果我們看看Ed的id屬性,之前它是None,現(xiàn)在它有一個值:

>>>  ed_user.id
1

User的對象在這個寫入數(shù)據(jù)庫的過程中經(jīng)歷了以下三種狀態(tài)
transient, pending, persistent
一個對象在一個會話中可以擁有的狀態(tài)如下:

  • Transient:不在會話中且未保存到數(shù)據(jù)庫中的實(shí)例轴总;也就是說贬媒,它沒有數(shù)據(jù)庫標(biāo)識。這樣的對象與ORM的唯一關(guān)系是肘习,它的類有一個與之關(guān)聯(lián)的mapper()。
  • Pending:當(dāng)您使用Session.add()臨時實(shí)例時坡倔,它會變成掛起的漂佩。它實(shí)際上還沒有被刷新到數(shù)據(jù)庫中,但下一次刷新時就會刷新罪塔。
  • Persistent:在會話中出現(xiàn)并在數(shù)據(jù)庫中有記錄的實(shí)例投蝉。您可以通過刷新使掛起的實(shí)例變?yōu)槌志脤?shí)例,或者通過查詢現(xiàn)有實(shí)例的數(shù)據(jù)庫(或者將持久實(shí)例從其他會話移動到本地會話)來獲得持久實(shí)例征堪。
  • Deleted:一個實(shí)例瘩缆,在一次刷新中已被刪除,但事務(wù)尚未完成佃蚜。這種狀態(tài)下的對象本質(zhì)上與“掛起”狀態(tài)相反;當(dāng)提交會話事務(wù)時庸娱,對象將移動到分離狀態(tài)⌒乘悖或者熟尉,當(dāng)會話的事務(wù)回滾時,已刪除的對象將移動回持久狀態(tài)洲脂。
  • Detached:與數(shù)據(jù)庫中的記錄相對應(yīng)或以前相對應(yīng)的實(shí)例斤儿,但當(dāng)前不在任何會話中。分離的對象將包含一個數(shù)據(jù)庫標(biāo)識標(biāo)記恐锦,但是由于它與會話沒有關(guān)聯(lián)往果,因此無法知道這個數(shù)據(jù)庫標(biāo)識是否實(shí)際存在于目標(biāo)數(shù)據(jù)庫中。分離的對象正常使用是安全的一铅,除了它們沒有能力加載未加載的屬性或之前標(biāo)記為“過期”的屬性陕贮。
    深入了解“https://docs.sqlalchemy.org/en/13/orm/session_events.html#session-lifecycle-events
    任何映射對象的實(shí)際狀態(tài)可以在任何時候使用inspect()查看:
>>> from sqlalchemy import inspect
>>> insp = inspect(ed_user)
>>> insp.persistent
True

回滾

先亂改一波數(shù)據(jù)

>>> ed_user.name = 'Edwardo'
>>> fake_user = User(name='fakeuser', fullname='Invalid', nickname='12345')
>>> session.add(fake_user)

查詢會話時,可以看到它們被刷新到當(dāng)前事務(wù)中:

>>> session.query(User).filter(User.name.in_(['Edwardo', 'fakeuser'])).all()
[<User(name='Edwardo', fullname='Ed Jones', nickname='eddie')>, <User(name='fakeuser', fullname='Invalid', nickname='12345')>]

回滾一下馅闽,我們可以看到 ed_user 的name變回 ed 飘蚯,fake_user 已不在會話中:

>>> session.rollback()
>>> ed_user.name
u'ed'
>>> fake_user in session
False

再次查詢發(fā)現(xiàn)已經(jīng)恢復(fù)

>>> session.query(User).filter(User.name.in_(['ed', 'fakeuser'])).all()
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>]

查詢

>>> for instance in session.query(User).order_by(User.id):
...     print(instance.name, instance.fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone

>>> for name, fullname in session.query(User.name, User.fullname):
...     print(name, fullname)
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone

>>> for row in session.query(User, User.name).all():
...    print(row.User, row.name)
<User(name='ed', fullname='Ed Jones', nickname='eddie')> ed
<User(name='wendy', fullname='Wendy Williams', nickname='windy')> wendy
<User(name='mary', fullname='Mary Contrary', nickname='mary')> mary
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')> fred

>>> for row in session.query(User.name.label('name_label')).all():
...    print(row.name_label)
ed
wendy
mary
fred

>>> from sqlalchemy.orm import aliased
>>> user_alias = aliased(User, name='user_alias')
>>> for row in session.query(user_alias, user_alias.name).all():
...    print(row.user_alias)
<User(name='ed', fullname='Ed Jones', nickname='eddie')>
<User(name='wendy', fullname='Wendy Williams', nickname='windy')>
<User(name='mary', fullname='Mary Contrary', nickname='mary')>
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')>

>>> for u in session.query(User).order_by(User.id)[1:3]:
...    print(u)
<User(name='wendy', fullname='Wendy Williams', nickname='windy')>
<User(name='mary', fullname='Mary Contrary', nickname='mary')>

>>> for name, in session.query(User.name).\
...             filter_by(fullname='Ed Jones'):
...    print(name)
ed

>>> for name, in session.query(User.name).\
...             filter(User.fullname=='Ed Jones'):
...    print(name)
ed


>>> for user in session.query(User).\
...          filter(User.name=='ed').\
...          filter(User.fullname=='Ed Jones'):
...    print(user)
<User(name='ed', fullname='Ed Jones', nickname='eddie')>

更多的filter操作:

query.filter(User.name == 'ed')
query.filter(User.name != 'ed')
query.filter(User.name.like('%ed%'))
query.filter(User.name.ilike('%ed%'))
query.filter(User.name.in_(['ed', 'wendy', 'jack']))
query.filter(User.name.in_(
    session.query(User.name).filter(User.name.like('%ed%'))
))
query.filter(~User.name.in_(['ed', 'wendy', 'jack']))
query.filter(User.name == None)
query.filter(User.name.is_(None))
query.filter(User.name != None)
query.filter(User.name.isnot(None))

# and在filter中的三種方式
from sqlalchemy import and_
query.filter(and_(User.name == 'ed', User.fullname == 'Ed Jones'))
query.filter(User.name == 'ed', User.fullname == 'Ed Jones')
query.filter(User.name == 'ed').filter(User.fullname == 'Ed Jones')

from sqlalchemy import or_
query.filter(or_(User.name == 'ed', User.name == 'wendy'))

query.filter(User.name.match('wendy'))

返回列表和標(biāo)量

>>> query = session.query(User).filter(User.name.like('%ed')).order_by(User.id)
>>> query.all()
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>,
      <User(name='fred', fullname='Fred Flintstone', nickname='freddy')>]

>>> query.first()
<User(name='ed', fullname='Ed Jones', nickname='eddie')>

>>> user = query.one()#不是只有一行就報(bào)錯(0行)
Traceback (most recent call last):
...
MultipleResultsFound: Multiple rows were found for one()

>>> user = query.filter(User.id == 99).one()#不是只有一行就報(bào)錯(2行以上)
Traceback (most recent call last):
...
NoResultFound: No row was found for one()

>>> user = query.filter(User.id == 99).one_or_none()#超過一行就報(bào)錯,一行或零行不報(bào)錯
Traceback (most recent call last):
...
NoResultFound: No row was found for one()

>>> query = session.query(User.id).filter(User.name == 'ed').order_by(User.id)
>>> query.scalar()#scalar() 調(diào)用 one() 方法福也,并在成功時返回行的第一列:
1

使用文本SQL

>>> from sqlalchemy import text
>>> for user in session.query(User).\
...             filter(text("id<224")).\
...             order_by(text("id")).all():
...     print(user.name)
ed
wendy
mary
fred

使用冒號指定綁定參數(shù)局骤,用 params() 方法指定值:

>>> session.query(User).filter(text("id<:value and name=:name")).\
...     params(value=224, name='fred').order_by(User.id).one()
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')>

把表示完整語句的text()傳遞給 from_statement():

>>> session.query(User).from_statement(
...                     text("SELECT * FROM users where name=:name")).\
...                     params(name='ed').all()
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>]

text() 允許我們將文本SQL按位置鏈接到ORM映射的列;具體的操作是將列作為位置參數(shù)傳遞給 TextClause.columns():

>>> stmt = text("SELECT name, id, fullname, nickname "
...             "FROM users where name=:name")
>>> stmt = stmt.columns(User.name, User.id, User.fullname, User.nickname)
>>> session.query(User).from_statement(stmt).params(name='ed').all()
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>]

或者

>>> stmt = text("SELECT name, id FROM users where name=:name")
>>> stmt = stmt.columns(User.name, User.id)
>>> session.query(User.id, User.name).\
...          from_statement(stmt).params(name='ed').all()
[(1, u'ed')]

計(jì)數(shù)

>>> session.query(User).filter(User.name.like('%ed')).count()
2

>>> from sqlalchemy import func
>>> session.query(func.count(User.name), User.name).group_by(User.name).all()
[(1, u'ed'), (1, u'fred'), (1, u'mary'), (1, u'wendy')]


>>> session.query(func.count('*')).select_from(User).scalar()#SELECT count(*) FROM table
4

>>> session.query(func.count(User.id)).scalar()#也可以這樣
4

建立關(guān)聯(lián)

User表中的每一用戶可以存儲任意數(shù)量的與其用戶名相關(guān)聯(lián)的電子郵件地址暴凑。從User表到存儲電子郵件地址(我們稱之為Address)的新表存在一對多關(guān)聯(lián)峦甩。我們定義這個表及其映射類Address:

>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy.orm import relationship

>>> class Address(Base):
...     __tablename__ = 'addresses'
...     id = Column(Integer, primary_key=True)
...     email_address = Column(String, nullable=False)
...     user_id = Column(Integer, ForeignKey('users.id'))
...
...     user = relationship("User", back_populates="addresses")
...
...     def __repr__(self):
...         return "<Address(email_address='%s')>" % self.email_address

>>> User.addresses = relationship(
...     "Address", order_by=Address.id, back_populates="user")

上面的類引入了ForeignKey,它是一個應(yīng)用于列的指令,指示應(yīng)該將該列中的值限制在指定的另一張表的對應(yīng)列中可能出現(xiàn)的值的范圍內(nèi)凯傲。這是關(guān)系數(shù)據(jù)庫的核心特性犬辰,也是將原本不連接的表集合轉(zhuǎn)換為具有豐富的重疊關(guān)系的粘合劑。上面的外鍵表示Address中的user_id列應(yīng)該被約束為User表中的id列冰单,即它的主鍵幌缝。

第二個指令稱為relationship(),它告訴ORM诫欠,Address類本身應(yīng)該使用屬性Address. user鏈接到User類涵卵。relationship()使用兩個表之間的外鍵關(guān)系來確定這個鏈接的性質(zhì),從而確定該Address與User的關(guān)系是多對一荒叼。

附加的relationship()放在User類下新建的User.address屬性上轿偎。在這兩個relationship()中,back_populates指定為互相引用的屬性名被廓;通過這樣做坏晦,每個relationship()可以用back_populates傳遞關(guān)聯(lián):一端Address.user引用一個user實(shí)例,另一端則是User.addresses引用Address實(shí)例的列表嫁乘,我們可以通過User表的back_populates反向查出所有它在addresses表里的關(guān)聯(lián)項(xiàng)昆婿。

我們需要在數(shù)據(jù)庫中創(chuàng)建地址表,所以我們將從我們的元數(shù)據(jù)發(fā)出另一個創(chuàng)建亦渗,它將跳過已經(jīng)創(chuàng)建的表:

>>> Base.metadata.create_all(engine)

現(xiàn)在挖诸,當(dāng)我們創(chuàng)建一個User時,將出現(xiàn)一個空白addresses集合法精。在這里可以使用各種集合類型多律,比如集合和字典,但是在默認(rèn)情況下搂蜓,集合是一個Python列表狼荞。

>>> jack = User(name='jack', fullname='Jack Bean', nickname='gjffdd')
>>> jack.addresses
[]

我們可以自由地在User對象上添加Address對象:

>>> jack.addresses = [
...                 Address(email_address='jack@google.com'),
...                 Address(email_address='j25@yahoo.com')]

當(dāng)使用雙向關(guān)系時,在一個方向上添加的元素在另一個方向上自動變得可見帮碰。此行為基于on-change事件屬性發(fā)生相味,并在Python中進(jìn)行計(jì)算,不使用任何SQL:

>>> jack.addresses[1]
<Address(email_address='j25@yahoo.com')>

>>> jack.addresses[1].user
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>

我們現(xiàn)在就向數(shù)據(jù)庫添加并提交Jack Bean殉挽。jack和對應(yīng)addresses集合中的兩個Address成員都被一次性添加到會話中丰涉,這個添加關(guān)聯(lián)數(shù)據(jù)的過程叫級聯(lián)(cascading):

>>> session.add(jack)
>>> session.commit()

查詢 Jack,只返回 Jack斯碌。沒有關(guān)于Jack的addresses的SQL:

>>> jack = session.query(User).\
... filter_by(name='jack').one()
>>> jack
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>

讓我們看看addresses集合一死。注意觀察SQL:

>>> jack.addresses
[<Address(email_address='jack@google.com')>, <Address(email_address='j25@yahoo.com')>]

當(dāng)我們訪問addresses集合時,SQL指令才被發(fā)出傻唾。這是一個延遲加載(lazy loading)關(guān)系的例子⊥洞龋現(xiàn)在已經(jīng)加載了addresses集合承耿,其行為與普通列表一樣。我們將簡要介紹優(yōu)化這個集合的加載的方法伪煤。

基于外鍵關(guān)聯(lián)的多表聯(lián)合查詢

要在User和Address之間構(gòu)造一個簡單的隱式連接加袋,可以使用Query.filter()將它們的相關(guān)列等同起來。下面我們用這個方法加載用戶和地址實(shí)體

>>> for u, a in session.query(User, Address).\
...                     filter(User.id==Address.user_id).\
...                     filter(Address.email_address=='jack@google.com').\
...                     all():
...     print(u)
...     print(a)
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>
<Address(email_address='jack@google.com')>

另一方面抱既,實(shí)際的SQL JOIN語法最容易使用Query.join()方法實(shí)現(xiàn):

>>> session.query(User).join(Address).\
...         filter(Address.email_address=='jack@google.com').\
...         all()
[<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>]

Query.join()知道如何在用戶和地址之間進(jìn)行連接职烧,因?yàn)樗鼈冎g只有一個外鍵。如果沒有外鍵防泵,或者有多個外鍵阳堕,那么Query.join()在使用以下一種形式時工作得更好:

query.join(Address, User.id==Address.user_id)    # 顯式關(guān)系
query.join(User.addresses)                       # 右連接指定關(guān)系
query.join(Address, User.addresses)              # 顯式關(guān)系指定

正如您所期望的,使用Query.outerjoin()函數(shù)對外連接使用了相同的思想:

query.outerjoin(User.addresses)   # LEFT OUTER JOIN

join()方法通常從實(shí)體列表中最左邊的項(xiàng)進(jìn)行連接择克,如果省略了ON子句,或者ON子句是一個普通的SQL表達(dá)式前普。要控制連接列表中的第一個實(shí)體肚邢,可以使用Query.select_from()方法:

query = session.query(User, Address).select_from(Address).join(User)

使用別名

在跨多個表進(jìn)行查詢時,如果需要多次引用同一個表拭卿,SQL通常要求該表使用另一個名稱作為別名骡湖,這樣就可以根據(jù)該表的其他情況對其進(jìn)行區(qū)分。使用aliased()構(gòu)造支持這一點(diǎn)峻厚。在使用aliased()連接關(guān)系時响蕴,可以使用特殊屬性方法PropComparator.of_type()來更改關(guān)系連接的目標(biāo),以引用給定的aliased()對象惠桃。下面浦夷,我們兩次連接到Address實(shí)體,以定位具有兩個不同電子郵件地址的用戶:

>>> from sqlalchemy.orm import aliased
>>> adalias1 = aliased(Address)
>>> adalias2 = aliased(Address)
>>> for username, email1, email2 in \
...     session.query(User.name, adalias1.email_address, adalias2.email_address).\
...     join(User.addresses.of_type(adalias1)).\
...     join(User.addresses.of_type(adalias2)).\
...     filter(adalias1.email_address=='jack@google.com').\
...     filter(adalias2.email_address=='j25@yahoo.com'):
...     print(username, email1, email2)
jack jack@google.com j25@yahoo.com

除了使用PropComparator.of_type()方法之外辜王,還經(jīng)撑可以看到Query.join()方法通過單獨(dú)指示一個特定的目標(biāo)來連接:

# 等同于 query.join(User.addresses.of_type(adalias1))
q = query.join(adalias1, User.addresses)

使用子查詢

Query可以用作生成子查詢的語句。假設(shè)我們想加載User對象以及每個用戶有多少Address記錄的計(jì)數(shù)呐馆。生成這樣的SQL的最佳方法是獲得按用戶id分組的地址計(jì)數(shù)肥缔,并連接到父地址。在這種情況下汹来,我們使用左外連接续膳,這樣我們可以為那些沒有任何地址的用戶返回行,例如:

SELECT users.*, adr_count.address_count FROM users LEFT OUTER JOIN
    (SELECT user_id, count(*) AS address_count
        FROM addresses GROUP BY user_id) AS adr_count
    ON users.id=adr_count.user_id

把這段SQL換成我們的query:

>>> from sqlalchemy.sql import func
>>> stmt = session.query(Address.user_id, func.count('*').\
...         label('address_count')).\
...         group_by(Address.user_id).subquery()

func生成SQL函數(shù)收班,Query上的subquery()方法生成一個SQL表達(dá)式構(gòu)造坟岔,表示嵌入在別名中的SELECT語句(實(shí)際上是Query.statement.alias()的簡寫形式)。
一旦有了語句闺阱,它的行為就像一個表結(jié)構(gòu)炮车,就像我們在本教程開始時為用戶創(chuàng)建的表結(jié)構(gòu)一樣。語句的列可以通過一個名為c的屬性訪問:

>>> for u, count in session.query(User, stmt.c.address_count).\
...     outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id):
...     print(u, count)
<User(name='ed', fullname='Ed Jones', nickname='eddie')> None
<User(name='wendy', fullname='Wendy Williams', nickname='windy')> None
<User(name='mary', fullname='Mary Contrary', nickname='mary')> None
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')> None
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')> 2

從子查詢中選擇對象

上面,我們只是從子查詢中選擇了一個包含列的結(jié)果
如果我們希望我們的子查詢映射到一個對象呢瘦穆?
可以使用aliased()將映射類的“別名”關(guān)聯(lián)到子查詢

>>> stmt = session.query(Address).\
...                 filter(Address.email_address != 'j25@yahoo.com').\
...                 subquery()
>>> adalias = aliased(Address, stmt)
>>> for user, address in session.query(User, adalias).\
...         join(adalias, User.addresses):#這樣就返回Address的對象纪隙,而不是列了
...     print(user)
...     print(address)
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>
<Address(email_address='jack@google.com')>

如果不加aliased()是這樣的,stmt被拆成列:

>>> stmt = session.query(Address).\
...                 filter(Address.email_address != 'j25@yahoo.com').\
...                 subquery()
>>> for user in session.query(User, stmt).\
...         join(stmt, User.addresses):
...     print(user)
(<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>, 1, 'jack@google.com', 5)

判斷數(shù)據(jù)的存在性

SQL中的EXISTS關(guān)鍵字是一個布爾運(yùn)算符扛或,如果給定的表達(dá)式包含任何行绵咱,它將返回True。它可以在許多場景中代替join熙兔,并且對于定位相關(guān)表中沒有對應(yīng)行的行也很有用悲伶。
存在一個顯式的結(jié)構(gòu),它看起來像這樣:

>>> from sqlalchemy.sql import exists
>>> stmt = exists().where(Address.user_id==User.id)
>>> for name, in session.query(User.name).filter(stmt):
...     print(name)
jack

上面的語句換成可以沿著User.addresses的關(guān)聯(lián)使用Comparator.any():

>>> for name, in session.query(User.name).\
...         filter(User.addresses.any()):
...     print(name)
jack

也可以給它個判斷依據(jù)住涉,來限制它返回的行數(shù):

>>> for name, in session.query(User.name).\
...     filter(User.addresses.any(Address.email_address.like('%google%'))):
...     print(name)
jack

Comparator.has() 跟 Comparator.any()一樣都是用于多對一的關(guān)系:

>>> session.query(Address).\
...         filter(~Address.user.has(User.name=='jack')).all()
[]

各種基于關(guān)系的操作:

#Comparator.__eq__() (多對一麸锉,等于):
query.filter(Address.user == someuser)
#Comparator.__ne__() (多對一,不等于):
query.filter(Address.user != someuser)
#IS NULL (多對一):
query.filter(Address.user == None)
#Comparator.contains() (用于一對多的集合):
query.filter(User.addresses.contains(someaddress))
#Comparator.any() (用于集合):
query.filter(User.addresses.any(Address.email_address == 'bar'))

# 也接受關(guān)鍵字參數(shù)
query.filter(User.addresses.any(email_address='bar'))
#Comparator.has() (Address.user.has(User.name=='ed')):
query.filter(Address.user.has(name='ed'))
#Query.with_parent() (用于任何關(guān)聯(lián)):
session.query(Address).with_parent(someuser, 'addresses')

預(yù)加載

回想一下前面我們在訪問User.addresses時演示的延遲加載操作舆声,在訪問User的address集合時才發(fā)出SQL花沉。如果您想要減少查詢的次數(shù)(在很多情況下是顯著減少),我們可以對查詢操作應(yīng)用即時加載媳握。
SQLAlchemy提供了三種即時加載類型碱屁,其中兩種是自動加載的,第三種是自定義標(biāo)準(zhǔn)蛾找。這三種方法通常都是通過稱為query options的函數(shù)調(diào)用的娩脾,該函數(shù)通過query.options()方法向查詢提供關(guān)于我們希望如何加載各種屬性的附加指令。

Selectin Load

在這種情況下打毛,我們想要User.addresses應(yīng)盡快加載柿赊。加載一組對象及其相關(guān)的集合的一個很好的選擇是selectinload(),它發(fā)出第二個SELECT語句幻枉,該語句完全加載與剛剛加載的結(jié)果相關(guān)聯(lián)的集合闹瞧。名稱“selectin”源于SELECT語句使用IN子句來同時定位多個對象的相關(guān)行:

>>> from sqlalchemy.orm import selectinload
>>> jack = session.query(User).\
...                 options(selectinload(User.addresses)).\
...                 filter_by(name='jack').one()
>>> jack
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>

>>> jack.addresses
[<Address(email_address='jack@google.com')>, <Address(email_address='j25@yahoo.com')>]

Joined Load

這種加載方式會發(fā)出連接,默認(rèn)情況下是左外連接展辞,以便在一次操作中預(yù)先加載主對象及其相關(guān)對象(注意:這個不是JOIN)

>>> from sqlalchemy.orm import joinedload

>>> jack = session.query(User).\
...                        options(joinedload(User.addresses)).\
...                        filter_by(name='jack').one()
>>> jack
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>

>>> jack.addresses
[<Address(email_address='jack@google.com')>, <Address(email_address='j25@yahoo.com')>]

Explicit Join + Eagerload

第三種快速加載方式是奥邮,當(dāng)我們顯式地構(gòu)造一個JOIN以定位主行,并且希望將額外的表應(yīng)用于主對象上的相關(guān)對象或集合時罗珍。該特性是通過contains_eager()函數(shù)提供的洽腺,通常用于在需要對同一對象進(jìn)行篩選的查詢中預(yù)加載多對一對象。下面的例子做到了預(yù)先加載每一條Address表的記錄和與之關(guān)聯(lián)的User對象(例子中只篩選出名為“jack”的User)覆旱,使用contains_eager()將“User”表中的列預(yù)加載進(jìn)Address.user蘸朋。

>>> from sqlalchemy.orm import contains_eager

>>> jacks_addresses = session.query(Address).\
...                             join(Address.user).\
...                             filter(User.name=='jack').\
...                             options(contains_eager(Address.user)).\
...                             all()
>>> jacks_addresses
[<Address(email_address='jack@google.com')>, <Address(email_address='j25@yahoo.com')>]

>>> jacks_addresses[0].user
<User(name='jack', fullname='Jack Bean', nickname='gjffdd')>

刪除操作

讓我們試著刪除jack,看看結(jié)果如何扣唱。我們會在會話中將對象標(biāo)記為已刪除藕坯,然后我們會發(fā)出一個計(jì)數(shù)查詢用以確認(rèn)团南。

>>> session.delete(jack)
sql
>>> session.query(User).filter_by(name='jack').count()
0

到目前為止,一切順利炼彪。那Jack的Address對象呢?

>>> session.query(Address).filter(
...     Address.email_address.in_(['jack@google.com', 'j25@yahoo.com'])
...  ).count()
2

它還在吐根!SQLAlchemy并沒有級聯(lián)刪除它。

配置級聯(lián)刪除

我們將在User.addresses的關(guān)聯(lián)上配置級聯(lián)選項(xiàng)辐马。雖然SQLAlchemy允許您在任何時間點(diǎn)向映射添加新的屬性和關(guān)系拷橘,但是在這種情況下,需要刪除現(xiàn)有的關(guān)系喜爷,所以我們需要完全刪除映射并重新開始—先關(guān)閉會話:

>>> session.close()
ROLLBACK

創(chuàng)建一個新的declarative_base():

>>> Base = declarative_base()

接下來冗疮,我們將聲明User類,添加addresses關(guān)聯(lián)檩帐,包括級聯(lián)配置(我們也將不使用構(gòu)造函數(shù)):

>>> class User(Base):
...     __tablename__ = 'users'
...
...     id = Column(Integer, primary_key=True)
...     name = Column(String)
...     fullname = Column(String)
...     nickname = Column(String)
...
...     addresses = relationship("Address", back_populates='user',
...                     cascade="all, delete, delete-orphan")
...
...     def __repr__(self):
...        return "<User(name='%s', fullname='%s', nickname='%s')>" % (
...                                self.name, self.fullname, self.nickname)

然后我們重建Address類术幔,注意,在本例中我們已經(jīng)通過User類創(chuàng)建了Address.user關(guān)聯(lián):

>>> class Address(Base):
...     __tablename__ = 'addresses'
...     id = Column(Integer, primary_key=True)
...     email_address = Column(String, nullable=False)
...     user_id = Column(Integer, ForeignKey('users.id'))
...     user = relationship("User", back_populates="addresses")
...
...     def __repr__(self):
...         return "<Address(email_address='%s')>" % self.email_address

下面使用Query.get()加載user jack(它按主鍵加載)湃密,從對應(yīng)的addresses集合中移除一個地址將會導(dǎo)致該地址也被刪除:

# 按主鍵加載jack
>>> jack = session.query(User).get(5)

# 刪除一個地址(延遲加載被觸發(fā))
>>> del jack.addresses[1]

# 只剩下一個地址
>>> session.query(Address).filter(
...     Address.email_address.in_(['jack@google.com', 'j25@yahoo.com'])
... ).count()
1

刪除Jack將同時刪除Jack和與用戶關(guān)聯(lián)的剩余Address:

>>> session.delete(jack)

>>> session.query(User).filter_by(name='jack').count()
0

>>> session.query(Address).filter(
...    Address.email_address.in_(['jack@google.com', 'j25@yahoo.com'])
... ).count()
0

建立多對多的關(guān)聯(lián)

對于普通的多對多特愿,我們需要創(chuàng)建一個未映射的表結(jié)構(gòu)作為關(guān)聯(lián)表。這看起來像如下:

>>> from sqlalchemy import Table, Text
>>> # 關(guān)聯(lián)表
>>> post_keywords = Table('post_keywords', Base.metadata,
...     Column('post_id', ForeignKey('posts.id'), primary_key=True),
...     Column('keyword_id', ForeignKey('keywords.id'), primary_key=True)
... )

在上面勾缭,我們可以看到直接聲明一個表與聲明一個映射類有一點(diǎn)不同。Table是一個構(gòu)造函數(shù)目养,因此每個列參數(shù)之間用逗號分隔俩由。列對象的名稱也是顯式給定的,而不是從已分配的屬性名稱中獲取的癌蚁。
接下來幻梯,我們定義BlogPost和Keyword結(jié)構(gòu),互相relationship()努释,每個都將post_keywords表引用為一個關(guān)聯(lián)表:

>>> class BlogPost(Base):
...     __tablename__ = 'posts'
...
...     id = Column(Integer, primary_key=True)
...     user_id = Column(Integer, ForeignKey('users.id'))
...     headline = Column(String(255), nullable=False)
...     body = Column(Text)
...
...     # many to many BlogPost<->Keyword
...     keywords = relationship('Keyword',
...                             secondary=post_keywords,
...                             back_populates='posts')
...
...     def __init__(self, headline, body, author):
...         self.author = author
...         self.headline = headline
...         self.body = body
...
...     def __repr__(self):
...         return "BlogPost(%r, %r, %r)" % (self.headline, self.body, self.author)


>>> class Keyword(Base):
...     __tablename__ = 'keywords'
...
...     id = Column(Integer, primary_key=True)
...     keyword = Column(String(50), nullable=False, unique=True)
...     posts = relationship('BlogPost',
...                          secondary=post_keywords,
...                          back_populates='keywords')
...
...     def __init__(self, keyword):
...         self.keyword = keyword

我們還希望我們的BlogPost類有一個author字段碘梢。我們將把它添加為另一個雙向關(guān)系,因?yàn)閱蝹€用戶可能有很多博客文章伐蒂。當(dāng)我們訪問User.posts時煞躬,我們希望能夠在不加載整個集合的前提下進(jìn)一步filter結(jié)果。為此逸邦,我們使用了一個名為lazy='dynamic'的relationship()加載策略:

>>> BlogPost.author = relationship(User, back_populates="posts")
>>> User.posts = relationship(BlogPost, back_populates="author", lazy="dynamic")

創(chuàng)建新表

>>> Base.metadata.create_all(engine)

用法和我們之前做的沒有太大區(qū)別恩沛。讓我們給Wendy一些博客文章:

>>> wendy = session.query(User).\
...                 filter_by(name='wendy').\
...                 one()
>>> post = BlogPost("Wendy's Blog Post", "This is a test", wendy)
>>> session.add(post)

我們將關(guān)鍵字唯一地存儲在數(shù)據(jù)庫中,但我們知道我們還沒有任何關(guān)鍵字缕减,所以我們先創(chuàng)建它們

>>> post.keywords.append(Keyword('wendy'))
>>> post.keywords.append(Keyword('firstpost'))

我們現(xiàn)在可以用“firstpost”這個關(guān)鍵字查找所有的博客文章雷客。我們將使用any操作符來定位“其中的任何關(guān)鍵詞含有字符串' firstpost '的博客文章”:

>>> session.query(BlogPost).\
...             filter(BlogPost.keywords.any(keyword='firstpost')).\
...             all()
[BlogPost("Wendy's Blog Post", 'This is a test', <User(name='wendy', fullname='Wendy Williams', nickname='windy')>)]

如果我們想要查找用戶wendy擁有的文章,我們可以告訴查詢將范圍縮小到User對象作為父對象:

>>> session.query(BlogPost).\
...             filter(BlogPost.author==wendy).\
...             filter(BlogPost.keywords.any(keyword='firstpost')).\
...             all()
[BlogPost("Wendy's Blog Post", 'This is a test', <User(name='wendy', fullname='Wendy Williams', nickname='windy')>)]

或者我們可以使用Wendy自己的posts關(guān)系桥狡,這是一種“動態(tài)”關(guān)系搅裙,直接從wendy.posts查詢:

>>> wendy.posts.\
...         filter(BlogPost.keywords.any(keyword='firstpost')).\
...         all()
[BlogPost("Wendy's Blog Post", 'This is a test', <User(name='wendy', fullname='Wendy Williams', nickname='windy')>)]

(完)
參考資料:[SQLAlchemy 1.3 Documentation]https://docs.sqlalchemy.org/en/13/orm/tutorial.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末皱卓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子部逮,更是在濱河造成了極大的恐慌娜汁,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甥啄,死亡現(xiàn)場離奇詭異存炮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蜈漓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門穆桂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人融虽,你說我怎么就攤上這事享完。” “怎么了有额?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵般又,是天一觀的道長。 經(jīng)常有香客問我巍佑,道長茴迁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任萤衰,我火速辦了婚禮堕义,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脆栋。我一直安慰自己倦卖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般朴上。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上褐捻,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音椅邓,去河邊找鬼舍扰。 笑死,一個胖子當(dāng)著我的面吹牛希坚,可吹牛的內(nèi)容都是我干的边苹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼裁僧,長吁一口氣:“原來是場噩夢啊……” “哼个束!你這毒婦竟也來了慕购?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茬底,失蹤者是張志新(化名)和其女友劉穎沪悲,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阱表,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡殿如,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了最爬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涉馁。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖爱致,靈堂內(nèi)的尸體忽然破棺而出烤送,到底是詐尸還是另有隱情,我是刑警寧澤糠悯,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布帮坚,位于F島的核電站,受9級特大地震影響互艾,放射性物質(zhì)發(fā)生泄漏试和。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一纫普、第九天 我趴在偏房一處隱蔽的房頂上張望阅悍。 院中可真熱鬧,春花似錦局嘁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至晌畅,卻和暖如春但指,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抗楔。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工棋凳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人连躏。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓剩岳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親入热。 傳聞我的和親對象是個殘疾皇子拍棕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355