Сохранение данных с помощью SQLite

Сохранение данных в базе данных идеально подходит для повторяющихся или структурированных данных, таких как контактная информация. Предполагается, что вы знакомы с базами данных SQL в целом. Эта страница поможет вам начать работу с базами данных SQLite на Android. API, которые вам понадобятся для использования базы данных на Android, доступны в пакете android.database.sqlite.

Внимание! Хотя эти API очень эффективны, они являются низкоуровневыми и требуют много времени и усилий для использования.

  • Необработанные SQL-запросы не проверяются во время компиляции. При изменении графа данных необходимо вручную обновлять затронутые SQL-запросы. Это может занять много времени и привести к ошибкам.
  • Вам нужно использовать много шаблонного кода для преобразования SQL-запросов в объекты данных и наоборот.

Поэтому мы настоятельно рекомендуем использовать библиотеку персистентности Room в качестве уровня абстракции для доступа к информации в базах данных SQLite вашего приложения.

Определите схему и контракт

Один из основных принципов баз данных SQL – это схема, то есть формальное описание структуры базы данных. Схема отражается в инструкциях SQL, которые вы используете для создания базы данных. Вам может быть полезно создать сопутствующий класс, известный как класс контракта, который явно определяет макет вашей схемы систематическим и самодокументируемым способом.

Класс контракта – это контейнер для констант, определяющих имена URI, таблиц и столбцов. Класс контракта позволяет использовать одни и те же константы во всех остальных классах одного пакета. Это позволяет вам изменить имя столбца в одном месте и распространить его по всему коду.

Хороший способ организовать класс контракта – поместить определения, которые являются глобальными для всей базы данных, на корневой уровень класса. Затем создайте внутренний класс для каждой таблицы. Каждый внутренний класс перечисляет столбцы соответствующей таблицы.

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

Например, в следующем контракте определены названия таблицы и столбцов для одной таблицы, представляющей RSS-фид:

Kotlin

object FeedReaderContract {
    // Table contents are grouped together in an anonymous object.
    object FeedEntry : BaseColumns {
        const val TABLE_NAME = "entry"
        const val COLUMN_NAME_TITLE = "title"
        const val COLUMN_NAME_SUBTITLE = "subtitle"
    }
}

Java

public final class FeedReaderContract {
    // To prevent someone from accidentally instantiating the contract class,
    // make the constructor private.
    private FeedReaderContract() {}

    /* Inner class that defines the table contents */
    public static class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    }
}

Как создать базу данных с помощью помощника SQL

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

Kotlin

private const val SQL_CREATE_ENTRIES =
        "CREATE TABLE ${FeedEntry.TABLE_NAME} (" +
                "${BaseColumns._ID} INTEGER PRIMARY KEY," +
                "${FeedEntry.COLUMN_NAME_TITLE} TEXT," +
                "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)"

private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"

Java

private static final String SQL_CREATE_ENTRIES =
    "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
    FeedEntry._ID + " INTEGER PRIMARY KEY," +
    FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
    FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

private static final String SQL_DELETE_ENTRIES =
    "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;

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

Класс SQLiteOpenHelper содержит полезный набор API для управления базой данных. Когда вы используете этот класс для получения ссылок на базу данных, система выполняет потенциально продолжительные операции по созданию и обновлению базы данных только при необходимости, а не во время запуска приложения. Для этого вызовите метод getWritableDatabase() или getReadableDatabase().

Примечание. Поскольку эти методы могут выполняться долго, вызывайте getWritableDatabase() или getReadableDatabase() в фоновом потоке. Подробнее о потоках в Android…

Чтобы использовать SQLiteOpenHelper, создайте подкласс, который переопределяет методы обратного вызова onCreate() и onUpgrade(). Вы также можете реализовать методы onDowngrade() или onOpen(), но это не обязательно.

Ниже приведен пример реализации SQLiteOpenHelper с использованием некоторых из указанных выше команд.

Kotlin

class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(SQL_CREATE_ENTRIES)
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES)
        onCreate(db)
    }
    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        onUpgrade(db, oldVersion, newVersion)
    }
    companion object {
        // If you change the database schema, you must increment the database version.
        const val DATABASE_VERSION = 1
        const val DATABASE_NAME = "FeedReader.db"
    }
}

Java

public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

Чтобы получить доступ к базе данных, создайте экземпляр подкласса SQLiteOpenHelper:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Ввод информации в базу данных

Вставьте данные в базу данных, передав объект ContentValues в метод insert():

Kotlin

// Gets the data repository in write mode
val db = dbHelper.writableDatabase

// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
    put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)
}

// Insert the new row, returning the primary key value of the new row
val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)

Java

// Gets the data repository in write mode
SQLiteDatabase db = dbHelper.getWritableDatabase();

// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);

Первый аргумент для insert() – это просто имя таблицы.

Второй аргумент указывает фреймворку, что делать, если ContentValues пуст (т.е. вы не put никаких значений). Если вы укажете название столбца, фреймворк вставит строку и задаст для этого столбца значение null. Если вы укажете null, как в этом примере кода, фреймворк не будет вставлять строку, если нет значений.

