Bases de Datos: Guardar y Recuperar imágenes (API 2.0)

Cómo guardar y recuperar imágenes en una base de datos siempre ha sido una de las preguntas más recurrentes entre las que he recibido, y como respuesta a la misma ya se publicó en 2016 un tutorial en el que se mostraba una forma sencilla de cómo hacerlo.

Desde entonces Xojo ha cambiado sustancialmente, y uno de los cambios más notables no ha sido otro sino la introducción de la API 2.0 con la que se facilitan la comprobación de errores y también se modifica el lenguaje (entre otros aspectos). Por tanto, y como respuesta a una reciente consulta de un usuario de Xojo sobre este mismo tema, he decidido retomar y actualizar el tutorial para adecuarlo precisamente a la API 2.0.

Como verás se ha simplificado sobremanera todo el asunto, dado que ahora ya no es necesario enredarse con el tema de los PreparedStatements tal y como era necesario en la API 1.0. El paso de los parámetros sobre los campos variables de una sentencia SQL se realiza ahora en la misma instrucción.

También se mejora todo el tratamiento de errores, puesto que ahora cualquiera de las operaciones realizadas sobre instancias de bases de datos arrojan una excepción que podemos capturar en el caso de que se produzca algún error.

Teniendo esto en cuenta, veremos a continuación una de las formas en las que podemos guardar imágenes JPEG en una base de datos SQLite (esto mismo puede servirte también cuando utilices otros motores de bases de datos y también otros formatos gráficos, si bien quizá tengas que modificar la sentencia SQL para adaptarla a las necesidades específicas en cada caso).

Puedes descargar el proyecto de ejemplo utilizado desde este enlace

Diseñar la interfaz de usuario

Nuestra interfaz de usuario va a ser realmente sencilla:

  • Un TextField en el que introduciremos el texto asociado a cada una de las imágenes que deseemos añadir a la base de datos.
  • Un ListBox que contendrá el texto asociado con cada una de las imágenes añadidas a la base de datos.
  • Un ImageWell que mostrará el previo de la imagen seleccionada por el usuario, y que será la insertada en la base de datos.
  • El botón (PushButton) encargado de ejecutar el código que insertará la imagen, junto con el texto asociado, como un nuevo registro en la tabla de la base de datos.

Para ello, puedes arrastrar cada uno de dichos controles sobre la ventana por omisión Window1 de modo que el diseño de la interfaz sea similar a la mostrada en la siguiente captura de pantalla:

Utiliza el Editor de Diseño, junto con el Panel Inspector con cada control, para definir los anclajes encargados de mantener la posición y variar el tamaño de los controles en respuesta a los cambios de tamaño de la ventana.

Crear una instancia de base de datos

En nuestro tutorial trabajaremos con una base de datos SQLite (archivo) que ya está creada, básicamente para ahorrarnos la necesidad de crear desde cero tanto la estructura de la base de datos como su almacenamiento mediante código; cuestiones ambas que ya han sido tratadas previamente en múltiples tutoriales.

A la hora de trabajar con bases de datos SQLite todo lo que necesitamos en nuestras aplicaciones Xojo son contemplar básicamente los siguientes pasos:

  1. Crear una propiedad en nuestro proyecto que no se destruya durante toda la ejecución de la aplicación; es decir, el objeto ha de perdurar mientras que deseemos utilizarlo. Un buen sitio para ello es declarar dicha propiedad bajo el objeto App o, en nuestro caso, en la ventana por omisión Window1. Para ello definiremos la propiedad en Window1 con el nombre DB y asignando el tipo SQLiteDatabase mediante el Panel Inspector.
  2. Definir una propiedad es como definir una variable cualquiera. Es decir, por si mismas no hacen referencia a ningún valor o instancia hasta que no la inicialicemos. Hasta ahora, todo lo que sabe nuestra aplicación es que la propiedad DB sólo podrá contener referencias a instancias cuyo tipo sean SQLiteDatabase. Por tanto, aun tendremos que crear una instancia y asignarla a dicha propiedad; es decir, inicializarla. Esto es algo que podemos hacer añadiendo el Evento Open en Window1 y escribiendo el siguiente fragmento de código:
// Asignamos la nueva instancia SQLite a la propiedad db
db = New SQLiteDatabase
Try
  // Asignamos el archivo de base de datos SQLite a la instancia db
  // e intentamos conectar con dicho archivo
  db.DatabaseFile = New folderitem("imagenes.sqlite")
  db.Connect
  // Obtenemos la columna 'name' de todos los registros de la base de datos
  // para añadir los nombres de las imágenes al ListBox de la ventana.
  Var rs As RowSet = db.SelectSQL("select name from images order by name asc")
  For Each r As DatabaseRow In rs
    Listbox1.AddRow r.Column("name").StringValue
  Next
// En el caso de que se produzca un error en las operaciones
// con la base de datos, lo capturamos y mostramos el mensaje
// que describe el error en cuestión
Catch e As DatabaseException
  MessageBox e.Message
End Try

