JDAL - Aplicación de Ejemplo - Capa de Integración
Capa de acceso a datos
La base de datos utilizará tres tablas:
- Books: Tabla para almacenar los libros.
- Authors: Tabla para almacenar los autores.
- Categories: Tabla para almacenar categorías de libros.
Script de creación de la base de datos para MySql se encuentra en /db/create.sql :
CREATE TABLE authors ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100), surname VARCHAR(100) ) ENGINE=InnoDB; CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) ) ENGINE=InnoDB; CREATE TABLE books ( id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(250), ISBN VARCHAR(32), publishedDate DATE, authorid INTEGER, categoryid INTEGER ) ENGINE=InnoDB; ALTER TABLE books ADD CONSTRAINT category_fk FOREIGN KEY (categoryid) REFERENCES categories (id); ALTER TABLE books ADD CONSTRAINT author_fk FOREIGN KEY (authorid) REFERENCES authors (id);
El script se encuentra en el directorio db/create.sql del código del ejemplo
Clases de los modelos
Vamos a utilizar hibernate y JPA para mapear el estado de los modelos del dominio a la base de datos. Al ser un ejemplo sencillo, los modelos no tendrán comportamiento y tendrán una correspondencia uno a uno con el esquema de la base de datos. Evidentemente esto no es un requerimiento en absoluto.
los modelos del dominio se encuentran en el paquete org.jdal.samples.library.model
Book.java
@Entity @Table(name="books") public class Book extends info.joseluismartin.model.Entity { private static final long serialVersionUID = 1L; @ManyToOne @JoinColumn(name="authorid") private Author author; @ManyToOne @JoinColumn(name="categoryid") private Category category; private String isbn; private Date publishedDate; public String toString() { return name; } // Getters and Setters }
Category.java
@Entity @Table(name="categories") public class Category extends info.joseluismartin.model.Entity { // nothing to add a Entity class. }
Author.java
@Entity @Table(name="authors") @Filter(name="patternFilter", condition="name like :pattern or surname like :pattern") @FilterDef(name="patternFilter", parameters=@ParamDef(name="pattern", type="string")) public class Author extends info.joseluismartin.model.Entity { private String surname; public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String toString() { return name + " " + surname; } // Overwrite equals and hashcode using name and surname ... }
El filtro authorByNameFilter lo utilizaremos en combos con auto-completado del autor en el alta de un nuevo libro.
Configuración de hibernate: hibernate.cfg.xml
<!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="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> <property name="hibernate.transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.cache.region.factory_class">net.sf.ehcache.hibernate.EhCacheRegionFactory</property> <property name="hibernate.cache.use_second_level_cache">true</property> <property name="hibernate.cache.use_query_cache">true</property> <mapping class="org.jdal.samples.library.model.Book"/> <mapping class="org.jdal.samples.library.model.Author"/> <mapping class="org.jdal.samples.library.model.Category"/> </session-factory> </hibernate-configuration>
Hasta ahora no hay nada nuevo. Hemos creado la base de datos y los modelos de la aplicación como hubiéramos hecho en cualquier aplicación similar. En la siguiente sección veremos como utilizar JDAL para crear la aplicación de gestión.
Configurando JDAL
Crearemos cuatro ficheros de configuración de contexto de Spring Framework:
- applicationContex.xml: Definición de beans de uso general.
- applicationContext-dao.xml: Definción de beans de acceso a datos, DataSource y TransactionManager
- applicationContext-service.xml: Definición de servicios persistentes. Las transacciones se demarcarán en la entrada/salida de las interfaces de los servicios.
- applicationContext-view.xml: Definición de beans de la interfaz de usuario
<!-- DAOs --> <bean id="dao" abstract="true"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <bean id="basicDao" class="info.joseluismartin.dao.hibernate.HibernateDao" parent="dao" /> <bean id="bookDao" class="info.joseluismartin.dao.hibernate.HibernateDao" parent="dao"> <constructor-arg value="org.jdal.samples.library.model.Book" /> <!-- Add the bookCriteriaBuilder to map whit 'filterName' as key --> <property name="criteriaBuilderMap"> <map key-type="java.lang.String" value-type="info.joseluismartin.dao.hibernate.CriteriaBuilder"> <entry key="bookFilter" value-ref="bookCriteriaBuilder" /> </map> </property> </bean> <!-- Book Criteria Builder --> <bean id="bookCriteriaBuilder" class="org.jdal.samples.library.dao.filter.BookCriteriaBuilder" /> <bean id="authorDao" class="info.joseluismartin.dao.hibernate.HibernateDao" parent="dao"> <constructor-arg value="org.jdal.samples.library.model.Author" /> </bean> <bean id="categoryDao" class="info.joseluismartin.dao.hibernate.HibernateDao" parent="dao"> <constructor-arg value="org.jdal.samples.library.model.Category" /> </bean>
Objetos de Acceso a Datos
La interfaz info.joseluismartin.dao.PageableDatasource define el método Page getPage(Page p) que nos permiten obtener datos mediante páginas de resultados.
/** * Simple interface to get data by pages * * @author Jose Luis Martin - (jlm@joseluismartin.info) */ public interface PageableDataSource { /** * Fill and return a page of data * @return the page */ public Page getPage(Page page); /** * return a Set with keys of page * @param page * @return */ public List<Serializable> getKeys(Page page); }
la interfaz info.joseluismartin.dao.Dao que extiende PageableDataSource define los métodos CRUD genéricos sobre los modelos persistentes. Si por ejemplo queremos tener acceso a la interfaz para el modelo Book, la declararemos como:
Dao<Book> bookDao;
Posteriormente podremos inyectar una implementación concreta, en este caso info.joseluismartin.dao.hibernate.HibernateDao donde se utiliza el constructor de la clase para especificar la clase del modelo sobre la que se realizan las operaciones de persistencia.
<bean id="bookDao" class="info.joseluismartin.dao.hibernate.HibernateDao" parent="dao"> <!-- Set model class via constructor injection --> <constructor-arg value="org.jdal.samples.library.model.Book" /> <!-- CriteriaBuilderMap is a Map<String, CriteriaBuilder> that maps Page.getFilter().getFilterName() to Criteria Builders --> <property name="criteriaBuilderMap"> <map key-type="java.lang.String" value-type="info.joseluismartin.dao.hibernate.CriteriaBuilder"> <entry key="bookFilter" value-ref="bookCriteriaBuilder" /> </map> </property> </bean> <bean id="bookCriteriaBuilder" class="org.jdal.samples.library.dao.filter.BookCriteriaBuilder" />
Al solicitar páginas de datos, puede utilizarse un objeto filtro mediante la propiedad "filter" del objeto Page. La interpretación del filtro depende de cada implementación del DAO. En el caso de HibernateDao tenemos dos posibilidades:
- El filtro es un objeto persistente: HibernateDao realizará una consulta similar a Session.findByExample
- El filtro es una instancia de la interfaz info.joseluismartin.dao.Filter: En este caso se probará de forma secuencial
las siguientes posibilidades:
- filter.getFilterName() corresponde al nombre una consulta declarada de hibernate: Se ejecutará la consulta aplicando como parámetros filter.getParameterMap()
- filter.getFilterName() corresponde al nombre de un filtro declarado de hibernate: se habilitará el filtro aplicando los parámetros filter.getParameterMap()
- filter.getFiterName() corresponde a la clave de una implementación CriteriaBuilder en el mapa criteriaBuilderMap: Se ejecutará la consulta a partir del objeto criteria que devuelve CriteriaFilterBuilder.build(criteria, filter)
- Finalmente se intentará ejecutar el método "createCriteria" + filter.getFilterName() para obtener el objeto Critera que dará lugar a la consulta.
En ninguno de los cinco casos anteriores debe preocuparse del orden o la paginación del resultado de la consulta. HibernateDao establecerá los valores correctos a partir de la información suministrada en el objeto Page.
En la configuración de bookDao se ha optado por la implementación del constructor de consultas, que generalmente es la opción más sencilla. .
El filtro sobre los libros debe filtrar mediante los siguientes campos:
- Título del libro: mediante un ILIKE sobre el título.
- Nombre y apellido del autor: mediante un ILIKE sobre el nombre y el apellido.
- Fecha de publicación del libro: se permite especificar, anterior a, posterior a y entre dos fechas.
- ISBN: mediante un ILIKE sobre el ISBN.
- Categoría del libro: sólo se incluirán los libros pertenecientes a la categoría.
Sobre esta especificación creamos un bean que extienda info.joseluismartin.dao.filter.BeanFilter que contenga las propiedades de filtrado:
public class BookFilter extends BeanFilter { private String name; private String authorName; private String authorSurname; private Date before; private Date after; private String isbn; private Category category; public BookFilter() { this("bookFilter"); } public BookFilter(String filterName) { super(filterName); } // Getter And Setters... }
El siguiente paso es el constructor de Criterias en base al filtro. Recordar que no debemos preocuparnos sobre la paginación o el orden de los resultados, HibernateDao ya lo hace por nosotros. Extendemos AbstractCriteriaBuilder e implementamos el el método build():
public class BookCriteriaBuilder extends AbstractCriteriaBuilder { public Criteria build(Criteria criteria, Object filter) { BookFilter f = (BookFilter) filter; like(criteria, "name", f.getName()); eq(criteria, "category", f.getCategory()); le(criteria, "publishedDate", f.getBefore()); ge(criteria, "publishedDate", f.getAfter()); // Author, add alias (join) only if needed. if (StringUtils.hasText(f.getAuthorName()) || StringUtils.hasText(f.getAuthorSurname())) { criteria.createAlias("author", "author"); like(criteria, "author.name", f.getAuthorName()); like(criteria, "author.surname", f.getAuthorSurname()); } return criteria; }
Como podemos ver, la implementación del constructor de Criterias es bastante sencilla y se lee directamente de la especificación de la funcionalidad del filtro.
La capa de acceso a datos está finalizada. Resumiendo hemos codificado las siguientes clases:
- Tres beans (Autor, Category y Book) con sus correspondientes mapeos utilizando anotaciones en código de JPA.
- Un bean (BookFilter) que contiene la información de filtrado sobre la tabla de libros.
- Un CriteriaBuilder que aplica las restricciones de filtrado definidas en BookFilter.
El siguiente paso es crear la capa de Servicios