Daten mit SQLite speichern

Das Speichern von Daten in einer Datenbank ist ideal für wiederkehrende oder strukturierte Daten wie Kontaktdaten. Auf dieser Seite wird davon ausgegangen, dass Sie mit SQL-Datenbanken im Allgemeinen vertraut sind, und hilft Ihnen beim Einstieg in SQLite-Datenbanken unter Android. Die APIs, die Sie für die Verwendung einer Datenbank unter Android benötigen, sind im Paket android.database.sqlite verfügbar.

Achtung:Auch wenn diese APIs leistungsstark sind, sind sie eher niedrig und erfordern viel Zeit und Aufwand:

  • Es gibt keine Verifizierung während der Kompilierung von SQL-Rohabfragen. 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 wird dringend empfohlen, die Room Persistence Library 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 formelle Erklärung zur Organisation der Datenbank. Das Schema spiegelt sich in den SQL-Anweisungen wider, mit denen Sie Ihre Datenbank erstellen. Es kann hilfreich sein, eine Begleitklasse zu erstellen, die auch als Vertragsklasse bezeichnet wird. Diese Klasse legt das Layout Ihres Schemas systematisch und selbstdokumentierend explizit fest.

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. Auf diese Weise können Sie einen Spaltennamen an einer Stelle ändern und ihn im gesamten Code anwenden.

Eine gute Möglichkeit, eine Vertragsklasse zu organisieren, besteht darin, globale Definitionen für Ihre gesamte Datenbank auf der Stammebene der Klasse zu platzieren. Erstellen Sie dann für jede Tabelle eine innere Klasse. Jede innere Klasse listet die Spalten der entsprechenden Tabelle auf.

Hinweis:Durch Implementierung der Schnittstelle BaseColumns 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 dabei helfen, dass Ihre Datenbank harmonisch mit dem Android-Framework funktioniert.

Der folgende Vertrag definiert beispielsweise den Tabellennamen und die Spaltennamen für eine einzelne Tabelle, 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 das Erscheinungsbild Ihrer Datenbank definiert haben, sollten Sie Methoden zum Erstellen und Verwalten der Datenbank und Tabellen implementieren. 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 bei Dateien, die du im internen Speicher des Geräts abspeicherst, speichert Android deine Datenbank im privaten Ordner deiner App. Ihre Daten sind sicher, da dieser Bereich standardmäßig nicht für andere Anwendungen oder den Nutzer zugänglich ist.

Die Klasse SQLiteOpenHelper enthält eine Reihe nützlicher APIs zum Verwalten Ihrer Datenbank. Wenn Sie diese Klasse verwenden, um Verweise auf die Datenbank abzurufen, führt das System die Vorgänge zum Erstellen und Aktualisieren der Datenbank nur bei Bedarf und nicht beim Start der Anwendung aus. Sie müssen lediglich getWritableDatabase() oder getReadableDatabase() aufrufen.

Hinweis: Da sie lange andauern können, sollten Sie getWritableDatabase() oder getReadableDatabase() in einem Hintergrundthread aufrufen. Weitere Informationen finden Sie unter Threading unter Android.

Wenn Sie SQLiteOpenHelper verwenden möchten, erstellen Sie eine abgeleitete Klasse, die die Callback-Methoden onCreate() und onUpgrade() überschreibt. Sie können auch die Methoden onDowngrade() und onOpen() implementieren. Diese sind jedoch nicht erforderlich.

Hier sehen Sie als Beispiel eine Implementierung von SQLiteOpenHelper, die einige der oben gezeigten 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

Übergeben Sie ein ContentValues-Objekt an die Methode insert(), um Daten in die Datenbank einzufügen:

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 mit put gesendet). 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 insert()-Methoden geben die ID für die neu erstellte Zeile oder -1 zurück, wenn beim Einfügen der Daten ein Fehler aufgetreten ist. Dies kann bei einem Konflikt mit bereits vorhandenen Daten in der Datenbank vorkommen.

Informationen aus einer Datenbank lesen

Verwenden Sie zum Lesen aus einer Datenbank die Methode query() und übergeben Sie Ihre Auswahlkriterien und die gewünschten Spalten. Die Methode kombiniert die Elemente von insert() und update(), mit dem Unterschied, dass die Spaltenliste die abzurufenden Daten (die "Projektion") und nicht die einzufügenden Daten definiert. 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 maskiert. Dadurch werden Ihre Auswahlanweisungen nicht mehr gegen SQL-Injektionen geschützt. Weitere Informationen zu allen Argumenten finden Sie in der Referenz zu query().

Wenn Sie eine Zeile im Cursor betrachten 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 an Position -1 beginnt, wird durch das Aufrufen von moveToNext() die Leseposition auf den ersten Eintrag der Ergebnisse gesetzt und es wird zurückgegeben, ob der Cursor bereits nach dem letzten Eintrag im Ergebnissatz liegt. Für jede Zeile können Sie den Wert einer Spalte lesen, indem Sie eine der Cursor-get-Methoden aufrufen, z. B. getString() oder getLong(). Für jede der get-Methoden müssen Sie die Indexposition der gewünschten Spalte übergeben, die Sie durch Aufrufen von getColumnIndex() oder getColumnIndexOrThrow() abrufen können. Wenn Sie mit dem Iterieren der Ergebnisse fertig sind, rufen Sie close() für den Cursor auf, um die Ressourcen freizugeben. Im folgenden Beispiel wird gezeigt, wie Sie alle in einem Cursor gespeicherten Element-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 Auswahlkriterien angeben, die die Zeilen für die Methode delete() identifizieren. Der Mechanismus funktioniert wie die Auswahlargumente für die Methode query(). Sie teilt die Auswahlspezifikation in eine Auswahlklausel und Auswahlargumente auf. Die Klausel definiert die zu untersuchenden Spalten und ermöglicht Ihnen außerdem, Spaltentests zu kombinieren. Die Argumente sind zu testende Werte, die an die Klausel gebunden sind. Da das Ergebnis nicht wie eine reguläre SQL-Anweisung gehandhabt wird, ist es nicht gegen eine SQL-Injection gefährdet.

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 für die Methode delete() gibt die Anzahl der Zeilen an, die aus der Datenbank gelöscht wurden.

Datenbank aktualisieren

Wenn Sie einen Teil der 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 Zeilen, die in der Datenbank betroffen sind.

Persistente Datenbankverbindung

Da getWritableDatabase() und getReadableDatabase() teuer sind, wenn die Datenbank geschlossen ist, sollten Sie die Datenbankverbindung so lange geöffnet lassen, wie Sie möglicherweise darauf zugreifen müssen. In der Regel ist es am besten, 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();
}

Datenbankfehler beheben

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 ausgeben.