关于 SQLAlchemy 的 Mode.query 和 session.query 的区别请教

2018-05-11 17:19:44 +08:00
 n3hatv2
class MfAdmin(Base):
__tablename__ = 'mf_admin'

id = Column(Integer, primary_key=True)
name = Column(String(255, 'utf8mb4_unicode_ci'))
password = Column(String(255, 'utf8mb4_unicode_ci'))

针对 MfAdmin,看到 sqlalchemy 有两种 query 方法,
方法 1:
创建 session 的代码就不写了
admin = session.query(MfAdmin).filter(MfAdmin.name == "test1").first()
方法 2:
admin = MfAdmin.query.filter(xxx).filter(MfAdmin.name == "test1").first()

问题 1:
1.两种查询的区别是什么?什么场景用哪一种?
2. 针对方法 2 查出来的 admin,修改后再基于 session 提交,是否可靠?
比如:
admin = MfAdmin.query.filter(xxx).filter(MfAdmin.name == "test1").first()
admin.password = "123456"
session.commit()
3214 次点击
所在节点    Python
12 条回复
Philippa
2018-05-11 19:29:46 +08:00
这里没有写清楚是 flask-sqlalchemy 还是 sqlalchemy。flask-sqlalchemy 的文档写的是 Model.query,它会返回一个 BaseQuery 的类,继承 sqlalchemy 的 Query 类,实现了一些额外的东西,比如 paginate 的方法。但看看上面方法 2 就知道,Model 和 session 一点关系都没有,这是因为在 app 里面封装了,不然 session.commit()怎么会知道上面那个 Model 做了什么。这也造成,当你只想使用 sqlalchemy 而不是 flask app 时,你会发现用不了,因为没有共享上下文,因此在调试和写测试时耦合度会比较高,那些喜欢自己写代码而不是学习一整套方案的人就不喜欢 flask-sqlalchemy 的,比如我。同时从代码看,sqlalchemy 的方案更加直接,session 可以简单理解成用于建立连接,session 传参去 query 模型,最后还要 add 一下加入事务,commit 提交,虽然代码多一点,但逻辑很清洗。而 class 只是映射 sql 逻辑成一个面向对象的 object,通过操作 object 来操作 sql。

session 是抽象出来的概念,实质关键是用来处理链接问题。建议用 sqlalcehmy 而不是 flask-sqlalchemy,因为解耦方便,组件化复用也可行,而不是跟 flask 耦合在一起。sqlalchemy 可以用 session_scope,create_engine 自己写个连接方法来处理,虽然入手肯定是没 flask-sqlalchemy 那么方便,但也是一次编写多几行代码,终身使用了。另外 peewee 是没有处理这个问题的,看起来更简单但不懂用埋了坑。peewee 和 flask-sqlalchemy 一样,也是 Model.select 这种形式的,但没有自动关闭连接。因此,对于长期运行的任务,队列等,某些数据库由于太长时间接受到请求就会直接中断,因此 peewee 在这种情况会出现 client 报错的原因,因为 timeout 了。peewee 的 issues 的官方给出解决方案竟然是 conn.close()这种东西,这在涉及 import/复杂业务容易出现单个 instance 里过早关闭的问题,感觉就像一不小心就变野指针,是个 bad design。
n3hatv2
2018-05-11 19:54:07 +08:00
@Philippa 感谢解答,我确实没讲清楚是 flask-sqlalchemy 还是 sqlalchemy,我正式因为在 flask-sqlalchemy 里遇到了进程不安全的问题想改成 sqlalchemy,每个进程独立的 session ;然后在查文档的时候发现 sqlalchemy 也有直接基于对象 query 的写法,比如 https://gist.github.com/podhmo/4345741,不过这个例子能看出对象 query 本质上就是 session.query(),通过“ query = DBSession.query_property()” 做了关联。。。
Philippa
2018-05-11 22:19:32 +08:00
@n3hatv2 #2 Yeah~我还没遇到过进程不安全的问题!那我想请教一下大概原理,或有链接分享一下吗?是多进程,多线程还是协程下不安全,竞态?还是别的原因?我现在有很多和别人一起写的用到 flask-sqlalchemy 项目是在 docker 集群上跑的,web 服务层有的也用到,新的才是纯 sqlalchemy,想了解一下。
neoblackcap
2018-05-11 23:28:44 +08:00
@n3hatv2 SQLalchemy 明明是类似 Hibernate 一样的 Data Mapper 模型,偏要往 Active Record 套,这何苦呢?喜欢用 Active Record 那套换别的不好么?开箱即用。
至于 session 的问题,进程不安全能描述一下吗?
lolizeppelin
2018-05-12 00:40:19 +08:00
进程不安全什么鬼

多进程 惰性初始化保证没问题

不知道怎么做的话 抄 openstack 的做法
n3hatv2
2018-05-12 00:59:50 +08:00
@Philippa @neoblackcap 我的模型有点复杂,是在初始化 flask app 的主进程里,通过 multiprocessing.Process 启了很多子进程,每个子进程会 query 出对象,然后还需要通过动态 pool.map 再传递到子子进程里做并发处理,遇到了数据库的值不符合预期,我担心 flask-sqlalchemy 这种全局 session 在不同进程里 query 的对象会被共享,进而导致互串;没有明确的证据,我对 Python 生态的 orm 也不太熟悉,目前的想法是改用原生的 sqlalchemy (非 flask 扩展),然后在每个子进程里创建 session,进程退出时 close,保证每个进程里的 query 对象是唯一的实例。

