Сессия: Session, add и commit
Session — блокнот изменений: add копит объекты, commit отправляет их в базу, rollback выбрасывает черновик. Разбираем, где теряют данные.
Редакция Питоники
Модель описана, таблицы созданы — урок 4 закончился на пороге. За дверью начинается главный объект повседневной работы с ORM: сессия. Session — это блокнот изменений: всё, что ты добавляешь и меняешь, сначала ложится в него, а не в базу. Одна команда — commit — переводит черновики в чистовик, другая — rollback — выбрасывает их. На этой аналогии и построим урок.
Session: блокнот изменений
Сессия создаётся от движка — Session(engine) — и почти всегда живёт внутри блока with: на выходе сессия закрывается сама, вместе со своими соединениями. Первая запись выглядит так:
from sqlalchemy import create_engine, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
done: Mapped[bool] = mapped_column(default=False)
priority: Mapped[int] = mapped_column(default=1)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
task = Task(title="вынести мусор", priority=2)
print("id до commit:", task.id)
session.add(task)
session.commit()
print("id после commit:", task.id)
id до commit: None id после commit: 1
Смотри внимательно на две строки вывода: до commit у объекта нет id — он ещё не записан в базу, номер ему взять неоткуда. commit отправляет INSERT, база назначает id из счётчика первичного ключа, и объект тут же подхватывает значение. Никакого ручного чтения из базы не понадобилось — сессия синхронизирует объект сама.
add и add_all: кладём объекты в блокнот
session.add(obj) ставит в блокнот один объект, session.add_all([...]) — список сразу. Вплоть до commit можно добавлять сколько угодно: сессия отправит всё одной пачкой, одним INSERT на объект, но в рамках одной транзакции.
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
done: Mapped[bool] = mapped_column(default=False)
priority: Mapped[int] = mapped_column(default=1)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add_all([
Task(title="вынести мусор", priority=2),
Task(title="купить хлеб", priority=1),
Task(title="полить цветы", priority=3, done=True),
])
session.commit()
for t in session.scalars(select(Task).order_by(Task.id)):
print(t.id, t.title, t.done, t.priority)
1 вынести мусор False 2 2 купить хлеб False 1 3 полить цветы True 3
| Метод сессии | Что делает |
|---|---|
| session.add(obj) | ставит объект в блокнот на вставку |
| session.add_all([...]) | то же для списка объектов |
| session.commit() | записывает накопленное в базу и закрепляет |
| session.rollback() | отменяет всё накопленное с последнего commit |
| session.get(Модель, id) | достаёт объект по первичному ключу |
| session.close() | закрывает сессию — в with вызывается сама |
Без commit данных нет
А что если add был, а commit — нет? Объект честно лежит в блокноте, и внутри своей сессии его даже видно. Но база-то ничего не знает. Проверим двумя сессиями подряд:
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Task(title="черновик"))
here = session.scalars(select(Task).order_by(Task.id)).all()
print("Внутри сессии:", len(here))
with Session(engine) as session:
again = session.scalars(select(Task).order_by(Task.id)).all()
print("Новая сессия:", len(again))
Внутри сессии: 1 Новая сессия: 0
rollback: выбрасываем черновик
Обратная команда — session.rollback(): сессия выбрасывает всё, что накоплено с последнего commit, и возвращает объекты к состоянию, которое лежит в базе. Отменить можно и добавление, и изменение.
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Task(title="ошибочное дело"))
session.rollback()
left = session.scalars(select(Task).order_by(Task.id)).all()
print("После rollback:", len(left))
После rollback: 0
Изменение уже существующей строки отменяется так же. Достанем задачу по первичному ключу методом session.get, переименуем — и откатим:
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Task(title="купить хлеб"))
session.commit()
with Session(engine) as session:
task = session.get(Task, 1)
task.title = "купить багет"
session.rollback()
print("После rollback:", task.title)
После rollback: купить хлеб
Сессия заметила, что атрибут title изменился после загрузки, и при commit отправила бы UPDATE. Rollback сбросил намерение: объект вернулся к значению из базы — «купить хлеб». Работает это через истечение атрибутов: rollback объявляет кэш недействительным, и при следующем обращении значение читается из базы заново.
Что происходит внутри commit
Commit не отправляет объекты в базу по одному в момент add — add только кладёт в блокнот. Непосредственно перед закреплением транзакции сессия выполняет flush: посылает в базу все накопленные INSERT и UPDATE. То есть commit — это flush плюс настоящая команда COMMIT. Разница видна, если вызвать flush вручную: INSERT уйдёт, id назначится, но транзакция ещё не закреплена.
from sqlalchemy import create_engine, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
task = Task(title="срочная задача")
print("После Task(): id =", task.id)
session.add(task)
session.flush()
print("После flush: id =", task.id)
session.commit()
print("После commit: id =", task.id)
После Task(): id = None После flush: id = 1 После commit: id = 1
Именно flush объясняет и трюк из урока 3: значения по умолчанию появляются у объекта в момент записи — то есть во время flush. В обычном коде flush вызывают редко: commit делает его сам, да и почти любая выборка внутри сессии сперва сбрасывает блокнот автоматически.
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Task(title="дело"))
session.commit()
task = session.scalars(select(Task).order_by(Task.id)).first()
task.title = "поменяно в конце"
session.commit()
try:
print(task.title)
except Exception as e:
print(type(e).__name__)
with Session(engine) as session:
fresh = session.scalars(select(Task).order_by(Task.id)).first()
print(fresh.title)
DetachedInstanceError поменяно в конце
Присмотрись: само изменение никуда не делось — свежая сессия читает из базы «поменяно в конце». Не работает только доступ к атрибутам оторванного объекта. Отсюда и лечение: всё чтение — внутри блока with, где сессия жива.
Первая программа: одна транзакция на всё
Соберём приёмы вместе: в одной сессии добавляем новые задачи и правим существующую — и всё это уходит в базу одним commit. Если посреди блока что-то упадёт, rollback вернёт базу к исходному состоянию целиком: изменений «наполовину» не бывает. В этом и смысл транзакции, о которой подробно говорили в уроке про транзакции SQLite.
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String)
priority: Mapped[int] = mapped_column(default=1)
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Task(title="вынести мусор", priority=2))
session.commit()
with Session(engine) as session:
session.add(Task(title="купить хлеб", priority=1))
urgent = session.get(Task, 1)
urgent.priority = 3
session.commit()
for t in session.scalars(select(Task).order_by(Task.id)):
print(t.id, t.title, "- приоритет", t.priority)
1 вынести мусор - приоритет 3 2 купить хлеб - приоритет 1
Что дальше
Ты владеешь блокнотом: add и add_all копят, commit отправляет, rollback отменяет, get достаёт по ключу. Записи в базе уже есть — пора научиться их доставать по-настоящему: в следующем уроке появляется select, строки из базы возвращаются объектами моделей, и мы наконец перестанем печатать поля вручную после каждой вставки.
Session — блокнот изменений: add копит, commit переводит черновик в чистовик, а rollback выбрасывает страницу целиком.
Сначала предскажи ответ в голове — это главный навык программиста.
# модель Task: id (primary_key), title
with Session(engine) as session:
a = Task(title="первая")
b = Task(title="вторая")
print("1:", a.id, b.id)
session.add_all([a, b])
session.commit()
print("2:", a.id, b.id)
# в базе одна задача: "купить хлеб"
with Session(engine) as session:
task = session.get(Task, 1)
task.title = "купить молоко"
session.rollback()
with Session(engine) as session:
print(session.get(Task, 1).title)
with Session(engine) as session:
session.add(Task(title="задача"))
session.flush()
print("A:", len(session.scalars(select(Task).order_by(Task.id)).all()))
session.rollback()
with Session(engine) as session:
print("B:", len(session.scalars(select(Task).order_by(Task.id)).all()))
1. Когда объект Task получает значение id?
2. Сессия добавила объект через add, но commit не вызывался. Что увидит новая сессия?
3. Что делает session.rollback()?
4. Чем flush отличается от commit?
5. Обращение к атрибуту объекта после выхода из блока with подняло DetachedInstanceError. Что случилось?
В одной сессии добавь две задачи: «сдать отчёт» с приоритетом 2 и «ответить на письма» с приоритетом 1. Затем достань первую через session.get и переименуй её в «сдать годовой отчёт». Выполни commit и выведи все задачи циклом в формате: id, название, «- приоритет», приоритет.
Что такое Session в SQLAlchemy?
Это рабочая единица ORM: сессия отслеживает загруженные и новые объекты, копит изменения и отправляет их в базу командой commit. Session создают от движка — Session(engine) — и живут с ней внутри блока with, который закрывает сессию на выходе. Connection при этом низкоуровневый: в 95% случаев работаешь именно с Session.
Урок 6: select — первые запросыПочему данные не сохраняются после add?
add только кладёт объект в блокнот сессии — до commit база о нём не знает. Внутри своей сессии объект даже виден, но новая сессия или перезапуск программы покажут пустоту. Добавь session.commit() после добавления: он выполнит INSERT и закрепит транзакцию.
Что делает session.flush()?
Flush отправляет накопленные INSERT и UPDATE в базу, не закрепляя транзакцию: у новых объектов появляется id, но при откате вставка всё равно пропадёт. commit вызывает flush сам, поэтому вручную flush нужен редко — например, чтобы получить id до конца блока или выловить ошибку записи раньше времени.
Как откатить ошибочные изменения?
Командой session.rollback(): она отменяет всё накопленное с последнего commit — и новые объекты, и изменённые атрибуты. Объекты возвращаются к состоянию из базы при следующем обращении. Rollback не удаляет то, что уже было закоммичено ранее, — для этого нужен delete, о нём позже в курсе.
Понравился урок? Сошлитесь на него
«Без commit сессия похожа на блокнот с черновиками: всё записано, но ни одна строчка не дошла до базы.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
SQLAlchemy · Урок 4
create_all: таблицы из моделей
Одна строка Base.metadata.create_all(engine) превращает модели в настоящие таблицы. Инспектор, безопасный повтор и границы магии.
SQLAlchemy · Урок 6
select: первые запросы к таблице
select(Task) возвращает строки-объекты: перебор циклом, одна строка через first(), одно значение через scalar().
sqlite3 · Урок 9
Транзакции и индексы в SQLite: целостность и скорость
Перевод денег не бывает наполовину: либо обе операции, либо ни одной. Разбираем транзакции и индексы SQLite — кнопку «отменить» и оглавление для таблиц.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 7
Подключаем базу данных SQLite к FastAPI
Словарь задач из урока 5 умирает при перезапуске. Ставим на его место SQLite через SQLAlchemy: движок, сессии, Depends и CRUD, который переживает uvicorn --reload.
fastapi база данныхsqlalchemy fastapi
BeautifulSoup / Scrapy · Урок 7
Сохранение результатов: CSV, JSON и базы данных
Список словарей собран — теперь доведём его до файла: CSV для таблиц, JSON для вложенных структур, sqlite3 для больших объёмов. Плюс две классические ловушки: пустые строки и кракозябры в Excel.
сохранение данных pythonпарсинг данных с сайта в excel
SQLAlchemy · Урок 8
Обновление и удаление записей
Данные записаны — теперь их надо менять: два пути SQLAlchemy, ручной через объект и массовый через update() и delete().
sqlalchemy update deletesqlalchemy обновление записей