SQLAlchemy: создание и повторное использование сеанса

100

Просто быстрый вопрос: SQLAlchemy говорит о вызове sessionmaker()один раз, но вызывает результирующий Session()класс каждый раз, когда вам нужно поговорить с вашей БД. Для меня это означает, что когда я сделаю свой первый session.add(x)или что-то подобное, я сначала сделаю

from project import Session
session = Session()

До сих пор я делал вызов session = Session()в своей модели один раз, а затем всегда импортировал тот же сеанс в любое место моего приложения. Поскольку это веб-приложения, это обычно означает то же самое (когда выполняется одно представление).

Но где разница? В чем недостаток постоянного использования одного сеанса по сравнению с его использованием для моей базы данных до тех пор, пока моя функция не будет выполнена, а затем создать новый, когда я в следующий раз захочу поговорить с моей БД?

Я понимаю, что если я использую несколько потоков, у каждого из них должен быть свой сеанс. Но используя scoped_session(), я уже убеждаюсь, что этой проблемы не существует, не так ли?

Уточните, пожалуйста, неверно ли какое-либо из моих предположений.

javex
источник

Ответы:

229

sessionmaker()- это фабрика, она предназначена для поощрения размещения параметров конфигурации для создания новых Sessionобъектов в одном месте. Это необязательно, так как вы можете так же легко позвонить в Session(bind=engine, expire_on_commit=False)любое время, когда вам понадобится новый Session, за исключением того, что он многословен и избыточен, и я хотел остановить распространение небольших "помощников", каждый из которых подходил к проблеме этой избыточности в каком-то новом и более запутанный способ.

Так sessionmaker()это просто инструмент , чтобы помочь вам создать Sessionобъекты , когда вы нуждаетесь в них.

Следующая часть. Я думаю, что вопрос в том, в чем разница между созданием нового Session()на разных этапах и постоянным использованием одного. Ответ: не очень. Sessionявляется контейнером для всех объектов, которые вы в него помещаете, а также отслеживает открытую транзакцию. В тот момент, когда вы вызываете rollback()или commit(), транзакция завершена, и у Sessionнего нет связи с базой данных, пока он не будет вызван для повторной генерации SQL. Связи, которые он держит с вашими сопоставленными объектами, являются слабыми ссылками, при условии, что объекты не содержат ожидающих изменений, поэтому даже в этом отношении Sessionон опустошит себя обратно до совершенно нового состояния, когда ваше приложение потеряет все ссылки на сопоставленные объекты. Если вы оставите значение по умолчанию"expire_on_commit"установки, то все объекты истекли после фиксации. Если это будет Sessionдлиться пять или двадцать минут, и при следующем использовании в базе данных изменились все виды вещей, при следующем обращении к этим объектам будет загружено все новое состояние, даже если они находились в памяти. в течение двадцати минут.

В веб-приложениях мы обычно говорим: «Эй, почему бы вам не создать новый Sessionдля каждого запроса, а не использовать один и тот же снова и снова». Такая практика гарантирует, что новый запрос начинается «чистым». Если некоторые объекты из предыдущего запроса еще не были собраны мусором и, возможно, вы отключили его "expire_on_commit", возможно, какое-то состояние из предыдущего запроса все еще существует, и это состояние может быть даже довольно старым. Если вы осторожно оставляете expire_on_commitвключенным и обязательно звоните commit()или rollback()в конце запроса, тогда все в порядке, но если вы начнете с совершенно нового Session, тогда нет даже никаких сомнений в том, что вы начинаете чистую. Итак, идея начинать каждый запрос с новогоSessionна самом деле это самый простой способ убедиться, что вы начинаете с нуля, и сделать использование expire_on_commitпрактически необязательным, так как этот флаг может повлечь за собой много дополнительных SQL для операции, которая вызывается commit()в середине серии операций. Не уверен, что это ответ на ваш вопрос.

Следующий раунд - это то, что вы упомянули о многопоточности. Если ваше приложение многопоточное, мы рекомендуем убедиться, что Sessionиспользуется локально для ... чего-то. scoped_session()по умолчанию делает его локальным для текущего потока. На самом деле в веб-приложении даже лучше локально для запроса. Flask-SQLAlchemy фактически отправляет настраиваемую «функцию области видимости», чтобы scoped_session()вы получали сеанс с областью действия запроса. Среднее приложение Pyramid помещает сеанс в реестр «запросов». При использовании подобных схем идея «создать новый сеанс при запуске запроса» продолжает выглядеть как самый простой способ сохранить ясность.

Zzzeek
источник
17
Вау, это отвечает на все мои вопросы по части SQLAlchemy и даже добавляет некоторую информацию о Flask и Pyramid! Дополнительный бонус: ответ разработчиков;) Хотел бы я проголосовать больше одного раза. Большое спасибо!
javex
Одно уточнение, если возможно: вы говорите, что expire_on_commit "может повлечь за собой много дополнительных SQL" ... не могли бы вы дать более подробную информацию? Я думал, что expire_on_commit касается только того, что происходит в ОЗУ, а не того, что происходит в базе данных.
Veky
3
expire_on_commit может привести к большему количеству SQL, если вы снова используете один и тот же сеанс, и некоторые объекты все еще остаются в этом сеансе, когда вы обращаетесь к ним, вы получите однорядный SELECT для каждого из них, поскольку каждый из них обновляется индивидуально их состояние с точки зрения новой сделки.
zzzeek
1
Привет, @zzzeek. Спасибо за отличный ответ. Я очень новичок в python и хочу уточнить несколько вещей: 1) Правильно ли я понимаю, когда я создаю новый «сеанс», вызывая метод Session (), он создает транзакцию SQL, затем транзакция будет открываться, пока я не зафиксирую / откат сеанса ? 2) Использует ли session () какой-то пул соединений или каждый раз создает новое соединение с sql?
Alex Gurskiy
27

В дополнение к отличному ответу zzzeek, ​​вот простой рецепт быстрого создания одноразовых, замкнутых сеансов:

from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

@contextmanager
def db_session(db_url):
    """ Creates a context with an open SQLAlchemy session.
    """
    engine = create_engine(db_url, convert_unicode=True)
    connection = engine.connect()
    db_session = scoped_session(sessionmaker(autocommit=False, autoflush=True, bind=engine))
    yield db_session
    db_session.close()
    connection.close()

Использование:

from mymodels import Foo

with db_session("sqlite://") as db:
    foos = db.query(Foo).all()
Берислав Лопац
источник
3
Есть ли причина, по которой вы создаете не только новый сеанс, но и новое соединение?
danqing
Не совсем - это быстрый пример, демонстрирующий механизм, хотя имеет смысл создавать все свежее при тестировании, где я использую этот подход больше всего. Должно быть легко расширить эту функцию с помощью соединения в качестве необязательного аргумента.
Берислав Лопач