Como puedes ver, uno de los cambios fundamentales con la API 2.0 es que ahora debemos envolver todas las operaciones realizadas sobre bases de datos dentro de un bloque Try… Catch, de forma que si se produce un error lo podamos capturar y mostrar la información detallada sobre el error en cuestión. Esto evita la necesidad que existía previamente de realizar diversas comprobaciones mediante bloques If… Then… Else, simplificando así la cantidad de código necesaria y también su legibilidad.

Por otro lado, la creación de una nueva instancia SQLite y su asignación a nuestra propiedad se realiza en la línea:

db = New SQLiteDatabase

A partir de que se ejecute dicha línea de código, ya podremos operar sobre la propiedad DB accediendo a todos los métodos y propiedades disponibles en la clase SQLiteDatabase. Esto es lo que se hace en la líneas siguientes para sociar, por ejemplo, sobre la propiedad DatabaseFile una nueva instancia de la clase FolderItem que apunta al archivo de base de datos SQLite con el nombre “imagenes.sqlite” y que reside en la misma carpeta del proyecto.

Es decir, hasta este punto nuestra propiedad DB ya referencia una instancia válida de la clase SQLiteDatabase y, además, le hemos indicado como se llama y donde se encuentra el archivo de base de datos propiedamente dicho con el que debe trabajar. Por tanto, el siguiente paso será conectar con el archivo de base de datos propiamente dicho, algo que hacemos invocando el método Connect mediante la siguiente instrucción de código:

db.Connect

Recuerda que si en cualquiera de los anteriores pasos se hubiese producido algún error, este sería capturado mediante el correspondiente Catch y nos mostraría el mensaje de error correspondiente.

Seleccionar archivo de imagen… y guardarlo en la base de datos

Una vez se ha ejecutado el código del evento Open en nuestra ventana por omisión (Window1) ya contamos con una referencia válida para trabajar con la base de datos SQLite subyacente (el archivo “imagenes.sqlite”). Por tanto, en este punto nos encargaremos de escribir el código encargado de permitirnos seleccionar cualquier imagen JPEG y guardarla como un nuevo registro en la base de datos. Si la operación tiene éxito, dicha imagen no sólo se mostrará en el ImageWell que hemos añadido a nuestra interfaz de usuario minimalista, sino que también se añadirá una nueva entrada en el ListBox de nuestra interfaz de usuario con el nombre que le hayamos asignado.

Para simplificar al máximo el proyecto de ejemplo, todo esto ocurre en el evento Action que habremos añadido en el objeto PushButton1 de nuestra interfaz de usuario. A continuación escribiremos el siguiente código en el Editor de Código de dicho evento, y que será el que se ejecute cada vez que el usuario pulse el botón en cuestión:

If TextField1.Value <> "" Then
  // Displaying dialog to select a picture and asigning it to a FolderItem variable
  Var f As FolderItem = FolderItem.ShowOpenFileDialog("image/jpeg")
  // Go ahead if the FolderItem variable is a valid instance (not nil)
  If  f <> Nil Then
    // Assigning the selected picture to the ImageWell
    ImageWell1.Image = Picture.Open( f )
    // Getting picture data as String
    Var d As String = ImageWell1.Image.ToData(Picture.Formats.JPEG)
    Try
      // Inserting the image as string (blob) into the database table, using implicit preparedstatment
      db.ExecuteSQL("insert into images(name, image) values(?,?)", TextField1.value, d)
      // Adding assigned image name to the ListBox
      Listbox1.AddRow TextField1.Value
    Catch e As DatabaseException
      // Something went wrong…
      MessageBox e.Message
    End Try
  End If
Else
  // Image name is mandatory, so it can't be empty
  MessageBox "Please, type a name first for the inserted image"
End If

Lo primero que comprobamos es que el TextField contenga texto, puesto que en nuestro programa es obligatorio que cada una de las imágenes que insertemos en la base de datos tenga asociado un nombre (o cualquier texto, para el caso). Si no es así, entonces se obvia el resto del código y se muestra un mensaje indicando que debe introducirse texto en el campo.

La parte que nos interesa viene a continuación, y que es la ejecutada en el caso de que TextField1 no esté vacío. Como puedes ver, lo primero que haremos será asignar a la variable f la instancia de FolderItem resultante de invocar el método FolderItems.ShowOpenFileDialog. En dicho método pasamos como parámetro el filtro “image/jpeg” para indicar que se permita sólo la selección de aquellos archivos de imagen correspondientes con dicho formato.

Cuando la aplicacion ejectua dicha línea de código se mostrará el típico cuadro de diálogo en el que nos permite navegar por los dispositivos de almacenamiento disponibles, recorriendo sus directorios y/o carpetas, hasta que seleccionemos un archivo compatible o bien cancelemos la operación. Cuando seleccionamos un archivo y pulsamos el botón OK en el cuadro de diálogo, entonces se cerrará el cuadro de diálogo y se asignará a la variable f la instancia válida de FolderItem que apunta al archivo seleccionado. Por el contrario, si se pulsa el botón Cancelar en el cuadro de diálogo, entonces este se cerrará y la variable f seguirá sin inicializar; esto es, apuntará al valor Nil.

