Zapisz dane przy użyciu SQLite

Zapisywanie danych w bazie danych to idealne rozwiązanie w przypadku powtarzających się lub uporządkowanych danych, takich jak informacje kontaktowe. Na tej stronie zakładamy, że znasz ogólnie bazy danych SQL i pomagasz w rozpoczęciu korzystania z baz danych SQLite na Androidzie. Interfejsy API, których potrzebujesz do używania bazy danych na Androidzie, są dostępne w pakiecie android.database.sqlite.

Uwaga: chociaż te interfejsy API są bardzo wydajne, mają one stosunkowo niski poziom, a ich używanie wymaga dużo czasu i wysiłku:

  • Nie ma weryfikacji w czasie kompilowania zapytań SQL. Gdy wykres danych ulegnie zmianie, konieczne będzie ręczne aktualizowanie zapytań SQL, których dotyczy ten problem. Ten proces może być czasochłonny i podatny na błędy.
  • Do konwertowania między zapytaniami SQL a obiektami danych trzeba używać obszernego kodu.

Dlatego zdecydowanie zalecamy korzystanie z biblioteki trwałości sal jako warstwy abstrakcji umożliwiającej dostęp do informacji w bazach danych SQLite aplikacji.

Zdefiniuj schemat i umowę

Jedną z głównych zasad baz danych SQL jest schemat – formalna deklaracja organizacji bazy danych. Schemat jest odzwierciedlany w instrukcjach SQL, których używasz do tworzenia bazy danych. Warto utworzyć klasę towarzyszącą, tzw. klasę contract, która wyraźnie określa układ schematu w sposób systematyczny i samodokumentujący.

Klasa umowy to kontener na stałe, które definiują nazwy identyfikatorów URI, tabel i kolumn. Klasa umowy pozwala używać tych samych stałych w przypadku wszystkich innych klas w tym samym pakiecie. Dzięki temu możesz zmienić nazwę kolumny w jednym miejscu i rozpowszechnić ją w kodzie.

Dobrym sposobem na uporządkowanie klasy umowy jest umieszczenie definicji, które są globalne dla całej bazy danych, na poziomie głównym klasy. Następnie utwórz klasę wewnętrzną dla każdej tabeli. Każda klasa wewnętrzna wyświetla odpowiednie kolumny tabeli.

Uwaga: po wdrożeniu interfejsu BaseColumns klasa wewnętrzna może odziedziczyć pole klucza podstawowego o nazwie _ID, które powinny mieć niektóre klasy Androida, takie jak CursorAdapter. Nie jest to wymagane, ale może ułatwić współpracę bazy danych z platformą Androida.

Na przykład ta umowa określa nazwę tabeli i nazwy kolumn dla pojedynczej tabeli reprezentującej kanał 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";
    }
}

Tworzenie bazy danych za pomocą asystenta SQL

Po zdefiniowaniu wyglądu bazy danych należy wdrożyć metody, które umożliwią tworzenie i utrzymywanie bazy danych oraz tabel. Oto kilka typowych instrukcji tworzenia i usuwania tabel:

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 przechowuje bazę danych w prywatnym folderze aplikacji tak samo jak pliki zapisywane w pamięci wewnętrznej urządzenia. Twoje dane są bezpieczne, ponieważ domyślnie inne aplikacje i użytkownik nie mają dostępu do tego obszaru.

Klasa SQLiteOpenHelper zawiera przydatny zestaw interfejsów API umożliwiających zarządzanie bazą danych. Gdy używasz tej klasy do uzyskiwania odwołań do bazy danych, system wykonuje potencjalnie długotrwałe operacje tworzenia i aktualizowania bazy danych tylko wtedy, gdy jest to konieczne, nie podczas uruchamiania aplikacji. Wystarczy, że wywołasz działanie getWritableDatabase() lub getReadableDatabase().

Uwaga: ponieważ mogą one być długotrwałe, pamiętaj, by wywołać getWritableDatabase() lub getReadableDatabase() w wątku w tle. Więcej informacji znajdziesz w artykule na temat wątków na urządzeniach z Androidem.

Aby użyć funkcji SQLiteOpenHelper, utwórz podklasę, która zastąpi metody wywołania zwrotnego onCreate() i onUpgrade(). Możesz też wdrożyć metody onDowngrade() lub onOpen(), ale nie są one wymagane.

Oto przykładowa implementacja SQLiteOpenHelper wykorzystująca niektóre z powyższych poleceń:

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);
    }
}

Aby uzyskać dostęp do bazy danych, utwórz instancję podklasy SQLiteOpenHelper:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Umieszczanie informacji w bazie danych

