Перейти к содержанию

212 уроков, 15 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 5 из 20 Средний 35 мин 130 XP

Сессия: 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, и возвращает объекты к состоянию, которое лежит в базе. Отменить можно и добавление, и изменение.

rollback отменяет добавление
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, переименуем — и откатим:

rollback отменяет изменение
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 назначится, но транзакция ещё не закреплена.

add, flush и commit по шагам
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.

несколько изменений — один 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)
    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()))
Проверь себя
0 / 5

1. Когда объект Task получает значение id?

2. Сессия добавила объект через add, но commit не вызывался. Что увидит новая сессия?

3. Что делает session.rollback()?

4. Чем flush отличается от commit?

5. Обращение к атрибуту объекта после выхода из блока with подняло DetachedInstanceError. Что случилось?

Карточки терминов
Запомнено: 0 / 6
Практика

В одной сессии добавь две задачи: «сдать отчёт» с приоритетом 2 и «ответить на письма» с приоритетом 1. Затем достань первую через session.get и переименуй её в «сдать годовой отчёт». Выполни commit и выведи все задачи циклом в формате: id, название, «- приоритет», приоритет.

practice.py
Вопросы и ответы по уроку

Что такое 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.