Construcció d'Aplicacions de Programari Lliure en J2EE/Tècniques avançades: mapatge objecte-relacional, test unitaris i inversió de control

Salta a la navegació Salta a la cerca

Mapatge objecte-relacional[modifica]

Què és ?[modifica]

El mapatge objecte-relacional és una tècnica que té com a finalitat principal facilitar la tasca d'emmagatzemar i recuperar objectes d'un llenguatge de dades orientat a objectes en bases de dades relacionals.

Vam indicar al començament d'aquest curs que les variables d'un objecte son les que guarden el seu estat, de forma que són aquestes les que s'han d'emmagatzemar a la base de dades per a poder recuperar l'objecte en les mateixes condicions que quan es va guardar.

Hi ha estudis que conclouen que el 35% del codi d'una aplicació que utilitzi alguna mena de mecanisme de persistència d'objectes, està relacionat amb aquest aspecte. L'ús d'aquestes tecniques és, per tant, un factor molt important de reducció del cost de construcció d'una aplicació.


Hibernate[modifica]

Hibernate és un mapejador objecte-relacional de codi obert.

El seu funcionament es basa en la creació d'un fitxer de configuració, anomenat fitxer de mapatge, per a cada classe que es vol emmagatzemar a la base de dades. Aquest fitxer relaciona la classe amb una taula de la base de dades i cada variable de la classe amb una columna d'aquesta taula.

D'aquesta forma, cada cop que es vulgui emmagatzemar o recuperar l'objecte a la base de dades, Hibernate genera tot el codi SQL necessari, i el programador no deixa mai de treballar en un llenguatge orientat a objecte.

Funcionament[modifica]

Per tal de fer que una classe es pugui emmagatzemar i recuperar de la base de dades amb Hibernate, ha de complir tres requisits:

  • Disposar d'una variable que pugui actuar com a identificador únic de l'objecte. Aquest identificador el generarà Hibernate, de forma que no serà manipulat pel nostre codi.
  • Disposar d'un constructor sense arguments.
  • Disposar, per a cada variable de la classe, d'un mètode per a donar-li valor i un altre per a obtenir-lo. Aquests mètodes s'anomenen setters i getters respectivament i han de començar amb la paraula set i get seguida del nom de la variable a la que donen o consulten valor, amb la primera lletra en majúscules.

Exemple: Fem la classe TipusAvio candidata a ser persistent amb Hibernate

package ca.udl.estiu;

public class TipusAvio {

    private int  id;                     //Id del tipus d'avió

    private String  nom;                    //Nom del tipus d'avió
    private int     nombreSeients;          //nombre de seients

    
    public TipusAvio(){}

    public TipusAvio(String nom, int nombreSeients){
    	
    	this.nom = nom;
    	this.nombreSeients = nombreSeients;
    }
    
    public void setId(int id){
            this.id = id;
    }

    public int getId(){
            return id;
    }
    public void setNom(String nom){
            this.nom= nom;
    }

    public String getNom(){
            return nom;
    }

    public void setNombreSeients(int nombreSeients){
            this.nombreSeients = nombreSeients;
    }
    public int getNombreSeients(){
            return nombreSeients;
    }
	
}

El fitxer de mapatge[modifica]

Un cop una classe reuneix les condicions per a poder ser persistida per Hibernate, haurem de crear el fitxer de mapatge que indica com ha de ser emmagatzemada a la base de dades.

Aquest fitxer ha de tenir el mateix nom que la classe, afegint l'extensió .hbm.xml i per tal que Hibernate el trobi, s'ha de situar al mateix directori on estarà la classe un cop compilada.

En el nostre cas, situarem els fitxers de mapatge al directori conf/, creant sobre aquest l'estructura de directoris ca/udl/estiu. Amb Ant farem que cada cop que compilem els fitxers de mapatge es copiïn al directori build/classes/ca/udl/estiu/, on estaran les classes compilades del nostre projecte.


