El API básico de SQLite
Acceder a la BD
Vamos a ver aquí cómo abrir la base de datos para comenzar a trabajar con ella.
Abrir y cerrar la BD
En el API C de SQLite la base de datos se representa en nuestro código mediante un puntero a un struct
de tipo sqlite3
. En swift podemos representar un puntero de C con el tipo OpaquePointer
, así que en nuestro código tendremos algo como
Para abrir la BD se usa la función sqlite3_open
, a la que hay que pasarle el path de la base de datos y la referencia a este struct
Vamos a crear una clase DBManager
en la que implementaremos los métodos que interactúan con la base de datos. En esta clase creamos una variable miembro privada para almacenar la referencia a la base de datos.
Podemos implementar un inicializador que abra la base de datos suponiendo que está incluida en el bundle
Del código anterior debemos destacar los siguientes aspectos:
Las llamadas al API de SQLite devuelven la constante
SQLITE_OK
si no ha habido error, así que para saber sisqlite3_open
ha tenido éxito comprobamos si ha devuelto este valor.Si se produce un error, para ver cuál es podemos usar la función
sqlite3_errmsg
, que devuelve el mensaje del error más reciente asociado a una llamada hecha al API de SQLite. Al ser código C el mensaje de error se devuelve como unchar *
, ya que en C no existe el tipoString
. En nuestro código convertimos este tipo a unString
de Swift, para poder tratarlo (imprimirlo, en nuestro caso). Esta conversión la hacemos con el inicializadorString(cString:)
A la inversa, las funciones C que requieren un
char *
como parámetro aceptan directamente unString
de Swift. Esto por ejemplo pasa al hacer elsqlite3_open
, que en realidad requiere que se le pase unchar *
con el path de la BD.
Para cerrar la base de datos se usa la función sqlite3_close()
a la que se le pasa como parámetro el puntero al struct con la información de la base de datos.
Abrir la BD con “permiso de escritura”
Un problema que tiene incluir físicamente la base de datos en el bundle de la aplicación es que este es de solo lectura. Eso quiere decir que no podríamos ejecutar consultas de actualización sobre la base de datos.
La forma más habitual de resolver este problema es crear una copia de la base de datos en algún directorio con permiso de escritura cuando se arranca la aplicación. Típicamente se usa el directorio Documents
ya que es el más apropiado para este propósito.
Aquí tenemos un método que copia la BD del bundle al directorio Documents
, si es que no se ha copiado ya:
Tendremos que modificar el inicializador del apartado anterior para que llame a este método y después no abra la base de datos del bundle sino la de Documents
:
Consultas de selección
Compilar y ejecutar una consulta de selección
El esquema general para ejecutar una consulta de selección y recorrer los registros resultantes es el siguiente:
Compilar la query (convertirla de SQL en "modo texto" a un formato "ejecutable" por SQLite)
Ir avanzando registro a registro por los resultados, mientras queden registros
Obtener los campos que nos interesen del registro actual, sabiendo el número de columna que ocupan. Las columnas empiezan en 0
Habitualmente “empaquetaremos” los valores del registro actual en un objeto o
struct
de Swift, e iremos construyendo una lista de resultados (un array es lo más sencillo).
Liberar la memoria ocupada por la query compilada
Vamos a ver un ejemplo de código que implementa este esquema:
Los dos primeros argumentos son el
struct
que representa la base de datos y la cadena con la query.Aunque en el ejemplo no usamos esta funcionalidad se podrían poner varias sentencias SQL en la misma cadena y compilar solo una de ellas. El tercer argumento de indica la longitud en caracteres que queremos tomar de la query (-1 indica leer toda la cadena).
Con el último argumento SQLite nos informa qué parte de query queda por ejecutar, en este caso pasamos
nil
ya que no nos interesa esta funcionalidad.
Vamos avanzando por los registros con
sqlite_step()
. Mientras que esta función devuelvaSQLITE_ROW
tenemos una nueva fila y podemos obtener sus campos consqlite_column_XXX()
, dondeXXX
es el tipo de campoDe nuevo tenemos el problema de que como el API de SQLite está en C, devuelve
char *
para los campos de tipo cadena, y noString
por lo que debemos hacer la conversión con el inicializadorString(cString:)
Aquí usamos la estrategia de “empaquetar” los datos obtenidos de la BD en un objeto y guardar todos los objetos en un array.
Finalmente
sqlite3_finalize
libera la memoria ocupada por la query
Fechas en SQLite
Consultas con parámetros
En el ejemplo del apartado anterior la consulta SQL era una cadena fija pero en muchos casos necesitaremos parametrizar la consulta (por ejemplo obtener todos los alumnos cuyo apellido empiece por una letra, o los nacidos después de una fecha determinada).
Aunque podemos construir el String
concatenando subcadenas es más "limpio" usar parámetros, que además nos protegen contra posibles intentos de inyección SQL. Al igual que en la mayoría de BD los parámetros se especifican con el símbolo ?
Para instanciar el parámetro con un determinado valor usaremos la familia de funciones sqlite3_bind_XXX
, en la que las XXX
indican el tipo de datos del parámetro (int
, double
, text
,…). Esta instanciación se debe hacer después de "preparar" la query con sqlite3_prepare_v2
pero antes de ejecutarla con sqlite3_step()
.
Por ejemplo, vamos a buscar todas las personas que tengan más de 18 años (cuya fecha de nacimiento sea anterior a la actual restándole 18 años)
Nótese que para instanciar un parámetro debemos conocer su posición y que las posiciones de los parámetros comienzan en 1, no en 0.
En SQLite también se pueden definir parámetros por nombre con el formato :nombre
, por ejemplo
No obstante el API C no nos permite instanciar el parámetro directamente por nombre. Primero debemos obtener su posición con la función sqlite3_bind_parameter_index(sentencia,nombre)
y luego aplicar la ya conocida sqlite3_bind_XXX()
.
Consultas de actualización
Como ya hemos comentado, para que se puedan ejecutar consultas de actualización sobre la base de datos esta tiene que estar almacenada en un directorio con permisos de escritura
Las consultas de actualización se manejan de un modo similar a las de selección: se preparan con sqlite3_prepare_v2()
, se vinculan los parámetros si estos existen con sqlite3_bind_XXX()
y se ejecutan con sqlite3_step()
. Por supuesto en ellas no hay bucle para recorrer los resultados.
Como vemos, cuando se ejecuta con éxito una consulta de actualización, la ejecución devuelve el valor SQLITE_DONE
.
Podemos contar cuántas filas han sido afectadas por la consulta de actualización (en el ejemplo cuántas filas se han insertado, o sea 1) con la función sqlite3_changes()
a la que hay que pasarle el struct
que representa a la base de datos:
Last updated
Was this helpful?