SQLAlchemy、Flask和交叉污染?

3

我接手了一个Flask应用,但它没有使用flask-sqlalchemy插件。我很难理解它的设置。

它有一个database.py文件。

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session, Session


_session_factory = None
_scoped_session_cls = None
_db_session: Session = None


def _get_session_factory():
    global _session_factory
    if _session_factory is None:
        _session_factory = sessionmaker(
            bind=create_engine(CONNECTION_URL)
        )
    return _session_factory


def new_session():
    session_factory = _get_session_factory()
    return session_factory()


def new_scoped_session():
    global _scoped_session_cls
    if _scoped_session_cls is None:
        session_factory = _get_session_factory()
        if not session_factory:
            return
        _scoped_session_cls = scoped_session(session_factory)
    return _scoped_session_cls()


def init_session():
    global _db_session

    if _db_session is not None:
        log.warning("already init")
    else:
        _db_session = new_scoped_session()
    return _db_session


def get_session():
    return _db_session

当我们启动Flask应用时,它会调用database.init_session(),然后每当我们想要使用数据库时,它会调用database.get_session()
这是一种正确/安全的与数据库交互的方式吗?如果有两个请求同时由不同线程处理,会发生什么情况?这会导致两个请求使用相同的会话而产生交叉污染吗?

2
不同的线程会产生不同的会话,但是你可以通过使用自定义作用域来使其更安全,将其绑定到你的网络请求上。 - undefined
好的,所以这段代码没有问题,并且会按预期工作。由于每个请求在一个线程中处理,没有任何线程/多进程在每个单独的请求中。谢谢! - undefined
1
因为我还没有得到一个令我满意的答案,尽管代码能够运行,但我不喜欢我无法完全确定它是否正确的事实。 - undefined
在文件之外,但是在某个地方,一旦请求被处理完毕,你应该调用session.Remove - undefined
请参考我对类似实现sqlalchemy的问题的回答:https://stackoverflow.com/a/63989043/7929036 - undefined
显示剩余2条评论
2个回答

5
要解释代码中发生的情况,让我们首先深入了解sqlalchemy如何连接到您的数据库:

1. 创建一个引擎

  • 连接到您的数据库存储在一个引擎中。
  • 使用create_engine()函数创建一个引擎。
    • 建议重复使用同一个引擎,因为每个引擎都会保留数据库连接(用于保留连接池)。
  • 使用engine.connect()连接到您的数据库 -> 这将打开一个连接到您的数据库。
    • 连接不是线程安全的。
在这个阶段,你可以直接连接到你的数据库:
engine = create_engine(...)

with engine.connect() as connection:
    result = connection.execute(...)

请注意,在此连接上创建/修改的对象在连接上下文之外不能保证共享状态,直到事务完成/连接关闭。
要执行更复杂的查询(例如:select和insert的混合),您需要使用一个会话。
2. 打开一个会话
一个会话是从一个engine对象创建的:
with Session(engine) as session:
    ...

更常见的是,会话将通过sessionmaker()工厂方法创建:
Session = sessionmaker(engine)

会话可以在整个项目中使用,并且通常是将对象映射到共享全局状态的方式。 3. 检查上面的代码:
代码的简化目的是通过`get_session()`创建一个会话对象。
最初,这将返回`None`,因为这是`_db_session`的值。
因此,您必须调用`init_session()`,它将创建返回`get_session()`的会话所需的各种对象。
以下是`init_session()`中发生的事情:
  1. 调用new_scoped_session()函数,该函数将返回一个session_factory
  2. 调用_get_session_factory()函数,该函数将创建一个session_factory。请注意,它会从CONNECTION_URL创建一个engine
  3. 调用scoped_session(session_factory)函数,该函数创建一个scoped session并赋值给_scoped_session_cls注意:这与常规会话非常相似,但更加隔离(且安全)。

许多复杂性与状态缓存有关。基本上,代码执行以下操作:

_session_factory = sessionmaker(
    bind=create_engine(CONNECTION_URL)
)

_scoped_session_cls = scoped_session(session_factory)

def new_session():
    return _scoped_session_cls()

希望这对你有所帮助。祝你好运!

非常感谢您的详细解释。所以,如果我在Flask请求期间调用db_client.get_session(),并且在commit()或者在出现错误时rollback(),是否存在线程之间或者同一线程处理的不同请求之间的交叉污染的可能性? - undefined
1
在您的情况下,所有请求将使用相同的会话。由于没有明确提供scopescoped_session,所以一个会话中的任何对象都将被共享。除非您调用db_client.new_session(),在这种情况下,将返回一个隔离的会话(每次调用)。@user7692855 - undefined
scoped_session不是由线程作用域的吗? - undefined
它由request-id限定范围 - 是的,确实如此。在不同线程之间共享是不安全的。@user7692855 - undefined
很不幸的是,我现在更加困惑了 - “在您的情况下,所有请求都将使用相同的会话”和“它是由请求ID限定的”。我们的问题是我们只调用了一次“scoped-session”并将其分配给了类吗? - undefined
可以根据请求ID进行范围限定 - 文档直接解释了这一点 @user7692855 https://docs.sqlalchemy.org/en/20/orm/contextual.html#using-custom-created-scopes - undefined

-2
使用db.session的方式如下:
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def init_app(app):
    app.config['SQLALCHEMY_DATABASE_URI'] = 'your_connection_url_here'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db.init_app(app)

from flask import Flask
from database import init_app

app = Flask(__name__)
init_app(app)


from flask import request
from database import db

@app.route('/some_endpoint')
def some_endpoint():
context
    data = db.session.query(SomeModel).filter_by(some_field=request.args.get('param')).all()
    return jsonify(data)


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