Урок 38. Транзакции в SQLite. Небольшой FAQ по SQLite.
В этом уроке:
- используем транзакции при работе с БД
Что такое БД-транзакция, думаю объяснять особо не надо. Она используется при работе с данными по принципу «все или ничего». Т.е., например, вам нужно вставить пачку данных. Но вставить надо так, чтобы или все вставилось или ничего не вставилось. И если в процессе половина записей прошла, а другая нет – должна быть возможность откатить изменения.
Напишем простое приложение и исследуем возможности 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());
}
Затем мы из того же 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