Persistencia de Objetos Java utilizando Hibernate

Puede encontrar la versión original de este artículo en Inglés en:

http://www.devx.com/Java/

Introducción

Para la mayoría de las aplicaciones, almacenar y recuperar información implica alguna forma de interacción con una base de datos relacional. Esto ha representado un problema fundamental para los desarrolladores porque algunas veces el diseño de datos relacionales y los ejemplares orientados a objetos comparten estructuras de relaciones muy diferentes dentro de sus respectivos entornos.

Las bases de datos relacionales están estructuradas en un configuración tabular y los ejemplares orientados a objetos normalmente están relacionados en forma de árbol. Esta 'diferencia de impedancia' ha llevado a los desarrolladores de varias tecnologías de persistencia de objetos a intentar construir un puente entre el muno relacional y el mundo orientado a objetos.

Este artículo es el segundo de una serie, en la que discutiremos cómo tres de estas tecnologías de persistencia de objetos (EJB, Java Data Objects, e Hibernate) intentan simplificar la tarea de conectar bases de datos relacionales y el lenguaje Java.

Persistencia de Objetos

La tarea de persistir objetos Java en una base de datos relacional actualmente está siendo facilitada por un gran número de herramientas que permiten a los desarrolladores dirigir motores de persistencia para convertir objetos Java a columnas/registros de una base de datos y viceversa. Esta tarea implica serializar objetos Java estructurados en forma de árbol a una base de ratos relacional estructurada de forma tabular y viceversa. Esencial para este esfuerzo es la necesidad de mapear los objetos Java a columnas y registros de la base de datos de una manera optimizada en velocidad y eficiencia.

El marco de trabajo Hibernate se enfrenta al problema "objeto-java-a-base-de-datos" de forma tan elegante como cualquier otro marco de trabajo disponible. Hibernate funciona persistiendo y restaurando viejos objetos Java (POJOs) utilizando un modelo de programación muy transparente y poco exigente.

Introducción a Hibernate

Hibernate es un marco de trabajo Java que proporciona mecanismos de mapeo objeto/relacional para definir cómo se almacenan, eliminan, actualizan y recuperan los objetos Java. Además, Hibernate ofrece servicios de consulta y recuperación que pueden optimizar los esfuerzos de desarrollo dentro de entornos SQL y JDBC. Por último, Hibernate reduce el esfuerzo necesario para convertir hojas de resultados de la base de datos relacional en gráficos de objetos Java.

Una de las características únicas de Hibernate es que no requiere que los desarrolladores implementen interfaces propietarios o extiendan clases base propietarias para poder persistir las clases. En vez de eso, Hibernate trata con la reflection de Java y el aumento de clases en tiempo de ejecución utilizando una librería de generación de código Java muy poderosa y de alto rendimiento llamada CGLIB. CGLIB se utiliza para extender clases Java e implementar interfaces Java en tiempo de ejecución.

El Fichero de Configuración de Hibernate

Se puede configurar el entorno Hibernate de un par de formas. Una forma estándard que se declara como muy flexible y conveniente es almacenar la configuración en un fichero llamado hibernate.cfg.xml. Este fichero se sitúa en la raíz del classpath del contexto de la aplicación (por ejemplo: WEB-INF/classes). Se puede acceder a este fichero utilizando la clase net.sf.hibernate.cfg.Configuration en tiempo de ejecución.

El fichero hibernate.cfg.xml define la información sobre la conexión a la base de datos, la clase factoría de transaciones, los recursos de mapeo, etc. El siguiente código muestra una configuración típica de este fichero:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
   "-//Hibernate/Hibernate Configuration DTD 2.0//EN"
   "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
   <session-factory>
   
      <property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
      <property name="connection.url">jdbc:hsqldb:data/userejb</property>
      <property name="connection.username">sa</property>
      <property name="connection.password"></property>
      <property name="show_sql">true</property>
      <property name="dialect">net.sf.hibernate.dialect.HSQLDialect</property>
      <property name="transaction.factory_class">
         net.sf.hibernate.transaction.JDBCTransactionFactory
      </property>
      
      <mapping resource="com/jeffhanson/businesstier/model/UserInfo.hbm.xml"/>
   </session-factory>
</hibernate-configuration>

El Fichero de Configuración de Mapeo de Hibernate

Las aplicaciones Hibernate hacen uso de ficheros de mapeo que contienen metadatos que definen los mapeos objeto/relacional para las clases Java. Un fichero de mapeo tiene el sufijo .hbm.xml. Dentro de cada fichero de configuración, se mapean a tablas de la base de datos las clases que se van a persistir y las propiedades se definen con mapeos de campo/columna y claves primarias. El siguiente código ilustra un típico fichero de configuración de Hibernate llamado UserInfo.hbm.xml:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
          "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <class name="com.jeffhanson.businesstier.model.UserInfo" table="USER">

        <id name="id" type="string" unsaved-value="null" >
            <column name="USER_ID" not-null="true"/>
            <generator class="uuid.hex"/>
        </id>

        <property name="fullName">
            <column name="FULLNAME" length="32" not-null="true"/>
        </property>
        <property name="address"/>
        <property name="city"/>
        <property name="state"/>
        <property name="zip"/>

    </class>

</hibernate-mapping>

Sesiones Hibernate

Pada poder utilizar los mecanismos de persistencia de Hibernate se debe inicializar el entorno Hibernate y obtener un objeto Session utilizando la clase SessionFactory de Hibernate. El siguiente fragmento de código ilustra este proceso:

// Initialize the Hibernate environment
Configuration cfg = new Configuration().configure();

// Create the session factory
SessionFactory factory = cfg.buildSessionFactory();

// Obtain the new session object
Session session = factory.openSession();

La llamada a Configuration().configure() carga el fichero de configuración hibernate.cfg.xml e inicializa el entorno de Hibernate. Una vez inicializada la configuración, se puede hacer cualquier modificación adicional de forma programática. Sin embargo, estas modificaciones se deben hacer antes de crear el ejemplar de SessionFactory.

Normalmente, el ejemplar de SessionFactory sólo se crea una vez y luego se utiliza para crear todas las sesiones relacionadas con un contexto dado.

Un objeto Session Hibernate representa una única unidad-de-trabajo para un almacen de datos dado y lo abre un ejemplar de SessionFactory. Se deben cerrar las sesiones cuando se haya completado todo el trabajo de una transación. El siguiente código ilustra una sesión típica de Hibernate:

Session session = null;
UserInfo user = null;
Transaction tx = null;

try {
   session = factory.openSession();
   tx = session.beginTransaction();

   user = (UserInfo)session.load(UserInfo.class, id);

   tx.commit();
}
catch(Exception e) {
   if (tx != null)    {
      try  {
         tx.rollback();
      }
      catch (HibernateException e1)  {
         throw new DAOException(e1.toString());
      }
   }
   throw new DAOException(e.toString());
}
finally {
   if (session != null) {
      try {
         session.close();
      }
      catch (HibernateException e)  {
      }
   }
}

El Lenguaje de Consultas de Hibernate

Hibernate ofrece un lenguaje de consultas que agrupa un potente y flexibe mecanismo de consulta, almacenamiento, actualización y recuperación de objetos desde una base de datos. Este lenguaje, el Hibernate Query Language (HQL), es una extensión orientada a objetos de SQL. HQL permite acceder a los datos de varias formas, incluyendo consultas orientadas a objetos, como en el método find() del siguiente ejemplo:

List users = session.find("from UserInfo as u where u.fullName = ?",
                "John Doe",
                Hibernate.STRING );

Se pueden construir consultas dinámicas utilizando el API criteria de Hibernate:

Criteria criteria = session.createCriteria(UserInfo.class);
criteria.add(Expression.eq("fullName", "John Doe"));
criteria.setMaxResults(20);
List users = criteria.list();

Si se prefiere, se puede utilizar SQL o expresar una consulta SQL, utilizando createSQLQuery():

List users =  session.createSQLQuery("SELECT {user.*} FROM USERS AS {user}", 
                          "user",
                          UserInfo.class).list();

