В этом уроке:

- используем транзакции при работе с БД

 

Что такое БД-транзакция, думаю объяснять особо не надо. Она используется при работе с данными по принципу «все или ничего». Т.е., например, вам нужно вставить пачку данных. Но вставить надо так, чтобы или все вставилось или ничего не вставилось.  И если в процессе половина записей прошла, а другая нет – должна быть возможность откатить изменения.

Напишем простое приложение и исследуем возможности SQLite в этом плане.

Создадим проект:

Project name: P0381_SQLiteTransaction
Build Target: Android 2.3.3
Application name: SQLiteTransaction
Package name: ru.startandroid.develop.p0381sqlitetransaction
Create Activity: MainActivity

 

Открываем MainActivity.java и пишем:

package ru.startandroid.develop.p0381sqlitetransaction;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

  final String LOG_TAG = "myLogs";

  DBHelper dbh;
  SQLiteDatabase db;

  /** Called when the activity is first created. */
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Log.d(LOG_TAG, "--- onCreate Activity ---");
    dbh = new DBHelper(this);
    myActions();
  }

  void myActions() {
    db = dbh.getWritableDatabase();
    delete(db, "mytable");
    insert(db, "mytable", "val1");
    read(db, "mytable");
    dbh.close();
  }

  void insert(SQLiteDatabase db, String table, String value) {
    Log.d(LOG_TAG, "Insert in table " + table + " value = " + value);
    ContentValues cv = new ContentValues();
    cv.put("val", value);
    db.insert(table, null, cv);
  }

  void read(SQLiteDatabase db, String table) {
    Log.d(LOG_TAG, "Read table " + table);
    Cursor c = db.query(table, null, null, null, null, null, null);
    if (c != null) {
      Log.d(LOG_TAG, "Records count = " + c.getCount());
      if (c.moveToFirst()) {
        do {
          Log.d(LOG_TAG, c.getString(c.getColumnIndex("val")));
        } while (c.moveToNext());
      }
      c.close();
    }
  }

  void delete(SQLiteDatabase db, String table) {
    Log.d(LOG_TAG, "Delete all from table " + table);
    db.delete(table, null, null);
  }

  // класс для работы с БД
  class DBHelper extends SQLiteOpenHelper {

    public DBHelper(Context context) {
      super(context, "myDB", null, 1);
    }

    public void onCreate(SQLiteDatabase db) {
      Log.d(LOG_TAG, "--- onCreate database ---");

      db.execSQL("create table mytable ("
          + "id integer primary key autoincrement," 
          + "val text"
          + ");");
    }

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
  }

}

 

Разбираем код. Я создал несколько методов, где сгруппировал операции: insert – набор операций для вставки записи, read – чтение всех записей, delete – удаление всех записей. Класс DBHelper – для управления БД. Интересовать нас будет метод myActions. Сейчас в нем мы подключаемся к БД, очищаем таблицу mytable, вставляем строку с значением val1, выводим в лог все записи из таблицы и отключаемся.

Все сохраним, запустим приложение. Смотрим лог:

--- onCreate Activity ---
--- onCreate database ---
Delete all from table mytable
Insert in table mytable value = val1
Read table mytable
Records count = 1
val1

Все верно, запись вставилась и отобразилась.

Теперь попробуем использовать транзакцию. Поправим код myActions на этот:

  void myActions() {
    db = dbh.getWritableDatabase();
    delete(db, "mytable");
    db.beginTransaction();
    insert(db, "mytable", "val1");
    db.endTransaction();
    insert(db, "mytable", "val2");
    read(db, "mytable");
    dbh.close();
  }

Мы подключаемся к базе, чистим таблицу, открываем транзакцию методом beginTransaction, вставляем val1, закрываем транзакцию методом endTransaction, вставляем val2, выводим содержимое в лог и отключаемся. Все сохраняем, запускаем и смотрим лог:

