Mapeo de vistas con Hibernate
Índice de contenidos
- Introducción
- Clases del modelo
- Configuración de Hibernate
- Uso de @Subselect
- Uso de <database-object>
- Conclusiones
1. Introducción
Hibernate es una herramienta ORM que nos permite mapear nuestras clases del modelo con tablas de la B.D.
¿Pero qué es lo que ocurre si lo que queremos mapear es una vista en
lugar de una tabla?. En principio no hay ningún problema, ya que
podemos realizar el mapeo sobre la vista igual que si se tratase de una
tabla.
El problema viene a la hora de utilizar la función «hbm2ddl» de
Hibernate para generar nuestra B.D. automáticamente. Hibernate creará
tablas para todas nuestras clases del modelo, independientemente de que
nosotros pensemos en alguna de ellas como en una vista.
Evidentemente podríamos borrar las tablas innecesarias y crear las
vistas correspondientes de forma manual, pero esto no vamos a poder
hacerlo en algunos contextos. Por ejemplo, si tenemos una B.D. para los
tests, la cual se generará cada vez que se lancen los mismos, Hibernate
nos creará tablas en lugar de vistas. Y si estamos utilizando DBUnit
para cargar los datos de prueba, deberemos añadir los datos de nuestras
vistas (porque en realidad van a ser tablas normales).
Para intentar solucionar este problema, en este tutorial vamos a ver dos aproximaciones diferentes.
2. Clases del modelo
Para ambas aproximaciones vamos a utilizar las mismas clases del modelo: Profesor y Clase.
package com.autentia.tutorial; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.validation.constraints.NotNull; @Entity public class Profesor implements Serializable { private static final long serialVersionUID = 4648652280201898240L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @NotNull private String nombre; @NotNull private String apellidos; // Getters y Setters ... }
Por cuestiones de legibilidad se han omitido los getters y los setters.
// Declaración de paquete e importaciones ... @Entity public class Clase implements Serializable { private static final long serialVersionUID = -5734276483633457611L; @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @NotNull private Integer curso; @NotNull private Integer grupo; @NotNull private String descripcion; @NotNull private Integer numAlumnos; @ManyToOne @JoinColumn(name = "idTutor") @NotNull private Profesor tutor; // Getters y Setters ... }
Cada clase tendrá, además de otros campos, un número de alumnos y un
tutor (de tipo Profesor). Imaginemos que en nuestra aplicación vamos a
querer obtener frecuentementeel número de alumnos que tiene a cargo
cada tutor. Podríamos tener una vista que obtuviese los datos
correspondientes:
create or replace view AlumnosTutor as select c.idTutor, sum(c.numAlumnos) totalAlumnos from Clase c group by c.idTutor order by c.idTutor
Para poder obtener desde nuestro código los valores de esta vista, tendremos otra entidad en nuestro modelo:
// Declaración de paquete e importaciones ... @Entity public class AlumnosTutor implements Serializable { private static final long serialVersionUID = 1226577089185303114L; @Id @Column(insertable = false, updatable = false) private Long idTutor; @OneToOne @JoinColumn(name = "idTutor", insertable = false, updatable = false) private Profesor tutor; @Column(insertable = false, updatable = false) private Integer totalAlumnos; // Getters y Setters ... }
Esta clase contendrá un tutor y su número de alumnos. Se han marcado
todas las columnas con «insertable» y «updatable» a false, ya que en
teoría es una vista y no tendremos que insertar ni actualizar nada en
ella.
3. Configuración de Hibernate
Si estamos utilizando Spring en nuestro proyecto, deberemos añadir la configuración de Hibernate en el «applicationContext.xml». Si queréis saber más sobre cómo configurar una aplicación que utilice Spring, Hibernate y anotaciones, os recomiendo consultar este tutorial de Alex.
<?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:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <context:component-scan base-package="com.autentia" /> <tx:annotation-driven transaction-manager="transactionManager" /> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" /> <property name="username" value="username" /> <property name="password" value="********" /> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" /> <property name="configLocation"> <value>classpath:hibernate.cfg.xml</value> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop> <prop key="hibernate.hbm2ddl.auto">create-drop</prop> <prop key="hibernate.show_sql">update</prop> </props> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> </beans>
En mi caso utilizaré una B.D. Oracle Database 10g Express
Edition, en mi propia máquina. Hay que fijarse en la propiedad «hibernate.hbm2ddl.auto» que se
ha puesto a «create-drop», para que cada vez que arranquemos la
aplicación se creen las tablas de la B.D. desde cero y después se
borren. Esto sólo es recomendable para hacer pruebas, después de lo
cual sería conveniente cambiar el valor por «update».
Además, estamos haciendo referencia al fichero «hibernate.cfg.xml», en el que indicamos las clases mapeadas por Hibernate:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="current_session_context_class">thread</property> <mapping class="com.fia.auditorium.model.Clase" /> <mapping class="com.fia.auditorium.model.Profesor" /> <mapping class="com.fia.auditorium.model.AlumnosTutor" /> </session-factory> </hibernate-configuration>
4. Uso de @Subselect
Lo que hemos visto hasta ahora no es nada nuevo y, si dejamos así
las cosas, nuestra aplicación funcionará sin problemas. Pero si
Hibernate está generando nuestras tablas automáticamente, nos generará «AlumnosTutor» como una tabla y no como una vista.
Nuestra primera alternativa para evitar esto, es utilizar la anotación @Subselect para mapear la entidad AlumnosTutor a
una subselect de SQL. De esta manera, la entidad será de sólo lectura y
no buscará los datos en ninguna tabla, sino que los sacará de la propia
consulta proporcionada.
Sólamente tendremos que añadir la anotación @Subselect en nuestra clase AlumnosTutor, pasándole como valor la consulta SQL:
... @Entity @Subselect("select c.idTutor, sum(c.numAlumnos) totalAlumnos from Clase c group by c.idTutor order by c.idTutor") public class AlumnosTutor implements Serializable { ...
De esta manera, en nuestro esquema no se creará la tabla
AlumnosTutor, aunque tampoco se creará la vista. Cada vez que queramos
recuperar datos de esta entidad, se ejecutará una consulta que obtendrá
los datos de la subconsulta que hemos indicado.
Este método tiene la ventaja de que podemos usarlo aunque tengamos
un esquema de B.D. que no podamos modificar para crear la vista que
necesitamos.
5. Uso de <database-object>
Como se indica en la documentación de Hibernate, los
objetos auxiliares de base de datos permiten hacer CREATE y DROP de
objetos arbitrarios de B.D. De esta manera, podemos definir
completamente un esquema a partir de los ficheros de mapeo de Hibernate.
A pesar de estar específicamente
diseñados para crear o borrar cosas como triggers o procedimientos, es
válido cualquier comando SQL que pueda ser ejecutado por medio del
método «java.sql.Statement.execute()» (por ejemplo, ALTERs, INSERTs,
etc.).
Lo que vamos a hacer nosotros, es
aprovechar esta configuración para crear nuestra vista en B.D. Para
ello crearemos el fichero «hibernate.mapping.xml» que colocaremos en la
misma carpeta que el fichero de configuración de Hibernate.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <database-object> <create> create or replace view AlumnosTutor as select c.idTutor, sum(c.numAlumnos) totalAlumnos from Clase c group by c.idTutor order by c.idTutor </create> <drop>drop view AlumnosTutor</drop> <dialect-scope name='org.hibernate.dialect.Oracle10gDialect' /> </database-object> </hibernate-mapping>
Cuando Hibernate genere nuestra B.D. ejecutará la sentencia indicada
en <create>, aunque sólamente en el caso de que el dialecto
utilizado corresponda con el indicado en <dialect-scope>.
Debemos referenciar el fichero anterior desde «hibernate.cfg.xml»,
añadiendo la siguiente línea dentro del elemento
<session-factory>:
<mapping resource="hibernate.mapping.xml" />
Además, tendremos que quitar la anotación @Subselect de nuestra clase AlumnosTutor.
Si ahora dejamos que Hibernate genere nuestras tablas… ¡tendremos
un problema! Los <database-object> se ejecutarán siempre después
de crear las tablas correspondientes a las entidades que tenemos
mapeadas, por lo que Hibernate generará primero la tabla AlumnosTutor y
fallará al intentar crear una vista con el mismo nombre.
Después de intentar varias soluciones, como el uso de @Immutable y
algunas otras anotaciones de Hibernate sin resultados positivos, me he
quedado con dos alternativas que sí funcionan.
5.1 Alternativa utilizando @Subselect
Antes de nada debo advertir que esta alternativa no es recomendable
en absoluto: simplemente funciona y por eso la menciono. Se trata de
dejar todo lo anterior como está, pero incluir una anotación @Subselect
de nuevo en nuestra clase AlumnosTutor:
... @Entity @Subselect("select * from AlumnosTutor") public class AlumnosTutor implements Serializable { ...
De esta manera le decimos a Hibernate que no cree una tabla para
esta entidad, sino que utilice los valores obtenidos de la consulta
(que lo que hace es obtener los valores de la vista). Lo malo de esta
opción es que cada vez que queramos recuperar datos de esta entidad, se
ejecutará una
consulta con un subselect.
En realidad estamos «haciendo trampa», engañando a Hibernate para que
utilice una subconsulta cuando en realidad sí que vamos a tener una
vista en la B.D.
5.2 Alternativa utilizando un segundo <database-object>
Esta alternativa tampoco es perfecta, pero es más recomendable que
la anterior y que otras que he probado. Lo que vamos a hacer, ya que no
podemos evitar que Hibernate nos cree la tabla antes que nuestra vista,
es forzarle a borrar de nuevo la tabla.
Esta vez tenemos que modificar únicamente «hibernate.mapping.xml», para que quede de la siguiente manera:
<hibernate-mapping> <database-object> <create> drop table AlumnosTutor </create> <drop></drop> <dialect-scope name='org.hibernate.dialect.Oracle10gDialect' /> </database-object> <database-object> <create> create or replace view AlumnosTutor as select c.idTutor, sum(c.numAlumnos) totalAlumnos from Clase c group by c.idTutor order by c.idTutor </create> <drop>drop view AlumnosTutor</drop> <dialect-scope name='org.hibernate.dialect.Oracle10gDialect' /> </database-object> </hibernate-mapping>
De esta manera, Hibernate generará tablas para todas nuestras
entidades y, después, creará los «database-object» indicados por orden
(ejecutará las siguientes sentencias SQL):
- drop table AlumnosTutor
- create or replace view AlumnosTutor as …
Al final en nuestro esquema tendremos únicamente las tablas Profesor y Clase,
y la vista AlumnosTutor. Reconozco que no es la manera más elegante del
mundo, ya que sería preferible evitar que Hibernate crease inicialmente
la tabla AlumnosTutor. Pero eso es algo que no he conseguido, por más
pruebas que he podido hacer.
6. Conclusiones
- La generación automática de nuestro esquema de B.D.
es una característica muy potente y útil de Hibernate. Si queremos que
nuestras vistas se generen también de forma automática, deberemos
añadir un poco de configuración adicional. - Si necesitamos crear vistas pero no podemos tocar el esquema de
la B.D., el uso de @Subselect nos permitirá mapear una entidad
utilizando una subconsulta. - Si tenemos la posibilidad de crear la vista en B.D. es preferible
hacerlo utilizando los elementos <database-object> que permiten
ejecutar sentencias sobre la propia B.D. - Al utilizar <database-object> casi siempre dependeremos
del dialecto de la B.D. utilizado, por lo que si cambiamos de dialecto,
deberemos añadir nuevos <database-object> para poder soportarlos.
@OneToOne
@JoinColumn(name = \\\»idTutor\\\», insertable = false, updatable = false)
private Profesor tutor;
con esto puedes obtener el registro de tutor como un objeto directamente, pero como puedes obtener una instancia del alumno desde el tutor:
tutor.getAlumno(), suponiendo que es una relacion de 1 a 1?
SELECT alumno.nombre FROM alumno, tutor WHERE alumno.id_tutor=tutor.id_tutor AND tutor.name=\\\»el que tienes\\\»;
no volvere a ver, asi que –> chessmastersport@hotmail.com