Aby wstawić dane do bazy danych, przekaż obiekt ContentValues do metody 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);

Pierwszym argumentem insert() jest po prostu nazwa tabeli.

Drugi argument informuje platformę, co ma zrobić, gdy pole ContentValues jest puste (czyli nie ma żadnych wartości typu put). Jeśli podasz nazwę kolumny, platforma wstawi wiersz i ustawi wartość tej kolumny na null. Jeśli podasz null, tak jak w tym przykładowym kodzie, platforma nie wstawi wiersza, gdy nie będzie żadnych wartości.

Metody insert() zwracają identyfikator nowo utworzonego wiersza lub zwracają -1, jeśli podczas wstawiania danych wystąpił błąd. Może się tak zdarzyć w przypadku konfliktu z danymi znajdującymi się wcześniej w bazie danych.

Odczytywanie informacji z bazy danych

Aby odczytać dane z bazy danych, użyj metody query(), przekazując jej kryteria wyboru i odpowiednie kolumny. Ta metoda łączy elementy elementów insert() i update(), z wyjątkiem danych, które chcesz pobrać („projekcja”), a nie wstawianych przez listę kolumn. Wyniki zapytania są zwracane w obiekcie 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
    );

Trzeci i czwarty argument (selection i selectionArgs) są połączone, aby utworzyć klauzulę WHERE. Argumenty są podawane niezależnie od zapytania wyboru, więc przed połączeniem ich znaczenie jest zmieniane. Dzięki temu instrukcje wyboru są odporne na wstrzykiwanie SQL. Więcej informacji o wszystkich argumentach znajdziesz w dokumentacji query().

Aby wyświetlić wiersz w miejscu kursora, użyj jednej z metod przenoszenia Cursor, które musisz wywołać zawsze przed rozpoczęciem odczytywania wartości. Kursor zaczyna się w pozycji -1, więc wywołanie metody moveToNext() powoduje umieszczenie „pozycji odczytu” w pierwszym wpisie w wynikach i zwraca, czy kursor przesunął się już poza ostatnią pozycję w zestawie wyników. W przypadku każdego wiersza możesz odczytać wartość kolumny, wywołując jedną z metod get Cursor, np. getString() lub getLong(). W przypadku każdej z metod get musisz przekazać pozycję indeksu kolumny, którą chcesz uzyskać. Można ją uzyskać, wywołując metodę getColumnIndex() lub getColumnIndexOrThrow(). Po zakończeniu iteracji wyników wywołaj close() za pomocą kursora, aby zwolnić zasoby. Na przykład poniżej pokazano, jak uzyskać wszystkie identyfikatory produktów zapisane w kursie i dodać je do listy:

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();

Usuwanie informacji z bazy danych

Aby usunąć wiersze z tabeli, musisz podać w metodzie delete() kryteria wyboru, które identyfikują wiersze. Mechanizm działa tak samo jak argumenty wyboru w metodzie query(). Dzieli ona specyfikację wyboru na klauzulę wyboru i argumenty wyboru. Klauzula określa kolumny, które mają się sprawdzać, a także umożliwia łączenie testów kolumn. Argumenty to wartości powiązane z klauzulą w ramach testu. Wynik nie jest obsługiwany tak samo jak zwykła instrukcja SQL, dlatego jest odporny na wstrzykiwanie 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);

Wartość zwracana w przypadku metody delete() wskazuje liczbę wierszy usuniętych z bazy danych.

Zaktualizowanie bazy danych

Jeśli musisz zmodyfikować podzbiór wartości bazy danych, użyj metody update().

Aktualizowanie tabeli łączy składnię ContentValues klasy insert() ze składnią WHERE parametru 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);

Wartość zwracana metody update() to liczba wierszy, których dotyczy problem w bazie danych.

Trwałe połączenie z bazą danych

Wywoływanie metod getWritableDatabase() i getReadableDatabase() przy zamykaniu bazy danych jest kosztowne, dlatego pozostaw połączenie z bazą otwarte na tyle, na ile jest to możliwe. Zazwyczaj optymalne jest zamknięcie bazy danych w elemencie onDestroy() aktywności wywołującej.

Kotlin

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

Java

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

Debugowanie bazy danych

Pakiet Android SDK zawiera narzędzie powłoki sqlite3, które pozwala przeglądać zawartość tabeli, uruchamiać polecenia SQL i wykonywać inne przydatne funkcje na bazach danych SQLite. Więcej informacji znajdziesz w artykule o tworzeniu poleceń powłoki.