sábado, 8 de septiembre de 2012

Model Project con Maven Spring Hibernate Annotation

Vamos a crear la capa de acceso a datos de un proyecto que posteriormente será usado por otro proyecto web q sea capaz de implementar todas las funcionalidades CRUD del mismo.
Para ello vamos a empezar por la parte de base de datos, en principio podríamos generar todo el código java y luego por medio de un mandato se crearían los esquemas y tablas correspondientes, peeeero para esta primera parte iremos paso a paso y dejaremos esa fase para post más avanzados, sobre todo porque todavía tengo q estudiar yo esa característica, si alguien quiere comentar y hablar de ello pues estupendo, vamos a ello!, abrimos la terminal y accedemos a la base de datos:

$ mysql -u root -p

Aprovechamos la coyuntura para ver un par de temas sencillos en BBDD MySQL, a continuación pongo una serie de comandos con los siguientes objetivos: crear una base de datos específica para el proyecto, entrar en ella, crear la tabla base, crear un usuario que será quien nacceda a esa base de datos ya que no quiero que el programa vaya con root por razones obvias (si yo soy un DBA y he de crear un esquema o una base de datos para un programador, le daré un usuario específico para atacar esa base de datos, no la password de root):


Create Database Test;

Use Test;

Create Table (
 rut    int, 
 nombre           varchar(100),
 apellido_paterno  varchar(100),
 apellido_materno  varchar(100)
);

GRANT ALL PRIVILEGES ON Test.* TO 'usuario'@'localhost' IDENTIFIED BY 'user' WITH GRANT OPTION;


Con esto ya tenemos el modelo de datos preparado, procedemos a crear el arquetipo:

$ mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=org.nak.dataTest -DartifactId=dataTest

Entramos en la raiz del proyecto "cd dataTest" y pasamos a editar el pom.xml para decirle cuales son todas las dependencias q vamos a necesitar: spring, hibernate, mysql, log4j y sfl4j. Debajo de la etiketa "properties":
<dependencies>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.10</version>
   <scope>test</scope>
  </dependency>
  <!-- Para poder usar hibernate con spring -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>3.1.2.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring</artifactId>
   <version>2.5.6</version>
  </dependency>
  <!-- Hibernate framework -->
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate</artifactId>
   <version>3.2.7.ga</version>
  </dependency>
  <!-- Para usar anotaciones con hibernate -->
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-annotations</artifactId>
   <version>3.4.0.GA</version>
  </dependency>

  <!-- mysql conector -->
  <dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.21</version>
  </dependency>
  <!-- Log4j y Sfl4j -->
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.5.8</version>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-log4j12</artifactId>
   <version>1.5.8</version>
   <scope>runtime</scope>
  </dependency>
  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.16</version>
  </dependency>
 </dependencies>

Para saber la ubicación, artefacto y versión de una dependencia del repositorio de maven vamos a http://mvnrepository.com/. Compilamos:
 $ mvn -U clean compile

Los xml descriptores de Spring, Hibernate y log4j van a ir en una carpeta llamada resources, el arquetipo no nos ha creado dicha carpeta, asi q la haremos nostr@s:
$ mkdir src/main/resources

Ahora llega el momento de ir a nuestro IDE, there's a goal de maven que sirve para preparar los descriptores del proyecto y q sea abiero por dicho IDE "mvn eclipse:clean eclipse:eclipse", pero en versiones superiores al Indigo nos encontramos que cuando importamos el proyecto nos sorprende con un error de este tipo:

An internal error occurred during: "Importing Maven projects".
Unsupported IClasspathEntry kind=4


Mmmm, la solución de este error es tan simple como ahorrarse este último goal de maven antes de importar el proyecto, vamos que no es necesario, máxime de la forma en la que estamos trabajando, en la que el eclipse hace la función poco más que de un bloc de notas, ya que los test, compilaciones y enpaquetado final los haremos desde la konsola. Asi pues tras compilar ya importamos directamente el proyecto, akí dejo el link de la página de maven al respecto para kien aun no tenga el plugin del IDE a punto. Este es el aspecto final de la estructura de clases y pakete q veremos en el Project Explorer:




Pasamos pues a detallar cada uno de los componentes, empezamos por el bean "Persona.java", en principio todas sus anotaciones son facilmente entendibles:

package org.nak.dataTest.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "personas")
public class Persona implements Serializable {

 @Id
 @Column(name = "rut")
 private int rut = 0;
 @Column(name = "nombre")
 private String nombre = null;
 @Column(name = "apellido_paterno")
 private String apellidoPaterno = null;
 @Column(name = "apellido_materno")
 private String apellidoMaterno = null;

 public Persona(int rut, String nombre, String apellidoPaterno,
   String apellidoMaterno) {
  this.rut = rut;
  this.apellidoMaterno = apellidoMaterno;
  this.apellidoPaterno = apellidoPaterno;
  this.nombre = nombre;
 }

 public Persona() {
  super();
 }

 public int getRut() {
  return rut;
 }

 public void setRut(int rut) {
  this.rut = rut;
 }

 public String getNombre() {
  return nombre;
 }

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

 public String getApellidoPaterno() {
  return apellidoPaterno;
 }

 public void setApellidoPaterno(String apellidoPaterno) {
  this.apellidoPaterno = apellidoPaterno;
 }

 public String getApellidoMaterno() {
  return apellidoMaterno;
 }

 public void setApellidoMaterno(String apellidoMaterno) {
  this.apellidoMaterno = apellidoMaterno;
 }

 @Override
 public String toString() {
  return this.nombre + " " + this.apellidoPaterno + " "
    + this.apellidoMaterno + " (" + this.rut + ")";
 }
 
 @Override
 public int hashCode() {
  return this.rut;
 }
 
 @Override
 public boolean equals(Object obj) {
  boolean result = false;
  if (obj != null && getClass() == obj.getClass()) {
   final Persona other = (Persona) obj;
   if (this.rut == other.rut) {
    result = true;
   }
  }
  return result;
 }

}


La parte DAO, primero la interface donde definimos los métodos CRUD q vamos a usar y posteriormente la clases que implementará está interface y hará la operaciones propias de hibernate:

package org.nak.dataTest.dao;

import java.util.List;

import org.nak.dataTest.model.Persona;

public interface PersonaDAO {

 public void saveOrUpdate(Persona persona);

 public void delete(Persona persona);

 public void find(int rut);

 public List findAll();
 
 public void remove(Integer rut);

}

package org.nak.dataTest.hibernate;

import java.util.List;

import org.hibernate.SessionFactory;
import org.nak.dataTest.dao.PersonaDAO;
import org.nak.dataTest.model.Persona;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.stereotype.Service;

@Service(value="personaDAO")
public class HibernatePersonaDao  extends HibernateDaoSupport implements PersonaDAO{

 @Autowired
 public HibernatePersonaDao(@Qualifier("sessionFactory") SessionFactory sessionFactory) {
  this.setSessionFactory(sessionFactory);
 }
 
 public void saveOrUpdate(Persona persona) {
  this.getHibernateTemplate().saveOrUpdate(persona);
 }

 public void delete(Persona persona) {
  this.getHibernateTemplate().delete(persona);
 }

 public void find(int rut) {
  this.getHibernateTemplate().load(Persona.class, rut);
 }

 public List findAll() {
  return this.getHibernateTemplate().find("from Persona");
 }

 public void remove(Integer rut) {
  Persona persona = (Persona)this.getHibernateTemplate().load(Persona.class, rut); 
  if (persona != null){
   this.getHibernateTemplate().delete(persona);
  }
 }

}

Well, hasta aki el que piense que estoy copiando vilmente las indicaciones de esta página tiene toda la razón, pero hay ciertos matices que difieren (especialmente en los xml de configuración) y esa es la razón por la que cuento el proyecto a mi manera, en cualquier caso cito dicha web de forma literal para explicar algunas de las anotaciones anteriormente vistas:
" @Service le dice a Spring que queremos darle un nombre a esta clase para que sea instanciada automáticamente por el framework cada vez que usemos el identificador "personaDAO" ya seaen una inyección de dependencia o en una llamada explicita acontextoSpring.getbean("personaDAO"). Para quienes tengan experiencia previa con Spring elresumen es que estamos reemplazando lo siguiente:
<bean name="personaDAO" class="cl.ariel.ejemplo.dao.HibernatePersonaDao">
<property name="sessionFactory" ref="sessionFactory" /></bean>