Exemple: Fitxer de mapatge de la classe TipusAvio

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" 
 "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>

     <class name="ca.udl.estiu.TipusAvio" table="TIPUSAVIO">

          <id name="id" type="int" column="TIPUSAVIO_ID">
               <generator class="native"/>
          </id>
 
          <property name="nom" type="string" not-null="true"/>

          <property name="nombreSeients" type="int" not-null="true"/>

     </class>
</hibernate-mapping>
         


Les etiquetes

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" 
 "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>


</hibernate-mapping>
         

corresponen a l'estructura exterior del fitxer de mapatge.


La primera etiqueta que ha de contenir aquesta estructura és la que indica a quina classe correspon el fitxer i en quina taula de la base de dades es guardarà.

     <class name="ca.udl.estiu.TipusAvio" table="TIPUSAVIO">

En aquest cas veiem que el nom de la classe a la que correspon aquest fitxer és TipusAvio i que la taula de la base de dades on es guardaran els objectes d'aquesta classe s'anomena TIPUSAVIO.


A continuació donem a Hibernate la propietat que volem utilitzar com identificador únic (id) dels objectes d'aquesta classe, especificant de quin tipus és (en aquest cas int) i en quina columna de la base de dades persistirà (en aquest cas TIPUSAVIO_ID).

          <id name="id" type="int" column="TIPUSAVIO_ID">
               <generator class="native"/>
          </id>

L'etiqueta

               <generator class="native"/>

indica que aquest identificador serà generat per Hibernate. Amb la directiva native indiquem que la generació d'aquest identificador es deixa en mans de la base de dades, que utilitzarà el mecanisme que utilitzi per defecte. Si vulguessim gestionar nosaltres la generació de l'identificador, hauriem d'indicar en l'atribut class la classe que se'n ocuparia.

Finalment, indiquem en quina columna de la taula on s'emmagatzemen els objectes d'aquesta classe s'han d'emmagatzemar cadascuna de les variables (nom i nombreSeients).

          <property name="nom" type="string" not-null="true"/>

          <property name="nombreSeients" type="int" not-null="true"/>

Generació de l'esquema de la base de dades[modifica]

Hibernate facilita una eina per a crear automàticament l'esquema de la classe persistent a la base de dades, utilitzant el fitxer de mapatge. D'aquesta forma evitem haver de fer-ho manualment.

Per a generar l'esquema haurem de:

  1. Crear un fitxer de configuració d'Hibernate, per a indicar-li els paràmetres de connexió a la base de dades
  2. Crear un fitxer de configuració d'Ant amb una tasca que genera l'esquema


Fitxer de configuració d'Hibernate (hibernate.properties):

hibernate.dialect=net.sf.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class=com.mysql.jdbc.Driver
hibernate.connection.url=jdbc:mysql://localhost/prova
hibernate.connection.username=estiu
hibernate.connection.password=estiu

Amb aquest fitxer informem a Hibernate sobre la ubicació de la base de dades que ha d'utilitzar, el llenguatge que ha de parlar amb aquesta base de dades, quin driver JDBC ha d'utilitzar i el nom d'usuari i contrasenya de connexió. Perquè Hibernate el trobi, s'ha de situar al directori executable de la nostra aplicació. En el nostre cas, el situarem al directori conf/ i utlitzarem Ant per a copiar-lo al directori build/classes cada cop que compilem.


Fitxer de configuració d'Ant (build.xml):

<?xml version="1.0"?>
<project name="ProvaHibernate" default="compile" basedir=".">

	<!-- Configuració de les propietats que contenen els directoris del projecte -->

	<property name="source.root" value="src" />
	<property name="class.root" value="build/classes"/>
	<property name="properties.root" value="conf/"/>
	<property name="hibernate.dir" value="/usr/local/hibernate-2.1/" />
	<property name="mysql.dir" value="/root/" />

	<!-- Configuració del classpath del projecte -->
	<path id="project.class.path">

		<!-- Incloure les nostres propies classes -->
		<pathelement location="${class.root}" />

		<!-- Incloure Hibernate -->
		<fileset dir="${hibernate.dir}">
			<include name="hibernate2.jar" />
		</fileset>

		<!-- Incloure les llibreries requerides per Hibernate -->
		<fileset dir="${hibernate.dir}">
			<include name="lib/*.jar" />
		</fileset>

		<!-- Incloure el connector mysql -->
		<fileset dir="${mysql.dir}">
			<include name="mysql-connector-java-3.1.13-bin.jar" />
		</fileset>
	</path>


         <!-- Prepara el projecte per ser compilat -->
	 <target name="prepare">

	    <mkdir dir="${class.root}"/>

	    <!-- Copia el fitxer de configuració d'Hibernate i els fitxers de mapping al directori d'execució -->
	    <copy todir="${class.root}" >
	      <fileset dir="${properties.root}">
	         <include name="**/*.properties"/>
	         <include name="**/*.hbm.xml"/>
	      </fileset>
	    </copy>

	 </target>
	

	<!-- Compila les classes del projecte i les situa al directori d'execució -->

	<target name="compile" depends="prepare" description="Compiles all Java classes">
		<javac srcdir="${source.root}" destdir="${class.root}" debug="on" optimize="off" deprecation="on">
			<classpath refid="project.class.path" />
		</javac>	
	</target>


        <!-- Genera l'esquema a la base de dades de les classes amb mapping -->

	<target name="schema" depends="compile">

		<taskdef name="schemaexport" 
			classname="net.sf.hibernate.tool.hbm2ddl.SchemaExportTask"
			classpathref="project.class.path"/>
			
    	<schemaexport properties="${class.root}/hibernate.properties" quiet="no" text="no" drop="no" delimiter=";">

                      <!-- Generar esquema per a totes les classes que tinguin un fitxer de mapping -->

		      <fileset dir="${class.root}">
	        	<include name="**/*.hbm.xml" />
	    	  </fileset>
    	</schemaexport>
	</target>

   </project>

A partir d'aquest moment, quan vulguem generar l'esquema de la base de dades haurem d'executar:

ant schema

a l'arrel del projecte o des de Eclipse.

Emmagatzemament d'objectes[modifica]

A continuació anem a veure un exemple de codi per a emmagatzemar objectes a la base de dades utilitzant Hibernate.

Cal resaltar que per a utilitzar Hibernate des d'Eclipse, cal afegir el seu .jar com a llibreria del projecte. Per a fer-ho, primer haurem d'instal·lar Hibernate al nostre disc dur.

package ca.udl.estiu;

import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;

public class CreateTest {

    public static void main(String args[]) throws Exception{

        //Crea una configuració basada en les propietats especificades
        Configuration config = new Configuration();

        //Inclou a la configuració el mapeig de les classes que vol utilitzar
        config.addClass(TipusAvio.class);

        //Carrega la factoria que utilitzem per a fer la persistencia
        SessionFactory sessionFactory = config.buildSessionFactory();

        //Obté una sessió utilitzant la informació de configuració
        Session session = sessionFactory.openSession();
        Transaction tx = null;
        try{

             //Comença una transacció amb la base de dades.
             //Una transacció permet fer operacions de forma conjunta
             //podent desfer els canvis si hi ha algun error en alguna de les operacions

             tx = session.beginTransaction();


             //Crea un nou objecte TipusAvio
             TipusAvio tipusavio = new TipusAvio("A", 200);

             //Sol·licita a Hibernate que el gravi a la base de dades
             session.save(tipusavio);

             //Es crea un altre objecte i es demana la seva gravació
             tipusavio = new TipusAvio("B", 350);
             session.save(tipusavio);

             //Es crea un altre objecte i es demana la seva gravació
             tipusavio = new TipusAvio("C", 400);
             session.save(tipusavio);

             //Finalitzem la transacció
             tx.commit();       

        } 
        catch (Exception e){

             //S'ha produït un error, al començar la transacció o al gravar algun objecte

             //Si ja s'havia iniciat la transacció, desfem els canvis
             if (tx != null){
                   tx.rollback();
             }
             throw e;
        }
        //Tant si s'ha produït error com si no, s'executarà el mètode finally
        finally {
             //Tanquem la sessió
             session.close();
        }
        // Tanquem la factoria de sessions
        sessionFactory.close();
   }
	
}