那么,我想请教下频繁 db_session = Session() db_session.close() 这个开销对性能的影响大不大?对于我这种场景正确的姿势是什么?我的担心是否是多余的?
Philippa
2018-05-12 01:42:16 +08:00
@n3hatv2 pool 子进程没用过耶,pool 线程池就用过,还是 subprocessing ? executor ?所以我猜你有两个担心的地方,一是 multiprocessing 这块可能共享,二是 threading 这边可能会共享。前者我一般用一个 docker 一个 process 所以不熟悉,不过据搜索结果看 multiproccessing.array 专门用于共享内存的,并用 actor 模型处理消息,所以我理解是不会共享的,所以这里我们是一样的,不用管。至于 threading 嘛,是会的,我也很常用 threading_pool.map 然后通道来传消息,还能共享变量。假如你的 Model 绑定了 session,在使用 Model 时,session 在 commit 的时候会把所有都包含进来……所以每个线程应该单独使用 session 来完成。比如这样:

```
# 用调用函数的方式加括号(),一种明确的信号来建立连接
with session_scope() as session:
do_something()
```
不过我觉得更好的方法是用 Queue (线程安全的),集中处理所有的存储步骤。pool.map(worker_lambda, args)来执行任务,在 def worker_lambda 里面处理好后结果 push 到 Queue 里面:queue.push(result),然后做一个 while True 的消费者,从 queue 取出结果,然后 session.add(result)来保存结果,result 包含多个结果,单独塞 X, Y, Z 变量也可以,但是多线程下顺序有可能会乱,所以有多个结果就一起塞。设置 timeout/retry/queue.empty 作为超时 /重试 /中止信号。这样就不用担心这种问题了,虽然每个自己处理也可以,但是我觉得那样会增加许多脑资源消耗,而且更容易出错和更难维护,其他人接手修改也容易出现 bug。不然你可以用协程,最后看看数据库的锁默认设置。最后最好避免全局变量这种东西,以后会搞坏自己。

flask-sqlalchemy 那个我不大记得,我记得是最后 db.session.commit()就提交了,或者有的人直接 auto_commit。这在多线程里面的确会有问题。如有错,请指出。
Philippa
2018-05-12 01:47:41 +08:00
性能还好,我对比过 peewee 和 sqlalchemy 的性能都差不多。如果不存在锁或其他等待,速度一般卡在大批量数据插入时比较慢,sqlalchemy 有个 bulk_insert 的方法,不过大量时间会花在构建对象或 append 插入数据里面。但具体执行插入到插入完成这个阶段,到底是数据库是瓶颈,还是网络,还是对象的构建销毁比较耗资源,就没测过了。
n3hatv2
2018-05-12 02:14:30 +08:00
@Philippa
假设我有表
class MfAdmin(Base):
id =xxx
....

我是在主进程里初始化 flask-sqlalchemy 的 db, db = SQLAlchemy(app) ,然后在主进程里以 MfAdmin.query 方式查出一组 user_list,然后将 user_list 以 pool.map()的方式( multiprocessing.Pool 创建进程池)在子进程里对不同的 user(MfAdmin 的 id 不一样)进行并发处理,这里有几个疑问:
1. 如果 pool.map ()产生的不同子进程里处理相同 id 的 user,是不是会被共享?从你的回答来看,不会共享。
2. 如果 pool.map ()产生的不同子进程里,其中一个子进程执行 session.commit() ,其他子进程里修改的对象会不会被 commit ?
出于以上担心,我现在的做法是,属于同一个 db = SQLAlchemy(app)的主进程以及通过 pool.map 产生的所有子进程都只做读操作,如果要修改,扔到 redis 的 list 里,由独立的 db = SQLAlchemy(app) 进行同步的修改。
neoblackcap
2018-05-12 14:07:21 +08:00
@n3hatv2 你的说法是不会发生问题的,具体你可以模拟一下。scope_session 是用了 thread-local 去实现的。而且进程安全什么鬼?只有线程安全。
你进程拿到的东西都不是一个东西,大家都有自己的调用栈,有自己的堆,不同进程不通过通讯怎么能影响其他进程?不要说 Python,哪怕你用 C,你能影响不同进程里面的对象?除非你的对象自带通讯同步功能,要不然想都别想。
然后你说的场景啊,我觉得你现在的做法问题不大了,你创建进程的时候本身就有开销,而且你的进程里面创建 Session 本身就是额外创建连接。如果是子进程没有额外数据库操作,你大可创建一个额外的进程进行 reduce 操作
lolizeppelin
2018-05-12 14:12:31 +08:00
Linux 就不要用 multiprocessing

老老实实学下 fork 标准写法去实现多进程

一般简单点的用 multiprocessing 还好
复杂的你不熟悉一遍 multiprocessing 源码也随便用是给自己找坑
Philippa
2018-05-12 17:49:49 +08:00
@n3hatv2 #9 我觉得你现在的方法很好了,那样做就完全不用考虑原先的问题了,walk round 同时也是一种风格更好的 design。Queue 模块放在 multiprocessing 虽然没问题但共享 Queue 比较麻烦,所以 Redis 更方便,就是多了个 Redis 依赖。

1.不会
2.是的,假如 session 是同一个,就会覆盖掉了。因为 Model.attr = 'xxx'时相当于塞进了预备被提交的 box 里面,commit 就全部提交了。假如相同 id,应该是原有 box 已有内容被覆盖了一遍(我猜,源码我就不看了= =)。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/454118

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX