Saltar la navegación

4.4.3. Bases de datos

Una base de datos o BBDD es un software que permite almacenar y recuperar datos mediante consultas inteligentes, manteniendo la coherencia entre las estructuras de datos. Existen principalmente dos tipos de BBDD: relacionales y no relacionales.

Para la mayoría de los casos de uso, una BBDD relacional será idónea. Por ello, los sistemas operativos móviles tienen integrado software de gestión de bases de datos para que las aplicaciones puedan utilizarlas.

Cuando creamos una base de datos desde nuestra aplicación, básicamente estamos creando un archivo que el sistema guardará en la carpeta privada de la app, de la misma forma que con los archivos del almacenamiento interno. De este modo se mantiene la privacidad de los datos, pues el directorio de la aplicación no es accesible para otros usuarios o aplicaciones.

Desde las primeras versiones de iOS y Android, la mejor forma de mantener una base de datos local ha sido mediante SQLite. Aunque existen muchos otros, el sistema de gestión de base de datos SQLite está escrito en C, por lo que es muy eficiente, es simple pero completo y ocupa muy poco.

Hoy en día sigue siendo el preferido y está preinstalado en el sistema. Sin embargo, debemos tener algunas consideraciones antes de usar SQLite de forma directa, porque los sistemas relacionales como este tienen algunos inconvenientes. Su lenguaje de consulta, llamado SQL (Structured Query Language), es potente, pero nada tiene que ver con los lenguajes de programación orientados a objetos actuales, de modo que existe un desacople entre el código de la aplicación y el que entiende la base de datos.

Para solucionar ese problema, podemos usar una librería ORM (Object Relational Mapping) que traducirá de un lenguaje al otro dentro de la aplicación sin esfuerzo por nuestra parte. Existen muchas librerías que nos ayudan a usar SQLite, pero actualmente la preferida es Room.

SQL es sencillo y potente, solo requiere algo de estudio y práctica. Sin embargo, como programadores, no seremos nosotros los que hablemos con la BBDD, será nuestra aplicación.

Veamos entonces cómo podríamos crear y usar una base de datos SQLite desde nuestra aplicación Android con las funciones integradas de bajo nivel, es decir, sin utilizar ningún ORM como Room que nos facilite las cosas.

Las tablas

Imaginemos que nuestra aplicación es un videojuego y necesitamos guardar los datos de los usuarios y los puntos de cada partida. Con el DDL de SQL crearíamos dos tablas:

CREATE TABLE jugadores (
    jugador_id INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL UNIQUE,
    avatar TEXT,
    cinturon INTEGER,
);
CREATE TABLE partidas (
    partida_id INTEGER PRIMARY KEY,
    jugador_id INTEGER,
    fecha INTEGER,
    puntos INTEGER,
    nivel INTEGER
);

La tabla jugadores almacenará a los jugadores que han echado una partida hasta ahora. Sus campos serán:

  • jugador_id, un identificador único para identificar al jugador;
  • nombre, que será el alias del jugador en la interfaz gráfica del juego;
  • avatar, que será una URL o ruta del sistema de archivos hacia una imagen que identifique gráficamente al jugador en el juego;
  • cinturon, que no es más que el nivel que ha alcanzado el jugador en el juego por puntos o niveles superados.

Por otro lado, la tabla partidas guardará los datos de todas las partidas jugadas por los diferentes jugadores, y sus campos serán:

  • partida_id, que es el identificador único de cada partida;
  • jugador_id, que identifica qué jugador en la partida;
  • fecha de la partida;
  • puntos conseguidos por el jugador en la partida;
  • nivel máximo alcanzado en la partida.

Las consultas

Imaginemos que un nuevo jugador echa una partida: mediante el DML de SQL guardaríamos esa nueva información:

INSERT INTO jugadores (jugador_id, nombre, avatar, cinturon) VALUES (69, “Ninja Rookie”, “http://img.com/ninja”, 0)
INSERT INTO partidas (partida_id, jugador_id, fecha, puntos, nivel) VALUES (3, 69, strftime('%s','now'), 30, 2)

Con el comando INSERT INTO introducimos los datos del jugador Ninja Rookie con el código identificador 69 en la tabla jugadores, y la partida que ha jugado con el identificador 3 y los 30 puntos obtenidos hasta el nivel 2 del juego se guardan en partidas.

Como SQLite no tiene un tipo de datos para fechas, usaremos un número entero, como el número de segundos desde 1970, una forma clásica de contar el tiempo en computación (timestamp). De ahí la función strftime, que convierte la fecha now a segundos %s y lo pasa a entero. Cuando después quisiéramos recuperar los datos del jugador 69, haríamos la consulta:

SELECT * FROM jugadores WHERE jugador_id = 69

Y si quisiéramos recuperar todas las partidas del jugador 69 haríamos:

SELECT * FROM partidas WHERE jugador_id = 69

Estructuramos en Android

Lo primero es definir las estructuras que almacenarán nuestros datos, las tablas. Para ello heredamos de la clase SQLiteOpenHelper, que será la responsable de mantener esas
estructuras en el archivo de base de datos:

class JuegoDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    ....
}

Como vemos, necesita un contexto, el nombre del archivo donde se almacenará la BBDD y la versión actual, que como en nuestro caso será la primera, podríamos definir como 1.

Después sobrescribimos la función onCreate para definir las tablas que darán forma a nuestra base de datos:

override fun onCreate(db: SQLiteDatabase) {
    db.execSQL(SQL_CREATE_JUGADORES)
    db.execSQL(SQL_CREATE_PARTIDAS)
}

Se nos pasa un objeto db de base de datos y llamamos a su execSQL, una función que ejecutará el código SQL que se le pase.

