Zapisz dane przy użyciu SQLite

Zapisywanie danych w bazie danych jest idealne w przypadku danych powtarzających się lub uporządkowanych, takich jak informacje kontaktowe. Na tej stronie zakładamy, że znasz bazy danych SQL, i pomagamy Ci rozpocząć pracę z bazami danych SQLite na Androidzie. Interfejsy API potrzebne do korzystania z bazy danych na Androidzie są dostępne w pakiecie android.database.sqlite.

Uwaga: te interfejsy API są zaawansowane, ale mają dość niski poziom dostępu i wymagają sporo czasu i wysiłku:

  • Nie ma weryfikacji nieprzetworzonych zapytań SQL w czasie kompilacji. Gdy wykres danych się zmienia, musisz ręcznie zaktualizować zapytania SQL, których dotyczy problem. Ten proces może być czasochłonny i podatny na błędy.
  • Aby dokonać konwersji między zapytaniami SQL a obiektami danych, musisz używać dużego stałego kodu.

Z tego powodu zdecydowanie zalecamy korzystanie z biblioteki trwałości Room jako warstwy abstrakcji do uzyskiwania dostępu do informacji w bazach danych SQLite aplikacji.

Definiowanie schematu i umowy

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

Klasa kontraktu to kontener dla stałych wartości, które definiują nazwy identyfikatorów URI, tabel i kolumn. Klasa kontraktowa pozwala używać tych samych stałych we wszystkich innych klasach w tym samym pakiecie. Dzięki temu możesz zmienić nazwę kolumny w jednym miejscu, a zmianę tę rozpropagować w całym kodzie.

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

Uwaga: dzięki implementacji interfejsu BaseColumns klasa wewnętrzna może dziedziczyć podstawowe pole klucza o nazwie _ID, którego oczekują niektóre klasy Androida, np. CursorAdapter. Nie jest to wymagane, ale może pomóc w harmonicznej współpracy bazy danych z platformą Androida.

Na przykład poniższy kontrakt definiuje 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ą narzędzia pomocniczego SQL

Po zdefiniowaniu bazy danych należy wdrożyć metody tworzenia i utrzymywania bazy danych oraz tabel. Oto kilka typowych instrukcji tworzenia i usuwania tabeli:

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;

Podobnie jak w przypadku plików zapisywanych na wewnętrznej pamięci urządzenia, Android przechowuje bazę danych w prywatnym folderze aplikacji. Twoje dane są bezpieczne, ponieważ domyślnie ten obszar nie jest dostępny dla innych aplikacji ani dla użytkownika.

Klasa SQLiteOpenHelper zawiera przydatny zestaw interfejsów API do zarządzania bazą danych. Jeśli używasz tej klasy do uzyskania odwołań do bazy danych, system wykonuje potencjalnie długotrwałe operacje tworzenia i aktualizowania bazy danych tylko wtedy, gdy jest to konieczne, a nie podczas uruchamiania aplikacji. Wystarczy zadzwonić pod numer getWritableDatabase() lub getReadableDatabase().

Uwaga: ponieważ mogą one trwać długo, zadbaj o to, aby wywołać funkcję getWritableDatabase() lub getReadableDatabase() w wątku tła. Więcej informacji znajdziesz w artykule Threading na urządzeniach z Androidem.

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

Oto przykład implementacji SQLiteOpenHelper, która używa niektórych poleceń wymienionych powyżej:

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

Przekazywanie informacji do bazy 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 funkcji insert() jest po prostu nazwa tabeli.

Drugi argument określa, co ma się stać, gdy argument ContentValues jest pusty (czyli nie zostały podane żadne wartości). Jeśli określisz nazwę kolumny, framework wstawi wiersz i ustawi wartość tej kolumny na null. Jeśli określisz wartość null, jak w tym przykładzie kodu, framework nie wstawia wiersza, gdy nie ma żadnych wartości.

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

Odczytywanie informacji z bazy danych

Aby odczytać dane z bazy danych, użyj metody query(), przekazując jej kryteria wyboru i wybrane kolumny. Metoda łączy elementy metod insert()update(), z tym że lista kolumn określa dane, które chcesz pobrać („projekcję”), a nie dane do wstawienia. 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 (selectionselectionArgs) są łączone w ramach klauzuli WHERE. Ponieważ argumenty są podawane niezależnie od zapytania wyboru, przed połączeniem za ich pomocą zmienia się znaczenie ich znaczenia. Dzięki temu instrukcje wyboru są odporne na wstrzyknięcia kodu SQL. Więcej informacji o wszystkich argumentach znajdziesz w dokumentacji query().

Aby wyświetlić wiersz w kursorze, użyj jednej z metod Cursor move, którą musisz zawsze wywołać przed odczytaniem wartości. Kursor zaczyna się w pozycji -1, dlatego wywołanie funkcji moveToNext() umieszcza „pozycję odczytu” na pierwszym wpisie wyników i zwraca, czy kursor znajduje się już po ostatniej pozycji w zestawie wyników. W przypadku każdego wiersza możesz odczytać wartość kolumny, wywołując jedną z metod metody Cursor get, na przykład getString() lub getLong(). W przypadku każdej z metod get musisz podać pozycję indeksu kolumny, którą możesz uzyskać, wywołując metodę getColumnIndex() lub getColumnIndexOrThrow(). Po zakończeniu iteracji z wynikami wywołaj na kursorze funkcję close(), by zwolnić jej zasoby. Na przykład poniżej pokazujemy, jak pobrać wszystkie identyfikatory elementó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ć kryteria wyboru, które identyfikują wiersze za pomocą metody delete(). Mechanizm działa tak samo jak argumenty wyboru w metodzie query(). Dzieli specyfikację wyboru na klauzulę wyboru i argumenty wyboru. Klauzula definiuje kolumny, które ma być analizowane, oraz umożliwia łączenie testów kolumn. Argumenty to wartości powiązane z klauzulą, względem których sprawdzany jest test. Wynik nie jest obsługiwany w taki sam sposób jak zwykła instrukcja SQL, więc 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ść zwrócona przez metodę delete() wskazuje liczbę wierszy usuniętych z bazy danych.

Aktualizowanie bazy danych

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

Aktualizacja tabeli łączy składnię ContentValues z insert() i składnię WHERE z 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ścią zwracaną metody update() jest liczba wierszy w bazie danych, na które wpłynęła zmiana.

Trwałe połączenie z bazą danych

Wywołanie funkcji getWritableDatabase() i getReadableDatabase() po zamknięciu bazy danych jest drogie, dlatego nie wyłączaj połączenia z bazą danych tak długo, jak to możliwe. Zwykle najlepiej jest zamknąć bazę danych w 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 umożliwia przeglądanie zawartości tabeli, uruchamianie poleceń SQL i wykonywanie innych przydatnych funkcji w bazach danych SQLite. Więcej informacji znajdziesz w artykule o wydawaniu poleceń w powłoce.