CRUD: создание, чтение, обновление, удаление

CRUD — четыре базовые операции с данными: Create (создать), Read (прочитать), Update (изменить), Delete (удалить). На них держится почти любое приложение.
Все операции идут через db.session — «рабочий стол» изменений. Добавил/изменил/удалил объекты — и зафиксировал всё одним commit. В современном SQLAlchemy чтение делают через select(), а старый Model.query считается легаси.

Сессия — это единица работы: ты набрасываешь в неё изменения, а commit применяет их к базе одной транзакцией. Если что-то пошло не так — rollback отменяет всё разом. Разберём каждую операцию.

Create — создать объект, добавить в сессию, зафиксировать:

user = User(name="Анна", email="[email protected]")
db.session.add(user)
db.session.commit()

Read — современный способ через select и db.session.execute:

from sqlalchemy import select

# все пользователи
users = db.session.execute(select(User)).scalars().all()
# один по условию
u = db.session.execute(
    select(User).filter_by(email="[email protected]")
).scalar_one_or_none()
# по id с авто-404
user = db.get_or_404(User, 1)

Update — просто меняешь атрибут и коммитишь, ORM сам сформирует UPDATE:

user.name = "Анна Петрова"
db.session.commit()

Delete — удаляешь объект из сессии и коммитишь:

db.session.delete(user)
db.session.commit()

Ключ к пониманию SQLAlchemy — концепция сессии как единицы работы. Сессия — это не соединение с базой, а «рабочий стол», на котором копятся изменения: добавленные, изменённые и удалённые объекты. Пока ты не вызвал commit, ничего в базу не ушло — всё живёт в памяти сессии. commit применяет накопленное одной транзакцией по принципу «всё или ничего», а при ошибке rollback откатывает разом. Именно поэтому самая частая ошибка новичка — забытый commit: объект добавлен, но не сохранён. Второй сдвиг — отказ от легаси Model.query в пользу select() с db.session.execute: это единый современный интерфейс чтения, который к тому же одинаков в чистом SQLAlchemy и во Flask-SQLAlchemy.

Как работает под капотом

Сессия отслеживает состояние объектов: новые (add), изменённые (грязные), удалённые. При commit она вычисляет, какой SQL нужен, и выполняет всё в одной транзакции. До commit изменения живут только в памяти сессии.

  db.session  (рабочий стол)
  ┌────────────────────────────┐
  │ add(user)    → INSERT       │
  │ user.name=.. → UPDATE       │
  │ delete(old)  → DELETE       │
  └────────────────────────────┘
        │ commit()
        ▼
  одна транзакция в БД: всё или ничего
        │ ошибка? → rollback (откат всего)

Смоделируем сессию как «рабочий стол» с отложенным commit.

class Session:
    def __init__(self):
        self.db = {}
        self.pending = []
    def add(self, key, value):
        self.pending.append(("add", key, value))
    def delete(self, key):
        self.pending.append(("del", key, None))
    def commit(self):
        for op, key, value in self.pending:
            if op == "add": self.db[key] = value
            elif op == "del": self.db.pop(key, None)
        self.pending.clear()

s = Session()
s.add("u1", {"name": "Анна"})
s.add("u2", {"name": "Боб"})
s.commit()
s.delete("u1")
print("до commit:", "u1" in s.db)   # ещё True
s.commit()
print("после commit:", "u1" in s.db, "| осталось:", s.db)

Запусти: изменения копятся и применяются только на commit — точно как в SQLAlchemy. Это объясняет, почему без commit данные «не сохраняются».

Частые ошибки

  • Забыть commit. Самая частая ошибка: add без commit ничего не сохранит.
  • Использовать легаси Model.query. В новых проектах предпочитай select() + db.session.execute.
  • Ловить отсутствие записи вручную. Для «найти или 404» есть db.get_or_404 — короче и правильнее.

Best practices

  • Группируй связанные изменения и коммить одной транзакцией.
  • При ошибке делай db.session.rollback(), не оставляй сессию в подвешенном состоянии.
  • Читай через select(...).scalars(); для поиска по id — get_or_404.

Что запомнить

  • CRUD идёт через db.session: add, изменение атрибута, delete.
  • Изменения применяются транзакцией только на commit; иначе теряются.
  • Чтение — через select() + db.session.execute().scalars(); по id — get_or_404.
  • При ошибке делают rollback, не оставляя сессию подвешенной.

Итог: CRUD идёт через db.session — add/commit/delete и чтение через select(). Изменения применяются транзакцией на commit. Дальше — связи между таблицами.

Проверьте себя
1. Что произойдёт, если вызвать db.session.add(user) без commit?
AПользователь сразу попадёт в базу
BИзменение останется в памяти сессии и не сохранится в базе
CБудет ошибка 500
DСоздастся дубликат
2. Какой способ чтения рекомендован в современном SQLAlchemy/Flask-SQLAlchemy 3.x?
AModel.query.all()
Bdb.session.execute(select(Model)).scalars()
Craw SQL строкой
Drequest.form