Android Coden
Android 9 min lesen

SQLite-Grundlagen

SQLite ist die lokale SQL-Basis vieler Android-Apps. Du lernst, wie Tabellen, Abfragen und Constraints Room erklärbar machen.

SQLite ist für viele Android-Apps die lokale Grundlage, wenn Daten dauerhaft, strukturiert und offline verfügbar gespeichert werden sollen. Auch wenn du meistens mit Room arbeitest, lohnt sich ein solides SQL-Grundverständnis: Du erkennst schneller, warum eine Abfrage langsam ist, warum eine Migration fehlschlägt oder warum ein Constraint genau den Fehler verhindert, der sonst erst bei Nutzenden sichtbar würde.

Was ist das?

SQLite ist eine eingebettete relationale Datenbank. Eingebettet bedeutet: Die Datenbank läuft nicht als eigener Server, sondern direkt in deiner App-Umgebung. Relational bedeutet: Daten werden in Tabellen organisiert, Tabellen bestehen aus Spalten, und jede Zeile beschreibt einen Datensatz. SQL ist die Sprache, mit der du diese Daten liest, schreibst, filterst, sortierst und verknüpfst.

In Android begegnet dir SQLite heute oft indirekt über Room. Room ist eine Jetpack-Bibliothek, die Kotlin-Klassen, DAO-Interfaces und Annotationen nutzt, um Datenbankzugriffe typsicherer und wartbarer zu machen. Trotzdem bleibt SQLite darunter die eigentliche Datenbank. Wenn du Room nur als magische Persistenzschicht behandelst, fehlt dir bei Fehlern oft das richtige Modell. Room generiert und prüft SQL, aber es kann dir nicht das Denken über Tabellenstruktur, Beziehungen, Constraints und Abfragekosten abnehmen.

Das Problem, das SQLite löst, ist sehr praktisch: Eine App braucht lokale, verlässliche Daten. Eine Aufgaben-App speichert Tasks, eine Fitness-App speichert Trainingseinheiten, eine News-App speichert Artikel für Offline-Nutzung. Diese Daten sind nicht nur lose JSON-Blöcke. Sie haben Regeln: Eine Aufgabe hat eine ID, ein Titel darf nicht leer sein, ein Artikel kann zu einer Quelle gehören, ein Messwert hat einen Zeitpunkt. Genau hier helfen relationale Datenmodelle.

Für dich als Android-Lernende oder Android-Lernender ist das Ziel nicht, jede SQL-Spezialität zu kennen. Du solltest genug verstehen, um Room-Verhalten und Performance einschätzen zu können. Wenn du weißt, wie Tabellen, Schlüssel, Constraints, Joins und Indizes zusammenspielen, kannst du Architekturentscheidungen in der Data Layer fundierter treffen. Das passt direkt zur modernen Android-Architektur: Repositorys kapseln Datenquellen, Room stellt die lokale Quelle bereit, und ViewModels oder Use Cases bekommen stabile Datenströme, die in Compose angezeigt werden können.

Ein hilfreiches mentales Modell ist: SQLite ist nicht nur ein Speicherort, sondern ein Regelwerk für Daten. Du definierst nicht nur, was gespeichert wird, sondern auch, welche Zustände erlaubt sind. Eine gute Datenbankstruktur macht ungültige Zustände schwerer. Eine schlechte Struktur verschiebt Datenprobleme in UI-Code, Mapper oder spätere Synchronisationslogik.

Wie funktioniert es?

SQLite speichert Daten in Tabellen. Eine Tabelle hat ein Schema: Spaltennamen, Datentypen und Regeln. In Room beschreibst du dieses Schema meist mit einer Entity-Klasse. Eine Entity ist aber nicht nur ein Kotlin-Datenmodell. Sie wird zu einer Tabelle, und jede Property wird zu einer Spalte. Darum solltest du bei jeder Entity fragen: Was ist die eindeutige Identität? Welche Werte dürfen fehlen? Welche Werte müssen eindeutig sein? Welche Beziehungen gibt es zu anderen Tabellen?

Der wichtigste Baustein ist der Primary Key. Er identifiziert eine Zeile eindeutig. In Android nutzt du dafür oft eine lokale ID, zum Beispiel Long, oder eine Server-ID, wenn Daten synchronisiert werden. Ohne klare Identität werden Updates, Deletes und Diffing in Listen unnötig fehleranfällig. Für Compose ist das ebenfalls relevant: Stabile IDs helfen, Listen korrekt zu aktualisieren und UI-Zustand nicht an die falsche Zeile zu hängen.

SQL-Abfragen beschreiben, welche Daten du brauchst. Ein einfaches SELECT liest Zeilen aus einer Tabelle. Mit WHERE filterst du, mit ORDER BY sortierst du, mit LIMIT begrenzt du Ergebnisse. Room erlaubt dir, diese Abfragen als Annotationen in einem DAO zu schreiben. Das wirkt bequem, bleibt aber SQL. Wenn du in einem DAO eine Abfrage formulierst, entscheidest du auch über Datenmenge, Sortierung und mögliche Kosten.