Per a poder executar aquest codi utilitzant Ant, podem afegir les següents línies al fitxer build.xml:

         <target name="create" depends="compile">
		<java classname="ca.udl.estiu.CreateTest">
			<classpath refid="project.class.path"/>
		</java>
	</target>

Un cop afegides les línies, podrem executar el codi mitjançant Ant amb la comanda

ant create

Recuperar dades[modifica]

A continuació veiem un exemple de com recuperar les dades

package ca.udl.estiu;

import net.sf.hibernate.*;
import net.sf.hibernate.cfg.Configuration;

public class QueryTest{

     public static void main(String args[]) throws Exception{

          // Crea una configuració basada en les propietats de l'arxiu definit
          Configuration config = new Configuration();


          //Inclou a la configuració el mapeig de les classes que vol utilitzar
          config.addClass(TipusAvio.class);


          //Carrega la factoria de sessions que utilitzem per fer la persistència
          SessionFactory sessionFactory = config. buildSessionFactory();


          // Obté una sessió utilitzant la informació de configuració
          Session session = sessionFactory.openSession();

          try{

               //Demana a Hibernate que retorni un objecte de la classe TipusAvio amb identificador 1
               TipusAvio ta = (TipusAvio)session.get(TipusAvio.class, 1);

               //Imprimeix les dades de l'objecte retornat
               System.out.println("Tipus Avió:  ID - " + ta.getId() + " Nom - " + 
                                   ta.getNom() + " Nombre seients - " + ta.getNombreSeients());
          }
          catch (Exception e){

               //S'ha produït un error cercant l'objecte
               System.out.println("ERROR: L'objecte TipusAvio amb identificador 1 no existeix a la base de dades");
          }
          //Tant si hi ha error com si no
          finally {
               // Tanca la sessió
               session.close();
          }           
          // Tanca la factoria de sessions
          sessionFactory.close();
     }
}

Per a poder executar aquest codi utilitzant Ant, podem afegir les següents línies al fitxer build.xml:

    <target name="qtest" depends="compile">
        <java classname="ca.udl.estiu.QueryTest" fork="true">
	    <classpath refid="project.class.path"/>
	</java>
    </target>

Un cop afegides les línies, podrem executar el codi mitjançant Ant amb la comanda

ant qtest

Proves unitàries[modifica]

Què són ?[modifica]

Les proves unitàries, també anomenades proves d'objecte, son mètodes automatitzats de prova del funcionament d'un objecte de forma aïllada. Per implementar una prova unitària es crea una aplicació que fa crides als mètodes de l'objecte i compara els resultats obtinguts amb els desitjats.


JUnit[modifica]

JUnit és un conjunt d'utilitats que facilita la creació de proves unitàries d'aplicacions implementades amb Java.

Per a realitzar tests unitaris amb JUnit haurem de:

  • Crear una classe que extengui a la classe TestCase
  • Per a cada prova que vulguem realitzar, crearem un mètode amb un nom que comenci amb la paraula test seguida de qualsevol combinació de paraules
  • Aquests mètodes no retornaran cap valor ni acceptaran paràmetres
  • Dins d'aquests mètodes utilitzarem els mètodes assertXXX(objecte A, objecte B) de la classe TestCase, que retornen cert si els objectes A i B compleixen la propietat que dóna nom al mètode i fals en cas contrari. Per exemple, assertEquals("a","b") retornaria fals.

Quan executem aquesta classe com a test JUnit, aquest buscarà tots els mètodes amb nom començat per test i els executarà un a un, generant un informe dels mètodes que han fallat i els mètodes que no ho han fet.