Precisamente por esto nos encargamos en la siguiente línea de código de comprobar si la variable f contiene una referencia válida a un objeto FolderItem. De ser así, entonces utilizaremos el método compartido Open de la clase Picture (recuerda que para utilizar un método compartido no es preciso que se cree previamente una instancia de la clase), y asignaremos el resultado a la propiedad Image de nuestra instancia ImageWell1.

Sin embargo, el paso realmente importante es el de convertir los datos binarios de la imagen JPEG en una secuencia de caracteres de texto que nos permitirá insertarlo posteriormente en la columna Image (definida como BLOB) en nuestra base de datos.

Esta operación es la que llevamos a cabo mediante la siguiente línea de código:

Var d As String = ImageWell1.Image.ToData(Picture.Formats.JPEG)

A continuación, y nuevamente contenida en un bloque Try… Catch nos encargamos de ejecutar sobre la base de datos la instancia SQL que se encargará de añadir un nuevo registro utilizando como parámetros tanto el texto que hayamos introducido en TextField1 como la secuencia de caracteres (nuestra imagen JPEG) que hemos obtenido en el paso anterior.

Lo importante en este caso es que observes una de las principales mejoras de la API 2.0, y es que ahora el binding entre los marcadores de posición (los definidos con el caracter “?” en la sentencia SQL) y las variables propiamente dichas se realiza como parte del método ExecuteSQL, evitando así la necesidad de tener que utilizar toda la parafernalia de PreparedStatements tal y como era necesario en el caso de que escribir código correspondiente a la API 1.0 de Xojo. ¡Mucho más sencillo!

En fin, la responsable de crear un nuevo registro insertando nuestra imagen como BLOB en la base de datos se realiza mediante la ejecución de una sóla línea de código:

db.ExecuteSQL("insert into images(name, image) values(?,?)", TextField1.value, d)

Sencillo y protegidos contra los clásicos ataques de inyección SQL.

Recuperar imágenes desde la base de datos

Ya sólo nos queda realizar la operación inversa; es decir, leer los campos correspondientes a un registro de la base de datos y volver a transformar el BLOB en una imagen (Picture) que podamos utilizar en nuestro código Xojo.

Para ello añadiremos al ListBox1 el evento Change; de modo que cuando el usuario de la aplicación seleccione cualquier entrada del ListBox se lance una consulta contra la base de datos con la intención de recuperar el registro cuya columna name se corresponda con el texto de la fila seleccionada en el ListBox. Para ello, una vez añadido el evento Change copiaremos el siguiente código en el Editor de Código asociado con dicho evento:

If Me.SelectedRowIndex <> -1 Then
  Try
  // We try to get a RowSet with the image whose name equals to the selected row text in the ListBox
    Var rs As RowSet = db.SelectSQL("select image from images where name=?", Me.SelectedRowValue)
    If rs <> Nil Then
      // If we reach this point, then the RowSet is valid; so we transform the string
      //  (from the RowSet "image" column" into a Picture
      // assigning it to the ImageWell
      ImageWell1.Image = Picture.FromData( rs.Column("image").StringValue )
    End If
  Catch e As DatabaseException
    // Something went wrong with the database query
    MessageBox e.Message
  End Try
End If

Lo primero que comprobamos es que el cambio de fila en el ListBox se corresponda con una fila que contenga datos (si se hace clic con el ratón en una área vacía del ListBox, entonces se ejecutará el evento Change pero la propiedad SelectedRowIndex tendrá el valor -1 para indicar que no hay ninguna fila seleccionada).

Si realmente se ha seleccionado una fila con datos, entonces pasaremos a obtener un RowSet (RecordSet en la API 1.0) como resultado de ejecutar la consula SQL:

Var rs As RowSet = db.SelectSQL("select image from images where name=?", Me.SelectedRowValue)

Observa que se pasa como parámetro el contenido de la fila seleccionada, para lo cual invocamos el método SelectedRowValue del ListBox.

Para simplificar, en este ejemplo se da por hecho que sólo obtendremos un registro coincidente con el texto correspondiente a la fila seleccionada (podemos definir una restricción en la definición de la tabla en la base de datos para que esto sea así). De modo que si el RowSet contiene una instancia válida como resultado de la consulta, simplemente nos limitamos a convertir los datos obtenidos como caracteres de la columan “image” nuevamente en una imagen válida. Para ello, utilizamos ahora el método compartido FromData de la clase Picture, y el resultado lo asignamos nuevamente al control ImageWell1 de nuestra interfaz de usuario.

Conclusiones

¡Y eso es todo! Como puedes ver, la cantidad de código es realmente mínima, y en realidad son sólo unas pocas líneas de código las responsables reales de convertir nuestra imagen en datos que podemos insertar en la base de datos… y de realizar también la operación inversa. Todo ello con un control de errores en las operaciones de la base de datos mediante el uso de Try… Catch, y también observando en todo momento la protección frente a ataques de inyección SQL.

Por supuesto, puedes ampliar fácilmente este proyecto para que sea compatible también con otros formatos de imagen, así como introducir otra serie de comprobaciones y mejoras según tus necesidades.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *