Guardar datos en una base de datos es ideal para los datos estructurados o que se repiten, como la información de contacto. En esta página, en la que se asume que estás familiarizado con las bases de datos SQL en general, encontrarás información que te ayudará a comenzar a usar bases de datos SQLite en Android. Las APIs que necesitarás para utilizar una base de datos en Android están disponibles en el paquete android.database.sqlite
.
Precaución: Si bien estas APIs son potentes, se caracterizan por ser bastante específicas y su uso requiere de mucho tiempo y esfuerzo.
- No hay verificación en tiempo de compilación de las consultas en SQL sin procesar. A medida que cambia tu grafo de datos, debes actualizar manualmente las consultas de SQL afectadas. Este proceso puede llevar mucho tiempo y causar errores.
- Debes usar mucho código estándar para convertir entre consultas en SQL y objetos de datos.
Por estos motivos, recomendamos enfáticamente usar la Biblioteca de persistencias Room como una capa de abstracción para acceder a la información de las bases de datos SQLite de tu app.
Cómo definir un esquema y un contrato
Uno de los principios fundamentales de las bases de datos SQL es el esquema: una declaración formal de la manera en la que la base de datos está organizada. El esquema se refleja en las instrucciones de SQL que utilizas para crear la base de datos. Tal vez te resulte útil crear una clase complementaria, denominada clase de contratos, que indique explícitamente el diseño del esquema de forma sistemática y autodocumentada.
La clase de contratos es un contenedor de constantes que definen nombres de URI, tablas y columnas. Esta clase te permite utilizar las mismas constantes en todas las otras clases del mismo paquete, por lo que puedes cambiar el nombre de una columna en un lugar y propagar ese cambio en todo el código.
Una forma adecuada de organizar una clase de contratos consiste en incluir definiciones que sean globales para toda la base de datos en el nivel raíz de la clase. Luego, se debe crear una clase interna para cada tabla. Cada clase interna enumera las columnas de tabla correspondientes.
Nota: Si implementas la interfaz BaseColumns
, tu clase interna puede heredar un campo de clave primaria llamado _ID
que algunas clases de Android, como CursorAdapter
, esperan tener. Aunque esta acción no es obligatoria, ayuda a que la base de datos funcione de manera óptima con el framework de Android.
Por ejemplo, en el siguiente contrato, se define el nombre de la tabla y los nombres de columna de una sola tabla que representa 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"; } }
Cómo crear una base de datos con un asistente de SQL
Una vez que hayas definido el aspecto de tu base de datos, debes implementar métodos que creen y mantengan la base de datos y las tablas. A continuación, puedes ver algunas instrucciones típicas que crean y borran una tabla:
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;
Al igual que los archivos que guardas en el almacenamiento interno del dispositivo, Android almacena tu base de datos en la carpeta privada de tu app. Los datos están seguros porque, de forma predeterminada, esta área no es accesible para otros usuarios y apps.
La clase SQLiteOpenHelper
contiene un conjunto útil de APIs para administrar tu base de datos.
Cuando utilizas esta clase para obtener referencias a tu base de datos, el sistema realiza las operaciones de larga duración para crear y actualizar la base de datos solo cuando es necesario y no durante el inicio de la app. Lo único que debes hacer es llamar a getWritableDatabase()
o getReadableDatabase()
.
Nota: Dado que las operaciones pueden ser de larga duración, asegúrate de llamar a getWritableDatabase()
o getReadableDatabase()
en un subproceso en segundo plano.
Consulta cómo administrar subprocesos en Android para obtener más información.
Si deseas usar SQLiteOpenHelper
, crea una subclase que anule los métodos de devolución de llamada onCreate()
y onUpgrade()
. También puedes implementar los métodos onDowngrade()
o onOpen()
, pero no son obligatorios.
Como ejemplo, se ofrece a continuación una implementación de SQLiteOpenHelper
en la que se usan algunos de los comandos anteriores:
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); } }
Para acceder a la base de datos, crea una instancia de la subclase de SQLiteOpenHelper
:
Kotlin
val dbHelper = FeedReaderDbHelper(context)
Java
FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());
Cómo ingresar información en una base de datos
Inserta datos en la base de datos pasando un objeto ContentValues
al método 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);
El primer argumento de insert()
es simplemente el nombre de la tabla.
El segundo argumento le indica al framework qué hacer en caso de que ContentValues
esté vacío (es decir, si no incluiste ningún valor con put
).
Si especificas el nombre de una columna, el framework inserta una fila y establece el valor de esa columna como nulo. Si especificas null
, como en esta muestra de código, el framework no insertará una fila cuando no haya valores.
Los métodos insert()
devuelven el ID de la fila recién creada o -1 si hubo un error al insertar los datos. Esto puede suceder si hay conflicto con los datos preexistentes en la base de datos.
Cómo leer información de una base de datos
Para leer desde una base de datos, usa el método query()
, pasando los criterios de selección y columnas deseadas.
El método combina elementos de insert()
y update()
, excepto que la lista de columnas define los datos que deseas recuperar (la "proyección") en lugar de datos para insertar. Los resultados de la consulta se devuelven en un objeto 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 );
El tercer y el cuarto argumento (selection
y selectionArgs
) se combinan para crear una cláusula WHERE. Como los argumentos se proporcionan por separado de la consulta de selección, se escapan antes de combinarse, lo que hace que tus instrucciones de selección sean inmunes a la inyección de SQL. Para obtener más detalles sobre todos los argumentos, consulta la referencia de query()
.
Para ver una fila en el cursor, utiliza uno de los métodos de movimiento Cursor
, a los que siempre debes llamar antes de comenzar a leer valores. Dado que el cursor comienza en la posición -1, la llamada a moveToNext()
coloca la "posición de lectura" en la primera entrada de los resultados y muestra si el cursor ya pasó la última entrada del conjunto de resultados. Para cada fila, puedes leer el valor de una columna llamando a uno de los métodos GET de Cursor
, como getString()
o getLong()
. Por cada uno de los métodos GET, debes pasar la posición del índice de la columna que desees, que puedes obtener llamando a getColumnIndex()
o getColumnIndexOrThrow()
. Cuando termines de iterar los resultados, llama a close()
en el cursor para liberar sus recursos.
Por ejemplo, a continuación se muestra cómo obtener todos los IDs de artículos almacenados en un cursor y agregarlos a una lista:
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();
Cómo borrar información de una base de datos
Para borrar filas de una tabla, debes proporcionar criterios de selección que identifiquen las filas para el método delete()
. El mecanismo funciona igual que los argumentos de selección del método query()
. Divide la especificación de selección en una cláusula de selección y argumentos de selección. La cláusula define las columnas que se comprobarán y también permite combinar pruebas de columnas. Los argumentos son valores para probar que están vinculados a la cláusula.
Como el resultado no se controla del mismo modo que una instrucción de SQL normal, es inmune a la inyección de 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);
El valor que se devuelve con el método delete()
indica el número de filas que se borraron de la base de datos.
Cómo actualizar una base de datos
Cuando debas modificar un subconjunto de los valores de la base de datos, usa el método update()
.
La actualización de la tabla combina la sintaxis ContentValues
de insert()
con la sintaxis WHERE
de 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);
El valor que se devuelve con el método update()
es la cantidad de filas afectadas en la base de datos.
Conexión persistente a la base de datos
Dado que llamar a getWritableDatabase()
y getReadableDatabase()
es costoso cuando la base de datos está cerrada, debes dejar abierta la conexión con la base de datos durante el tiempo que posiblemente necesites acceder a ella. Por lo general, lo óptimo es cerrar la base de datos en el método onDestroy()
de la actividad de llamada.
Kotlin
override fun onDestroy() { dbHelper.close() super.onDestroy() }
Java
@Override protected void onDestroy() { dbHelper.close(); super.onDestroy(); }
Cómo depurar tu base de datos
El SDK de Android incluye una herramienta de shell sqlite3
que te permite explorar el contenido de las tablas, ejecutar comandos de SQL y realizar otras funciones útiles en las bases de datos de SQLite. Para obtener más información, consulta cómo enviar comandos de shell.