Per a poder utilitzar Junit des d'Eclipse, haurem d'incloure el seu .jar com a llibreria del sistema. Per a fer-ho, en primer lloc haurem d'instal·lar Junit al nostre disc dur.

Veiem un exemple de prova unitària de la classe TipusAvio:

 
package ca.udl.estiu;

import junit.framework.TestCase;

public class TipusAvioTest extends TestCase {
	
    //Comprova que el nom d'una nova instància de la classe TipusAvio es nul quan el creem amb el constructor buit.
    public void testTipusAvioConstructorBuit(){

        TipusAvio ta = new TipusAvio();
        String s = ta.getNom();
        assertEquals(null,s);
   }


    //Comprova que el constructor amb paràmetres funciona correctament.
    public void testTipusAvioConstructor(){
    	
    	TipusAvio ta = new TipusAvio("nom",200);
    	
    	assertEquals("nom",ta.getNom());
    	assertEquals(200,ta.getNombreSeients());
    }
}


És important notar que les proves unitàries ens serveixen, a més de com a proves del bon funcionament d'un objecte, com a documentació del seu funcionament.

Inversió de control[modifica]

Dependències[modifica]

Les dependències entre classes es donen quan una classe utilitza una altra classe per a realitzar una determinada acció.

Què és la inversió de control ?[modifica]

La inversió de control és una tècnica que permet minimitzar les dependències entre classes en una aplicació. Per a fer-ho, aporta més control sobre aquestes dependències facilitant formes de configurar-les i canviar-les en temps d'execució.

La inversió de control requereix la utilització d'una tècnica anomenada Programació contra interfícies.


Programació contra interfícies[modifica]

S'anomena programació contra interfícies al fet de cridar als mètodes d'una interfície en comptes de fer-ho d'una classe concreta.

Exemple: Utilització de la interfície ResultSet de JDBC

ResultsetSet rs = stmt.executeQuery("SELECT * FROM TAULA");

while (rs.next()){
    System.out.println("El id és: "+ rs.getString("ID"));
    System.out.println("El valor del camp1 és: "+ rs.getInt("CAMP1"));
    System.out.println("El valor del camp2 és: "+ rs.getString("CAMP2");
}

En aquest codi, l'objecte ResultSet és en realitat una interfície. No sabem quina és la classe concreta que estem utilitzant per a realitzar les operacions. De fet, el mètode stmt.executeQuery() podria retornar qualsevol classe que implementés la interfície ResultSet.

Ara suposem que volem implementar una classe per emmagatzemar les dades d'una aplicació.

Una primera aproximació seria fer-ho, com es mostra a l'exemple següent, creant una classe Persistencia que implementi un mètode guardarDades(Dades objecteDades).

 
class Persistencia{
     public static void guardarDades(Dades objecteDades){
          //Implementació
          }
     }

L'aplicació hauria d'utilitzar aquesta classe de la forma que es mostra al programa

class Aplicació{
     public void guardarDades(Dades objecteDades){
          Persistencia.guardarDades(objeceDades);
     }
}

Utilitzar aquesta aproximació fa que únicament tinguem una manera d'emmagatzemar les dades de la nostra aplicació, tot i que sempre tindrem la possibilitat de canviar la implementació de la classe Persistencia per a canviar el seu comportament.

Però que passa si volem disposar de diverses formes d'emmagatzemar la informació i decidir quina utilitat en temps d'execució?

Aquest requisit s'anomena ampliació per connectors (plug-ins). L'aplicació facilita la implementació per defecte, a vegades buida, d'una funcionalitat, permet enganxar noves implementacions i escollir quina s'utilitza en temps d'execució, ja sigui mitjançant fitxers de configuració o a elecció de l'usuari.

Per a afegir aquest comportament a l'aplicació anterior, hauriem de transformar la classe Persistencia en una interficie, deixar que l'aplicació utilitzi aquesta interfície per a cridar al mètode guardarDades(Dades objecteDades), i facilitar-li una forma d'obtenir diverses implementacions de la interfície.

A continuació veiem la interfície Persistencia i dos implementacions diferents d'aquesta.

interface Persistencia{
     public void guardarDades(Dades objecteDades);
}
class PersistenciaFitxers implements Persistencia{
     public void guardarDades(Dades objecteDades){
          //Implementació
     }
}
class PersistenciaBaseDades implements Persistencia{
     public void guardarDades(Dades objecteDades){
          //Implementació
     }
}

Càrrega d'implementacions[modifica]

Com fariem per a carregar una determinada implementació d'una interfície en l'exemple anterior ? Podem utilitzar una Factoria d'implementacions o la Inversió de control

Factories d'implementacions[modifica]

Per complertar la nostra aplicació, caldria escollir quina implementació de la interfície Persistencia utilitzem en cada moment. Utilitzant l'aproximació de la factoria d'implementacions, ho fariem de la forma que presentem a continuació:


class Aplicació{
     public void guardarDades(Dades objecteDades){
          Persistencia p = PersistenciaFactory.getPersistencia();
          p.guardarDades(objecteDades);
     }
}

La classe PersistenciaFactory serà l'encarregada de faciliar una implementació de la interfície Persistencia a l'aplicació. Al següent codi veiem un exemple de factoria que retorna sempre la mateixa implementació.

class PersistenciaFactory{
     public static Persistencia getPersistencia(){
          PersistenciaFitxers pf = new  PersistenciaFitxers();
          return (pf);
     }
}

Una última millora seria que la factoria d'implementacions obtingués d'un fitxer de configuració el nom de la implementació a carregar, o que el prengués per paràmetre a l'operació getPersistencia(). Veiem a continuació com es pot implementar aquest comportament

class PersistenciaFactory{
     public static Persistencia getPersistencia(String implementacio){
          try{
               Class persistencia = Class.forName(implementació);
               Object e = persistencia.newInstance();
               return ((Persistencia)e);
          }
          catch(Exception e){
               return null;
          }    
     }
}


Inversió de control[modifica]

La inversió de control és una tècnica molt similar a una factoria d'implementacions que obté el nom de la implementació a carregar d'un fitxer de configuració. La única diferència és que a la classe que utilitza la interfície no li cal preocuparse de cridar a la factoria, és el propi sistema el que li facilita de forma automàtica la implementació configurada.

Spring[modifica]

Spring és un framework d'inversió de control per injecció de dependències. Facilita la obtenció d'implementacions eliminant tot el codi necessari per a fer-la dels objectes de les nostres aplicacions.

Exemple: Codi de la classe Aplicacio amb Spring

public class Aplicacio {

	Persistencia persistencia;
	
	public Persistencia getPersistencia(){
		
		return persistencia;
	}
	
	public void setPersistencia(Persistencia persistencia){
		
		this.persistencia = persistencia;
	}
	
	public void guardarDades(String dades){
		persistencia.guardarDades("Dades importants");
	}
}

L'únic que cal fer per a configurar Spring és facilitar-li un fitxer de configuració que indiqui quina implementació de la interfície Persistencia utilitzarà la classe Aplicacio.

Exemple: Fitxer de configuració d'Spring

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="fitxer" class="PersistenciaFitxers"/>
    <bean id="bd" class="PersistenciaBaseDades"/>
        
    <bean id="aplicacio" class="Aplicacio">
        <property name="persistencia">
            <ref bean="fitxer"/>
        </property>
    </bean>
</beans>

Ara, crearem una classe per a cridar a Aplicacio que serà la que farà ús d'Spring

Exemple: Classe CridaAplicacio, crida a l'aplicació injectant les dependències amb Spring

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CridaAplicacio {

	public static void main(String [] args){
		ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aplicacio.xml");
		
		Aplicacio app = (Aplicacio)ctx.getBean("aplicacio");
		app.guardarDades("DadesAGuardar");
	}
}