如何防止SQLAlchemy对过期对象重新运行查询?

4 投票
2 回答
6795 浏览
提问于 2025-04-17 16:00

我在处理Flask请求中的过期SQLAlchemy对象时遇到了一些困惑。假设我做了以下操作:

from models import Foo, Bar

@app.route("/page")
def page():
  foos = Foo.query.all()

  for foo in foos:
    b = Bar(foo.data)
    db.session.add(b)

  db.session.commit()

  return render_template('page.html', foos=foos)

然后在page.html中:

{% for foo in foos %}
  {{ foo. name }}
{% endfor %}

因为session.commit()foos这个集合标记为过期,所以SQLAlchemy会在模板循环中对每个foo执行一次查询。如果我知道foos实际上没有发生变化,有什么方法可以防止执行len(foos)的查询呢?同样,如果foos确实发生了变化,有什么方法可以用一次查询来刷新数据,而不是执行多次查询?

2 个回答

3

我有一种稍微不同的方法,不过我不推荐在99%的情况下使用。(但我还是分享一下)

我对通过SqlAlchemy获取的数据进行了激进的缓存。为了让实时的SqlAlchemy对象和缓存的数据看起来差不多,我做了以下几步(参考 https://gist.github.com/jvanasco/01af92e100769d52f7b8

  • 当数据进入缓存时,我把它转成一个原始的字典(也就是说,去掉所有SqlAlchemy的信息。我只想要表格里的信息)

  • 当我从缓存中提取数据时,我把它转成一个“对象化字典”。这个字典可以通过点号来访问属性——就像SqlAlchemy对象一样。

  • 从缓存中提取数据的过程也可以指定一些属性作为延迟加载的函数(这是在我从缓存中提取东西时写的)。这样,我可以把“用户账户”对象的照片属性作为一个函数,来从缓存中提取特定的照片。

通过这种方法,我的只读部分使用和可写部分相同的模板——唯一的区别是,如果你查看视图,一个部分的对象是字典的版本,而另一个部分则是真正的SqlAlchemy对象。

我不推荐在99%的情况下使用这种方法。但在那1%的情况下,当你想要持久化缓存数据时,我发现这是最好的解决方案。

6

如果你确定没有任何foos被更新,那为什么还要执行db.session.commit()呢?如果有时候需要,那就加一些逻辑,只在有更新的时候才触发提交。

你可以在db.session.commit()下面加一句foos = Foo.query.all()。这样就只会发出一次查询来获取所有数据,而不是每一行都发一次。

正如你所说,提交数据会让它们的状态变为过期,所以需要重新查询。也许你可以刷新会话,而不是重新查询,更多信息可以参考SQLAlchemy文档,里面提到你可以使用session.refresh(object)

更新:使用两个会话

你可以使用第二个会话,先用它来查询Foo,然后用另一个会话来处理Bars。这样在提交时foos就不会被修改,你就不需要再去查询它了。

这里有个粗略的例子:

from flask.ext.sqlalchemy import Session

@app.route('/example/')
def home():
    session_two = Session(bind=db.engine.connect())
    foos = session_two.query(Foo).all()

    for foo in foos:
        db.session.add(Bar(foo))
    db.session.commit()

    return render_template_string('''
        {% for foo in foos %}
            {{ foo.name }}
        {% endfor %}
    ''', foos=foos)

另外,我在想是否可以用一个会话来处理,配置为expire_on_commit=False,具体可以参考文档

“commit()的另一个行为是,默认情况下,它会在提交完成后使所有实例的状态过期。这是为了确保当下次访问这些实例时,无论是通过属性访问还是在查询结果集中,它们都能接收到最新的状态。要禁用这种行为,可以将sessionmaker配置为expire_on_commit=False。”

使用Session.expunge

根据需要将对象从会话中移除。

@app.route('/')
def home():
    foos = Foo.query.all()
    for foo in foos:
        db.session.add(Bar(foo))
        db.session.expunge(foo)
    db.session.commit()

    return render_template_string('''
        {% for foo in foos %}
            {{ foo.name }}
        {% endfor %}
    ''', foos=foos)

撰写回答