如何让 Flask SQLAlchemy 重用数据库连接?

Posted

技术标签:

【中文标题】如何让 Flask SQLAlchemy 重用数据库连接?【英文标题】:How do I make Flask SQLAlchemy reuse db connections? 【发布时间】:2012-05-23 17:40:49 【问题描述】:

我似乎无法让我的 Flask 应用程序关闭或重用数据库连接。我正在使用 PostgreSQL 9.1.3 和

Flask==0.8
Flask-SQLAlchemy==0.16
psycopg2==2.4.5

当我的测试套件运行时,打开的连接数不断攀升,直到达到 20(postgresql.conf 中的 max_connections 设置),然后我看到了:

OperationalError: (OperationalError) FATAL:  sorry, too many clients already
 None None

我已将代码缩减到只调用create_alldrop_all 的程度(但没有发出任何sql,因为没有模型)。

我在日志中看到正在签入和签出的连接:

DEBUG:sqlalchemy.pool.QueuePool:Connection <connection object at 0x101c1dff0; dsn: 'dbname=cx_test host=localhost', closed: 0> checked out from pool
DEBUG:sqlalchemy.pool.QueuePool:Connection <connection object at 0x101c1dff0; dsn: 'dbname=cx_test host=localhost', closed: 0> being returned to pool
WARNING:root:impl   <-------- That's the test running
DEBUG:sqlalchemy.pool.QueuePool:Connection <connection object at 0x101c1dff0; dsn: 'dbname=cx_test host=localhost', closed: 0> checked out from pool
DEBUG:sqlalchemy.pool.QueuePool:Connection <connection object at 0x101c1dff0; dsn: 'dbname=cx_test host=localhost', closed: 0> being returned to pool

对于每个测试运行,连接的地址(“xyz 处的连接对象”部分)是不同的。我怀疑这与问题有关,但我不确定如何进一步调查。

下面的代码在一个新的 venv 中重现了这个问题:

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from unittest import TestCase

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('sqlalchemy.pool').setLevel(logging.DEBUG)
logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.DEBUG)
logging.getLogger('sqlalchemy.orm').setLevel(logging.DEBUG)


db = SQLAlchemy()

def create_app(config=None):
    app = Flask(__name__)
    app.config.from_object(config)
    db.init_app(app)
    return app


class AppTestCase(TestCase):
    SQLALCHEMY_DATABASE_URI = "postgresql://localhost/cx_test"
    TESTING = True

    def create_app(self):
        return create_app(self)

    def setUp(self):
        self.app = self.create_app()
        self.client = self.app.test_client()
        self._ctx = self.app.test_request_context()
        self._ctx.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self._ctx.pop()


class TestModel(AppTestCase):
    def impl(self):
        logging.warn("impl")
        pass

    def test_01(self):
        self.impl()

    def test_02(self):
        self.impl()

    def test_03(self):
        self.impl()

    def test_04(self):
        self.impl()

    def test_05(self):
        self.impl()

    def test_06(self):
        self.impl()

    def test_07(self):
        self.impl()

    def test_08(self):
        self.impl()

    def test_09(self):
        self.impl()

    def test_10(self):
        self.impl()

    def test_11(self):
        self.impl()

    def test_12(self):
        self.impl()

    def test_13(self):
        self.impl()

    def test_14(self):
        self.impl()

    def test_15(self):
        self.impl()

    def test_16(self):
        self.impl()

    def test_17(self):
        self.impl()

    def test_18(self):
        self.impl()

    def test_19(self):
        self.impl()



if __name__ == "__main__":
    import unittest
    unittest.main()

这是我第一次在烧瓶中使用应用工厂,我从Flask-SQLAlchemy docs 复制了部分代码。 Elseware 那些文档提到在错误的上下文中使用数据库会导致连接泄漏 - 也许我的初始化不正确?

【问题讨论】:

【参考方案1】:

在阅读了 SQLAlchemy 文档并摆弄了一些数据库实例之后,我终于得到了解决方案。在tearDown() 中添加db.get_engine(self.app).dispose() 使其看起来像:

def tearDown(self):
    db.session.remove()
    db.drop_all()
    db.get_engine(self.app).dispose()
    self._ctx.pop()

【讨论】:

【参考方案2】:

自从大约一年前提出问题以来,我认为 OP 一定已经解决了他的问题。但对于那些徘徊到这里(像我一样)试图弄清楚发生了什么的人来说,这是我最好的解释:

正如 van 所说,问题确实出在每个测试调用 setUptearDown 的测试用例上。虽然连接并没有完全从 SQLAlchemy 泄漏,而是因为每个测试都有自己的setUp,因此创建了应用程序的多个实例:每个应用程序都有自己的数据库连接池,可能不会被重用或在测试完成时回收。

换句话说,连接被检出并正确返回到池中,但是连接作为空闲连接继续存在,用于同一内的未来事务应用程序(连接池的点)。

在上面的测试用例中,创建了大约 20 个连接池(每个都有空闲连接,因为 create/drop_all)并占用了 postgres 连接限制。

【讨论】:

【参考方案3】:

编辑:SQLALCHEMY_COMMIT_ON_TEARDOWN 在 Flask-SQLAlchemy 版本 2.4.3 中已弃用。您可以在此处查看更改说明,他们建议您自行致电 .commit()

https://flask-sqlalchemy.palletsprojects.com/en/2.x/changelog/?highlight=sqlalchemy_commit_on_teardown#version-2-4-3

我所做的是注册我自己的app.after_request,如果响应状态代码 .commit()。这需要您正确构建应用程序以确保响应状态代码

----下面的旧答案----

在最新版本的 Flask-SQLAlchemy 中,session.remove() 会自动在 app.after_request 中调用。

另外,请参阅此处的 SQLALCHEMY_COMMIT_ON_TEARDOWN 设置:

https://pythonhosted.org/Flask-SQLAlchemy/config.html?highlight=sqlalchemy_commit_on_teardown

这也会自动提交事务。

【讨论】:

【参考方案4】:

您知道setUp and tearDown 在每个test method 之前和之后都会被调用。从您的代码看来,您需要它们以确保数据库为空。 不过也有setUpClass and tearDownClass,每个测试类调用一次。 我相信您可以拆分您当前拥有的代码并将db-connection 相关部分移动到Class 级别,同时将test-method 相关部分保留在需要的位置。

【讨论】:

谢谢 Van - 这解决了测试套件的问题,但这不只是意味着我泄漏的连接更少了吗? @TomDunham:我想你是对的。没有postgres,所以无法为您提供帮助,抱歉..

以上是关于如何让 Flask SQLAlchemy 重用数据库连接?的主要内容,如果未能解决你的问题,请参考以下文章

Flask-SQLAlchemy安装及设置

如何自动填充 SQLAlchemy 数据库字段? (Flask-SQLAlchemy)

Flask-SQLAlchemy - 会话如何与多个数据库一起工作?

如何防止flask-sqlalchemy中的双重数据提交

一周掌握Flask框架学习笔记Flask中使用数据库(使用Flask-SQLAlchemy管理数据库)

如何在一段时间后更改Flask-SQLAlchemy URI?