--- onCreate Activity ---
Delete all from table mytable
Insert in table mytable value = val1
Insert in table mytable value = val2
Read table mytable
Records count = 1
val2

По логу видно, что вставляли мы две записи, но прошла только вторая. Та, которая была в транзакции – не записалась. Это произошло потому, что мы явно не указали, что транзакция должна быть успешно закрыта. Если этого не сделать, то при закрытии транзакции все операции отменятся. Давайте исправимся. Снова перепишем myActions:

  void myActions() {
    db = dbh.getWritableDatabase();
    delete(db, "mytable");
    db.beginTransaction();
    insert(db, "mytable", "val1");
    db.setTransactionSuccessful();
    insert(db, "mytable", "val2");
    db.endTransaction();
    insert(db, "mytable", "val3");
    read(db, "mytable");
    dbh.close();
  }

 

Подключаемся к БД, чистим таблицу, открываем транзакцию, вставляем val1, подтверждаем успешность транзакции методом setTransactionSuccessful, вставляем val2, закрываем транзакцию, вставляем val3, выводим содержимое и отключаемся.

Сохраняем, запускаем, смотрим лог:

--- onCreate Activity ---
Delete all from table mytable
Insert in table mytable value = val1
Insert in table mytable value = val2
Insert in table mytable value = val3
Read table mytable
Records count = 3
val1
val2
val3

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

 

Транзакция при открытии ставит блокировку на базу. Убедимся в этом, попробуем создать новое подключение к БД во время транзакции. Перепишем myActions:

  void myActions() {
    try {
      db = dbh.getWritableDatabase();
      delete(db, "mytable");

      db.beginTransaction();
      insert(db, "mytable", "val1");

      Log.d(LOG_TAG, "create DBHelper");
      DBHelper dbh2 = new DBHelper(this);
      Log.d(LOG_TAG, "get db");
      SQLiteDatabase db2 = dbh2.getWritableDatabase();
      read(db2, "mytable");
      dbh2.close();

      db.setTransactionSuccessful();
      db.endTransaction();

      read(db, "mytable");
      dbh.close();

    } catch (Exception ex) {
      Log.d(LOG_TAG, ex.getClass() + " error: " + ex.getMessage());
    }
  }

 

Подключаемся к базе, чистим таблицу, открываем транзакцию, вставляем запись, создаем новое подключение к БД - db2, читаем содержимое вторым подключением, закрываем второе подключение, успешно закрываем транзакцию, читаем содержимое первым подключением, закрываем первое подключение.

Все сохраним и запустим. Смотрим лог:

--- onCreate Activity ---
Delete all from table mytable
Insert in table mytable value = val1
create DBHelper
get db
class android.database.sqlite.SQLiteException error: database is locked

Мы видим, что при попытке создать второе подключение к базе произошла ошибка SQLiteException – база заблокирована открытой транзакцией. Если вы теперь закоментите или удалите строки управления транзакцией и снова выполните код, то все пройдет успешно, т.к. никаких блокировок не будет.

 

Наверняка есть некоторые вопросы по этой теме. Попробую здесь же ответить на некоторые.

close

Метод close есть и у SQLiteDatabase и у SQLiteOpenHelper. Какая между ними разница? Каким из них пользоваться для закрытия подключения?

Тут надо понимать один момент – объект SQLiteOpenHelper всегда предоставляет только одно подключение. Попробую объяснить этот механизм. У объекта SQLiteOpenHelper есть внутренний атрибут mDatabase типа SQLiteDatabase. Когда мы вызываем метод getWritableDatabase, объект SQLiteOpenHelper проверяет: если mDatabase не null и не закрыт, то он и идет в качестве return. Иначе  SQLiteOpenHelper выполняет подключение к БД, записывает новый SQLiteDatabase-объект в mDatabase и возвращает нам его. Т.е. метод getWritableDatabase либо возвращает существующее подключение к БД, либо создает новое в случае отсутствия подключения. Когда же выполняется метод close для SQLiteOpenHelper, то происходит вызов close для mDatabase и выполняется код mDatabase = null.

Рассмотрим на примере. Снова меняем метод myActions:

  void myActions() {
    db = dbh.getWritableDatabase();
    SQLiteDatabase db2 = dbh.getWritableDatabase();
    Log.d(LOG_TAG, "db = db2 - " + db.equals(db2));
    Log.d(LOG_TAG, "db open - " + db.isOpen() + ", db2 open - " + db2.isOpen());
    db2.close();
    Log.d(LOG_TAG, "db open - " + db.isOpen() + ", db2 open - " + db2.isOpen());
  }

Сначала мы получаем db. При этом dbh проверяет свой внутренний атрибут mDatabase. Т.к. это первая попытка подключения, то mDatabase пуст, поэтому внутри dbh производится подключение и в mDatabase записывается свежесозданный SQLiteDatabase, и он же и возвращается в db из метода getWritableDatabse .

Затем мы из того же dbh получаем db2. dbh снова проверяет свой внутренний mDatabse, видит, что он уже не null и не закрыт, и возвращает нам его в наш db2. В итоге db и db2 равны и ссылаются на один и тот же объект. Проверяем это с помощью метода equals. Далее проверим, что db и db2 открыты. Потом закроем только db2, и еще раз проверим на открытость оба объекта.

Сохраняем, запускаем, смотрим лог:

--- onCreate Activity ---
db = db2 – true
db open - true, db2 open – true
db open - false, db2 open - false

Видим, что equals вернул true. Затем видно, что db и db2 открыты. А после закрытия db2 видим, что закрыты оба объекта. Все оттого, что «оба объекта» – это всего лишь две ссылки на один объект.

 

Если в коде вместо db2.close() поставить dbh.close() - эффект будет тот же. dbh вызовет метод close для mDatabase и обнулит его - mDatabase = null. А db и db2 будут ссылаться на закрытый SQLiteDatabase.

Я думаю, что правильнее вызывать close для SQLiteOpenHelper, а не для SQLiteDatabase. Т.к. гарантировано закрывается текущее открытое соединение и обнуляется внутренняя ссылка на объект.

Если вам надо получить второе открытое подключение к БД, то надо создавать новый экземпляр DBHelper и вызывать getWritableDatabase. Мы так делали чуть выше в примере с блокировкой транзакции.

 

read write

В чем разница между getWritableDatabase и getReadableDatabase? Судя по хелпу, в обычной ситуации оба метода возвращают одно и то же. И оба позволят читать и менять БД. В случае же, например, проблемы отсутствия свободного места на устройстве, метод getReadableDatabase вернет БД только для чтения, а getWritableDatabase выдаст ошибку.

 

_id, как имя поля-идентификатора

В различных источниках при работе с БД в качестве наименования поля-идентификатора в таблице используют не просто id, а _id. Почему?

Ответ нашелся в доках по Cursor-адаптерам. Цитата: "The Cursor must include a column named "_id" or this class will not work.". Т.е. если вы планируете использовать Cursor-адаптеры, то необходимо, чтобы таблица содержала поле _id, иначе адаптер не будет работать. 

 

Блокировка

Метод открытия транзакции beginTransaction ставит блокировку в режиме EXCLUSIVE. Т.е. БД блокируется и на чтение и на запись для других подключений. В SDK Android версии старше 2.3.3 появился метод beginTransactionNonExclusive, который ставит блокировку в режиме IMMEDIATE. Я подозреваю, что это позволит читать данные другим подключениям.

Если есть желание подробнее погрузиться в тему, вам сюда.

 

Синтаксис

И кстати, рекомендуемая форма для использования транзакций такая:

    db.beginTransaction();
    try {
      ...
      db.setTransactionSuccessful();
    } finally {
      db.endTransaction();
    }

 

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

 

На следующем уроке:

- меняем версию и обновляем структуру БД в onUpgrade

 


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal