sábado, 16 de octubre de 2010

Ingeniería reversa de base de datos con Spring Roo

Desde hace ya tiempo quería probar esta nueva funcionalidad de Spring Roo.

La versión de Roo que estoy usando es: 1.1.0.RC1.

Hace ya algunas versiones anteriores de Spring Roo, los desarrolladores de esta herramienta habían prometido implementar algún mecanismo para poder hacer ingeniería reversa de un esquema de base de datos existente y generar código Java a partir de el. Pues ya han cumplido y han agregado los siguientes comandos a la consola de Roo:


  • database reverse engineer. Este comando es el que realiza la 'magia' de crear/sincronizar las entidades a partir de una base de datos existente.

  • database introspect. Este comando muestra los metadatos almacenados por Spring Roo de la base de datos.



Se recomienda que cuando estas haciendo un proyecto en el cual se esta usando un framework ORM como Hibernate o JPA el diseño del esquema de la base sea un derivado del diseño de las clases que contendrá el proyecto. Pero no siempre es así, ya sea porque en el equipo se tiene a una o varias personas especializadas en diseño de base de datos o porque se trata de un proyecto para el cual ya se tiene una base de datos existente.

Para empezar crearemos un proyecto con el clásico comando 'project':


roo> project --topLevelPackage com.jabaddon.roo.dbtopojo


La ejecución de este comando genera lo siguiente:


Created /Volumes/AbaddonData/Abaddon/Projects/JAbaddon/SpringRooProjects/BDToPojos/pom.xml
Created SRC_MAIN_JAVA
Created SRC_MAIN_RESOURCES
Created SRC_TEST_JAVA
Created SRC_TEST_RESOURCES
Created SRC_MAIN_WEBAPP
Created SRC_MAIN_RESOURCES/META-INF/spring
Created SRC_MAIN_RESOURCES/META-INF/spring/applicationContext.xml
Created SRC_MAIN_RESOURCES/log4j.properties


Lo siguiente es configurar la persistencia del proyecto:


roo> persistence setup --provider HIBERNATE --database MYSQL --databaseName roo_db_pojo --userName xxxx --password yyyy


Donde 'xxxx' se refiere al nombre del usuario y 'yyyy' al password del mismo para conectarse a la base de datos con nombre 'roo_db_pojo'.

La ejecucion del comando para configurar la persistencia genera lo siguiente:


anaged SRC_MAIN_RESOURCES/META-INF/spring/applicationContext.xml
Created SRC_MAIN_RESOURCES/META-INF/persistence.xml
Please enter your database details in src/main/resources/META-INF/spring/database.properties.
Created SRC_MAIN_RESOURCES/META-INF/spring/database.properties
Managed ROOT/pom.xml [Added dependency mysql:mysql-connector-java:5.1.13]
Managed ROOT/pom.xml [Added dependency org.hibernate:hibernate-core:3.5.5-Final]
Managed ROOT/pom.xml [Added dependency org.hibernate:hibernate-entitymanager:3.5.5-Final]
Managed ROOT/pom.xml [Added dependency org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.0.Final]
Managed ROOT/pom.xml [Added dependency org.hibernate:hibernate-validator:4.1.0.Final]
Managed ROOT/pom.xml [Added dependency javax.validation:validation-api:1.0.0.GA]
Managed ROOT/pom.xml [Added dependency cglib:cglib-nodep:2.2]
Managed ROOT/pom.xml [Added dependency javax.transaction:jta:1.1]
Managed ROOT/pom.xml [Added dependency org.springframework:spring-jdbc:${spring.version}]
Managed ROOT/pom.xml [Added dependency org.springframework:spring-orm:${spring.version}]
Managed ROOT/pom.xml [Added dependency commons-pool:commons-pool:1.5.4]
Managed ROOT/pom.xml [Added dependency commons-dbcp:commons-dbcp:1.3]
Managed ROOT/pom.xml


Si vemos el contenido del archivo 'src/main/resources/META-INF/spring/database.properties' debe lucir así:


database.password=yyyy
database.url=jdbc\:mysql\://localhost\:3306/roo_db_pojo
database.username=xxxx
database.driverClassName=com.mysql.jdbc.Driver


Muy bien, hasta aquí ya tenemos nuestro proyecto vacío el cual ya esta configurado para conectarse a una base de datos. Asi pues, lo siguiente es crear la base de datos (si aun no lo han hecho).

