Если вы используете Python Flask для своих веб-проектов, Flask-SQLAlchemy может стать вашим основным пакетом для обработки всего, что связано с базой данных. Он в основном охватывает внутренние сложности базовой SQLAlchemy и, таким образом, очень прост в использовании.

В некотором смысле это хорошо, но иногда, как и в случае с магическими функциями Ruby on Rails, простота мешает нам понять, что происходит под капотом.

Одна вещь, которую я нахожу довольно интересной, это то, что Flask-SQLAlchemy пропускает создание сеанса до и во время обработки запроса, т. е. Model.query(), по сравнению с обычной реализацией в исходном пакете SQLAlchemy, т. е. session.query(Model).

В качестве иллюстрации вот как выглядит вызов запроса на Flask-SQLAlchemy:

from flask_sqlalchemy import SQLAlchemy
# DB initialization
db = SQLAlchemy()
# Model class definition
class User(db.Model): 
   __tablename__ = “users”
   username = Column(db.String(80), unique=True, nullable=False)
   email = Column(db.String(80), unique=True, nullable=False)
 
User.query.filter_by(id=1) # ←- no session here or anywhere above!

В отличие от того, как обычно выглядит реализация запроса при использовании стиля Декларативное сопоставление SQLAlchemy:

Обратите внимание на маркеры [point 1] и [point 2] в приведенном ниже коде, так как они будут упоминаться далее в статье.

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
# DB initialization
engine = create_engine(os.environ[‘DATABASE_URL’], echo=True)
# Session initialization
Session = sessionmaker(bind=engine, class_=Session) # [point 1]
session = Session()
Base = declarative_base() # [point 2]
# Mapper class definition
class User(Base): # Model class definition
    __tablename__ = “users”
    username = Column(db.String(80), unique=True, nullable=False)
    email = Column(db.String(80), unique=True, nullable=False)
session.query(User).filter_by(id=1) # ←- session is involved!

Выглядит совсем иначе, не так ли? Означает ли это, что Flask-SQLAlchemy не использует сеанс для вызова запроса?

Я потратил довольно много времени, копаясь в исходном коде Flask-SQLAlchemy, и вот что я нашел:

Обратите внимание: когда я написал базовый SQLAlchemy, я имел в виду исходный пакет Python SQLAlchemy.

1. Flask-SQLAlchemy настраивает сеанс в фоновом режиме.

# flask-sqlalchemy/src/flask_sqlalchemy/__init__.py:
class SQLAlchemy:
    def __init__(...):
        ...
        self.session = self.create_scoped_session(session_options)

Из функции create_scoped_session(...) в функции __init__(...), если углубиться на пару уровней, можно обнаружить, что она возвращает сеанс, вызванный методом sessionmaker(...):

return orm.sessionmaker(class_=SignallingSession, db=self, **options) # [equivalent to point 1 above]

Теперь мы знаем, что Flask-SQLAlchemy также поддерживает сессию, и то, как она генерируется, очень похоже на то, как базовая SQLAlchemy генерирует сессию!

2. Flask-SQLAlchemy устанавливает свой атрибут модели, вызывая «declarative_base».

# flask-sqlalchemy/src/flask_sqlalchemy/__init__.py:
class SQLAlchemy:
    def __init__(...):
        ....
        self.Model = self.make_declarative_base(model_class, metadata)

где self.make_declarative_base(...) фактически устанавливает:

def make_declarative_base(self, model, metadata=None):
    ...
    model = declarative_base(...) # [equivalent to point 2 above]

И да, в базовом SQLAlchemy мы также вызываем declarative_base для определения объекта сопоставления.

3. Атрибут запроса модели Flask-SQLAlchemy на самом деле является объектом запроса SQLAlchemy (для которого требуется сеанс)

# flask-sqlalchemy/src/flask_sqlalchemy/__init__.py:
class SQLAlchemy:
    ...
    def make_declarative_base(self, model, metadata=None):
        ...
        model.query = _QueryProperty(self)
 

_QueryProperty возвращает type.query_class(mapper, session=self.sa.session()), где type.query_class(...) на самом деле устанавливается в класс Query SQLAlchemy, т.е.

sqlalchemy.orm.query.Query(entities, session).

В качестве параллельного сравнения, если вы посмотрите в Документации по запросам SQLAlchemy о том, как он строит запрос:

Query([User, Address], session=some_session)

эквивалентно:

some_session.query(User, Address)

… Вот так. Из приведенного выше сравнения видно, что в первом случае объект запроса назначается атрибуту запроса Flask-SQLAlchemy, а во втором случае объект запроса создается в базовом SQLAlchemy примерно так.

В заключение, model.query Flask-SQLAlchemy фактически включает сеанс через базовый SQLAlchemy sqlalchemy.orm.query.Query(entities, session). Вот где генерируется сессия.