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

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

Внимание: хотя эти API являются мощными, они довольно низкоуровневые и требуют много времени и усилий для использования:

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

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

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

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

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

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

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

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

Котлин

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"
    }
}

Ява

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

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

Котлин

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}"

Ява

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 , которая использует некоторые из команд, показанных выше:

Котлин

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"
    }
}

Ява

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 :

Котлин

val dbHelper = FeedReaderDbHelper(context)

Ява

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Поместите информацию в базу данных

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

Котлин

// 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)

Ява

// 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 , как в этом примере кода, платформа не вставит строку, если значений нет.

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

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

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

Котлин

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
)

Ява

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() помещает «позицию чтения» в первую запись в результатах и ​​возвращает информацию о том, прошел ли курсор уже за последней записью в наборе результатов. Для каждой строки вы можете прочитать значение столбца, вызвав один из методов get Cursor , например getString() или getLong() . Для каждого метода get вы должны передать позицию индекса желаемого столбца, которую вы можете получить, вызвав getColumnIndex() или getColumnIndexOrThrow() . Завершив перебор результатов, вызовите функцию close() для курсора, чтобы освободить его ресурсы. Например, ниже показано, как получить все идентификаторы элементов, хранящиеся в курсоре, и добавить их в список:

Котлин

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

Ява

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

Удалить информацию из базы данных

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

Котлин

// 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)

Ява

// 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() .

Котлин

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)

Ява

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() вызывающего действия.

Котлин

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

Ява

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

Отладка вашей базы данных

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