5.12. Использование базы данных SQLite.

5.12. Использование базы данных SQLite.

В ОС Андроид есть возможность использовать встроенный драйвер для баз данных SQLite. Каждая база данных SQLite обычно сохраняется в папке database после установки приложения. Ограничений по количеству используемых баз данных со стороны приложения нет, но обычно достаточно использование одной базы данных. 

В следующем примере разрабатывается приложение – справочник. Задача приложения выводить список и по клику на список открыть детальную информацию об элементе списка. Также предоставить возможность добавления новых элементов в базу данных. Рассматриваются, фрагменты, ListView, Floating Action Button, адаптер и DialogFragment. Если во время написания кода возникнут ошибки и Android Studio предложит нажать Alt + Enter, нажмите.

Как было сделано в одном из прошлых уроков добавьте новый Vector Asset в папку drawable ic_add (задайте белый цвет), ic_chevron_right (цвет черный), ic_delete_forever (цвет черный).

 

Добавьте класс под названием DBItem и напишите следующий код:

// Класс для хранения объектов запрошенных с базы данных структура класса соответствует структуре необходимых нам данных
public class DBItem {
int id;
String title;
String content;

// Пустой конструктор служит для инициализации экземпляров этого класса в других классах без первоначальных значений
public DBItem() {}

// Конструктор принимающий два значения служит для заполнения только id и заголовка элементов в этом примере этот конструктор используется в запросе функции getTitles() класса Database
public DBItem(int id, String title) {
this.id = id;
this.title = title;
}

// Конструктор принимающий три значений используется для инициализации переменной для получения данных по id через функцию getItemById, который возвращает значения строк1 для столбцов значения id, которых соответствует запрошенному id
public DBItem(int id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
}

Теперь необходимо создать класс для работы с базой данных. Добавьте ещё один класс и измените его код как в коде ниже:

// Класс помощник для работы с базой данных
public class Database extends SQLiteOpenHelper {

// Версия схемы базы данных. Под схемой понимается понятие количество и порядка столбцов базы данных. При изменении количества, названия или типа хранимой информации столбцов1 необходимо увеличить это значение и написать код применяющий изменения в уже существующие таблицы в onUpgrade
final static int VERSION = 1;

// Название базы данных. Название можно не писать здесь, а передавать через аргументы конструктора, если в вашем приложении используется несколько баз данных, но в этом примере название базы данных пишется в этом классе так, как используется только одна база данных для хранения информации
final static String DB_NAME = "info_sqlite";

// Переменная для хранения ссылки на контекст инициализирующий экземпляр класса
Context c;

// Конструктор класса для инициализации базы данных с соответствующей версией
public Database(Context context) {
// Вызов конструктора суперкласса (SQLiteOpenHelper) с передачей контекста, названия базы данных и версии
super(context, DB_NAME, null, VERSION);
// Присвоить переменной для хранения контекста значение
c = context;
}

// Функция вызывается во время устаноки приложения. Все необходимые для первой работы приложения действия надо прописать здесь. В нашем случае эта функция используется для создания таблицы в базе данных для хранения информации о странах
@Override
public void onCreate(SQLiteDatabase db) {
// Запрос на создание новой таблицы с 4 столбцами у каждого столбца как и у каждой переменной есть имя и тип, сначала указывается название столбца, затем тип хранимой информации длина хранимой информации указывается внутри скобок после названия типа: int - целочисленный тип, int(11) - для хранения целого числа длиной максимум 11 цифр. Например: 123647975621. Команды отправляемые к SQLite называются запросами и запрос CREATE TABLE служит для создания таблицы с указанным названием и столбцами в базе данных
db.execSQL("CREATE TABLE info (id INTEGER PRIMARY KEY AUTOINCREMENT, title varchar(250), search_field varchar(250), content text);");

// Запрос INSERT служит для добавления записей в базу данных. Формат написания: INSERT INTO название_таблицы VALUES(значения для столбцов таблицы). Количество и тип значений внутри скобок VALUES должны соответствовать столбцам базы данных и значение для каждого столбца должно быть разделено запятой друг от друга. Если после названия таблицы в скобках не указываются столбцы для которых следует добавить данные необходимо указать данные для всех столбцов по порядку их создания. В случае указания названия столбцов после названия таблицы надо указывать значения с соблюдением порядка и типа данных столбцов указываемых внутри скобок после названия таблицы, например INSERT INTO info(title, content) VALUES ('My sample title', 'Content for my title');
// При добавлении текстовых значений их следует указывать внутри 'одинарных кавычек'. Если в вашем тексте есть одинарные кавычки тогда их нужно экранировать путём добавления ещё одной одинарной кавычки перед существующей кавычкой, например 'My title's content', следует изменить на 'My title''s content'. При добавления текста из кода самый удобный вариант экранирования это функция replaceAll класса String
// mytitle = "It's my life";
// mytitle.replaceAll("'", "''"); - в данном случае mytitle равен It''s my life
db.execSQL("INSERT INTO info VALUES(1, 'Тоҷикистон', 'тоҷикистон', \"Маълумот дар бораи Тоҷикистон\")");
db.execSQL("INSERT INTO info VALUES(2, 'Ӯзбекистон', 'ӯзбекистон', \"Маълумот дар бораи Ӯзбекистон\")");
db.execSQL("INSERT INTO info VALUES(3, 'Қирғизистон', 'қирғизистон', \"Маълумот дар бораи Қирғизистон\")");
db.execSQL("INSERT INTO info VALUES(4, 'Қазоқистон', 'қазоқистон', \"Маълумот дар бораи Қазоқистон\")");
db.execSQL("INSERT INTO info VALUES(5, 'Афғонистон', 'афғонистон', \"Маълумот дар бораи Афғонистон\")");
}

// Фукнция вызывается при обновлении существующего приложения
// Код для изменения данных и структуры данных при обновлении приложения должен быть записан тут
@Override
public void onUpgrade(SQLiteDatabase db, int o, int n) {

}

// Функция для получения заголовков для показа в ListView в MasterFragment возвращает List с данными класса DBItem, который используем в качестве объекта с необходимой для нас структурой, который удобен при передаче между функциями, так как в одном объекте мы храним сразу три переменные двух типов
public List<DBItem> getTitles() {
// Объявление пустого массива
List<DBItem> items = new ArrayList<>();
// Получение ссылки к базе данных в режиме только чтение
SQLiteDatabase db = getReadableDatabase();

try {
    // Объявить экземпляр класса Cursor и инициализировать с результатами запроса для получения всех данных
    // Запрос SELECT служит для получения информации хранящейся в базе данных. После SELECT пишутся названия необходимых столбцов через запятую или пишется * для получения всех столбцов, затем FROM название_таблицы
    Cursor c = db.rawQuery("SELECT id, title FROM info", new String[]{});

    // Cursor по умолчанию ставится в конец полученных записей и необходимо проверить и заодно переместить курсор в начало коллекции делается это через функцию moveToFirst() который возвращает true, если в коллекции запрошенных данных есть записи
    if (c.moveToFirst()) {
        // Цикл do while нужен для перебора всех запрошенных данных по очереди
        do {
            // Добавление текущего объекта полученного в ходе запроса в массив
            items.add(new DBItem(c.getInt(0), c.getString(1)));
            // while указывает на условие и пока moveToNext() возвращает true, цикл продолжает выполнение, когда cursor в запрошенной коллекции достигает конца moveToNext() возвращает fale и цикл заканчивается
        } while (c.moveToNext());
    }
    // Перехват исключений для предотвращения закрытия приложения в случае сбоя
} catch (Exception e) {
    // Вывести в консоль LogCat детали ошибки
    e.printStackTrace();
}
// Вернуть массив items в которой хранятся результаты запроса всех заголовков
return items;
}

// Принцип работы нижеследующих функций аналогичен функции getTitles
public DBItem getItemById(int id) {
DBItem item = new DBItem();
SQLiteDatabase db = getReadableDatabase();
try {
    Cursor c = db.rawQuery("SELECT * FROM info WHERE id = ?", new String[]{String.valueOf(id)});
    if (c.moveToFirst()) {
        do {
            item = new DBItem(c.getInt(0), c.getString(1), c.getString(3));
        } while (c.moveToNext());
    }
} catch (Exception e) {
    e.printStackTrace();
}

return item;
}

// Функция для поиска информации и возвращения результатов в виде массива List с экземплярами элементова класса DBItem
// Получает запрос для поиска в виде строки и проверяет значения в столбце search_field на совпадение и выбирает все значения в которых содержится искомый текст
public List<DBItem> findItems(String s) {
// Пустой массив элементов для хранения результатов поиска и вовзращения в конце функции через инструкцию return
List<DBItem> items = new ArrayList<>();
// Получить ссылку к базе данных в режиме только чтение
SQLiteDatabase db = getReadableDatabase();

try {
    // Сформировать и выполнить запрос к базе данных для поиска необходимой информации и присвоение результатов запроса объекту экземпляра класса Cursor like в запросах SQL используется для получения результатов в которых совпадает весь текст или только часть искомого текста. В какой части искать совпадение зависит от расположения символов %, если символы расположены с обеих сторон искомой строки то выбираются записи в которых встречается поисковой запрос независимо от того в начале, в середине или в конце записи столбца встречается запись
    Cursor c = db.rawQuery("SELECT * FROM info WHERE search_field like ?", new String[]{"%" + s.toLowerCase() + "%"});

    // Метод moveToFirst() класса Cursor переводит курсор к первой записи и возвращает true, если записей нет возвращает false
    if (c.moveToFirst()) {
        // При выполнении данной строки значит хотя бы одна запись в базе данных есть, поэтому можно взять её значения, добавить в массив и затем перейти к следующей записи если следующей записи нет moveToNext() возращает false и цикл прерывается и код выполняется со следующей строки после цикла while
        do {
            items.add(new DBItem(c.getInt(0), c.getString(1), c.getString(3)));
        } while (c.moveToNext());
    }
} catch (Exception e) {
    e.printStackTrace();
}

// Вернуть заполненный массив в случае нахождения искомых записей или вернуть пустой массив если записей нет, потому что если записей нет, в строке c.moveToFirst() возвращает false и код написанный внутри тела if не выполняется
return items;
}

// Функция addItem служит для добавления новых записей в базу данных в качестве параметра источника информации для добавления передаётся объект класса DBItem в котором хранятся заголовок и содержимое для новой записи
public boolean addItem(DBItem item) {
// В отличии от других функций тут получаем экземпляр базы данных, в которой можно добавлять новые данные.
SQLiteDatabase db = getWritableDatabase();
try {
    // Для добавления новых записей служит запрос Insert, подробней об Insert написано в функции onCreate в этом файле. Параметр id, явно не вставляем, потому что в этой версии базы данных столбец id указан как PRIMARY KEY с возможностью AUTOINCREMENT, что означает столбец id служит для хранения уникальных значений, которые увеличиваются автоматически на единицу при добавлении новых записей
    Log.i("TAG", "Add items");
    db.execSQL("INSERT INTO info(title, content) VALUES(?,?)", new String[]{item.title, item.content});
    // В случае выполнения запроса без ошибок, вернуть true это необходимо для уведомления пользователя о том, что запись добавлена или нет
    return true;
} catch (Exception e) {
    // Если возникла какая-нибудь ошибка блок catch перехватывает её и в конце возвращаем false, чтобы уведомить пользователя о том, что возникла ошибка и запись добавить не получилось текст ошибки выводится через метод printStackTrace()
    e.printStackTrace();
    Log.i("TAG", "Error");
    return false;
}
}

// Удалить ненужную запись из списка
public boolean deleteItemById(int id) {
SQLiteDatabase db = getWritableDatabase();
try {
    db.execSQL("DELETE FROM info WHERE id = ?", new String[]{String.valueOf(id)});
    return true;
} catch (Exception e) {
    e.printStackTrace();
    return false;
}
}
}

Как знаете в каждом фрагменте или Activity мы обычно используем xml файл в качестве макета, все файлы макетов добавляются в папку res/layout. Для этого примера необходимо добавить ещё 4 файлов в папку layout. Нажмите правой кнопкой мыши на папку layout и выберите New -> Layout Resource File. Это файл макета для элементов списка. Укажите название adapter_item.xml, Root element – LinearLayout и после добавления файла измените его код таким образом:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">

<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableRight="@drawable/ic_chevron_right_black_24dp"
android:gravity="center_vertical"
android:text="Sample"
android:textColor="#006993"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>

Добавьте ещё один файл в папку layout с названием add_items.xml и Root element – LinearLayout Этот файл макет диалогового окна добавления новых записей и перед написанием кода необходимо открыть новый созданный файл, затем перейти в комбинированный режим, в котором видны и код и графическое отображением макета (средняя кнопка в верхнем правом углу в Android Studio 3.5 и выше и кнопка Design внизу слева в старых версиях Android Studio), нажать на кнопку Palette, в списке выбрать Text, в правой колонке нажать на значок "загрузки" справа от TextInputLayout.

После нажатия на кнопку "загрузка" нажмите "ОК" в окне запроса подключения библиотеки.
Пойдем, продолжение в следующем уроке....