En el archivo applicationContext.xml por una anotación. Por otro lado, @Autowired nos permiteindicarle a Spring que el constructor que estamos declarando requiere de una inyección dedependencias, en este caso sessionFactory. Como sabe Spring donde inyectar el objeto y queponer ahí? Pues bien, la anotación @Qualifier precede al parámetro del constructor y con eso indicamos donde debe ser inyectada la dependencia, mientras que el valor de la anotación indicacomo se llama el objeto que Spring debe inyecta.
"
Sigamos, dentro de lo que sería un proyecto MVC, podríamos decir q tenemos lista la capa del Model (evidentemente una muy básica), pasamos a preparar la Capa controladora, quien maneja la lógica de nuestro negocio, la capa de Vista queda emplazada a otro proyecto diferente al que importaremos como librería el .jar resultante aki, pasamos entonces a definir la interface y clase controladora:
package org.nak.dataTest.service;

import java.util.List;

import org.nak.dataTest.model.Persona;

public interface PersonaManager {

 public void insertarPersonas(List personas);
 public List obtenerPersonas();
 public void borrarPersonas(List personas);
 public void borrarPersona(Integer rut);
 
}

package org.nak.dataTest.service;

import java.util.List;

import org.nak.dataTest.dao.PersonaDAO;
import org.nak.dataTest.model.Persona;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service(value="personaManager")
public class PersonaManagerImpl implements PersonaManager{

 @Resource(name="personaDAO")
 private PersonaDAO personaDAO = null;
 
 public PersonaDAO getPersonaDAO() {
  return personaDAO;
 }

 public void setPersonaDAO(PersonaDAO personaDAO) {
  this.personaDAO = personaDAO;
 }

 @Transactional(propagation = Propagation.REQUIRED)
 public void insertarPersonas(List personas) {
  
  if (personas != null){
   for (Persona persona:personas){
    this.personaDAO.saveOrUpdate(persona);
   }
  }
  
 }

 public List obtenerPersonas() {
  return this.personaDAO.findAll();
 }

 @Transactional(propagation = Propagation.REQUIRED)
 public void borrarPersonas(List personas) {
  if (personas != null){
   for (Persona persona:personas){
    this.personaDAO.delete(persona);
   }
  }
  
 }

 public void borrarPersona(Integer rut) {
  this.personaDAO.remove(rut);
 }

}

Y como soy un perla, vuelvo a recurrir a Ariel para hablar de las anotaciones nuevas :) :
 " @Resource le dice a Spring que debe buscar el recurso"personaDAO" e inyectarlo a mi objeto; es en la practica un equivalente de @Autowired y@Qualifier pero en una sola línea. La otra anotación que encontramos es @Transactional y es laque nos permite definir un método o una clase que agrupa varias operaciones que deben ser commiteadas o rollbackeadas en conjunto
"

Archivos de Configuración:

 Dentro de la carpeta "resources" van los archivos de configuración: el applicationContext.xml de Spring (tamibén llamado spring-context); hibernate.cfg.xml, que en este caso uso para mapear las entidades en el session factory; log4j y database .properties para configurar la salida del log y los parámetros de conexión a la base de datos respectivamente, aunke finalmente como podréis var a continuación, opté por hardcodear esto último en el dataSource.