Ya con nuestra base de datos y nuestro proyecto de Spring Roo creado vamos a crear una tabla sencilla para almacenar personas:


CREATE TABLE `persona` (
`id_persona` int(11) NOT NULL,
`nombre` varchar(50) character set latin1 NOT NULL,
`apellido` varchar(50) character set latin1 NOT NULL,
PRIMARY KEY (`id_persona`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;


Si ejecutamos el comando para hacer ingeniería reversa como sigue:


roo> database reverse engineer --package com.jabaddon.roo.dbtopojo.dominio


Obtendremos lo siguiente:


Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona.java
Created com.jabaddon.roo.dbtopojo.dominio.Persona
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_Entity.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_DbManaged.aj
Managed SRC_MAIN_RESOURCES/META-INF/persistence.xml
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_Configurable.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_ToString.aj


La primera vez que se ejecute el comando de ingeniería inversa es necesario especificar el paquete en el cual las entidades serán creadas.

Como ven Roo ha creado una entidad Persona y ha generado los clásicos .aj derivados del archivo .java. Aunque existe un nuevo .aj llamado 'Persona_Roo_DbManaged.aj', este aspecto es el que contiene los campos de nuestra entidad que mapean a las columnas que Roo encontro en la tabla 'PERSONA'.


package com.jabaddon.roo.dbtopojo.dominio;

import java.lang.String;
import javax.persistence.Column;
import javax.validation.constraints.NotNull;

privileged aspect Persona_Roo_DbManaged {

@Column(name = "nombre", length = 50)
@NotNull
private String Persona.nombre;

@Column(name = "apellido", length = 50)
@NotNull
private String Persona.apellido;

public String Persona.getNombre() {
return this.nombre;
}

public void Persona.setNombre(String nombre) {
this.nombre = nombre;
}

public String Persona.getApellido() {
return this.apellido;
}

public void Persona.setApellido(String apellido) {
this.apellido = apellido;
}

}


Ahora supongamos que se nos olvido agregarle una columna a nuestra tabla PERSONA y en esa columna deseamos guardar la fecha de nacimiento.

Si agregamos la columna de fecha de nacimiento a nuestra tabla y corremos de nueva cuenta el comando 'database reverse engineer' obtendremos lo siguiente:


Managed ROOT/.roo-dbre
Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_DbManaged.aj
Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_ToString.aj


Y si abrimos el archivo 'Persona_Roo_DbManaged.aj' veremos que ya tiene un nuevo campo de fecha de nacimiento:


@Column(name = "fecha_nacimiento")
@NotNull
@Temporal(TemporalType.DATE)
@DateTimeFormat(style = "S-")
private Date Persona.fechaNacimiento;


Si se están preguntando ¿que pasa si yo agrego un campo al archivo Persona.java?, pues la respuesta es lo mismo que ha hecho Roo desde su primera version y es que simplemente regenerara los archivos .aj necesarios para que reflejen el nuevo campo. Por ejemplo si modificamos el archivo Pesona.java para agregarle una nueva propiedad 'nick':


package com.jabaddon.roo.dbtopojo.dominio;

import javax.persistence.Entity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.tostring.RooToString;
import org.springframework.roo.addon.entity.RooEntity;
import org.springframework.roo.addon.dbre.RooDbManaged;
import javax.persistence.Table;

@Entity
@RooJavaBean
@RooToString
@RooEntity(versionField = "")
@RooDbManaged(automaticallyDelete = true)
@Table(name = "persona", catalog = "roo_db_pojo")
public class Persona {

@NotNull
@Column(name = "nick", length = 10)
private String nick;
}


Al momento de salvar el archivo, Roo monitorea el cambio y genera el .aj JavaBean y administra el .aj ToString:


[Timer-0] Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_JavaBean.aj
[Timer-0] Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_ToString.aj


Interesante, ¿no lo creen?

Hasta ahorita se creo una tabla muy sencilla que Roo fue capaz de detectar y generar una entidad a partir de la definición de la tabla, pero pongamos a prueba esta funcionalidad de Roo generando tablas con relaciones 1:N, N:1 y el N:M.

Para la relación 1:N, agregaremos otra tabla que represente los teléfonos de una persona y su debida relación con un Foreign key:


CREATE TABLE `telefono` (
`id_telefono` int(11) NOT NULL,
`numero` varchar(10) character set latin1 NOT NULL,
`id_tipo_telefono` int(11) NOT NULL,
`id_persona` int(11) NOT NULL,
PRIMARY KEY (`id_telefono`),
KEY `id_persona` (`id_persona`),
CONSTRAINT `fk_telefono2persona` FOREIGN KEY (`id_persona`) REFERENCES `persona` (`id_persona`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;


Y si ejecutamos de nueva cuenta el comando 'database reverse engineer' obtendremos:


Managed ROOT/.roo-dbre
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono.java
Created com.jabaddon.roo.dbtopojo.dominio.Telefono
Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_DbManaged.aj
Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Persona_Roo_ToString.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono_Roo_Entity.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono_Roo_DbManaged.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono_Roo_Configurable.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono_Roo_ToString.aj


Roo a creado una nueva entidad Telefono y ha modificado la entidad Persona. Si abrimos los archivos Persona_Roo_DbManaged.aj y Telefono_Roo_DbManaged.aj:

NOTA: Voy a omitir los metodos get y set.


privileged aspect Persona_Roo_DbManaged {

@OneToMany(mappedBy = "persona")
private Set Persona.telefonoes;

@Column(name = "nombre", length = 50)
@NotNull
private String Persona.nombre;

@Column(name = "apellido", length = 50)
@NotNull
private String Persona.apellido;

@Column(name = "fecha_nacimiento")
@NotNull
@Temporal(TemporalType.DATE)
@DateTimeFormat(style = "S-")
private Date Persona.fechaNacimiento;

....
}



privileged aspect Telefono_Roo_DbManaged {

@ManyToOne
@JoinColumn(name = "id_persona", referencedColumnName = "id_persona")
private Persona Telefono.persona;

@Column(name = "numero", length = 10)
@NotNull
private String Telefono.numero;

@Column(name = "id_tipo_telefono")
@NotNull
private Integer Telefono.idTipoTelefono;

...
}


Perfecto! Roo ha relacionado las entidades de Persona y Telefono con un @OneToMany de Persona a Telefono y un @ManyToOne de Telefono a Persona.

Continuemos con la relación N:1 creando un catalogo para los tipos de telefono:


CREATE TABLE `cat_tipo_telefono` (
`id_tipo_telefono` int(11) NOT NULL,
`nombre` varchar(50) NOT NULL,
PRIMARY KEY (`id_tipo_telefono`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;


y modificando la tabla de Telefono para que tenga la relacion con su catalogo de tipo de telefono:


CREATE TABLE `telefono` (
`id_telefono` int(11) NOT NULL,
`numero` varchar(10) character set latin1 NOT NULL,
`id_tipo_telefono` int(11) NOT NULL,
`id_persona` int(11) NOT NULL,
PRIMARY KEY (`id_telefono`),
KEY `index_id_persona` (`id_persona`),
KEY `index_id_tipo_telefono` (`id_tipo_telefono`),
CONSTRAINT `fk_telefono2persona` FOREIGN KEY (`id_persona`) REFERENCES `persona` (`id_persona`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `fk_tipo2catalogo` FOREIGN KEY (`id_tipo_telefono`) REFERENCES `cat_tipo_telefono` (`id_tipo_telefono`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;


De nueva cuenta si ejecutamos de nueva cuenta el comando 'database reverse engineer' obtendremos:


Managed ROOT/.roo-dbre
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/CatTipoTelefono.java
Created com.jabaddon.roo.dbtopojo.dominio.CatTipoTelefono
Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono_Roo_DbManaged.aj
Managed SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Telefono_Roo_ToString.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/CatTipoTelefono_Roo_Entity.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/CatTipoTelefono_Roo_DbManaged.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/CatTipoTelefono_Roo_Configurable.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/CatTipoTelefono_Roo_ToString.aj


Roo ha creado una entidad que CatTipoTelefono y modifico la entidad Telefono. Si abrimos todos los archivos Persona_Roo_DbManaged.aj, Telefono_Roo_DbManaged.aj y CatTipoTelefono_Roo_DbManaged.aj:


privileged aspect Persona_Roo_DbManaged {

@OneToMany(mappedBy = "persona")
private Set Persona.telefonoes;

@Column(name = "nombre", length = 50)
@NotNull
private String Persona.nombre;

@Column(name = "apellido", length = 50)
@NotNull
private String Persona.apellido;

@Column(name = "fecha_nacimiento")
@NotNull
@Temporal(TemporalType.DATE)
@DateTimeFormat(style = "S-")
private Date Persona.fechaNacimiento;
...
}



privileged aspect Telefono_Roo_DbManaged {

@ManyToOne
@JoinColumn(name = "id_persona", referencedColumnName = "id_persona")
private Persona Telefono.persona;

@ManyToOne
@JoinColumn(name = "id_tipo_telefono", referencedColumnName = "id_tipo_telefono")
private CatTipoTelefono Telefono.catTipoTelefono;

@Column(name = "numero", length = 10)
@NotNull
private String Telefono.numero;
...
}



privileged aspect CatTipoTelefono_Roo_DbManaged {

@OneToMany(mappedBy = "catTipoTelefono")
private Set CatTipoTelefono.telefonoes;

@Column(name = "nombre", length = 50)
@NotNull
private String CatTipoTelefono.nombre;
...
}


Muy interesante.

NOTA: Obviamente no es una buena idea que la entidad CatTipoTelefono tenga una colección de los teléfonos que tiene relacionados. Esto es algo que tiene que mejorar Spring Roo. Me llevo de tarea investigar si hay alguna forma de decirle a Roo que evite hacer esto.

Ahora vamos a ver como se comporta con una relacion N:M. Asi que, vamos a crear una tabla de Solicitudes, otra de documentos y una que defina los documentos que tiene una solicitud:


CREATE TABLE `solicitud` (
`id_solicitud` int(11) NOT NULL,
`folio` varchar(10) NOT NULL,
PRIMARY KEY (`id_solicitud`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;



CREATE TABLE `documento` (
`id_documento` int(11) NOT NULL,
`nombre` varchar(50) NOT NULL,
PRIMARY KEY (`id_documento`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;



CREATE TABLE `solicitud_documento` (
`id_solicitud` int(11) NOT NULL,
`id_documento` int(11) NOT NULL,
PRIMARY KEY (`id_documento`,`id_solicitud`),
CONSTRAINT `fk_documento2documento` FOREIGN KEY (`id_documento`) REFERENCES `documento` (`id_documento`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `fk_solicitud2solicitud` FOREIGN KEY (`id_solicitud`) REFERENCES `solicitud` (`id_solicitud`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci;


al correr el comando 'database reverse engineer' obtenemos:


Managed ROOT/.roo-dbre
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Documento.java
Created com.jabaddon.roo.dbtopojo.dominio.Documento
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Solicitud.java
Created com.jabaddon.roo.dbtopojo.dominio.Solicitud
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Solicitud_Roo_Entity.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Solicitud_Roo_DbManaged.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Solicitud_Roo_Configurable.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Solicitud_Roo_ToString.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Documento_Roo_Entity.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Documento_Roo_DbManaged.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Documento_Roo_Configurable.aj
Created SRC_MAIN_JAVA/com/jabaddon/roo/dbtopojo/dominio/Documento_Roo_ToString.aj


Roo ha creado las entidades Solicitud y Documento y si vemos los archivos Solicitud_Roo_DbManaged.aj y Documento_Roo_DbManaged.aj lucen asi:


privileged aspect Solicitud_Roo_DbManaged {

@ManyToMany(mappedBy = "solicituds")
private Set Solicitud.documentoes;

@Column(name = "folio", length = 10)
@NotNull
private String Solicitud.folio;

...
}



privileged aspect Documento_Roo_DbManaged {

@ManyToMany
@JoinTable(name = "solicitud_documento", joinColumns = { @JoinColumn(name = "id_documento") }, inverseJoinColumns = { @JoinColumn(name = "id_solicitud") })
private Set Documento.solicituds;

@Column(name = "nombre", length = 50)
@NotNull
private String Documento.nombre;

...
}


Roo detecto la relación N:M entre Solicitud y Documento y puso una relaciones @ManyToMany en las entidades utilizando como @JoinTable la tabla SOLICITUD_DOCUMENTO.

Por ultimo, veamos que hace el comando 'database introspect':


roo> database introspect --schema roo_db_pojo


El comando 'database introspect' pide como argumento el esquema de la base de datos y nos da como salida:
























































































Roo tiene almacenado la estructura de la base de datos en el archivo ROOT/.roo-dbre (este archivo apareció varias veces al ejecutar el comando 'database reverse engineer').

Muy interesante y util esta funcionalidad de poder hacer ingenieria reversa de una base de datos existente con Spring Roo.

Creo que es todo para este post, por favor comenten.