Il salvataggio dei dati in un database è l'ideale per dati ripetuti o strutturati, come le informazioni di contatto. Questa pagina presuppone che tu conosca i database SQL in generale e ti aiuti a iniziare a usare i database SQLite su Android. Le API necessarie per utilizzare un database
su Android sono disponibili nel pacchetto android.database.sqlite
.
Attenzione:sebbene queste API siano potenti, sono piuttosto di basso livello e richiedono molto tempo e impegno per il loro utilizzo:
- Non esiste una 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 processo può richiedere molto tempo ed essere soggetto a errori.
- Devi utilizzare molto codice boilerplate per convertire query SQL e oggetti di dati.
Per questi motivi, ti consigliamo vivamente di utilizzare la libreria di persistenza delle stanze come livello di astrazione per accedere alle informazioni nei database SQLite della tua app.
Definire uno schema e un contratto
Uno dei principi fondamentali dei database SQL è lo schema: una dichiarazione formale di come è organizzato il database. Lo schema si riflette nelle istruzioni SQL che utilizzi per creare il database. Può essere utile creare una classe companion, nota come classe contract, che specifica esplicitamente il layout dello schema in modo sistematico e autodocumentante.
Una classe di contratto è un container di costanti che definiscono i nomi di URI, tabelle e colonne. La classe contratto consente di utilizzare le stesse costanti in tutte le altre classi dello stesso pacchetto. Ciò ti consente di modificare il nome di una colonna in un unico posto e di propagarlo nel codice.
Un buon modo per organizzare una classe di contratto è inserire le definizioni globali dell'intero database nel livello principale della classe. Poi crea una classe interna per ogni tabella. Ogni classe interna enumera le colonne della tabella corrispondente.
Nota: se implementi l'interfaccia BaseColumns
, la tua classe interna può ereditare un campo chiave primaria denominato _ID
che alcune classi Android, come CursorAdapter
, si aspettano di avere. Non è obbligatorio, ma può aiutare il database
a funzionare 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
Dopo aver definito l'aspetto del database, devi implementare i metodi che creano e gestiscono il database e le tabelle. Di seguito sono riportate alcune istruzioni tipiche che creano ed eliminano 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 archivia il database nella cartella privata dell'app. I tuoi dati sono protetti perché per impostazione predefinita quest'area non è accessibile ad altre app o all'utente.
La classe SQLiteOpenHelper
contiene un set utile di API per la gestione del database.
Quando utilizzi questa classe per ottenere riferimenti al tuo database, il sistema esegue le operazioni potenzialmente a lunga esecuzione di creazione e aggiornamento del database solo quando necessario e non durante l'avvio dell'app. Non devi fare altro che chiamare
getWritableDatabase()
o
getReadableDatabase()
.
Nota: poiché possono essere di lunga durata,
assicurati di chiamare getWritableDatabase()
o getReadableDatabase()
in un thread in background.
Per ulteriori informazioni, vedi Threading su Android.
Per utilizzare SQLiteOpenHelper
, crea una sottoclasse che
sostituisca i metodi di callback onCreate()
e
onUpgrade()
. Puoi
anche implementare i metodi
onDowngrade()
o
onOpen()
,
ma non sono obbligatori.
Ad esempio, ecco un'implementazione di SQLiteOpenHelper
che
utilizza alcuni dei comandi riportati 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 per la sottoclasse di SQLiteOpenHelper
:
Kotlin
val dbHelper = FeedReaderDbHelper(context)
Java
FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());
Inserimento delle 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 specificato put
valori).
Se specifichi il nome di una colonna, il framework inserisce una riga e imposta il valore di tale colonna su null. Se specifichi null
, come in questo esempio di codice, il framework non inserisce una riga quando non ci sono valori.
Il metodo insert()
restituisce l'ID per la riga appena creata oppure restituisce -1 in caso di errore durante l'inserimento dei dati. Ciò può accadere in caso di conflitto con i dati preesistenti nel database.
Lettura di informazioni da un database
Per leggere da un database, utilizza il metodo query()
, trasmettendo i criteri di selezione e le colonne desiderate.
Il metodo combina gli elementi di insert()
e update()
, tranne per il fatto che l'elenco delle colonne
definisce i dati che vuoi 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 utilizzati caratteri di escape prima di essere combinati. Ciò rende le tue dichiarazioni di selezione immuni all'iniezione SQL. Per ulteriori dettagli su tutti gli argomenti, consulta la documentazione di riferimento query()
.
Per visualizzare una riga nel cursore, utilizza uno dei metodi di spostamento Cursor
, che devi sempre chiamare prima di iniziare a leggere i valori. Poiché il cursore inizia nella posizione -1, la chiamata a moveToNext()
consente di posizionare la "posizione di lettura" sulla prima voce dei risultati e restituisce se il cursore si trova già oltre l'ultima voce nel set di risultati. Per ogni riga, puoi leggere il valore di una colonna chiamando uno dei Cursor
metodi get, ad esempio getString()
o getLong()
. Per ciascuno dei metodi get, devi passare la posizione dell'indice della colonna che vuoi, che puoi ottenere chiamando getColumnIndex()
o getColumnIndexOrThrow()
. Al termine dell'iterazione dei risultati, chiama close()
sul cursore per rilasciare le risorse corrispondenti.
Ad esempio, quanto riportato di seguito mostra come recuperare tutti gli ID elemento archiviati 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();
Eliminare informazioni da un database
Per eliminare righe da una tabella, devi fornire al metodo delete()
criteri di selezione che le identificano. Il meccanismo funziona come gli argomenti di selezione del metodo query()
. Divide la specifica di selezione in una clausola di selezione e in argomenti di selezione. La clausola definisce le colonne da esaminare e consente anche di combinare i test delle colonne. Gli argomenti sono valori rispetto ai quali eseguire il test e associati alla clausola.
Poiché il risultato non viene gestito come una normale istruzione SQL, è immune all'iniezione 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);
Il valore restituito per il metodo delete()
indica il numero di righe che sono state eliminate dal database.
Aggiorna un database
Quando devi modificare un sottoinsieme di valori del database, utilizza il metodo 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 persistente
Poiché getWritableDatabase()
e getReadableDatabase()
sono costosi da chiamare quando il database è chiuso, devi lasciare la connessione al database aperta per tutto il tempo in cui è necessario accedervi. In genere, è ottimale chiudere il database nell'elemento onDestroy()
dell'attività di chiamata.
Kotlin
override fun onDestroy() { dbHelper.close() super.onDestroy() }
Java
@Override protected void onDestroy() { dbHelper.close(); super.onDestroy(); }
Esegui il debug del tuo database
L'SDK Android include uno strumento shell sqlite3
che consente di sfogliare i contenuti delle tabelle, eseguire comandi SQL ed eseguire altre funzioni utili sui database SQLite. Per maggiori informazioni, vedi come inviare i comandi della shell.