Daten mit SQLite speichern

Das Speichern von Daten in einer Datenbank eignet sich ideal für sich wiederholende oder strukturierte Daten wie Kontaktdaten. Auf dieser Seite wird davon ausgegangen, dass Sie mit SQL-Datenbanken im Allgemeinen vertraut sind, und Ihnen die ersten Schritte mit SQLite-Datenbanken unter Android erleichtern. Die APIs, die Sie zum Verwenden einer Datenbank unter Android benötigen, sind im Paket android.database.sqlite verfügbar.

Achtung: Diese APIs sind zwar leistungsstark, aber relativ niedrigschwellig und erfordern viel Zeit und Mühe:

  • Es gibt keine kompilierungszeitabhängige Überprüfung von Roh-SQL-Abfragen. Wenn sich Ihr Datendiagramm ändert, müssen Sie die betroffenen SQL-Abfragen manuell aktualisieren. Dieser Vorgang kann zeitaufwendig und fehleranfällig sein.
  • Sie müssen viel Boilerplate-Code verwenden, um zwischen SQL-Abfragen und Datenobjekten zu konvertieren.

Aus diesen Gründen empfehlen wir dringend, die Raumpersistenzbibliothek als Abstraktionsebene für den Zugriff auf Informationen in den SQLite-Datenbanken Ihrer Anwendung zu verwenden.

Schema und Vertrag definieren

Eines der Hauptprinzipien von SQL-Datenbanken ist das Schema: eine formale Erklärung dazu, wie die Datenbank organisiert ist. Das Schema wird in den SQL-Anweisungen widergespiegelt, mit denen Sie Ihre Datenbank erstellen. Es kann hilfreich sein, eine Begleitklasse zu erstellen, die als Vertragsklasse bezeichnet wird und das Layout Ihres Schemas auf systematische und selbstdokumentierende Weise explizit angibt.

Eine Vertragsklasse ist ein Container für Konstanten, die Namen für URIs, Tabellen und Spalten definieren. Mit der Vertragsklasse können Sie dieselben Konstanten für alle anderen Klassen im selben Paket verwenden. So können Sie einen Spaltennamen an einer Stelle ändern und die Änderung wird in Ihrem gesamten Code übernommen.

Eine gute Möglichkeit zum Organisieren einer Vertragsklasse besteht darin, globale Definitionen für die gesamte Datenbank auf der Stammebene der Klasse zu platzieren. Erstellen Sie dann für jede Tabelle eine innere Klasse. Jede innere Klasse zählt die Spalten der entsprechenden Tabelle auf.

Hinweis:Durch die Implementierung der BaseColumns-Schnittstelle kann die innere Klasse ein Primärschlüsselfeld namens _ID übernehmen, das von einigen Android-Klassen wie CursorAdapter erwartet wird. Dies ist nicht erforderlich, kann aber dazu beitragen, dass Ihre Datenbank reibungslos mit dem Android-Framework funktioniert.

Im folgenden Vertrag werden beispielsweise der Tabellenname und die Spaltennamen für eine einzelne Tabelle definiert, die einen RSS-Feed darstellt:

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

Datenbank mit einem SQL-Hilfsprogramm erstellen

Nachdem Sie die Struktur Ihrer Datenbank definiert haben, sollten Sie Methoden implementieren, mit denen die Datenbank und die Tabellen erstellt und verwaltet werden. Hier sind einige typische Anweisungen zum Erstellen und Löschen einer Tabelle:

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;

Genau wie Dateien, die Sie im internen Speicher des Geräts speichern, speichert Android Ihre Datenbank im privaten Ordner Ihrer App. Ihre Daten sind sicher, da dieser Bereich standardmäßig nicht für andere Anwendungen oder den Nutzer zugänglich ist.

Die SQLiteOpenHelper-Klasse enthält eine Reihe nützlicher APIs zum Verwalten Ihrer Datenbank. Wenn Sie diese Klasse verwenden, um Verweise auf Ihre Datenbank abzurufen, führt das System die potenziell langwierigen Vorgänge zum Erstellen und Aktualisieren der Datenbank nur bei Bedarf und nicht beim Starten der App aus. Rufen Sie einfach getWritableDatabase() oder getReadableDatabase() auf.

Hinweis: Da sie lange andauern können, sollten Sie getWritableDatabase() oder getReadableDatabase() in einem Hintergrundthread aufrufen. Weitere Informationen finden Sie unter Unterhaltungen auf Android-Geräten.

Erstellen Sie zur Verwendung von SQLiteOpenHelper eine abgeleitete Klasse, die die Callback-Methoden onCreate() und onUpgrade() überschreibt. Sie können auch die Methoden onDowngrade() oder onOpen() implementieren. Das ist jedoch nicht erforderlich.

Hier ist beispielsweise eine Implementierung von SQLiteOpenHelper, die einige der oben genannten Befehle verwendet:

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

Instanziieren Sie Ihre abgeleitete Klasse von SQLiteOpenHelper, um auf Ihre Datenbank zuzugreifen:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Informationen in einer Datenbank speichern

Fügen Sie Daten in die Datenbank ein, indem Sie ein ContentValues-Objekt an die Methode insert() übergeben:

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

Das erste Argument für insert() ist einfach der Tabellenname.

Das zweite Argument teilt dem Framework mit, was zu tun ist, wenn ContentValues leer ist (d.h., Sie haben keine Werte für put angegeben). Wenn Sie den Namen einer Spalte angeben, fügt das Framework eine Zeile ein und setzt den Wert dieser Spalte auf null. Wenn Sie null wie in diesem Codebeispiel angeben, fügt das Framework keine Zeile ein, wenn keine Werte vorhanden sind.

Die Methode insert() gibt die ID für die neu erstellte Zeile zurück. Wenn beim Einfügen der Daten ein Fehler aufgetreten ist, wird -1 zurückgegeben. Dies kann passieren, wenn es zu einem Konflikt mit bereits vorhandenen Daten in der Datenbank kommt.

Informationen aus einer Datenbank lesen

Verwenden Sie die Methode query(), um Daten aus einer Datenbank zu lesen. Geben Sie dabei Ihre Auswahlkriterien und die gewünschten Spalten an. Die Methode kombiniert Elemente von insert() und update(), außer dass in der Spaltenliste nicht die einzufügenden Daten, sondern die abzurufenden Daten (die "Projektion") definiert werden. Die Ergebnisse der Abfrage werden in einem Cursor-Objekt zurückgegeben.

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

Das dritte und vierte Argument (selection und selectionArgs) werden kombiniert, um eine WHERE-Klausel zu erstellen. Da die Argumente getrennt von der Auswahlabfrage bereitgestellt werden, werden sie vor der Kombination mit Escapezeichen maskiert. Dadurch sind Ihre Auswahlanweisungen vor SQL-Injection geschützt. Weitere Informationen zu allen Argumenten finden Sie in der query()-Referenz.

Wenn Sie sich eine Zeile im Cursor ansehen möchten, verwenden Sie eine der Cursor-Verschiebungsmethoden, die Sie immer aufrufen müssen, bevor Sie mit dem Lesen von Werten beginnen. Da der Cursor bei Position -1 beginnt, wird durch den Aufruf von moveToNext() die „Leseposition“ auf den ersten Eintrag in den Ergebnissen gesetzt und zurückgegeben, ob der Cursor bereits den letzten Eintrag im Ergebnissatz überschritten hat. Sie können den Wert einer Spalte für jede Zeile lesen, indem Sie eine der Cursor-Get-Methoden wie getString() oder getLong() aufrufen. Für jede der get-Methoden müssen Sie die Indexposition der gewünschten Spalte übergeben. Diese können Sie durch Aufrufen von getColumnIndex() oder getColumnIndexOrThrow() abrufen. Wenn Sie mit dem Iterieren der Ergebnisse fertig sind, rufen Sie close() am Cursor auf, um die zugehörigen Ressourcen freizugeben. Im folgenden Beispiel wird beispielsweise gezeigt, wie Sie alle in einem Cursor gespeicherten Artikel-IDs abrufen und einer Liste hinzufügen:

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

Informationen aus einer Datenbank löschen

Wenn Sie Zeilen aus einer Tabelle löschen möchten, müssen Sie für die Methode delete() Auswahlkriterien angeben, die die Zeilen identifizieren. Der Mechanismus funktioniert genauso wie die Auswahlargumente für die Methode query(). Dabei wird die Auswahlspezifikation in eine Auswahlklausel und Auswahlargumente unterteilt. Die Klausel definiert die zu prüfenden Spalten und ermöglicht Ihnen außerdem, Spaltentests zu kombinieren. Die Argumente sind Werte, mit denen getestet werden soll, die an die Klausel gebunden sind. Da das Ergebnis nicht wie eine reguläre SQL-Anweisung verarbeitet wird, ist es gegen eine SQL-Injection geschützt.

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

Der Rückgabewert der Methode delete() gibt die Anzahl der Zeilen an, die aus der Datenbank gelöscht wurden.

Datenbank aktualisieren

Wenn Sie einen Teil Ihrer Datenbankwerte ändern müssen, verwenden Sie die Methode update().

Beim Aktualisieren der Tabelle wird die ContentValues-Syntax von insert() mit der WHERE-Syntax von delete() kombiniert.

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

Der Rückgabewert der Methode update() ist die Anzahl der betroffenen Zeilen in der Datenbank.

Datenbankverbindung wird bestehen bleiben

Da der Aufruf von getWritableDatabase() und getReadableDatabase() beim Schließen der Datenbank teuer ist, sollten Sie die Datenbankverbindung so lange offen lassen, wie Sie möglicherweise darauf zugreifen müssen. In der Regel ist es optimal, die Datenbank im onDestroy() der aufrufenden Aktivität zu schließen.

Kotlin

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

Java

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

Datenbank debuggen

Das Android SDK enthält ein sqlite3-Shell-Tool, mit dem Sie Tabelleninhalte durchsuchen, SQL-Befehle ausführen und andere nützliche Funktionen in SQLite-Datenbanken ausführen können. Weitere Informationen finden Sie unter Shell-Befehle ausführen.