11 de febrero de 2019

Persistencia de Datos - Serialización Simple

Hasta ahora hemos visto objetos almacenados en memoria. Los instanciábamos directamente desde nuestro código fuente para usar objetos en nuestra aplicación. Está claro que cuando pasemos a aplicaciones reales no tiene sentido que los datos deban iniciarse "hardcodeados" y cada vez que haya que cambiarlos haya que editar el código fuente y recompilar.

Tampoco esperamos que siempre que arranque la aplicación tenga los mismos datos. Empezamos el curso hablando del concepto CRUD (crear, leer, actualizar y borrar). Si ejecuto mi aplicación en una sesión espero que los datos permanezcan en el estado en que la termine, no que vuelva al estado de cuando se compiló, perdiendo todo el trabajo.

En definitiva, necesitamos que los objetos se creen leyéndose desde un soporte en el que estén guardados, dándoles persistencia cuando se cierra nuestra aplicación y se libera la memoria

Necesitamos guardar nuestros datos en un soporte que nos permita conservarlos entre distintas ejecuciones de nuestra aplicación


Hay varias formas de hacerlo:
  • Almacenar en ficheros en disco (es lo que vamos a ver)
  • Guardar en una base de datos
  • Enviar por red

Serializar y Deserializar

Obviando por ahora las dos últimas opciones, para guardar en un fichero debemos transformar un objeto de cualquier complejidad a ceros y unos que será lo que se va a guardar en disco. Para ello necesitamos reducir todo a una "fila" de ceros y unos, ponerlos en serie: Serializar.

Cuando queramos leerlos debemos de hacer lo inverso, leer del fichero e ir leyendo las partes que nos permitan instanciar los objetos en memoria: Deserializar.

Serializar pasa objetos a un formato sencillo de guardar o transportar y Deserializar los reconstruye en sentido inverso


Aparte de serializar y deserializar, otra palabra que debemos incorporar a nuestro diccionario es persear. Parsear es transformar un tipo en otro como cuando por ejemplo transformamos la String "5" en el int 5 mediante Integer.parseInt("5"). Esta transformación más o menos "sencilla" puede ampliarse a estructuras más complejas que una vez analizas producen el resultado que esperamos.

Si los datos leidos no tienen la estructura bien formada, tanto el parse como el deserializado no podrán producir el objeto y desencadenará habitualmente una excepción.

¿Cómo serializar?

Te invito a que inventes por tu cuenta una forma en que podrías guardar los datos por ejemplo de nuestros objetos Coche. En general si es tu primera vez seguramente pienses en algo como una línea con los valores separados por comas:

Peugeot 3008;Negro;4

Esto podría ser modelo, color y numeroDeRuedas de uno de nuestros objetos Coche. Añadiendo más líneas tendríamos en un fichero todos los que quisieramos. También podríamos poner una primera línea de encabezado para saber qué es cada valor.

La otra forma rápida que se te puede haber ocurrido es haciendo pares clave-valor:

Modelo=Peugeot 3008
Color=Negro
numeroRuedas=4

Si es así, enhorabuena, prácticamente has dicho la base de las formas más populares de hacerlo y que usan muchos servicios como estándares y las dos son texto plano: XML y JSON.

Hay más formas de serializar, como en binario, pero vamos a centrarnos en hacerlo en texto plano.

Formatos en texto plano

Vamos a ver una manera rápida de cómo serializar en CSV (Comma-Separated Values). Son ficheros normalmente con esa extensión de archivo y que guardan valores separados por un carácter especial (habitualmente ';')

Seguirían el patrón del primer ejemplo que pusimos, es muy sencillo y todavía se usan.

También nos los podemos encontrar con los valores ocupando un ancho fijo rellenando el espacio sobrante con espacios, en vez de separados por un carácter. Digamos que al modelo le podríamos dar 20 espacios, al color 10 y al número de ruedas 1.

Como veréis son formatos muy fáciles de serializar (en nuestro caso pasar a texto CSV) y deserializar (reconstruir un objeto desde un texto CSV). Usamos el siguiente código para ver un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  public static String serializar (Coche coche) {
    return coche.modelo + ";" + coche.getColor() +
            ";" + coche.getNumeroDeRuedas() + ";" + coche.matricula;
  }

  public static Coche deserializar (String vehiculoCSV) {
    String[] campos = vehiculoCSV.split(";");
    Coche c = new Coche(campos[0], campos[1]);
    c.numeroDeRuedas = Integer.parseInt(campos[2]);
    c.setMatricula(campos[3]);
    
    return c;
  }

Y lo ejecutamos desde nuestro main:

1
2
3
4
  String coche1EnCsv = serializar(coche1);
  System.out.println(coche1EnCsv);
  Coche coche4 = deserializar(coche1EnCsv);
  System.out.println(coche4);

Esto nos dará la siguiente salida (serializar:cada campo separado por ";" y deserializar: dando un objeto igual al que se serializó):

Seat Ibiza;Rojo;4;1234 BBB
Placa 1234 BBB - Seat Ibiza (Rojo), 4 ruedas

Esta sencillez acarréa una serie de problemas:
  • Si usamos ancho fijo:
    • ¿Qué pasa cuando el tamaño marcado para el campo sea más grande? se truncará y no tendremos el valor completo.
  • Si usamos CSV
    • ¿Qué pasa cuando no tengamos unos valores fijos?
    • ¿Y si un campo contiene el caracter separador?
    • ¿Y si un valor es una colección? No podremos hacerla crecer hasta donde queramos
    • Si no tengo valor para un campo debo seguir poniendo ';' para que siga corriendo la posición al siguiente valor.
Independientemente de soluciones "mas o menos caseras", tenemos un problema en el horizonte y eso será un problema de mantenimiento y/o interoperabilidad de sistemas.

Por esas limitaciones existen formatos que nos permiten organizar esas estructuras más complejas. Los más habituales son XML y JSON que mejoran la interoperabilidad con otros sistemas

¿Qué tengo que serializar?

Lo primero que tengo que pensar es qué debo guardar y qué no. No se trata de dar persistencia a cada campo. Si vamos a usar serialización de Java debemos echar un vistazo a la interfaz Serializable.

Serializable es una interfaz muy fácil de implementar, simplemente se define que la clase lo implementa y sólo debemos añadir un número de versión (serialVersionUID) aunque si no lo ponemos nos lo marcará como un warning, no un error. Este número sirve para saber si es compatible el dato serializado con el objeto que se quiere obtener. No prestar atención a este número puede ser un quebradero de cabeza con nuevas versiones.

Aquí no vamos a ver esta interfaz, no vamos a usar la serialización nativa de Java, nos centraremos en guardar datos en JSON y lo veremos en futuras sesiones pues es el formato de notación nativa de JavaScript y nos va a servir también para trabajar fácilmente con datos en aplicaciones web.

Lo que vamos a ver para terminar esta sesión es cómo preparo mi clase para indicar qué se debe serializar y qué debemos tener en cuenta para que nos funcionen otras librerias de serializado como la que vamos a usar para JSON (jsonbeans de EsotericSoftware).

Normalmente se va a serializar:
  1. El nombre de la clase cuando haga falta.
  2. Los campos NO marcados como transient (esta palabra clave indica que un campo no forma parte de su estado persistente, normalmente son campos calculados: la edad si tengo la fecha de nacimiento o un mapa para mejorar el acceso a valores por clave, esos campos los puedo calcular al inicializar el objeto).
    Este modificador es usado por librerías que serializan en función de este modificador (como Serializable o jsonbeans). Sin embargo no hay que confundirlo con nuestro ejemplo sobre CSV en el que se ha implementado sin tener en cuenta el modificador transient y evidentemente no afecta a la serialización
  3. Los campos cuyo valor sea distinto al que reciben al inicializar la variable.
  4. Se serializarán los objetos internos conforme a su tipo: si modelo fuera un tipo que contuviese una un objeto de tipo Marca, se serializaría modelo como parte de Coche y marca como parte de Modelo, etc... es decir que se sigue profundizando hasta que todo lo que se debe serializar está reducido a texto plano que se pueda deserializar.
No obstante, todo esto depende de la implementación concreta de la librería que usemos y de cómo guardemos los datos, pero sería una lista de "sospechosos habituales" (por ejemplo algunas formas de serializar podrían sólo guardar los campos public).

Como verás no se guardan las variables de clase. Es normal teniendo en cuenta que normalmente son constantes y están definidos sus valores en la clase.

No debemos olvidar que lo normal es que para reconstruir el objeto se utilice la información de su clase (Class) para crear una nueva instancia e ir asignándole los valores a cada campo guardado. A menos que se haga un serializador personalizado, normalmente se usará el constructor vacío para instanciarlo, así que debemos asegurarnos que nuestros tipos serializables lo tengan.

¿Cómo preparo mi clase para que se serialice correctamente?

Básicamente debes hacer lo siguiente:
  1. Usar transient en los campos que no quiero serializar
  2. Asegurarme de tener un constructor vacío
  3. Que los otros tipos que se deben serializar como parte de mi clase también cumplen estas normas
Vamos a ver un ejemplo usando JSON en la siguiente sesión.

Compárteme

Entradas populares