Risparmia i dati con SQLite

Il salvataggio dei dati in un database è ideale per i dati ripetuti o strutturati, come i dati di contatto. Questa pagina presuppone che tu abbia familiarità con i database SQL in generale e ti aiuta a iniziare a utilizzare i database SQLite su Android. Le API necessarie per utilizzare un database su Android sono disponibili nel pacchetto android.database.sqlite.

Attenzione: anche se sono potenti, queste API sono abbastanza di basso livello e richiedono molto tempo e impegno per essere utilizzate:

  • Non viene eseguita alcuna verifica in fase di compilazione delle query SQL non elaborate. Man mano che il grafico dei dati cambia, devi aggiornare manualmente le query SQL interessate. Questo può richiedere molto tempo e causare errori.
  • Devi utilizzare molto codice boilerplate per la conversione tra query SQL e oggetti dati.

Per questi motivi, ti consigliamo vivamente di utilizzare Libreria sulla persistenza della stanza come livello di astrazione per accedere alle informazioni nella SQLite dell'app o Microsoft SQL Server.

Definisci uno schema e un contratto

Uno dei principi principali dei database SQL è lo schema: una dichiarazione di organizzazione del database. Lo schema è riportato negli statement SQL utilizzati per creare il database. Potrebbe essere utile creare una classe complementare, nota come classe contract, che specifichi esplicitamente il layout dello schema in modo sistematico e autodocumentante.

Una classe di contratto è un contenitore di costanti che definiscono i nomi di URI, tabelle e colonne. La classe di contratto ti consente di utilizzare le stesse costanti in tutte le altre classi nello stesso pacchetto. In questo modo puoi modificare il nome di una colonna in un punto e propagarlo in tutto il codice.

Un buon modo per organizzare una classe contratto è quello di inserire definizioni a livello globale all'intero database nel livello principale della classe. Quindi crea una finestra per ogni tabella. Ogni classe interna enumera le colonne della tabella corrispondente.

Nota: implementando l'interfaccia BaseColumns, la classe interna può ereditare un campo della chiave principale chiamato _ID che alcune classi Android come CursorAdapter si aspettano che abbia. Non è obbligatorio, ma può aiutare il tuo database lavorare in modo armonioso con il framework Android.

Ad esempio, il seguente contratto definisce il nome della tabella e i nomi delle colonne per una singola tabella che rappresenta un feed 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";
    }
}

Crea un database utilizzando un helper SQL

Una volta definito l'aspetto del database, devi implementare metodi che creano e gestiscono il database e le tabelle. Di seguito sono riportate alcune istruzioni comuni per creare ed eliminare una tabella:

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;

Proprio come i file salvati nello spazio di archiviazione interno del dispositivo, Android memorizza il database nella cartella privata dell'app. I tuoi dati sono protetti perché per impostazione predefinita questa area non è accessibile ad altre app o all'utente.

La classe SQLiteOpenHelper contiene un utile insieme di API per la gestione del database. Quando utilizzi questa classe per ottenere riferimenti al tuo database, il sistema esegue potenzialmente di creare e aggiornare il database solo quando e non durante l'avvio dell'app. Devi solo chiamare getWritableDatabase() o getReadableDatabase().

Nota: poiché possono richiedere molto tempo, assicurati di chiamare getWritableDatabase() o getReadableDatabase() in un thread in background. Per ulteriori informazioni, vedi Threading su Android.

Per utilizzare SQLiteOpenHelper, crea una sottoclasse esegue l'override di onCreate() e onUpgrade() metodi di callback. Puoi anche implementare i metodi onDowngrade() o onOpen(), ma non sono obbligatori.

Ad esempio, ecco un'implementazione di SQLiteOpenHelper che utilizza alcuni dei comandi mostrati sopra:

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

Per accedere al database, crea un'istanza della sottoclasse SQLiteOpenHelper:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Inserire informazioni in un database

Inserisci i dati nel database passando un oggetto ContentValues al metodo 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);

Il primo argomento per insert() è semplicemente il nome della tabella.

Il secondo argomento indica al framework cosa fare nel caso in cui ContentValues sia vuoto (ovvero non hai put alcun valore). Se specifichi il nome di una colonna, il framework inserisce una riga e imposta il valore della colonna su null. Se specifichi null, come in questo esempio di codice, il framework non inserisce una riga quando non sono presenti valori.

I metodi insert() restituiscono l'ID della riga appena creata o -1 se si è verificato un errore durante l'inserimento dei dati. Ciò può accadere in caso di conflitto con dati preesistenti nel database.

Leggere le informazioni da un database

Per leggere da un database, utilizza il metodo query(), passando i criteri di selezione e le colonne desiderate. Il metodo combina gli elementi di insert() e update(), tranne l'elenco di colonne definisce i dati da recuperare (la "proiezione") anziché i dati da inserire. I risultati della query ti vengono restituiti in un oggetto 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
    );

Il terzo e il quarto argomento (selection e selectionArgs) vengono combinati per creare una clausola WHERE. Poiché gli argomenti vengono forniti separatamente dalla query di selezione, vengono sottoposti a evocazione prima di essere combinati. Ciò rende le istruzioni di selezione immuni a SQL iniezione di codice. Per maggiori dettagli su tutti gli argomenti, consulta la documentazione di riferimentoquery().

Per esaminare una riga nel cursore, utilizza uno dei metodi Cursor move che devi sempre chiamare prima di iniziare a leggere i valori. Poiché il cursore inizia dalla posizione -1, la chiamata a moveToNext() posiziona la "posizione di lettura" sulla prima voce dei risultati e restituisce se il cursore ha già superato o meno l'ultima voce del set di risultati. Per ogni riga, puoi leggere il valore di una colonna chiamando uno dei metodi Cursor get, ad esempio getString() o getLong(). Per ognuno dei metodi get, devi passare la posizione di indice della colonna desiderata, che puoi ottenere chiamando getColumnIndex() o getColumnIndexOrThrow(). Al termine eseguendo l'iterazione dei risultati, richiama close() sul cursore per rilasciare le sue risorse. Ad esempio, di seguito viene mostrato come recuperare tutti gli ID elemento memorizzati in un cursore e aggiungerli a un elenco:

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

Elimina informazioni da un database

Per eliminare le righe da una tabella, devi fornire criteri di selezione che identificare le righe nel metodo delete(). Il meccanismo funziona come gli argomenti di selezione del metodo query(). Divide la specifica della selezione in una clausola e gli argomenti di selezione. La definisce le colonne da esaminare e consente anche di combinare le colonne test. Gli argomenti sono i valori rispetto ai quali eseguire il test e che sono associati alla clausola. Poiché il risultato non viene gestito come un'istruzione SQL normale, è immune agli attacchi di SQL injection.

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

Il valore restituito per il metodo delete() indica il numero di righe eliminate dal database.

Aggiorna un database

Quando devi modificare un sottoinsieme dei valori del database, utilizza update().

L'aggiornamento della tabella combina la sintassi ContentValues di insert() con la sintassi WHERE di 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);

Il valore restituito del metodo update() è il numero di righe interessate nel database.

Connessione al database permanente

Poiché le chiamate a getWritableDatabase() e getReadableDatabase() sono costose quando il database è chiuso, devi lasciare aperta la connessione al database per tutto il tempo necessario per accedervi. In genere, è ottimale chiudere il database in onDestroy() dell'Attività di chiamata.

Kotlin

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

Java

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

Eseguire il debug del database

L'SDK Android include uno strumento shell di sqlite3 che ti consente di esplorare contenuti della tabella, eseguire comandi SQL ed eseguire altre funzioni utili su SQLite o Microsoft SQL Server. Per ulteriori informazioni, scopri come emettere comandi shell.