【问题标题】:Proper way to prevent SQLAlchemy from re-running queries on expired objects?防止 SQLAlchemy 对过期对象重新运行查询的正确方法?
【发布时间】:2013-02-15 15:52:36
【问题描述】:

我在思考如何处理烧瓶请求中过期的 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 %}

SQLAlchemy 然后将为模板循环中的每个 foo 执行选择查询,因为 session.commit()foos 集合标记为过期。如果我知道foos 无法实际更改,那么阻止len(foos) 查询执行的正确方法是什么?同样,如果foos 发生了变化,那么使用单个查询而不是多次查询来刷新数据的正确方法是什么?

【问题讨论】:

    标签: python sqlalchemy flask flask-sqlalchemy


    【解决方案1】:

    如果您知道 foos 无法更新,为什么还要发出 db.session.commit() 呢?如果是有时,那么在其中添加一些逻辑,仅当某些内容已更新时才会触发提交。

    您可以在db.session.commit() 行下方添加一个foos = Foo.query.all()。那样只会对所有数据触发一个查询,而不是每行一个。

    正如您所说,提交数据会将其设置为过期,因此需要重新查询它们。也许您可以刷新会话而不是重新查询,有关 in the SQLAlchemy documentation 的更多信息似乎表明您可以执行 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 from the documentation

    “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)
    

    【讨论】:

    • 对不起,原来的问题不清楚,我已经更新了。第一次循环 foos 时,我确实使用了另一个数据库对象,这将导致需要提交的更改。但是,这些更改永远不会导致 foos 发生更改。
    • 看起来有几个选项:1)将我要再次使用的值复制到非模型中; 2) 渲染模板后提交; 3) 使用 db.session.expunge() 从会话中分离对象; 4)像你建议的那样使用2个会话。似乎每个都可能有适当的用例。谢谢!
    • 添加了 expunge 示例 -- 似乎不能只做 db.session.expunge(foos) 你必须在行级别上工作。
    • 是的,必须在循环中进行。您可以使用 `db.session.expunge_all() 来分离会话中的所有内容,但在这种情况下没有任何内容要提交。
    • @Doobeh 您在第一个链接中引用了 sqlalechemy 上的 0.5 分支。您也错过了文档中的这一行:docs.sqlalchemy.org/en/latest/orm/session.html#committing“commit() 的另一个行为是,默认情况下,它会使提交完成后存在的所有实例的状态过期。这是为了在下次访问实例时,通过属性访问或通过它们出现在查询结果集中,它们会收到最新状态。要禁用此行为,请将 sessionmaker 配置为 expire_on_commit=False"
    【解决方案2】:

    我有一个稍微不同的方法,我不建议在 99% 的情况下使用它。 (但无论如何我都会分享)

    我对通过 SqlAlchemy 获取的数据进行积极缓存。为了假装实时 SqlAlchemy 对象和缓存数据之间的级别奇偶校验,我执行以下操作(引用 https://gist.github.com/jvanasco/01af92e100769d52f7b8

    • 当数据进入缓存时,我将其转换为原始字典(即,删除所有 sqlalchemy 信息。我只想要表的信息)

    • 当我从缓存中提取数据时,我将其转换为“ObjectifiedDict”。这是一个简单地提供基于点的属性访问的字典——就像 SqlAlchemy 对象一样。

    • 从缓存中提取数据的例程也可以将属性指定为延迟加载函数(这是在我从缓存中拉出数据时编写的)。这样,我可以将“用户帐户”对象的照片属性关联为从缓存中拉出特定照片的函数。

    使用这种方法,我的只读部分使用与应用程序的可写部分相同的模板——唯一的区别是,如果您查看视图,一个部分上的对象是 dicts 的版本,而其他部分是真正的 SqlAlchemy。

    我不建议在 99% 的情况下这样做。但在 1% 的情况下,当您尝试持久化缓存数据时,我发现这是最好的解决方案。

    【讨论】:

    • 这当然很漂亮;但我看到了吸引力。它认为这对我的用例来说可能有点太多了。我的绝大多数对象永远不会在缓存中。不过我会记住这一点。
    猜你喜欢
    • 2010-10-29
    • 1970-01-01
    • 1970-01-01
    • 2013-02-20
    • 1970-01-01
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多