Cuando se devuelven muchos objetos desde una consulta, los objetos serán cargados según se necesite utilizando uno de los métodos iterate(). Los métodos iterate() ofrecen un mejor rendimiento porque cargan los objetos bajo demanda:

Iterator iter =    session.iterate("from UserInfo as u where u.city = New York"); 
while (iter.hasNext()) {
   UserInfo user = (UserInfo)iter.next();
   // process the user object here
}

La Aplicación y el Entorno de Ejecución

Este artículo utiliza JBoss 3.2.3 como entorno de despligue y ejecución de los ejemplos que siguen. Diseñaremos una sencilla aplicación Web que permite crear y recuperar cuentas de usuario utilizando un navegador Web. Las peticioens de cliente se pasarán desde un navegador a un servlet Java, que comunica con un servicio de usuario, que comunica con accesos a los objetos de datos basados en Hibernate (DAOs), como se muestra en la siguiente figura:

El patrón DAO abstrae y encapsula todos los accesos a la fuente de datos. La aplicación tiene un interface DAO, UserDao. La clase que lo implementa, HibernateUserDao contiene lógica específica de Hibernate para manejar las tareas de manejo de datos para un usuario dado.

Se debe construir o modificar algunos de los ficheros de configuracion para acomodar las necesidades de Hibernate. Primero, se modifica el fichero jaws.xml para definir la fuente de datos de la aplicación:

<?xml version="1.0" encoding="ISO-8859-1"?>

<jaws>
   <datasource>java:/DefaultDS</datasource>
   <type-mapping>Hypersonic SQL</type-mapping>
</jaws>

Luego, se modifica el fichero hibernate.cfg.xml para definir las propiedades Hibernate que cargará la aplicación cuando se configure Hibernate. Entre otras cosas, se configura el entorno para utilizar HSQL como la base de datos y se define un recurso de mapeo para la clase UserInfo:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
   "-//Hibernate/Hibernate Configuration DTD 2.0//EN"
   "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
   <session-factory>
   
      <property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
      <property name="connection.url">jdbc:hsqldb:data/userejb</property>
      <property name="connection.username">sa</property>
      <property name="connection.password"></property>
      <property name="show_sql">true</property>
      <property name="dialect">net.sf.hibernate.dialect.HSQLDialect</property>
      <property name="transaction.factory_class">
         net.sf.hibernate.transaction.JDBCTransactionFactory
      </property>
      <property name="hibernate.cache.provider_class">
         net.sf.hibernate.cache.HashtableCacheProvider
      </property>
      <property name="hibernate.hbm2ddl.auto">update</property>
      
      <mapping resource="com/jeffhanson/businesstier/model/UserInfo.hbm.xml"/>
   </session-factory>
</hibernate-configuration>

Configuración de la Capa Web

Cada petición de cliente HTTP la maneja un servlet al estilo FrontController embutido en un ejemplar de UserInfoServlet. Este ejemplar convierte cada petición en una petición a un servicio de negocio y luego llama al servicio de negocio apropiado para procesarla.

Aquí puede ver el código de la clase UserInfoServlet:


package com.jeffhanson.webtier;

import com.jeffhanson.businesstier.model.UserInfo;
import com.jeffhanson.businesstier.ServiceException;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;

public class UserInfoServlet extends HttpServlet {
   
   protected void doPost(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
      doGet(req, res);
   }

   protected void doGet(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
      res.setContentType("text/html");
      PrintWriter out = res.getWriter();

      String userID = req.getParameter("userID");
      String state = req.getParameter("state");

      if (userID != null && userID.length() > 0) {
         try {
            out.println(findSingleUser(userID));
         }
         catch (ServiceException e) {
            throw new ServletException(e.toString());
         }
      }
      else if (state != null && state.length() > 0) {
         try {
            out.println(findUsersByState(state));
         }
         catch (ServiceException e) {
            throw new ServletException(e.toString());
         }
      }
      else {
         out.println("No input parameters specified.");
      }
   }

