Привет! В этой статье я покажу один небольшой трюк с точками сохранения SQLite и расскажу, как я нашел этот трюк.

Google утверждает, что Android полностью поддерживает ядро ​​базы данных SQLite. Но код, который предоставляет нам фреймворк, иногда плохо реализуется, поэтому мы не можем использовать некоторые функции SQLite и вынуждены искать обходные пути.

Одна из функций, которая не работает должным образом, - это точка сохранения. Короче говоря, это тег в журнале операций SQLite, который может отметить важный момент в истории операций, чтобы вернуться к нему, если что-то пошло не так. Это похоже на транзакцию, но у точек сохранения есть одно большое преимущество перед транзакциями: у вас может быть столько точек сохранения, сколько вам нужно. У вас может быть только одна транзакция одновременно, но когда вам нужно отменить только определенные изменения внутри огромной транзакции, можно использовать точку сохранения. Однако точки сохранения не заменяют транзакции, потому что они не будут автоматически откатываться в случае ошибки, вам нужно управлять ими вручную.

Точки сохранения могут быть полезны для пошаговых операций, связанных с базой данных, где каждый шаг может быть прерван или что-то может пойти не так. Но даже в этом случае вы можете решить эту проблему с помощью конечного автомата в коде, без использования базы данных на промежуточных этапах.

Если вам действительно нужно использовать точки сохранения, вот трюк: всегда запускайте оператор SQL для отката к определенной точке сохранения с точки с запятой. Нравится:

sqliteDb.execSql(“;ROLLBACK TO savepointName;”);

Вот и все полезные приемы. Если вы хотите прочитать историю, стоящую за этой строкой кода и как мы ее получить, продолжайте читать.

На предыдущей работе мы активно использовали SQLite для работы с данными в автономном режиме и строго на него полагались. К сожалению, наша бизнес-логика была написана очень жестко, поэтому мы не можем легко смоделировать базу данных для тестирования. Поэтому мы решили протестировать его на реальной базе данных. Чтобы не загромождать базу данных и не делать тесты без сохранения состояния, мы решили использовать вложенные транзакции, как в Postgres.

После быстрого поиска мы выяснили, что точки сохранения - хороший выбор для нас. Я написал простой тестовый пример с точками сохранения, и… он рухнул. Исключение было странным: заявлено, что я пытаюсь откатить несуществующую транзакцию. Но ... была только одна корневая транзакция, которую нужно было откатить в методе AfterClass JUnit. Я потратил два дня на отладку, пока не нашел этот код в android.database.DatabaseUtils # getSqlStatementType (он вызывается внутри android.database.sqlite.SQLiteSession # executeSpecial для каждого оператора SQL, чтобы найти транзакционные операции, встроенные комментарии - мои ):

/**
     * Performs special reinterpretation of certain SQL statements such as "BEGIN",
     * "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are
     * maintained.
     *
     * This function is mainly used to support legacy apps that perform their
     * own transactions by executing raw SQL rather than calling {@link #beginTransaction}
     * and the like.
     *
     * @param sql The SQL statement to execute.
     * @param bindArgs The arguments to bind, or null if none.
     * @param connectionFlags The connection flags to use if a connection must be
     * acquired by this operation.  Refer to {@link SQLiteConnectionPool}.
     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
     * @return True if the statement was of a special form that was handled here,
     * false otherwise.
     *
     * @throws SQLiteException if an error occurs, such as a syntax error
     * or invalid number of bind arguments.
     * @throws OperationCanceledException if the operation was canceled.
     */
public static int getSqlStatementType(String sql) {
        sql = sql.trim();
        if (sql.length() < 3) {
            return STATEMENT_OTHER;
        }
        String prefixSql = sql.substring(0, 3).toUpperCase(Locale.ROOT); // yep, they're determining statement type by first 3 letters of the SQL statement string
        if (prefixSql.equals("SEL")) {
            return STATEMENT_SELECT;
        } else if (prefixSql.equals("INS") ||
                prefixSql.equals("UPD") ||
                prefixSql.equals("REP") ||
                prefixSql.equals("DEL")) {
            return STATEMENT_UPDATE;
        } else if (prefixSql.equals("ATT")) {
            return STATEMENT_ATTACH;
        } else if (prefixSql.equals("COM")) {
            return STATEMENT_COMMIT;
        } else if (prefixSql.equals("END")) {
            return STATEMENT_COMMIT;
        } else if (prefixSql.equals("ROL")) {
            return STATEMENT_ABORT;
        } else if (prefixSql.equals("BEG")) {
            return STATEMENT_BEGIN;
        } else if (prefixSql.equals("PRA")) {
            return STATEMENT_PRAGMA;
        } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") ||
                prefixSql.equals("ALT")) {
            return STATEMENT_DDL;
        } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) {
            return STATEMENT_UNPREPARED;
        }
        return STATEMENT_OTHER;
    }

Из-за этого очень умного кода, когда мы пытаемся откатить точку сохранения (что выполняется оператором ROLLBACK TO), этот метод решает, что мы собираемся откатить транзакцию оператором ROLLBACK. Это может иметь смысл, потому что точки сохранения широко не используются, и ребята из Google могли легко забыть об операторе ROLLBACK TO, но это не оправдывает этот изощренный подход к определению типа оператора SQL по первым 3 буквам строки SQL. ТБХ, я горько улыбнулся, глядя на этот код.

Впереди еще много интересного. В поисках обходного пути для такого поведения я задал вопрос о StackOverflow, много гуглил, пока не нашел эту проблему в Android Issue Tracker. Короче говоря, эта проблема была открыта в 2012 году, с тех пор повешена в назначенном состоянии, имела некоторые комментарии (включая один комментарий с возможным обходным путем, о котором я упоминал выше, и патч для добавления поддержки точки сохранения в структуру) в 2013 году, пока я не написал дополнительный комментарий в 2017 году. После моего комментария он был переназначен и исправлен через несколько дней с примечанием, что исправление будет доступно в Android P. Я понимаю, что у этой проблемы был низкий приоритет, но ... я раньше не видел такого большого разрыва между открытием проблемы и ее решением.

Эта история доказывает, что фреймворк Android несовершенен, особенно его поддержка SQLite. В заключение я бы порекомендовал вам разумно использовать точки сохранения в приложении для Android, и только тогда, когда у вас нет другого выбора.

Спасибо за чтение! Надеюсь, это будет вам полезно.