Las constantes string SQL_CREATE_JUGADORES y SQL_CREATE_PARTIDAS que más tarde mostraremos son el código SQL para crear las tablas que deseamos. Si mientras creamos la BBDD desde nuestra aplicación resulta que ya existía otra con una versión anterior, se llamará a onUpdate, que será la encargada de actualizar los datos antiguos a la nueva estructura:

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    db.execSQL(SQL_DELETE_JUGADORES)
    db.execSQL(SQL_DELETE_PARTIDAS)
    onCreate(db)
}

Para no complicar el código, y puesto que solo tenemos una versión de nuestras tablas, hacemos lo más sencillo, que es eliminar las tablas antiguas y volverlas a crear con la nueva estructura, sin tener en cuenta que perderemos los datos de la versión previa en caso de que los hubiera. El código completo sería:

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class JuegoDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    // Se llama cuando la base de datos aun no existe y debe crearse
    override fun onCreate(db: SQLiteDatabase) {
        // Ejecutamos el SQL que crea las tablas
        db.execSQL(SQL_CREATE_JUGADORES)
        db.execSQL(SQL_CREATE_PARTIDAS)
    }
    // Se llama cuando hemos modificado la estructura de las tablas
    // y hemos incrementado el número de la versión pero aún tenemos
    // una BBDD antigua con una versión anterior, aquí se actualizará
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // Ejecutamos el SQL que borra las tablas,
        // después el que las crea con la nueva versión
        db.execSQL(SQL_DELETE_JUGADORES)
        db.execSQL(SQL_DELETE_PARTIDAS)
        onCreate(db)
    }
    companion object {
        // Si necesitamos cambiar las tablas una vez que la
        // app está en producción, debemos incrementar este numero
        const val DATABASE_VERSION = 1
        const val DATABASE_NAME = "juego.db"
        const val TABLE_JUGADORES = "jugadores"
        private const val SQL_CREATE_JUGADORES = 
            "CREATE TABLE $TABLE_JUGADORES (" + 
            "jugador_id INTEGER PRIMARY KEY," + 
            "nombre TEXT NOT NULL UNIQUE," + 
            "avatar TEXT," + 
            "cinturon INTEGER"
        private const val SQL_DELETE_JUGADORES = "DROP TABLE IF EXISTS $TABLE_JUGADORES"
        const val TABLE_PARTIDAS = "partidas"
        private const val SQL_CREATE_PARTIDAS = 
            "CREATE TABLE $TABLE_PARTIDAS (" + 
            "partida_id INTEGER PRIMARY KEY," + 
            "jugador_id INTEGER NOT NULL," + 
            "fecha INTEGER NOT NULL," + 
            "puntos INTEGER NOT NULL," + 
            "nivel INTEGER NOT NULL"
        private const val SQL_DELETE_PARTIDAS = "DROP TABLE IF EXISTS $TABLE_PARTIDAS"
    }
}

Introducir datos

Hemos definido los comandos SQL como constantes string para tenerlos bien definidos solo en un lugar y no desperdigados por el código. Ahora podríamos introducir datos en nuestra nueva base de datos. Como programamos con Kotlin, nuestros datos serán objetos de alguna clase. Definamos nuestra clase Jugador, asociada a la tabla jugadores:

data class jugador(
    val id: Int,
    val nombre: String,
    val avatar: String?,
    val cinturon: Int?)

Nada más que un data class de Kotlin con los campos de nuestro jugador. Añadamos una función a la clase para insertar el propio objeto en la BBDD:

fun introducirEnBBDD(db: SQLiteDatabase) {
    // Juntamos todos los campos en una colección ContentValues
    val values = ContentValues().apply {
        put("nombre", jugador.nombre)
        put("avatar", jugador.avatar)
        put("cinturon", jugador.cinturon)
    }
    // Insertamos la fila de datos, y el sistema devuelve el id
    id = db?.insert(JuegoDbHelper.TABLE_JUGADORES, null, values)
}

Esta nueva función introducirEnBBDD recibe por parámetro un objeto SQLiteDatabase que crearemos mediante nuestro JuegoDbHelper. Este objeto de base de datos nos permite insertar, seleccionar, actualizar y borrar. Introducimos los datos en un objeto ContentValues, que no es más que un objeto contenedor de pares clave-valor, y lamamos a la función insert, que devolverá el identificador de la fila insertada en la tabla. Para introducir un nuevo jugador, el código sería:

val dbHelper = JuegoDbHelper(context)
val db = dbHelper.writableDatabase //Creamos base de datos en modo escritura
val jugador = Jugador(69, "Ninja Rookie", "http://img.com/ninja.png", 0)
jugador.insertarEnBBDD(db)

Primero creamos nuestra clase de ayuda para BBDD. Con ese objeto creamos nuestra base de datos db. Luego creamos el Jugador, y llamamos a su método insertarEnBBDD con el objeto db.

Conclusión

Utilizar las funciones de bajo nivel para manejar nuestra base de datos es posible, pero tiene ciertos inconvenientes e incomodidades:

  • requiere mucho código para cada tabla.
  • Pasar un objeto de Kotlin a una fila de base de datos SQL es incómodo y repetitivo, entre otras cosas porque para cada acción de BBDD necesitamos desgranar los campos del objeto en pares clave-valor.
  • Necesitamos conocer el lenguaje SQL para definir las tablas y sus relaciones, y para codificar las consultas de selección, actualización y borrado.

Room

Room, una librería con la que crear y acceder a los datos de nuestra BBDD SQLite de forma fácil, eficiente y orientada a objetos. Veamos cómo utilizar este ORM.