Constraints sind Regeln auf Datenbankebene. Ein NOT NULL-Constraint sagt, dass eine Spalte immer einen Wert braucht. Ein UNIQUE-Constraint verhindert doppelte Werte. Ein Foreign Key beschreibt eine Beziehung zwischen Tabellen, zum Beispiel zwischen einer Bestellung und ihren Positionen. Solche Regeln sind mehr als Formalitäten. Sie schützen deine Daten auch dann, wenn ein Fehler nicht aus dem UI kommt, sondern aus einer Migration, einem Import, einem Sync-Prozess oder einem Testaufbau.

Ein häufiger Denkfehler ist, Validierung nur im UI zu platzieren. Natürlich solltest du Nutzenden früh erklären, warum eine Eingabe ungültig ist. Aber UI-Validierung ersetzt keine Datenbankregeln. Wenn ein Titel fachlich nicht leer sein darf, sollte die Datenbankstruktur das widerspiegeln. Sonst kann später ein Repository, ein Worker oder ein Test doch ungültige Daten schreiben.

Performance hängt bei SQLite stark davon ab, wie du Daten abfragst. Kleine Datenmengen verzeihen viel. Sobald eine Tabelle wächst, werden unklare Abfragen teuer. Ein Index kann helfen, wenn du häufig nach einer Spalte filterst oder sortierst. Beispiel: Wenn du Aufgaben oft nach completed und createdAt abfragst, kann ein passender Index die Suche beschleunigen. Ein Index ist aber kein kostenloser Zusatz. Er braucht Speicher und macht Schreibvorgänge etwas teurer, weil der Index mitgepflegt werden muss.

Auch Transaktionen sind ein Kernkonzept. Eine Transaktion fasst mehrere Datenbankoperationen zu einer Einheit zusammen. Entweder werden alle Änderungen übernommen oder keine. Das ist wichtig, wenn zusammengehörige Daten geschrieben werden. Beispiel: Du speicherst eine Liste und mehrere Listeneinträge. Wenn nach der Liste, aber vor den Einträgen ein Fehler auftritt, soll die Datenbank nicht in einem halben Zustand bleiben. Room bietet dafür @Transaction.

Im Offline-First-Kontext wird SQLite besonders wichtig. Eine App kann lokale Daten anzeigen, auch wenn das Netzwerk fehlt. Der Server ist dann nicht die einzige Wahrheit im aktuellen Moment. Stattdessen arbeitet die App mit einer lokalen Datenquelle und synchronisiert später. Dafür brauchst du saubere lokale IDs, Zeitstempel, Statusfelder und Regeln für Konflikte. SQLite-Grundlagen helfen dir, diese lokalen Zustände verständlich zu modellieren, ohne die gesamte Sync-Strategie in die UI zu verschieben.

In der Praxis

Angenommen, du baust eine kleine Aufgabenfunktion. Du möchtest Aufgaben lokal speichern, nach offenen Aufgaben filtern und verhindern, dass zwei Aufgaben dieselbe externe ID bekommen. Mit Room sieht das Kotlin-seitig kompakt aus, aber die Konzepte darunter bleiben SQLite: Tabelle, Primary Key, Spalten, Index und Constraints.

import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Index
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Entity(
    tableName = "tasks",
    indices = [
        Index(value = ["remote_id"], unique = true),
        Index(value = ["completed", "created_at"])
    ]
)
data class TaskEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    @ColumnInfo(name = "remote_id")
    val remoteId: String?,

    val title: String,

    val completed: Boolean,

    @ColumnInfo(name = "created_at")
    val createdAt: Long
)

@Dao
interface TaskDao {
    @Query(
        """
        SELECT *
        FROM tasks
        WHERE completed = 0
        ORDER BY created_at DESC
        """
    )
    fun observeOpenTasks(): Flow<List<TaskEntity>>

    @Insert(onConflict = OnConflictStrategy.ABORT)
    suspend fun insert(task: TaskEntity)
}

In diesem Beispiel wird id zum lokalen Primary Key. remoteId ist optional, weil eine Aufgabe vielleicht lokal entsteht und erst später mit einem Server synchronisiert wird. Der eindeutige Index auf remote_id verhindert, dass dieselbe Server-Aufgabe doppelt gespeichert wird. Der Index auf completed und created_at passt zur Abfrage, weil offene Aufgaben gefiltert und nach Erstellzeit sortiert werden.

Wichtig ist die Entscheidung hinter dem Code. Du modellierst nicht nur Kotlin-Daten. Du beschreibst, welche Datenbankzustände erlaubt sind und welche Abfragen wahrscheinlich häufig auftreten. Diese Denkweise schützt dich vor einer typischen Stolperfalle: Du speicherst alles zuerst bequem ab und versuchst später, Datenqualität in Repository-Logik zu reparieren. Das führt schnell zu Sonderfällen. Besser ist: Einfache, stabile Regeln gehören ins Schema.

Eine zweite Stolperfalle betrifft OnConflictStrategy. Viele Lernende greifen vorschnell zu REPLACE, weil es Fehler scheinbar vermeidet. Das kann gefährlich sein. In SQLite bedeutet Ersetzen nicht immer „ändere nur diese Zeile wie ein Update“. Je nach Schema kann ein Replace intern wie Löschen und Neu-Einfügen wirken. Das kann Beziehungen, Zeitstempel oder abhängige Daten unerwartet beeinflussen. Wenn ein Konflikt fachlich ein Fehler ist, ist ABORT oft ehrlicher. Wenn du ein echtes Update möchtest, schreibe bewusst eine Update-Abfrage oder nutze eine passende Upsert-Strategie, wenn sie in deinem Setup klar unterstützt wird.

Eine praktische Regel lautet: Schreibe jede Room-Abfrage so, als müsstest du sie direkt in SQL erklären. Frage dich: Welche Tabelle wird gelesen? Welche Spalten filtern? Welche Sortierung ist nötig? Gibt es einen Constraint, der meine Annahme schützt? Gibt es einen Index, der zur häufigsten Abfrage passt? Wenn du diese Fragen nicht beantworten kannst, ist die Abfrage noch nicht wirklich verstanden.

Im Alltag erscheint dieses Wissen an mehreren Stellen. Beim Code-Review prüfst du, ob Entity-Felder nullable sein müssen oder ob NOT NULL sinnvoller wäre. Bei einer Migration prüfst du, ob neue Spalten Default-Werte brauchen. Beim Debugging untersuchst du, ob eine leere Liste aus der UI kommt oder ob die SQL-Abfrage keine Zeilen findet. Bei Performance-Problemen schaust du nicht nur auf Compose-Recomposition, sondern auch darauf, ob die App zu viele Daten lädt, unnötig sortiert oder ohne passenden Index filtert.

Du kannst dein Verständnis gezielt validieren. Schreibe einen DAO-Test mit einer In-Memory-Datenbank. Füge gültige und ungültige Datensätze ein. Prüfe, ob doppelte remoteId-Werte scheitern. Prüfe, ob observeOpenTasks() wirklich nur offene Aufgaben in der erwarteten Reihenfolge liefert. Solche Tests sind klein, schnell und sehr lehrreich, weil sie die Datenbankregeln direkt sichtbar machen.

Ein weiterer guter Übungsweg ist das Lesen generierter oder exportierter Schemas. Room kann Schema-Dateien ausgeben, die zeigen, welche Tabellen, Spalten, Indizes und Constraints wirklich entstehen. Vergleiche diese Ausgabe mit deiner Entity. Wenn dort etwas anderes steht, als du erwartest, hast du eine konkrete Lernchance. Genau solche Unterschiede erklären später viele Migrations- und Produktionsfehler.

Halte außerdem die Datenmenge im Blick. Eine Abfrage, die in einer Demo mit zehn Zeilen funktioniert, kann bei zehntausend Zeilen problematisch werden. Lade nur die Spalten und Zeilen, die du brauchst. Für Listenansichten ist es oft sinnvoll, eigene Projektionen oder reduzierte Modelle zu verwenden, statt immer ganze Entities zu lesen. Das ist keine Optimierung aus Prinzip, sondern eine Architekturfrage: Die Data Layer soll der UI passende Daten liefern, nicht rohe Datenberge.

Constraints helfen dir auch bei Offline-First-Szenarien. Wenn ein Sync-Worker Daten vom Server einfügt, sollten doppelte externe IDs nicht stillschweigend entstehen. Wenn ein lokaler Datensatz noch nicht synchronisiert ist, kann ein Statusfeld wie pendingSync sinnvoll sein. SQLite erzwingt nicht deine gesamte Fachlogik, aber es kann wichtige Grenzen setzen. Je klarer diese Grenzen sind, desto leichter bleibt die App testbar.

Fazit

SQLite-Grundlagen geben dir das Werkzeug, Room nicht nur zu benutzen, sondern zu verstehen. Du lernst, Daten als Tabellen mit Identität, Beziehungen und Regeln zu betrachten, SQL-Abfragen bewusst zu formulieren und Constraints als Schutz für Datenqualität einzusetzen. Nimm dir als Übung eine bestehende Room-Entity aus deinem Projekt, erkläre das resultierende Tabellenschema laut, schreibe einen DAO-Test für eine wichtige Abfrage und prüfe im Code-Review, ob Primary Key, Nullability, Index und Konfliktstrategie zur fachlichen Regel passen.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.