Guardando información en bases de datos SQL

Las base de datos son ideales para almacenar datos repetidos o estructurados, como información de contacto. Esta clase, que asume que estas familiarizado con las bases de datos SQL en general, te ayudará a empezar a usar las bases de datos SQLite de Android. Las APIs que necesitarás para usar una base de datos en Android están disponibles en el paquete android.database.sqlite.

Definir un esquema y un contrato


Uno de los conceptos fundamentales de las bases de datos SQL es el esquema: una declaración formal de cómo está organizada la base de datos. El esquema se refleja en las sentencias SQL que utilizas para crear tu base de datos. Puedes ser de utilidad crear una clase compañera, conocida como una clase de contrato, que especifique explicitamente el diseño de tu esquema de forma sistemática y auto documentada.

Una clase de contrato es un contenedor para constantes que definen nombres para URIs, tablas, y columnas. La clase de contrato te permite usar las mismas constantes a través de todas las clases del mismo paquete. Esto te permite cambiar el nombre de una columna en un único lugar y que ese cambio se progague automáticamente por todo tu código.

Una buena forma de organizar una clase de contrato es colocar las definiciones que sean globales a toda tu base de datos en la raíz de la clase. Una vez hecho esto crea una clase interna para cada tabla, enumerando sus columnas.

Nota: Al implementar la interfaz BaseColumns, tu clase interna hereda un campo de clave primaria llamado _ID que algunas clases Android, como los adaptadores de cursores, esperarán que tengas. No es algo obligatorio, pero puede ayudar a que tu base de datos trabaje de forma más armoniosa con la plataforma Android.

Por ejemplo, este fragmento de código define el nombre de la tabla y los nombres de las columnas para una única tabla:

public static abstract class FeedEntry implements BaseColumns {
    public static final String TABLE_NAME = "entry";
    public static final String COLUMN_NAME_ENTRY_ID = "entryid";
    public static final String COLUMN_NAME_TITLE = "title";
    public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    ...
}

Para prevenir que alguien instancie la clase de contrato de forma accidental, dale un constructor vacío.

// Previene que se pueda instanciar la clase FeedReaderContract.
private FeedReaderContract() {}

Crear una base de datos usando un ayudante SQL


Una vez definido el aspecto de tu base de datos, el siguiente paso es implementar métodos para crear y gestionar las bases de datos y las tablas. A continuación se muestran un par de sentencias típicas de creación y borrado de una tabla:

private static final String TEXT_TYPE = " TEXT";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_ENTRIES =
    "CREATE TABLE " + FeedReaderContract.FeedEntry.TABLE_NAME + " (" +
    FeedReaderContract.FeedEntry._ID + " INTEGER PRIMARY KEY," +
    FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
    FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
    ... // Cualquier otra opción para el comando CREATE
    " )";

private static final String SQL_DELETE_ENTRIES =
    "DROP TABLE IF EXISTS " + TABLE_NAME_ENTRIES;

De igual forma que los archivos que guardas en el almacenamiento interno del dispositivo, Android almacena tus bases de datos en el espacio privado asociado a tu aplicación. Tus datos están seguros, dado que por defecto esta área no puede ser accedida por otras aplicaciones.

Hay disponibles un conjunto de APIs útiles en la clase SQLiteOpenHelper. Cuando utilizas esta clase para obtener referencias a tu base de datos, el sistema lleva a cabo las operaciones de crear y actualizar la base de datos, que pueden llevar bastante tiempo, sólo cuando sea necesario y no durante el arranque de la aplicación. Todo lo que necesitas es llamar a getWritableDatabase() o getReadableDatabase().

Nota: Dado que pueden ser operaciones que lleven bastante tiempo, asegurate de llamar a getWritableDatabase() o getReadableDatabase() en un hilo en segundo plano, por ejemplo con AsyncTask o IntentService.

Para usar SQLiteOpenHelper, crea una subclase que sobreescriba los métodos de retrollamada onCreate(), onUpgrade() y onOpen(). También puedes querer implementar onDowngrade(), pero no es obligatorio.

Por ejemplo, a continuación se muestra una implementación de SQLiteOpenHelper que utiliza algunos de los comandos mostrados anteriormente:

public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // Si cambias el esquema de la base de datos, debes incrementar la versión.
    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) {
        // Esta base de datos no es más que una caché para datos online, por lo que
        // su política de actualización consiste sólo en descartar los datos y
        // volver a empezar
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

Para acceder a tu base de datos, instancia tu subclase de SQLiteOpenHelper:

FeedReaderDbHelper mDbHelper = new FeedReaderDbHelper(getContext());

Guardar información en una base de datos


Inserta información en la base de datos pasando un objeto ContentValues al métood insert():

// Obtenemos el repositorio de datos en modo escritura
SQLiteDatabase db = mDbHelper.getWritableDatabase();

// Creamos un nuevo mapeo de valores, donde las claves son los nombres de las
// columnas
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_CONTENT, content);

// Insertamos la nueva tupla, devolviendo la clave primaria de la nueva tupla
long newRowId;
newRowId = db.insert(
         FeedReaderContract.FeedEntry.TABLE_NAME,
         FeedReaderContract.FeedEntry.COLUMN_NAME_NULLABLE,
         values);

El primer argumento de insert() es simplemente el nombre de la tabla. El segundo argumento es el nombre de una columna en la que la plataforma puede insertar NULL en el caso de que ContentValues esté vacío (si en su lugar le das un valor de "null", la plataforma no insertará una tupla cuando no haya valores).

Leer información de una base de datos


Para leer de una base de datos, utiliza el método query(), pasando tus criterios de selección y las columnas deseadas. El método combina elementos de insert() y update(), excepto que la lista de columnas define los datos que queremos obtener, en lugar de los datos a insertar. El resultado de esta consulta se devuelve en forma de un objeto Cursor.

SQLiteDatabase db = mDbHelper.getReadableDatabase();

// Define una proyección que especifica qué columnas de la base de datos usarás.
String[] projection = {
    FeedReaderContract.FeedEntry._ID,
    FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE,
    FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED,
    ...
    };

// Cómo quieres que se ordenen los resultados en el Cursor
String sortOrder =
    FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED + " DESC";

Cursor c = db.query(
    FeedReaderContract.FeedEntry.TABLE_NAME,  // La tabla a consultar
    projection,                               // Las columnas a devolver
    selection,                                // Las columnas para la cláusula WHERE
    selectionArgs,                            // Los valores para la cláusula WHERE
    null,                                     // no agrupar las filas
    null,                                     // no filtrar por grupos de filas
    sortOrder                                 // El orden a utilizar
    );

Para obtener una fila del cursor, utiliza uno de los métodos de movimiento de Cursor, a los cuáles siempre debes llamar antes de empezar a leer valores. Generalmente, deberías empezar llamando a moveToFirst(), que coloca la "posición de lectura" en la primera entrada de los resultados. Para cada fila, puedes leer un valor de una columna llamando a uno de los métodos get del Cursor, como getString() o getLong(). Para cada método get, debes pasar el índice de la columna que desees, el cuál puedes obtener llamando a getColumnIndex() o getColumnIndexOrThrow(). Por ejemplo:

cursor.moveToFirst();
long itemId = cursor.getLong(
    cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID)
);

Borrar información de una base de datos


Para borrar tuplas de una tabla, necesitas proporcionar criterios de selección que identifiquen las tuplas. La API de base de datos proporciona un mecanismo para crear criterios de selección que proteje contra ataques de inyección SQL. El mecanismo divide la especificación de la selección en una cláusula de selección y en los argumentos de la selección. La cláusula define las columnas a comprobar, y también te permite combinar condiciones de columnas. Los argumentos son los valores contra los que probar, que se vinculan a la cláusula. Dado que el resultado no se maneja como una sentencia SQL normal, es inmune a la inyección SQL.

// Definimos la parte del 'where' de la consulta.
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
// Especificamos los argumentos en el orden de las posiciones reservadas.
String[] selelectionArgs = { String.valueOf(rowId) };
// Ejecutamos la sentencia SQL.
db.delete(table_name, selection, selectionArgs);

Actualizar una base de datos


Cuando necesites modificar un subconjunto de los valores de tu base de datos, utiliza el método update().

Las actualizaciones combinan la sintaxis de los valores de insert() con la sintaxis del where de delete().

SQLiteDatabase db = mDbHelper.getReadableDatabase();

// Nuevo valor para una columna
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);

// Qué fila actualizar, basándose en el ID
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selelectionArgs = { String.valueOf(rowId) };

int count = db.update(
    FeedReaderDbHelper.FeedEntry.TABLE_NAME,
    values,
    selection,
    selectionArgs);