Cómo guardar datos con SQLite

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.