Есть код:
app = Flask(__name__) db = SQLAlchemy(app) def make_db_session(engine): return scoped_session(sessionmaker(autocommit=False, autoflush=True, bind=engine)) def make_db_engine(): return create_engine(app.config['SQLALCHEMY_DATABASE_URI'], convert_unicode=True) engine = make_db_engine() db_session = make_db_session(engine)
Далее, я вижу несколько примеров обращения к сессии.
-
Первый вариант
db.session.some_action()
-
Второй вариант
db_session.some_action()
-
Третий вариант
session = db_session() session.some_action()
В чем их различия? Какой способ является более верным?
П.С. Еще интересно то, что если делать выборку следующим образом
model = SomeModel.query.filter(some_filter).first() model.field = new_value
То сохранять необходимо как
db.session.commit()
То есть, создается впечатление, что по умолчанию используется сессия db.session
.
Можно начать с отличий:
Функция make_db_session
возвращает объект типа ScopedSession
(scoped_session
— это класс такой, который зачем-то назвали как функцию). scoped_session
— обычная такая сессия, но помимо бла-бла-бла про потоки, основное ее отличие, видимое пользователям, в следующем:
# Обычные сессии Session = sessionmaker(__config_here__) session_1 = Session() session_2 = Session() my_db_entry = Entry(slug='Yolo') session_1.add(my_db_entry) # На этом моменте my_db_entry привязан в первой сессии session_2.add(my_db_entry)
# На этом моменте возникнет
исключение
То есть добавить один и тот же объект с разных сессий не выйдет.
ScopedSession — это на самом деле одна и та же сессия всегда (в данном потоке, пояснения ниже). Т.е.
session_factory = sessionmaker(bind=some_engine) print(type(session_factory)) >>> <class 'sqlalchemy.orm.session.sessionmaker'> session_registry = ScopedSession(session_factory) print(type(session_registry)) >>> <class 'sqlalchemy.orm.scoping.scoped_session'>
При вызове session_registry()
(__call__
) без параметров возвращается то, что лежит в регистре. Регистр — экземпляр класса ThreadLocalRegistry
. То, что туда однажды положили, изменить нельзя (На самом деле я не уверен — возможно, в методе баг или лишний код — он как-то странно написан — смотри с 64 строки в sqlalchemy/orm/scoping.py
). Также, если вызовете session_registry()
из другого потока — то в нем уже будет другая сессия, созданная специально для него. То есть «thread-local» в документации означает, что для каждого потока своя сессия, а не то, что сессии как-то хитро синхронизируются, ставят локи и т.д. Это достигается использованием threading.local() в регистре.
session_1 = session_registry() session_2 = session_registry() print(type(session1), type(session_2)) >>> <class 'sqlalchemy.orm.session.Session'> session_1 is session_2 >>> True
это одна и та же сессия. Это означает, например, что регистр с сессией можно сделать глобальным. Также необязательно создавать объекты класса Session
(session_1
и session-2
) — так как сессия одна вполне зайдет session_registry.query(MyClass).all()
Также в том же разделе есть наглядная диаграмма, показывающая когда нужно создавать регистр, когда нужно создавать и закрывать сессию.
Что использовать «правильнее» сказать сложно, но я бы сказал, что scoped_session
, а не создавать свою гольную Session(...)
, потому что сессии не потоко-безопасны и придется создавать на каждый поток новую сессию, либо синхронизироваться как-то.
Чтобы использовать свою сессию, а не предоставленную flask-sqlalchemy
, Есть несколько путей:
вызывать query
на нужной сессии:
session.query(MyObject.prop, AnotherOne.prop2).filter(blah-blah)
Также вы можете на свои модели установить «сессию по умолчанию». У каждой модели может быть поле query
, куда можно присунуть «запросник» (на этом моменте все начинает быть настолько запутанно, что не поддается объяснению). Примерно как-то так. Также есть небольшое обсуждение разницы между этими подходами на ENSO.
Update:
Возвращаясь к тому, какой вариант «верный», скажу, что второй и третий равнозначны, но использовать стоит третий, чтобы точно знать, что используется объект Session, а не какой-то иной. Но тогда зачем использовать flask-SQLAlchemy
? Выходит, что многое из того, что эта библиотека прячет от пользователя (настройки сессии, обратные вызовы типа before_rollback
, after_commit
, after_flush
и т.д. навешаны на библиотечную сессию. Также используется свой особенный «запросник» — класс orm.Query
со всякими плюшками, вроде автоматической отправки 404, если нет объекта в БД. Логи всякие, встроенная пагинация (постраничные запросы в БД).
Также сессия по умолчанию — db.session
— во время создания моделей именно она записывается в свойство query
. Вы можете убедиться в этом, отыскав класс SQLAlchemy
в пакете, в нем метод __init__
, а в нем уже строчку, в которой инициализируется поле Model
(которое затем используется как базовый класс). Это поле инициализируется результатом функции make_declarative_base
и вот как эта функция выглядит (довольно простая):
def make_declarative_base(self, metadata=None): """Creates the declarative base.""" base = declarative_base(cls=Model, name='Model', metadata=metadata, metaclass=_BoundDeclarativeMeta) base.query = _QueryProperty(self) return base
Как видно, заветный query
инициализируется своим классом, который создается именно с сессией flask-SQLAlchemy, а не какой-нибудь иной. Т.о. сессия по умолчанию была явно объявлена в базовом классе db.Model
. Можно поменять ее, перезаписав query
на тот, что нужен.