Метод insert() возвращает идентификатор новой строки или -1, если при вставке данных произошла ошибка. Это может произойти, если у вас есть конфликт с уже существующими данными в базе данных.

Чтение информации из базы данных

Чтобы прочитать данные из базы данных, используйте метод query(), передав ему критерии выбора и нужные столбцы. Этот метод сочетает в себе элементы insert() и update(), но список столбцов определяет данные, которые вы хотите получить ("проекция"), а не данные для вставки. Результаты запроса возвращаются в объекте Cursor.

Kotlin

val db = dbHelper.readableDatabase

// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)

// Filter results WHERE "title" = 'My Title'
val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
val selectionArgs = arrayOf("My Title")

// How you want the results sorted in the resulting Cursor
val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"

val cursor = db.query(
        FeedEntry.TABLE_NAME,   // The table to query
        projection,             // The array of columns to return (pass null to get all)
        selection,              // The columns for the WHERE clause
        selectionArgs,          // The values for the WHERE clause
        null,                   // don't group the rows
        null,                   // don't filter by row groups
        sortOrder               // The sort order
)

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
    };

// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// How you want the results sorted in the resulting Cursor
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

Третий и четвертый аргументы (selection и selectionArgs) объединяются для создания предложения WHERE. Поскольку аргументы указываются отдельно от запроса, они экранируются перед объединением. Это защищает операторы выбора от SQL-инъекций. Подробную информацию обо всех аргументах можно найти в справочнике по query().

Чтобы посмотреть строку в курсоре, используйте один из методов перемещения Cursor, который всегда нужно вызывать перед началом чтения значений. Поскольку курсор начинает работу с позиции -1, вызов moveToNext() помещает "позицию чтения" на первую запись в результатах и возвращает информацию о том, прошел ли курсор последнюю запись в наборе результатов. Для каждой строки можно прочитать значение столбца, вызвав один из методов Cursor get, например getString() или getLong(). Для каждого метода get необходимо передать индекс нужного столбца, который можно получить, вызвав getColumnIndex() или getColumnIndexOrThrow(). Завершив итерацию по результатам, вызовите close() для курсора, чтобы освободить его ресурсы. Например, в следующем примере показано, как получить все идентификаторы товаров, хранящиеся в курсоре, и добавить их в список:

Kotlin

val itemIds = mutableListOf<Long>()
with(cursor) {
    while (moveToNext()) {
        val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
        itemIds.add(itemId)
    }
}
cursor.close()

Java

List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
  long itemId = cursor.getLong(
      cursor.getColumnIndexOrThrow(FeedEntry._ID));
  itemIds.add(itemId);
}
cursor.close();

Удаление информации из базы данных

Чтобы удалить строки из таблицы, необходимо указать критерии выбора, которые идентифицируют строки для метода delete(). Этот механизм работает так же, как аргументы выбора для метода query(). Она разделяет спецификацию выбора на предложение выбора и аргументы выбора. Оператор WHERE определяет, какие столбцы нужно проверить, и позволяет комбинировать проверки столбцов. Аргументы – это значения, которые проверяются и связаны с предложением. Поскольку результат обрабатывается не так, как обычная инструкция SQL, он защищен от внедрения SQL-кода.

Kotlin

// Define 'where' part of query.
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
// Specify arguments in placeholder order.
val selectionArgs = arrayOf("MyTitle")
// Issue SQL statement.
val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)

Java

// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "MyTitle" };
// Issue SQL statement.
int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);

Возвращаемое значение метода delete() указывает количество строк, удаленных из базы данных.

Как обновить базу данных

Если вам нужно изменить подмножество значений базы данных, используйте метод update().

При обновлении таблицы синтаксис ContentValues из insert() объединяется с синтаксисом WHERE из delete().

Kotlin

val db = dbHelper.writableDatabase

// New value for one column
val title = "MyNewTitle"
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
}

// Which row to update, based on the title
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
val selectionArgs = arrayOf("MyOldTitle")
val count = db.update(
        FeedEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs)

Java

SQLiteDatabase db = dbHelper.getWritableDatabase();

// New value for one column
String title = "MyNewTitle";
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);

// Which row to update, based on the title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyOldTitle" };

int count = db.update(
    FeedReaderDbHelper.FeedEntry.TABLE_NAME,
    values,
    selection,
    selectionArgs);

Возвращаемое значение метода update() – это количество строк, затронутых в базе данных.

Сохранение подключения к базе данных

Поскольку вызовы getWritableDatabase() и getReadableDatabase() при закрытой базе данных требуют больших ресурсов, рекомендуется оставлять подключение к базе данных открытым до тех пор, пока вам не потребуется доступ к ней. Как правило, лучше всего закрывать базу данных в методе onDestroy() вызывающей Activity.

Kotlin

override fun onDestroy() {
    dbHelper.close()
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    dbHelper.close();
    super.onDestroy();
}

Отладка базы данных

Android SDK включает инструмент командной строки sqlite3, который позволяет просматривать содержимое таблиц, выполнять команды SQL и другие полезные функции в базах данных SQLite. Подробнее о том, как выполнять команды оболочки…