SAVEPOINT в SQLite — это именованная закладка внутри транзакции
Обычная транзакция работает по принципу «всё или ничего»: всё, что находится между BEGIN и COMMIT, либо коммитится целиком, либо целиком откатывается. Чаще всего это именно то, что нужно, — но иногда хочется большей гибкости. Что-то вроде: «Попробуй применить вот эту порцию изменений; если что-то пойдёт не так — откати только её, а остальную транзакцию оставь в живых».
Именно для этого и нужны точки сохранения SQLite. Ставите именованную закладку, делаете нужные изменения, а дальше либо фиксируете их (RELEASE), либо отматываете обратно к закладке (ROLLBACK TO).
Списание у Ады и зачисление у Бориса остались в базе. Ошибочное обновление строки Nobody откатилось, а остальная часть транзакции уцелела.
Три команды
Весь API — это три инструкции:
SAVEPOINT name— поставить закладку.RELEASE SAVEPOINT name— зафиксировать всё, что сделано после закладки, и убрать саму закладку.ROLLBACK TO SAVEPOINT name— откатить все изменения, сделанные после закладки; сама закладка остаётся, так что можно попробовать ещё раз.
Слово SAVEPOINT после RELEASE и ROLLBACK TO указывать необязательно — RELEASE risky и ROLLBACK TO risky сработают точно так же.
Строка попытка шага 2 для итоговой базы как будто никогда и не существовала. Всё остальное успешно сохраняется.
SAVEPOINT без внешней транзакции
И вот небольшой нюанс: команду SAVEPOINT можно выполнить и без предварительного BEGIN. SQLite в этом случае молча откроет транзакцию сам, а самая внешняя точка сохранения возьмёт на себя роль самой транзакции. RELEASE для такой точки выполнит коммит, а ROLLBACK TO откатит изменения без фиксации.
Именно поэтому точки сохранения иногда называют «именованными транзакциями». Но смешивать оба стиля в реальном коде — верный путь к путанице, так что лучше выбрать что-то одно. На практике чаще всего внешнюю границу транзакции задают через явные BEGIN ... COMMIT, а SAVEPOINT используют только как внутренние точки для частичного отката.
Вложенные точки сохранения в SQLite
Точки сохранения работают как стек. Можно поставить одну внутри другой и откатить вложенную, не трогая внешнюю:
В итоге в таблице остались: a, b, d. Откат к точке сохранения inner убрал c, но всё, что было сделано до inner (вставка b), уцелело, а сама транзакция продолжила работать.
Откат к внешней точке сохранения отменяет и всё, что происходило на внутренних уровнях, — вся часть стека выше указанного имени схлопывается за один раз:
Записи b и c исчезли. ROLLBACK TO outer откатывает всё, что произошло после установки outer, — в том числе inner и вставку c.
Зачем нужны точки сохранения SQLite
Классический сценарий — пакетная обработка, когда отдельные элементы могут падать с ошибкой, но это не должно ломать всю пачку. Оборачиваем каждый элемент в SAVEPOINT: если что-то пошло не так, откатываемся к точке сохранения и спокойно идём дальше:
В реальном коде «плохой» INSERT выбросит ошибку, приложение её поймает, выполнит ROLLBACK TO и продолжит работу. Две корректные строки попадут в базу, а проблемная не испортит весь пакет.
По такому же принципу ORM и инструменты миграций реализуют вложенные транзакции SQLite — они не вкладывают BEGIN друг в друга (SQLite этого не позволяет), а сопоставляют вложенные вызовы с точками сохранения.
На что стоит обратить внимание
Несколько нюансов, на которых спотыкаются те, кто только начинает работать с SAVEPOINT:
COMMITвсегда фиксирует транзакцию целиком. Сколько бы открытых точек сохранения у вас ни было —COMMIT(или его синонимEND) закрывает всю внешнюю транзакцию. Не воспринимайтеRELEASEкак «частичный коммит»: пока окружающая транзакция не зафиксирована, ничего на диск не записывается.ROLLBACKбезTOотменяет всё. Он завершает транзакцию и сбрасывает все открытые точки сохранения. Если хотите сохранить транзакцию живой — используйтеROLLBACK TO имя.- Точка сохранения живёт, пока её не освободят или не откатят. Забыли сделать
RELEASE— данные не пропадут, просто «закладка» будет висеть до конца транзакции. - Имена не обязаны быть уникальными. Если объявить
SAVEPOINT sдважды, тоROLLBACK TO sоткатится к самой последней. Удобно для рекурсии и сбивает с толку, если так получилось случайно.
Дальше: представления (views)
Точки сохранения дают тонкий контроль над записью. Следующий шаг — управлять тем, как вы читаете данные: сохранить запрос как именованный переиспользуемый объект, к которому можно обращаться через SELECT, как к обычной таблице. Это и есть представление (view), и о нём — в следующей главе.
Часто задаваемые вопросы
Что такое SAVEPOINT в SQLite?
SAVEPOINT — это именованная метка, которую вы ставите внутри транзакции. Позже можно сделать ROLLBACK TO с этим именем, чтобы откатить всё, что произошло после метки, либо RELEASE, чтобы зафиксировать изменения и убрать саму метку. По сути, savepoint позволяет разбить транзакцию на более мелкие куски, которые можно откатывать по отдельности.
Чем SAVEPOINT отличается от обычной транзакции?
Транзакция начинается с BEGIN и завершается через COMMIT или ROLLBACK. SAVEPOINT же ставится внутри уже открытой транзакции командой SAVEPOINT имя и даёт точку частичного отката. Откат до savepoint не закрывает внешнюю транзакцию — вы спокойно продолжаете работу и коммитите её позже.
Можно ли вкладывать SAVEPOINT друг в друга?
Да, и это нормальная практика. Точки сохранения складываются в стек, и ROLLBACK TO outer откатывает всё до этого уровня, включая все вложенные savepoint'ы внутри. Имена при этом могут повторяться — SQLite выберет самый свежий savepoint с этим именем, как и положено стеку.