SQLiteOpenHelper і SimpleCursorAdapter, отримання даних із SQLite

В попередній темі було розглянуто, як підключатися до бази даних SQLite та виконувати запити. Тепер рухаємося далі і створимо повністю інтерфейс для роботи з базою даних.

Отже, створимо новий проєкт.

Для спрощення роботи з базами даних SQLite в Android часто використовується клас SQLiteOpenHelper. Для використання необхідно створити клас-наслідник від SQLiteOpenHelper, перевизначивши як мінімум два його методи:

  • onCreate(): викликається при спробі доступу до бази даних, але коли ще ця база даних не створена
  • onUpgrade(): викликається, коли необхідне оновлення схеми бази даних. Тут можна пересоздати раніше створену базу даних в onCreate(), встановивши відповідні правила перетворення від старої БД до нової

Тому додамо в проєкт, у ту ж папку, де знаходиться клас MainActivity, новий клас DatabaseHelper:

package com.example.sqliteapp;
 
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import android.content.Context;
 
public class DatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "userstore.db"; // назва бд
    private static final int SCHEMA = 1; // версія бази даних
    static final String TABLE = "users"; // назва таблиці в бд
    // назви стовпців
    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_NAME = "name";
    public static final String COLUMN_YEAR = "year";
 
    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, SCHEMA);
    }
 
    @Override
    public void onCreate(SQLiteDatabase db) {
 
        db.execSQL("CREATE TABLE users (" + COLUMN_ID
                + " INTEGER PRIMARY KEY AUTOINCREMENT," + COLUMN_NAME
                + " TEXT, " + COLUMN_YEAR + " INTEGER);");
        // додавання початкових даних
        db.execSQL("INSERT INTO "+ TABLE +" (" + COLUMN_NAME
                + ", " + COLUMN_YEAR  + ") VALUES ('Том Смит', 1981);");
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion,  int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE);
        onCreate(db);
    }
}

Якщо база даних відсутня або її версія (яка задається в змінній SCHEMA) вища за поточну, то спрацьовує метод onCreate().

Для виконання запитів до бази даних нам знадобиться об'єкт SQLiteDatabase, який представляє базу даних. Метод onCreate() отримує як параметр базу даних додатку.

Для виконання запитів до SQLite використовується метод execSQL(). Він приймає SQL-вираз CREATE TABLE, який створює таблицю. Тут також, при необхідності, ми можемо виконати й інші запити, наприклад, додати якісь початкові дані. Так, у цьому випадку за допомогою того ж методу та виразу SQL INSERT додається один об'єкт у таблицю.

У методі onUpgrade() відбувається оновлення схеми БД. У цьому випадку для прикладу використано примітивний підхід з видаленням попередньої бази даних за допомогою SQL-виразу DROP та наступним її створенням. Але в реальності, якщо вам буде необхідно зберегти дані, цей метод може включати складнішу логіку - додавання нових стовпців, видалення непотрібних, додавання додаткових даних тощо.

Далі визначимо у файлі activity_main.xml таку розмітку:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">
 
    <TextView
        android:id="@+id/header"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        app:layout_constraintBottom_toTopOf="@+id/list"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />
    <ListView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@+id/header"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
 
</androidx.constraintlayout.widget.ConstraintLayout>

Тут визначено список ListView, для відображення отриманих даних, із заголовком, який виводитиме число отриманих об'єктів.

І змінимо код класу MainActivity таким чином:

package com.example.sqliteapp;
 
import androidx.appcompat.app.AppCompatActivity;
import android.widget.SimpleCursorAdapter;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
 
    ListView userList;
    TextView header;
    DatabaseHelper databaseHelper;
    SQLiteDatabase db;
    Cursor userCursor;
    SimpleCursorAdapter userAdapter;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        header = findViewById(R.id.header);
        userList = findViewById(R.id.list);
 
        databaseHelper = new DatabaseHelper(getApplicationContext());
    }
    @Override
    public void onResume() {
        super.onResume();
        // відкриваємо підключення
        db = databaseHelper.getReadableDatabase();
 
        // отримуємо дані з бд у вигляді курсора
        userCursor =  db.rawQuery("select * from "+ DatabaseHelper.TABLE, null);
        // визначаємо, які стовпці з курсору будуть виводитися в ListView
        String[] headers = new String[] {DatabaseHelper.COLUMN_NAME, DatabaseHelper.COLUMN_YEAR};
        // створюємо адаптер, передаємо в нього курсор
        userAdapter = new SimpleCursorAdapter(this, android.R.layout.two_line_list_item,
                userCursor, headers, new int[] {android.R.id.text1, android.R.id.text2}, 0);
        header.setText("Знайдено елементів: " +  userCursor.getCount());
        userList.setAdapter(userAdapter);
    }
 
    @Override
    public void onDestroy(){
        super.onDestroy();
        // Закриваємо підключення і курсор
        db.close();
        userCursor.close();
    }
}

В методі onCreate() відбувається створення об'єкта SQLiteOpenHelper. Ініціалізація об'єктів для роботи з базою даних відбувається в методі onResume(), який спрацьовує після методу onCreate().

Щоб отримати об'єкт бази даних, треба використовувати метод getReadableDatabase() (отримання бази даних для читання) або getWritableDatabase(). Оскільки в даному випадку ми будемо лише зчитувати дані з БД, то скористаємося першим методом:

db = sqlHelper.getReadableDatabase();

Отримання даних і Cursor

Android надає різні способи для здійснення запитів до об'єкта SQLiteDatabase. У більшості випадків ми можемо використовувати метод rawQuery(), який приймає два параметри: SQL-вираз SELECT та додатковий параметр, що задає параметри запиту.

Після виконання запиту rawQuery() повертає об'єкт Cursor, який зберігає результат виконання SQL-запиту:

userCursor =  db.rawQuery("select * from "+ DatabaseHelper.TABLE, null);

Клас Cursor пропонує ряд методів для керування вибіркою, зокрема:

  • getCount(): отримує кількість витягнутих з бази даних об'єктів
  • Методи moveToFirst() і moveToNext() дозволяють переходити до першого та наступного елементів вибірки. Метод isAfterLast() дозволяє перевірити, чи досягнуто кінець вибірки.
  • Методи get*(columnIndex) (наприклад, getLong(), getString()) дозволяють по індексу стовпця звертатися до даного стовпця поточного рядка.

CursorAdapter

Додатково для керування курсором в Android є клас CursorAdapter. Він дозволяє адаптувати отриманий за допомогою курсора набір до відображення в спискових елементах на зразок ListView. Як правило, при роботі з курсором використовується підклас CursorAdapterSimpleCursorAdapter. Хоча можна використовувати й інші адаптери, наприклад, ArrayAdapter.

userAdapter = new SimpleCursorAdapter(this, android.R.layout.two_line_list_item,
                userCursor, headers, new int[]{android.R.id.text1, android.R.id.text2}, 0);
userList.setAdapter(userAdapter);

Конструктор класу SimpleCursorAdapter приймає шість параметрів:

  1. Першим параметром виступає контекст, з яким асоціюється адаптер, наприклад, поточна activity.
  2. Другий параметр — ресурс розмітки інтерфейсу, який буде використовуватися для відображення результатів вибірки.
  3. Третій параметр — курсор.
  4. Четвертий параметр — список стовпців з вибірки, які будуть відображатися в розмітці інтерфейсу.
  5. П'ятий параметр — елементи всередині ресурсу розмітки, які будуть відображати значення стовпців з четвертого параметра.
  6. Шостий параметр — флаги, що задають поведінку адаптера.

При використанні CursorAdapter і його підкласів слід враховувати, що вибірка курсора повинна включати цілочисельний стовпець з назвою _id, який має бути унікальним для кожного елемента вибірки. Значення цього стовпця при натисканні на елемент списку потім передається в метод обробки onListItemClick(), завдяки чому ми можемо по id ідентифікувати натиснутий елемент.

У даному випадку у нас перший стовпець якраз називається "_id".

Після завершення роботи з курсором він має бути закритий методом close().

Також треба враховувати, що якщо ми використовуємо курсор у SimpleCursorAdapter, то не можемо використовувати метод close(), поки не завершим використання SimpleCursorAdapter. Тому метод cursor більш доцільно викликати в методі onDestroy() фрагмента або activity.

І якщо ми запустимо додаток, то побачимо список з одного доданого елемента: