在 Flask-SQLAlchemy 中聯(lián)表查詢

SQLAlchemy 是一個(gè)功能強(qiáng)大的 ORM 筝尾。 Flask-SQLAlchemy 是一個(gè) Flask 插件粟关,它讓我們?cè)?Flask 框架中使用 SQLAlchemy 變得更容易统诺。

本篇介紹我在使用 Flask-SQLAlchemy 2.1 時(shí)進(jìn)行聯(lián)表查詢的一些經(jīng)驗(yàn)

表定義


這里有兩個(gè)表袱蚓,account 表保存帳號(hào) ID 和昵稱肮塞,bind 表保存 account 之間的綁定關(guān)系。

# 省略了外鍵定義梯澜,請(qǐng)自行腦補(bǔ)
create table account
(
    gameuid int auto_increment primary key,
    nickname varchar(34) not null
);
create table bind
(
    bindid int auto_increment primary key,
    fromid int not null,
    toid int not null
);

對(duì)應(yīng)的 Model 如下:

class Account(db.Model):
    __tablename__ = 'account'
    gameuid = db.Column(db.INT, primary_key=True, nullable=False, autoincrement=True)
    nickname = db.Column(db.VARCHAR(64), nullable=False, unique=True)
    def __repr__(self):
        return '<Account %r>' % (self.gameuid)

class Bind(db.Model):
    __tablename__ = 'bind'
    bindid = db.Column(db.BIGINT, primary_key=True, autoincrement=True)
    # 綁定者和被綁定者的 gameuid
    fromid = db.Column(db.BIGINT, db.ForeignKey('account.gameuid'), nullable=False)
    toid = db.Column(db.BIGINT, db.ForeignKey('account.gameuid'), nullable=False)
    def __repr__(self):
        return '<Bind %r.%r>' % (self.fromid, self.toid)

關(guān)聯(lián)查詢


先來(lái)看一個(gè)簡(jiǎn)單的例子:查詢 gameuid 1000 賬號(hào)下綁定的所有帳號(hào)寞冯。

>>> db.session.query(Bind.bindid, Bind.fromid, Bind.toid, Account.gameuid, Account.nickname). \
    filter(Bind.toid == 1000). \
    filter(Bind.fromid == Account.gameuid). \
    all()
[(2, 10001, 1000, 10001, '玩家10001'), (3, 10002, 1000, 10002, '玩家10002'), (4, 10003, 1000, 10003, '玩家10003'), (5, 10004, 1000, 10004, '玩家10004'), (6, 10005, 1000, 10005, '玩家10005'), (7, 10006, 1000, 10006, '玩家10006'), (8, 10007, 1000, 10007, '玩家10007'), (9, 10008, 1000, 10008, '玩家10008'), (10, 10009, 1000, 10009, '玩家10009'), (53, 10000, 1000, 10000, '玩家10000'), (54, 11000, 1000, 11000, '玩家11000')]

看一看生成的 SQL 語(yǔ)句:

>>> print(db.session.query(Bind.bindid, Bind.fromid, Bind.toid, Account.gameuid, Account.nickname). \
    filter(Bind.toid == 1000). \
    filter(Bind.fromid == Account.gameuid))
SELECT bind.bindid AS bind_bindid, bind.fromid AS bind_fromid, bind.toid AS bind_toid, account.gameuid AS account_gameuid, account.nickname AS account_nickname
FROM bind, account
WHERE bind.toid = %(toid_1)s AND bind.fromid = account.gameuid

這里的聯(lián)表查詢使用的是 WHERE 語(yǔ)句。如果希望使用 JOIN 語(yǔ)句晚伙,可以這樣寫:

>>> print(db.session.query(Bind.bindid, Account.gameuid, Account.nickname). \
    join(Account, Account.gameuid==Bind.fromid). \
    filter(Bind.toid == 1000))
SELECT bind.bindid AS bind_bindid, bind.fromid AS bind_fromid, account.gameuid AS account_gameuid, account.nickname AS account_nickname
FROM bind INNER JOIN account ON account.gameuid = bind.fromid
WHERE bind.toid = %(toid_1)s

可以看出吮龄,現(xiàn)在生成的 SQL 語(yǔ)句已經(jīng)使用 JOIN 語(yǔ)句了。但上面的語(yǔ)意有點(diǎn)奇怪撬腾,既然已經(jīng)在 query 中使用了 Bind 和 Account螟蝙,后面再 join 一次 Account 總覺(jué)得有點(diǎn)多余恢恼。那么 SQLAlchemy 如何選擇 JOIN 的時(shí)候誰(shuí)先誰(shuí)后呢民傻?看看這個(gè)錯(cuò)誤就知道了:

>>> db.session.query(Bind.bindid, Bind.fromid, Account.gameuid, Account.nickname). \
    join(Bind, Account.gameuid==Bind.fromid). \
    filter(Bind.toid == 1000)
>>> Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/zrong/.pyvenv/api/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 1971, in join
    from_joinpoint=from_joinpoint)
  File "<string>", line 2, in _join
  File "/Users/zrong/.pyvenv/api/lib/python3.6/site-packages/sqlalchemy/orm/base.py", line 201, in generate
    fn(self, *args[1:], **kw)
  File "/Users/zrong/.pyvenv/api/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2115, in _join
    outerjoin, full, create_aliases, prop)
  File "/Users/zrong/.pyvenv/api/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2171, in _join_left_to_right
    l_info.selectable)
sqlalchemy.exc.InvalidRequestError: Can't join table/selectable 'bind' to itself

這個(gè)錯(cuò)誤顯然說(shuō)明,query 中參數(shù)的順序很重要场斑,第一個(gè)參數(shù)所代表的 table 就是 JOIN 時(shí)放在前面的那個(gè) table漓踢。因此,此處 JOIN 的目標(biāo)應(yīng)該是 Account漏隐, 而不應(yīng)該是 Bind 自身喧半。

分頁(yè)支持


上面的例子已經(jīng)解決了大多數(shù)需求了。我們?cè)賮?lái)看看分頁(yè)青责。在 Flask-SQLAlchemy 中封裝了一個(gè) paginate 方法挺据,可以方便地將查詢記錄進(jìn)行分頁(yè):

>>> db.session.query(Bind.bindid, Bind.fromid, Account.gameuid, Account.nickname). \
    join(Bind, Account.gameuid==Bind.fromid). \
    filter(Bind.toid == 1000). \
    paginate(1, 10)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Query' object has no attribute 'paginate'

報(bào)錯(cuò)的原因是db.session.query 默認(rèn)返回的是 orm.Query 對(duì)象,這個(gè)對(duì)象并不包含paginate方法脖隶。要解決這個(gè)問(wèn)題扁耐,需要修改 Flask-SQLAlchemy 的源碼。

找到 SQLAlchemy 對(duì)象的 __init__ 定義产阱,在其中加入 session_options['query_cls'] = BaseQuery 即可:

def __init__(self, app=None, use_native_unicode=True, session_options=None, metadata=None):

    if session_options is None:
        session_options = {}

    session_options.setdefault('scopefunc', connection_stack.__ident_func__)
    self.use_native_unicode = use_native_unicode
    self.app = app

    # 使用 BaseQuery婉称,這樣可以讓使用 db.session.query 等方法創(chuàng)建的 Query 對(duì)象支持 BaseQuery 的方法
    session_options['query_cls'] = BaseQuery

另一種關(guān)聯(lián)查詢語(yǔ)法

在 Flask-SQLAlchemy 提供的 Model 對(duì)象中,可以使用 Model.query 這樣的語(yǔ)法來(lái)直接得到一個(gè)查詢對(duì)象,這是由于 Flask-SQLAlchemy 中存在一個(gè) _QueryProperty 類王暗,每次調(diào)用 Model.__get__時(shí)悔据,會(huì)自動(dòng)生成一個(gè)基于當(dāng)前 session 的 query 對(duì)象:

class _QueryProperty(object):

    def __init__(self, sa):
        self.sa = sa

    def __get__(self, obj, type):
        try:
            mapper = orm.class_mapper(type)
            if mapper:
                return type.query_class(mapper, session=self.sa.session())
        except UnmappedClassError:
            return None

使用 Model.query得到的這個(gè) query 對(duì)象可以直接進(jìn)行 JOIN 操作,得到的結(jié)果是 Model 對(duì)象俗壹。這樣就方便多了:

>>> Account.query.join(Bind, Bind.fromid == Account.gameuid).filter(Bind.toid == 1000).all()
[<Account 10001>, <Account 10002>, <Account 10003>, <Account 10004>, <Account 10005>, <Account 10006>, <Account 10007>, <Account 10008>, <Account 10009>, <Account 10000>, <Account 11000>]

轉(zhuǎn)換成 SQL 是這樣的:

SELECT account.gameuid AS account_gameuid, account.nickname AS account_nickname
FROM account INNER JOIN bind ON bind.fromid = account.gameuid
WHERE bind.toid = %(toid_1)s

可以看出科汗,這樣的查詢結(jié)果和使用 db.session.query 并沒(méi)有什么不同。由于返回的是 Model 對(duì)象绷雏,使用上可能還更加方便了肛捍。

篩選字段


如何使用 Model.query.join 語(yǔ)法得到部分字段呢?這里可以使用 SQLAlchemy 提供的 with_entities 方法:

>>> Account.query.join(Bind, Bind.fromid == Account.gameuid). \
    filter(Bind.toid == 1000). \
    with_entities(Bind.bindid, Account.nickname).all()
[(2, '玩家10001'), (3, '玩家10002'), (4, '玩家10003'), (5, '玩家10004'), (6, '玩家10005'), (7, '玩家10006'), (8, '玩家10007'), (9, '玩家10008'), (10, '玩家10009'), (53, '玩家10000'), (54, '玩家11000')]
>>>

注意之众,列表中的項(xiàng) (2, '玩家10001') 并不是標(biāo)準(zhǔn)的 Python tuple拙毫。你如果查看它的類型,會(huì)發(fā)現(xiàn)一個(gè)奇怪的名稱: <class 'sqlalchemy.util._collections.result'> 棺禾。它是一個(gè) AbstractKeyedTuple 對(duì)象缀蹄,擁有一個(gè) keys() 方法,這樣可以很容易將其轉(zhuǎn)換成 dict :

>>> results = Account.query.join(Bind, Bind.fromid == Account.gameuid). \
    filter(Bind.toid == 1000). \
    with_entities(Bind.bindid, Account.nickname).all()
>>> [dict(zip(result.keys(), result)) for result in results]
[{'bindid': 2, 'nickname': '玩家10001'}, {'bindid': 3, 'nickname': '玩家10002'}, {'bindid': 4, 'nickname': '玩家10003'}, {'bindid': 5, 'nickname': '玩家10004'}, {'bindid': 6, 'nickname': '玩家10005'}, {'bindid': 7, 'nickname': '玩家10006'}, {'bindid': 8, 'nickname': '玩家10007'}, {'bindid': 9, 'nickname': '玩家10008'}, {'bindid': 10, 'nickname': '玩家10009'}, {'bindid': 53, 'nickname': '玩家10000'}, {'bindid': 54, 'nickname': '玩家11000'}]

想了解 AbstractKeyedTuple 膘婶,可以看看這篇文檔 New KeyedTuple implementation dramatically faster 缺前。

獲得多個(gè) Model 的記錄


除了篩選字段外,還可以用另一個(gè)方法獲取多個(gè) Model 的記錄悬襟。那就是衅码,返回兩個(gè) Model 的所有字段:

>>> db.session.query(Account, Bind).join(Bind, Account.gameuid==Bind.fromid).filter(Bind.toid==1000).all()
[(<Account 10001>, <Bind 10001, 1000>), (<Account 10002>, <Bind 10002, 1000>), (<Account 10004>, <Bind 10004, 1000>), (<Account 10005>, <Bind 10005, 1000>), (<Account 10006>, <Bind 10006, 1000>), (<Account 10007>, <Bind 10007, 1000>), (<Account 10008>, <Bind 10008, 1000>), (<Account 10009>, <Bind 10009, 1000>), (<Account 10000>, <Bind 10000, 1000>), (<Account 11000>, <Bind 11000, 1000>)]

使用上面的語(yǔ)法直接返回 Account 和 Bind 對(duì)象,可以進(jìn)行更加靈活的操作脊岳。

多表查詢


要聯(lián)結(jié)超過(guò) 2 張以上的表逝段,可以直接在 join 得到的結(jié)果之后鏈?zhǔn)秸{(diào)用 join 。也可以在 filter 的結(jié)果后面鏈?zhǔn)秸{(diào)用 join 割捅。join 和 filter 返回的都是 query 對(duì)象奶躯,因此可以無(wú)限鏈?zhǔn)秸{(diào)用下去。

寫完查詢后亿驾,應(yīng)該打印生成的 SQL 語(yǔ)句查看一下有沒(méi)有性能問(wèn)題嘹黔。

原文作者:zrong
原文鏈接:https://blog.zengrong.net/post/join-in-flash-sqlalchemy/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市莫瞬,隨后出現(xiàn)的幾起案子儡蔓,更是在濱河造成了極大的恐慌,老刑警劉巖疼邀,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喂江,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡檩小,警方通過(guò)查閱死者的電腦和手機(jī)开呐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人筐付,你說(shuō)我怎么就攤上這事卵惦。” “怎么了瓦戚?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵沮尿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我较解,道長(zhǎng)畜疾,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任印衔,我火速辦了婚禮啡捶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘奸焙。我一直安慰自己瞎暑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布与帆。 她就那樣靜靜地躺著了赌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玄糟。 梳的紋絲不亂的頭發(fā)上勿她,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音阵翎,去河邊找鬼逢并。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贮喧,可吹牛的內(nèi)容都是我干的筒狠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼箱沦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了雇庙?” 一聲冷哼從身側(cè)響起谓形,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎疆前,沒(méi)想到半個(gè)月后寒跳,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竹椒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年童太,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡书释,死狀恐怖翘贮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情爆惧,我是刑警寧澤狸页,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站扯再,受9級(jí)特大地震影響芍耘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜熄阻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一斋竞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秃殉,春花似錦窃页、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至巧颈,卻和暖如春畦木,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背砸泛。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工十籍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人唇礁。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓勾栗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親盏筐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子围俘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容