Paso pues a mostrar primero el aspecto completo de mi "applicationContext.xml":
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:tx="http://www.springframework.org/schema/tx"
 xmlns:context="http://www.springframework.org/schema/context"
 xsi:schemaLocation="
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">

 <!-- Declaramos el uso de anotaciones para Spring -->
 <context:annotation-config />
 <!-- Indicamos que clases deben ser leidas por Spring para buscar recursos -->
 <context:component-scan base-package="org.nak.dataTest" />

 <bean id="dataSource"
  class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="username" value="usuario" />
  <property name="password" value="user" />
  <property name="url" value="jdbc:mysql://localhost:3306/test" />
  <property name="driverClassName" value="com.mysql.jdbc.Driver" /> 
 </bean>

 <bean id="sessionFactory"
  class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="configLocation" value="hibernate.cfg.xml" />
  <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
  <!-- /WEB-INF/ <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration"/> -->
  <property name="hibernateProperties">
   <props>
    <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
    <prop key="show_sql">true</prop>
   </props>
  </property>
 </bean> 
 
    <!-- Soporte para transacciones via anotaciones --> 
    <tx:annotation-driven />
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean> 
   
</beans>

 Tanto el xml como los comentarios añadidos en el mismo hablan por si solos, es destacable la cantidad de código ahorrado gracias a las anotaciones.

 En "hibernate.cfg.xml" simplemente mapeamos las entidades:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" 
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

 <hibernate-configuration>
  <session-factory>
   
    <!-- ENTIDADES -->  
    <mapping class="org.nak.dataTest.model.Persona"/>
    
        </session-factory>
 </hibernate-configuration>


 La clase SpringContext nos sirve para acceder a los beans del applicationContext desdel código, creo que debe haber alguna forma mejor de hacer esta parte, de tal manera que se pueda invocar el xml por anotación, lo dejo en el aire hasta que tenga tiempo de mirarlo, invito a q alguien diga como hacerlo, por el momento accederemos usando la clase:
package org.nak.dataTest;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringContext {
 
 private static ApplicationContext context;
 private static SpringContext instance;
 private final static String CONFIG_FILE = "applicationContext.xml";
 
 public static SpringContext getInstance() {
  if (null == instance) {
   instance = new SpringContext();
   context = new ClassPathXmlApplicationContext(CONFIG_FILE);
  }
  return instance;
 }
 
 public Object getBean(String bean) {
  return context.getBean(bean);
 }

}

Y ya estaría, hacemos una clase de testeo para probar q todo funciona:

package org.nak.dataTest;

import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.nak.dataTest.dao.PersonaDAO;
import org.nak.dataTest.model.Persona;
import org.nak.dataTest.service.PersonaManager;
import org.junit.Test;

public class PersonaManagerTest {

 private static Logger logger = Logger.getLogger(PersonaManagerTest.class);
 
 @Test
 public void testInsertaPersona() throws Exception {
  logger.debug("Creando contexto spring");
  // Obtenemos el contexto Spring
  SpringContext context = SpringContext.getInstance();

  // Creo mis objetos
  Persona persona1 = new Persona(63676, "evaristo", "lpr", "jnvvhjk");
  logger.debug("persona1 = " + persona1);
  Persona persona2 = new Persona(452, "nak", "ñiak", "punk");
  logger.debug("persona2 = " + persona2);
  List personas = new ArrayList();
  personas.add(persona1);
  personas.add(persona2);
  logger.debug("personas = " + personas);
   
  // Obtengo el Manager
  PersonaManager personaManager = (PersonaManager) context.getBean("personaManager");
  // Inserto
  personaManager.insertarPersonas(personas);
  // Borro
  //personaManager.borraPersonas(personas);
  // Borro ppr Id
  //personaManager.borrarPersona(12651196);
  // Obtengo la lista le personas
  List listaPersonas = personaManager.obtenerPersonas();
  logger.debug("personas retornadas = " + listaPersonas);
  // Verifico que la persona existe
  assertTrue(listaPersonas.contains(persona1) && listaPersonas.contains(persona2));
 }
}

Si todo ha ido bien ya podemos empaquetar el proyecto y preparar el .jar en nuestro repositorio de maven para ser usado desde otros proyectos:
$ mvn -U clean package
$ mvn install:install-file -Dfile=target/dataTest-1.0-SNAPSHOT.jar -DgroupId=testNak -DartifactId=dataTest -Dversion=1.0 -Dpackaging=jar

Aki van los fuentes.

No hay comentarios: