Flask和SQLAlchemy:处理会话

15

我最近开始在我的项目中使用Flask+Sqlalchemy,并且在让服务器运行一天后发现了500错误。我认为这是由于数据库会话超时造成的,但我不确定。我们是否应该为每个请求创建一个新的会话,还是当Flask应用程序启动时只有一个会话?我在app.py的顶部有这个:

from sqlalchemy import Column, ForeignKey, Integer, String, create_engine, func, cast, Float 
from sqlalchemy.ext.declarative import declarative_base 
from sqlalchemy.orm import relationship,scoped_session,sessionmaker,aliased
engine = createengine(DB_PATH) 
Session = sessionmaker(bind=engine) 
session = Session() 
app = Flask(name_)

然后对于视图中的所有查询,我都会做类似于这样的事情:“session.query(Table)…” 这样做是错的吗?我应该为每个端点调用创建一个会话吗?

4个回答

19
有些情况下,使用Flask-SQLAlchemy Extension可能不合适。例如,如果您正在管理模型类和数据库连接细节的完全不同的Python模块,以便在Flask之外的软件中重用,您不希望/不需要扩展来管理这些事情。
假设您有自己的代码来连接数据库并通过类似以下方式创建Session类(也假设提供了engine):
Session = scoped_session(sessionmaker(bind=engine))

对于需要数据库连接的页面,您可以使用该对象创建一个会话实例:
# import the "Session" object created above from wherever you put it

def my_page():
    session = Session() # creates a new, thread-local session
    ...

当响应完成后,我们需要删除创建的会话。这需要在my_page函数结束之后进行(所以我们不能在那里关闭它),但在响应结束之前。为了在正确的时间删除它,请在创建Flask应用程序时添加以下代码。
# import the "Session" object created above from wherever you put it

# despite the name, this is called when the app is
# torn down _and_ when the request context is closed

@app.teardown_appcontext
def shutdown_session(exception=None):
    ''' Enable Flask to automatically remove database sessions at the
    end of the request or when the application shuts down.
    Ref: https://flask.palletsprojects.com/en/2.3.x/patterns/sqlalchemy/
    '''
    Session.remove()

请注意,在后一个实例中,remove()被调用的是Session(大写S),而不是session(小写s,线程本地实例)。SQLAlchemy知道哪个session在哪个线程中,并会关闭为当前线程创建的会话。

可能还有其他方法来做到这一点,但这就是基本思想。请注意,SQLAlchemy为您提供了连接池。


3
有人能否澄清一个问题?在 http://flask.pocoo.org/docs/0.12/patterns/sqlalchemy/ 的“Declarative”部分,建议直接使用从scoped_session返回的对象,而无需每次创建会话实例,也不需要使用g对象。哪种方法是正确的? - AlexVB
4
我认为你不需要使用 g 对象。scoped_session 会为你返回当前的会话,只有在必要时才会创建新的会话。详见 https://docs.sqlalchemy.org/en/latest/orm/contextual.html#using-thread-local-scope-with-web-applications。 - SmallJoeMan
我在示例中删除了使用全局"g"对象的操作,因为它是不必要的,由于SQLAlchemy内部自己维护会话对象的全局注册表。 - Demitri
1
只是为了澄清@SmallJoeMan对@AlexVB的回答。从scoped_session返回的对象可以在带有()和不带()的情况下互换使用,这没有任何区别。 - LucG

12

尽管已被接受的答案看起来可以工作,但它存在一些问题。

  1. 它隐式地依赖于threading.local()。虽然对于大多数应用程序是可以的,但它忽略了安装greenlet的可能性,在这种情况下,本地线程ID是不够的。
  2. 它不必要地使用了g。如评论中所述,scoped_session已经处理了这一部分。

Flask本身不管理线程,这是WSGI服务器的责任。因此,根据文档,依赖于线程范围不是存储db会话的推荐方式,尽管它应该能正常工作,因为请求很可能直接与线程相关联。

特别地,虽然使用线程局部变量可能很方便,但最好将Session与请求直接关联,而不是与当前线程关联。 因此,根据文档,最好使用自定义范围,这样我们可以直接将会话与请求上下文关联起来。可以使用定制创建的范围来完成这个过程。

SQLAlchemy文档中的伪代码

from my_web_framework import get_current_request, on_request_end
from sqlalchemy.orm import scoped_session, sessionmaker

Session = scoped_session(sessionmaker(bind=some_engine), scopefunc=get_current_request)

@on_request_end
def remove_session(req):
    Session.remove()

对于SQLAlchemy,最干净的对象来附加session似乎是“应用程序上下文”,因为这是与请求直接关联的最高级别变量。 这里Flask文档介绍了Flask上下文如何工作。 您可以通过_app_ctx_stack访问AppContext实例的内部LocalStack这个stackoverflow答案指向相同的解决方案。 _app_ctx_stack.__ident_func__函数非常有用,因为它将返回线程ID,或者调用greenlet函数以提供可用的标识符(如果已安装)。 话虽如此,Flask似乎确实在许多方面使用线程本地。 我搜索了很久,但找不到任何保证WSGI服务器(例如gunicorn或uwsgi)会为每个请求创建一个线程的内容。 如果有人有这方面的来源,我很想看看。 无论如何,推荐的方法是使用应用程序上下文,这比依赖线程具有相同生命周期更具语义清晰性。
最后,另一个评论提到使用Flask-SQLAlchemy。 对于大多数项目来说,这是一个不错的主意,但我认为并不总是合理的。 就个人而言,我希望我的模型定义是使用SQLAlchemy定义的,而不是通过Flask-SQLAlchemy定义的。 我认为,在不久的将来,模型很可能会在Flask之外使用。我也不想拥有一个与SQLAlchemy不同的API. 虽然它们可能非常相似,如果不是完全相同,但这并不是我喜欢的使用SQLAlchemy本身。 最后,我找到了一篇towardsdatascience的博客得出了相同的结论。
说了这么多,我的解决方案与towardsdatascience人员所做的几乎相同。 我正在添加相关部分从他们发布的存储库中进行此操作。

main.py

from flask import Flask, _app_ctx_stack
from sqlalchemy.orm import scoped_session
from .database import SessionLocal, engine

app = Flask(__name__)
app.session = scoped_session(SessionLocal, scopefunc=_app_ctx_stack.__ident_func__)

@app.teardown_appcontext
def remove_session(*args, **kwargs):
    app.session.remove()

database.py

  
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

这个主题相当复杂,所以我欢迎评论并会更新答案,但希望这项研究能帮助其他人。


嗨,Nic,我在这里发布了一个有关跨请求会话管理的问题链接。 你的解决方案是否适用于这个问题? - Avner Moshkovitz
1
不,在这个例子中,我将db会话绑定到应用程序上下文中,每个请求都是唯一的,然后在之后关闭它。 对我来说,在REQUEST之间尝试执行此操作-共享会话会有点不好。当然,您可以使用我定义的方法来完成这个操作,最好使用Redis,但是...我真的会考虑一下是否实际需要符合您的设计要求。我很难找到必须这样做的情况。 - Nick Brady
在 Flask 2.2.1 中似乎找不到 _app_ctx_stack.__ident_func__ - ggorlen
@ggorlen,你可以使用scopefunc=greenlet.getcurrent。请参见https://github.com/osohq/oso/pull/1559/files。 - Daniel E

4

我建议使用优秀的Flask SQLAlchemy Extension,它可以处理会话管理和连接池。此外,它还可以根据请求打开和关闭会话等。

您可以查看相关的SQLAlchemy文档以获取更多细节:http://docs.sqlalchemy.org/en/latest/orm/session_basics.html#session-frequently-asked-questions

从文档中得知:

一些Web框架包括基础设施以协助将会话的生命周期与Web请求对齐的任务。这包括Flask-SQLAlchemy等产品,用于与Flask Web框架一起使用,以及Zope-SQLAlchemy,通常与Pyramid框架一起使用。SQLAlchemy建议使用这些产品。


0
细读了Nick的回答以及SQLAlchemy在lib/python3.8/site-pakages/fkask_sqlalchemy/__init__.py中的类定义后,我做了以下操作:
from flask import request, current_app
from flask import _app_ctx_stack
from app import db

SessionLocal = db.create_scoped_session(options={'autocommit': False,
                                                      'autoflush': False,
                                                      'bind': db.engine,
                                                      'scopefunc':_app_ctx_stack.__ident_func__
                                                      })

with SessionLocal() as session:
    try:
        session.begin()        
        session.add(some_data_from_request)
        session.commit()
    except Exception as ex:
        session.rollback()
    finally:
        session.close()

在下面创建“db”对象。(与SQLAlchemy类定义的注释中的第二个初始化示例相同。)

db = SQLAlchemy()
def create_app():
    app = Flask(__name__)
    db.init_app(app)
    return app

1
你其实不需要这样做 - Flask-SQLAlchemy 默认已经使用应用上下文 - snakecharmerb
@snakecharmerb 感谢您的评论!这意味着我不需要为事务管理创建新的会话对象吗?只需执行 session = Session() 就足够了吗? - Koji
1
如果您正在使用Flask-SQLAlchemy,只需根据需要使用db.sessionMyModel.query即可。 Flask-SQLAlchemy抽象了一些您在使用原始的SQLAlchemy时需要进行的会话管理。 如果您没有进行任何奇特的操作,请遵循文档中的示例。 - snakecharmerb
@snakecharmerb 再次感谢!当我执行 db.session.begin() 时,我收到了“在此会话上已经开始了一个事务”的错误。如果我删除了 db.session.begin(),数据将正确存储在数据库中(db.session.commit()db.sessoin.rollback() 的工作方式符合我的预期)。然而,由于我感到非常不舒服,并且想要清楚地描述代码中的保存点,所以我执行了 with db.session.begin_nested():,如这个问题中所示。 - Koji

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接