   private String findUsersByState(String state)
      throws ServiceException {
      String retStr = "";

      UserInfo[] users =
         com.jeffhanson.businesstier.UserService.getUsersByState(state);
      if (users != null) {
         for (int i = 0; i < users.length; i++) {
            retStr += stringifyUserInfo(users[i]) + "<BR>";
         }
      }
      else {
         retStr = "<para>No users found";
      }

      return retStr;
   }

   private String findSingleUser(String userID)
      throws ServiceException {
      String retStr = "";

      UserInfo userInfo =
         com.jeffhanson.businesstier.UserService.getUser(userID);
      if (userInfo != null) {
         retStr = stringifyUserInfo(userInfo);
      }
      else {
         retStr = "<para>User not found";
      }

      return retStr;
   }

   private String stringifyUserInfo(UserInfo userInfo) {
      String retStr;
      StringBuffer strBuf = new StringBuffer();
      strBuf.append("<para>User info: <br>
");
      strBuf.append("User ID: " + userInfo.getId() + "<br>
");
      strBuf.append("User Full Name: " + userInfo.getFullName() + "<br>
");
      strBuf.append("User Address: " + userInfo.getAddress() + "<br>
");
      strBuf.append("User City: " + userInfo.getCity() + "<br>
");
      strBuf.append("User State: " + userInfo.getState() + "<br>
");
      strBuf.append("User Zip: " + userInfo.getZip() + "<br>
");
      retStr = strBuf.toString();
      return retStr;
   }
}

La capa de Negocio

Toda petición de cliente HTTP se convierte a una petición de servicio de negocio y se le pasa al servicio de negocio apropiado. Cada objeto de servicio de negocio realiza la lógica de negocio necesaria y hace uso del DAO apropiado para acceder al almacén de datos.

La clase UserService encapsula los métodos para operar sobre objetos UserInfo incluyendo almacenamiento, actualización, borrado y recuperación de ejemplares de UserInfo. Aquí puede ver la clase UserService:


package com.jeffhanson.businesstier;

import com.jeffhanson.businesstier.model.UserInfo;
import com.jeffhanson.datatier.HibernateUserDAO;

public class UserService {
   public static UserInfo addUser(String id,
                                  String fullName,
                                  String address,
                                  String city,
                                  String state,
                                  String zip)
      throws ServiceException {
      UserInfo userInfo = null;

      try {
         HibernateUserDAO dao = HibernateUserDAO.getInstance();
         userInfo = dao.createUser(id, fullName, address, city, state, zip);
      }
      catch (Exception e) {
         throw new ServiceException(e.toString());
      }

      return userInfo;
   }

   public static UserInfo getUser(String userID)
      throws ServiceException {
      UserInfo userInfo = null;

      try {
         HibernateUserDAO dao = HibernateUserDAO.getInstance();
         userInfo = dao.readUser(userID);
      }
      catch (Exception e) {
         throw new ServiceException(e.toString());
      }

      return userInfo;
   }

   public static UserInfo[] getUsersByState(String state)
      throws ServiceException {
      UserInfo[] users = null;

      try {
         HibernateUserDAO dao = HibernateUserDAO.getInstance();
         users = dao.readUsersByState(state);
      }
      catch (Exception e) {
         throw new ServiceException(e.toString());
      }

      return users;
   }

   public static void modifyUser(UserInfo userInfo)
      throws ServiceException {
      try {
         HibernateUserDAO dao = HibernateUserDAO.getInstance();
         dao.updateUser(userInfo);
      }
      catch (Exception e) {
         throw new ServiceException(e.toString());
      }
   }

   public static void removeUser(UserInfo userInfo)
      throws ServiceException {
      try {
         HibernateUserDAO dao = HibernateUserDAO.getInstance();
         dao.deleteUser(userInfo);
      }
      catch (Exception e) {
         throw new ServiceException(e.toString());
      }
   }
}

La clase UserService utiliza la clase UserInfo, que representa a un usuario dado. El siguiente listado muestra la clase UserInfo:


package com.jeffhanson.businesstier.model;

import java.io.Serializable;

public class UserInfo implements Serializable {
   private String id = "";
   private String fullName = "";
   private String address = "";
   private String city = "";
   private String state = "";
   private String zip = "";

   public String getId() {
      return id;
   }

   public void setId(String id) {
      this.id = id;
   }

   public String getFullName() {
      return fullName;
   }

   public void setFullName(String fullName) {
      this.fullName = fullName;
   }

   public String getAddress() {
      return address;
   }

   public void setAddress(String address) {
      this.address = address;
   }

   public String getCity() {
      return city;
   }

   public void setCity(String city) {
      this.city = city;
   }

   public String getState() {
      return state;
   }

   public void setState(String state) {
      this.state = state;
   }

   public String getZip() {
      return zip;
   }

   public void setZip(String zip) {
      this.zip = zip;
   }

   public String toString() {
      return "UserInfo { "
             + "ID: " + id
             + ", FullName: " + fullName
             + ", Address: " + address
             + ", City: " + city
             + ", State: " + state
             + ", Zip: " + zip
             +" }";
   }
}
 

La clase UserInfo representa un usuario dado y se configura para Hibernate en el fichero UserInfo.hbm.xml:

<?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>
   <!-- com.jeffhanson.businesstier.model.UserInfo root -->
   <class name="com.jeffhanson.businesstier.model.UserInfo" table="USEREJB">
      <id name="id" type="string">
         <column name="id" length="16"/>
         <generator class="uuid.hex"/>
      </id>
      <property name="address" column="address" type="string"/>
      <property name="zip" column="zip" type="string"/>
      <property name="state" column="state" type="string"/>
      <property name="city" column="city" type="string"/>
      <property name="fullName" column="fullName" type="string"/>
   </class>
</hibernate-mapping>

La Capa de Datos

Toda petición de cliente se pasa al servicio de negocio apropiado para su procesamiento. Un servicio de negocio realiza la lógica de negocio necesaria y hace uso del DAO apropiado para acceder al almacen de datos. Cada DAO realiza las interacciones necesarias con Hibernate para poder actúar sobre un almacen de datos dado. El interface UserDAO define los métodos que cada DAO debe implementar:

package com.jeffhanson.datatier;

import com.jeffhanson.businesstier.model.UserInfo;

public interface UserDAO {
   public UserInfo createUser(String id,
                              String fullName,
                              String address,
                              String city,
                              String state,
                              String zip) throws DAOException;

   public UserInfo readUser(String id)
      throws DAOException;

   public UserInfo[] readUsersByState(String state)
      throws DAOException;

   public void updateUser(UserInfo userInfo)
      throws DAOException;

   public void deleteUser(UserInfo userInfo)
      throws DAOException;
}

El siguiente listado muestra una implementación del interface UserDAO que permite aceder a los datos del tipo UserInfo almacenados mediante la clase HibernateUserDAO:


package com.jeffhanson.datatier;

import com.jeffhanson.businesstier.model.UserInfo;
import net.sf.hibernate.*;
import net.sf.hibernate.expression.Expression;
import net.sf.hibernate.cfg.Configuration;

import java.util.List;
import java.util.Iterator;

public class HibernateUserDAO implements UserDAO {
   private static Configuration cfg = null;
   private static SessionFactory factory = null;
   private static HibernateUserDAO instance = null;

   public static HibernateUserDAO getInstance()
      throws DAOException {
      if (instance == null) {
         try {
            cfg = new Configuration().configure();
            factory = cfg.buildSessionFactory();
         }
         catch (HibernateException e) {
            throw new DAOException(e.toString());
         }

         instance = new HibernateUserDAO();
      }

      return instance;
   }

   private HibernateUserDAO() {
   }

   public UserInfo createUser(String id,
                              String fullName,
                              String address,
                              String city,
                              String state,
                              String zip) throws DAOException {
      Session session = null;
      UserInfo user = null;

      try {
         session = factory.openSession();
         Transaction tx = session.beginTransaction();

         user = new UserInfo();
         user.setId(id);
         user.setFullName(fullName);
         user.setAddress(address);
         user.setCity(city);
         user.setState(state);
         user.setZip(zip);

         session.save(user);

         tx.commit();
      }
      catch (HibernateException e) {
         throw new DAOException(e.toString());
      }
      finally {
         if (session != null) {
            try {
               session.close();
            }
            catch (HibernateException e) {
            }
         }
      }

      return user;
   }

   public UserInfo readUser(String id) throws DAOException {
      Session session = null;
      UserInfo user = null;
      Transaction tx = null;

      try {
         session = factory.openSession();
         tx = session.beginTransaction();

         user = (UserInfo)session.load(UserInfo.class, id);

         tx.commit();
      }
      catch(Exception e) {
         if (tx != null) {
            try {
               tx.rollback();
            }
            catch (HibernateException e1) {
               throw new DAOException(e1.toString());
            }
         }
         throw new DAOException(e.toString());
      }
      finally {
         if (session != null) {
            try {
               session.close();
            }
            catch (HibernateException e) {
            }
         }
      }

      return user;
   }

   public UserInfo[] readUsersByState(String state)
      throws DAOException {
      Session session = null;
      UserInfo[] users = null;
      Transaction tx = null;

      try {
         session = factory.openSession();
         tx = session.beginTransaction();

         Criteria criteria =
            session.createCriteria(UserInfo.class);
         List list =
            criteria.add(Expression.eq("state", state)).list();
         if (list.size() > 0) {
            users = new UserInfo[list.size()];
            list.toArray(users);
         }

         tx.commit();
      }
      catch(Exception e) {
         if (tx != null) {
            try {
               tx.rollback();
            }
            catch (HibernateException e1) {
               throw new DAOException(e1.toString());
            }
         }
         throw new DAOException(e.toString());
      }
      finally {
         if (session != null) {
            try {
               session.close();
            }
            catch (HibernateException e) {
            }
         }
      }

      return users;
   }

   public void updateUser(UserInfo userInfo)
      throws DAOException {
      Session session = null;
      Transaction tx = null;

      try {
         session = factory.openSession();
         tx = session.beginTransaction();

         session.saveOrUpdate(userInfo);

         tx.commit();
      }
      catch(Exception e) {
         if (tx != null) {
            try {
               tx.rollback();
            }
            catch (HibernateException e1) {
               throw new DAOException(e1.toString());
            }
         }
         throw new DAOException(e.toString());
      }
      finally {
         if (session != null) {
            try {
               session.close();
            }
            catch (HibernateException e) {
            }
         }
      }
   }

   public void deleteUser(UserInfo userInfo)
      throws DAOException {
      Session session = null;
      Transaction tx = null;

      try {
         session = factory.openSession();
         tx = session.beginTransaction();

         session.delete(userInfo);

         tx.commit();
      }
      catch(Exception e) {
         if (tx != null) {
            try {
               tx.rollback();
            }
            catch (HibernateException e1) {
               throw new DAOException(e1.toString());
            }
         }
         throw new DAOException(e.toString());
      }
      finally {
         if (session != null) {
            try {
               session.close();
            }
            catch (HibernateException e) {
            }
         }
      }
   }
}

Acortando Diferencias

Las diferencias arquitecturales entre el árbol de objetos Java y las tablas de las bases de datos relacionales, hacen que sea un poco desalentadora para los desarrolladores la tarea de persistir objetos Java en una base de datos relacional. La diferencia de impedancia entre las tablas relacionales y el árbol de objetos Java ha llevado a los desarrolladores a tratar con muchas tecnologías de persistencia de objetos diferentes para intentar reducir la distancia entre el mundo relacional y el mundo orientado a objetos. El marco de trabajo Hibernate define un mecanismo de mapeo objeto/relacional y un lenguaje de consulta que hace que el almacenamiento y recuperación desde un almacen de datos sea una proposición relativamente sencilla.

COMPARTE ESTE ARTÍCULO

ENVIAR A UN AMIGO
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN GOOGLE +
¡SÉ EL PRIMERO EN COMENTAR!
Conéctate o Regístrate para dejar tu comentario.