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.
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.