SQLAlchemy 是一个功能强大的 ORM 。 Flask-SQLAlchemy 是一个 Flask 插件,它让我们在 Flask 框架中使用 SQLAlchemy 变得更容易。

本篇介绍我在使用 Flask-SQLAlchemy 2.1 时进行联表查询的一些经验。

表定义

这里有两个表,account 表保存帐号 ID 和昵称,bind 表保存 account 之间的绑定关系。

1
2
3
4
5
6
7
8
9
10
11
12
# 省略了外键定义,请自行脑补
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
);

对应的 Model 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,foreignKey也可以不用设置!!!
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)

关联查询

先来看一个简单的例子:查询 gameuid 1000 账号下绑定的所有帐号。

1
2
3
4
5
>>> 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 语句:

1
2
3
4
5
6
>>> 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

这里的联表查询使用的是 WHERE 语句。如果希望使用 JOIN 语句,可以这样写:

1
2
3
4
5
6
>>> 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

可以看出,现在生成的 SQL 语句已经使用 JOIN 语句了。但上面的语意有点奇怪,既然已经在 query 中使用了 Bind 和 Account,后面再 join 一次 Account 总觉得有点多余。那么 SQLAlchemy 如何选择 JOIN 的时候谁先谁后呢?看看这个错误就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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

这个错误显然说明,query 中参数的顺序很重要,第一个参数所代表的 table 就是 JOIN 时放在前面的那个 table。因此,此处 JOIN 的目标应该是 Account, 而不应该是 Bind 自身。

分页支持

上面的例子已经解决了大多数需求了。我们再来看看分页。在 Flask-SQLAlchemy 中封装了一个 paginate方法,可以方便地将查询记录进行分页:

1
2
3
4
5
6
7
>>> 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'

报错的原因是 db.session.query 默认返回的是 orm.Query 对象,这个对象并不包含 paginate 方法。要解决这个问题,需要修改 Flask-SQLAlchemy 的源码。

找到 SQLAlchemy 对象的 __init__ 定义,在其中加入 session_options['query_cls'] = BaseQuery 即可:flask-sqlalchemy 2.3.2 版本支持的,不用修改源码了!!!

1
2
3
4
5
6
7
8
9
10
11
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 等方法创建的 Query 对象支持 BaseQuery 的方法
session_options['query_cls'] = BaseQuery

另一种关联查询语法

在 Flask-SQLAlchemy 提供的 Model 对象中,可以使用 Model.query 这样的语法来直接得到一个查询对象,这是由于 Flask-SQLAlchemy 中存在一个 _QueryProperty 类,每次调用 Model.__get__ 时,会自动生成一个基于当前 session 的 query 对象:

1
2
3
4
5
6
7
8
9
10
11
12
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 得到的这个 query 对象可以直接进行 JOIN 操作,得到的结果是 Model 对象。这样就方便多了:

1
2
>>> 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>]

转换成 SQL 是这样的:

1
2
3
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

可以看出,这样的查询结果和使用 db.session.query 并没有什么不同。由于返回的是 Model 对象,使用上可能还更加方便了。

筛选字段

如何使用 Model.query.join 语法得到部分字段呢?这里可以使用 SQLAlchemy 提供的 with_eitities 方法:

1
2
3
4
5
>>> 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')]
>>>

注意,列表中的项 (2, '玩家10001') 并不是标准的 Python tuple。你如果查看它的类型,会发现一个奇怪的名称: <class 'sqlalchemy.util._collections.result'> 。它是一个 AbstractKeyedTuple 对象,拥有一个 keys() 方法,这样可以很容易将其转换成 dict :

1
2
3
4
5
>>> 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 。

获得多个 Model 的记录

除了筛选字段外,还可以用另一个方法获取多个 Model 的记录。那就是,返回两个 Model 的所有字段:

1
2
>>> 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>)]

使用上面的语法直接返回 Account 和 Bind 对象,可以进行更加灵活的操作。

多表查询

要联结超过 2 张以上的表,可以直接在 join 得到的结果之后链式调用 join 。也可以在 filter 的结果后面链式调用 join 。join 和 filter 返回的都是 query 对象,因此可以无限链式调用下去。

写完查询后,应该打印生成的 SQL 语句查看一下有没有性能问题。

https://blog.zengrong.net/post/2656.html

