Home
  Software
    HowTo
      MySQL glbos
        Intro

Idioma:
  English

Como almacenar información binaria en una tabla MySQL

El problema

MySQL require que otros programas envien sus comandos en un idioma especial. Este idioma (SQL) es mas natural para humanos, y usuarios que conocen el idioma pueden confeccionar las instrucciones para el motor MySQL (relativamente) facilmente. Las instrucciones se representan en un idioma natural (aunque basado en Ingles).

Este método funciona bien en un gran número de casos, cuando los datos a transferir a MySQL son 'normales' - textos o valores numéricos representables en text. Imágenes, código de programa u otros datos 'crudos' no respetan las reglas de texto, y los elementos (bytes) pueden tomar cualquier valor. Algunos de estos valores causan confusión en la comunicación de la orden entre el programa y el motor MySQL.

Un ejemplo de un código 'peligros' el el valor binario 0. Es un valor frecuentemente encontrado en datos binarios, pero nunca en textos (ni siquiera en la representación de caracteres internacionales en unicode). Entonces, si necesitamos comunicar el código 0 a MySQL, necesitamos marcar al '0' como peligroso, requeriendo un trato especial.

El truco utilizado por MySQL es de prefijar caracteres peligrosos con un caracter de aviso. Este caracter es la barra inversa (backslash) ('\'). Entonces, si queremos enviar un 0 a MySQL, enviaremos primero la barra inversa - avisando que sigue un caracter especial - y luego el caracter en sí. Luego del aviso, MySQL sabe que no tiene que procesar el caracter de la forma habitual, sino salvarlo literalmente. El caracter de aviso se llama caracter de escape.

Por supuesto, eso hace que la misma barra inversa, ahora tiene un valor especial! No pudemos enviar una barra inversa directamente, ya que MySQL consideraría el próximo caracter como algo especial. Para enviar la barra inversa, necesitamos escaparla con otra barra inversa (i.e. '\\')!

La solución

Hacer este trabajo a mano sería increiblemente laborioso. La librería MySQL nos brinde una función que hace el trabajo automáticamente: mysql_real_escape_string(). Esta función toma cualquier bloque de datos binarios y lo convierte en una cadena válida para enviar como parte de una sentencia SQL.

Ojo - los datos no se convierten en legibles! Imprimiendo la sentencia SQL todavía parecerá una 'sopa' de caracteres... Pero MySQL, recibiendo esta cadena, será capaz de convertir la cadena en sus valores correctos antes de salvarlos en la base de datos.

Un ejemplo sencillo

En la próxima sección presento un ejemplo relativamente sencillo, mostrando como guardar un bloque de bytes en una table. En este caso es un arreglo de enteros de 16 bits. Pero de la misma forma se puede procesar otros datos binarios, como imágenes, muestras de sonido, o cualquier otro dato.

Para mostrar los pasos, aquí sigue la secuencia de eventos - solo para los primeros bytes. Entre estos primeros bytes se encuentra justamente una barra inversa, la cual no podemos enviar directamente a MySQL.

Los enteros de nuestro caso se encuentran en un arreglo. Mirando los valores en su forma original (como enteros):

17767 9158 -26519 18547 -9135 23807 -27574 22764 7977 31949 22714 -10325 16882 7931 -22045 -7866 124 25282 2132 10232 8987 -5656 -12825 17293

Pero, en la memoria, estos valores están representados por bytes. Los bytes representando los enteros son:

67 45 C6 23 69 98 73 48 51 dc ff 5c 4a 94 ec 58 29 1f cd 7c ba 58 ab d7

(Los 67 y 45 corresponden al primer valor entero, 17767)

No puedo insertar esta secuencia en una sentencia SQL, ya que byte 11 es 0x5C (una barra inversa), el cual confundiría la comunicación con MySQL:

67 45 C6 23 69 98 73 48 51 dc ff 5c 4a 94 ec 58 29 1f cd 7c ba 58 ab d7

Entonces, llamaré a la función mysql_real_escape_string(), y utilizaré al resultado para formar la sentencia SQL:

67 45 c6 23 69 98 73 48 51 dc ff 5c 5c 4a 94 ec 58 29 1f cd 7c ba 58 ab d7

Observe el 0x5c extra. La primera instancia fue insertada, para advertir a MySQL de no interpretar la segunda instancia como caracter de escape..

El código completo sigue. En particular, las funciones ints_saved() y int_loaded() son los que hacen el trabajo interesante.

En el interés de clarificar la función, la detección y el manejo de errores se simplificó al máximo!

The example

/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*-  */
/*
 * mysql_blobs.c
 * Copyright (C) 2014 John Coppens <john@jcoppens.com>
 * 
 * blob_test is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * blob_test is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <mysql/mysql.h>

#define SIZE 	20000
#define USER    "user"
#define PASS    "pass"
#define DBASE   "localdata"

int16_t ints[SIZE],	     // Array of int16_t's to be written to a table 
        ints_read[SIZE];     // Space to read back the array from the table

MYSQL *con;

int
dbase_connected(void)	   // Connect to the database. Return TRUE if connected
{
  con = mysql_init(NULL);
  if (mysql_real_connect(con, "localhost", USER, PASS, DBASE, 
		0, NULL, CLIENT_MULTI_STATEMENTS) == NULL) {
    printf("%s\n", mysql_error(con));
    return(0);
  }
  
  return(1);
}

void
fill_ints(void)		// Fill the ints array with random 16-bit values
{
  int i;
  
  for (i = 0; i < SIZE; i++)
    ints[i] = random() & 0xffff;
}

int
arrays_equal(void)      // Compare ints and ints_read. Return TRUE if they
			// are completely equal.
{
  int i;
  
  for (i = 0; i < SIZE; i++)
    if (ints[i] != ints_read[i]) {
      printf("At %d: %04x <-> %04x\n", i, ints[i], ints_read[i]);
      return(0);
    }
  return(1);
}

void
show_hex(char *str)     // Show a (NULL-terminated) string as hex bytes
{
  int i;
  
  for (i = 0; str[i] != 0; i++) {
    if (i % 24 == 0)
      printf("\n");
    printf("%02x ", (unsigned char)str[i]);
  }
}

void
show_int(void)	      // Show the ints array (completely) as decimal numbers
{
  int i;
  
  for (i = 0; i < SIZE; i++) {
    if (i % 12 == 0)
      printf("\n");
    printf("%8d ", ints[i]);
  }
}

int
ints_saved(void)	// Store the ints array as a blob into the Ints
			// table. The Id field is always set to 1. Could be better
{
  char   *chunk, *st, *query;
  size_t ints_size = sizeof(ints), 
         st_len;
  int qlen;
  
  chunk = (char *)malloc(ints_size*2 + 1);
  st = "insert into Ints(Id, Data) values(1, '%s')";
  st_len = strlen(st);
  mysql_real_escape_string(con, chunk, (char *)ints, ints_size);
				// Note we have to call the escape function

  query = (char *)malloc(st_len + 2*ints_size+1); 
  qlen = snprintf(query, st_len + 2*ints_size+1, st, chunk);

  //printf("%s\n", query);      // Uncomment to print the query
  //show_hex(chunk);		// Uncomment to show the escaped chunk as hex
  show_int();			// Uncomment to show the ints array as integers
  if (mysql_real_query(con, query, qlen)) {
    printf("%s\n", mysql_error(con));
    return(0);
  };
  
  return(1);
}

int
ints_loaded(void)       // Read the integers back from the table, now into the
			// ints_read array.
{
  MYSQL_RES *result;
  
  if (mysql_query(con, "SELECT Data FROM Ints WHERE Id=1")) {
    printf("%s\n", mysql_error(con));
    return(0);
  }
  
  result = mysql_store_result(con);
  if (result == NULL) {
    printf("%s\n", mysql_error(con));
    return(0);
  }  

  MYSQL_ROW row = mysql_fetch_row(result);
  
  memcpy(ints_read, row[0], sizeof(ints));
  return(1);
}


int main()
{
  if (!dbase_connected())
    return(1);  // Can't connect
    
  fill_ints();
  if (!ints_saved())
    return(2);  // Can't save array as record
    
  if (!ints_loaded())
    return(3);  // Can't load array back
    
  if (!arrays_equal())
    return(4);  // Arrays are not equal
      
  return (0);
}
7829
(c) John Coppens ON6JC/LW3HAZ correo