Flask-SQLAlchemy 中多表链接查询(不使用外键)的更多相关文章

  1. sql查询指定表外键约束

    //////////////////查询指定表外键约束select a.name as 约束名, object_name(b.parent_object_id) as 外键表, d.name as 外 ...

  2. flask SQLAlchemy中一对多的关系实现

    SQLAlchemy是Python中比较优秀的orm框架,在SQLAlchemy中定义了多种数据库表的对应关系, 其中一对多是一种比较常见的关系.利用flask sqlalchemy实现一对多的关系如 ...

  3. mysql数据库中的多表查询(内连接,外连接,子查询)

    用两个表(a_table.b_table),关联字段a_table.a_id和b_table.b_id来演示一下MySQL的内连接.外连接( 左(外)连接.右(外)连接.全(外)连接). MySQL版 ...

  4. SQL 数据库 子查询、主外键

    子查询,又叫做嵌套查询. 将一个查询语句做为一个结果集供其他SQL语句使用,就像使用普通的表一样,被当作结果集的查询语句被称为子查询. 子查询有两种类型: 一种是只返回一个单值的子查询,这时它可以用在 ...

  5. Sql中常用的创建表 约束 主外键 增删改查的语句

    创建数据库 USE master; GO --日记数据库 create database DiaryBase on ( name=DiaryBase_Dat,--逻辑名称 FILENAME='c:\D ...

  6. T-SQL中找出一个表的所有外键关联表

    二种方法(下例中表名为T_Work) 1.SQL查询系统表 SELECT 主键列ID=b.rkey ,主键列名=(SELECT name FROM syscolumns WHERE colid=b.r ...

  7. 备忘:MySQL中修改表中某列的数据类型、删除外键约束

    -- MySQL中修改表中某列的数据类型 ALTER TABLE [COLUMN] 表名 MODIFY 列名 列定义; -- 删除外键约束 SHOW CREATE TABLE 表名; -- 复制CON ...

  8. 关于hibernate中映射中有many to one等外键关联时的问题

    hibernate中的对象的3种状态的理解及导致报错object references an unsaved transient instance - save the transient insta ...

  9. sqlserver2008中如何用右键可视化的设置外键

    右键->设计 然后打表设计界面打开了然后右键点你要设置与其它表关联的列然后点关系,选择外键表与列然后点保存,就这样  

随机推荐

  1. BZOJ2268 : Wormly

    考虑头部,一定是能向前就向前,因此是最左边的腿往右$b-1$个位置. 头部移动之后,腿部就要相应地移动到区间内最靠右的$l$个$1$之上. 若头部和腿部都不能移动,检查是否到达终点即可. 用前缀和以及 ...

  2. Linux硬盘管理

    管理好硬盘/dev/xxynsd SCSI SATA USBhd IDE主分区扩展分区 1-4逻辑分区5以后fdisk -l 硬盘名/分区名fdisk -l /dev/sda 如何给硬盘分区?把500 ...

  3. yii2小部件(widget)

    一.创建一个简单的小部件 namespace common\components; //common需要自己先设定一个别名 use yii\base\Widget; //小部件需要继承的基类 use ...

  4. Linux内容

    1.“~”:当前用户主目录“.”:当前工作目录“..”:当前工作目录的父目录使用pwd命令可以确定当前所在目录的绝对路径$ pwd2.shell命令:(which命令判断是否是外部命令)$ which ...

  5. 4989: [Usaco2017 Feb]Why Did the Cow Cross the Road

    题面:4989: [Usaco2017 Feb]Why Did the Cow Cross the Road 连接 http://www.lydsy.com/JudgeOnline/problem.p ...

  6. Linux之nginx服务

    作业一:nginx服务 1.二进制安装nginx包 2.作为web服务修改配置文件 3.让配置生效,验证配置 [root@localhost ~]# yum -y install epel-relea ...

  7. 编写CentOS的System V init启动脚本

    系统本身自带了说明,在/usr/share/doc/initscripts-(*)/sysvinitfiles,内容如下: 所有System V init脚本都命名为/etc/rc.d/init.d/ ...

  8. Siamese网络

    1.       对比损失函数(Contrastive Loss function) 孪生架构的目的不是对输入图像进行分类,而是区分它们.因此,分类损失函数(如交叉熵)不是最合适的选择,这种架构更适合 ...

  9. SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。

    又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...

  10. h264_rtp打包解包类及实现demo

    打包头文件: class CH2642Rtp { public: CH2642Rtp(uint32_t ssrc, uint8_t payloadType = 96, uint8_t fps = 25 ...