En este post vamos a hacer uso de Spring Data y de JPA estándar para crear dos DAOs diferentes que nos permitirán acceso a una base de datos en memoria para realizar operaciones CRUD tradicionales.
Se usará como parte del suite de Spring:
- Spring Boot, para inicializar y preconfigurar la aplicación con Spring, asi como para hacer uso del Tomcat y de la base de datos HSQLDB embebidos.
- Spring JPA, para implementar uno de los casos de acceso a datos.
1. Dependencias Maven
Partimos del ejemplo de uso de Spring Boot presentado en anteriores post. A partir de él, añadimos las siguiente dependencias al pom.xml, para incluir el uso de Spring Data:
1 2 3 4 5 6 7 8 9 10 11 |
<!-- Add typical dependencies for a spring JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Add typical dependencies for HSQLDB --> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> |
2. Dominio de la aplicación
Implementamos el modelo de datos de la aplicación usando POJOs clásicos. A estos les agregamos las anotaciones JPA tradicionales para establecer que se trata de elementos del dominio de la aplicación. En este caso creamos una sola entidad (Superhero). Esta entidad se verá mapeada automaticamente contra una tabla relacional del mismo nombre dentro de la base de datos en memoria que inicia Spring Boot en su arranque (HSQLDB).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
/** * @author lagarcia * */ @Entity public final class Superhero { /** The name */ private String name; /** The super power */ private String power; /** The evil actitude */ private Boolean isEvil; /** The creation date and time */ private LocalDateTime createdDate; /** The creator name */ private String creator; /** The superhero weakness */ private String weakness; /** * Default constructor. */ public Superhero() { /* Empty */ } /** * Constructor with args. * * @param nameArg * @param powerArg * @param isEvilArg */ public Superhero(String nameArg, String powerArg, Boolean isEvilArg) { name = nameArg; power = powerArg; isEvil = isEvilArg; } /** * @return the name */ @Id public String getName() { return name; } /** * @param nameArg * the name to set */ public void setName(String nameArg) { name = nameArg; } /** * @return the power */ public String getPower() { return power; } /** * @param powerArg * the power to set */ public void setPower(String powerArg) { power = powerArg; } /** * @return the isEvil */ public Boolean getIsEvil() { return isEvil; } /** * @param isEvilArg * the isEvil to set */ public void setIsEvil(Boolean isEvilArg) { isEvil = isEvilArg; } /** * @return the createdDate */ @CreatedDate public LocalDateTime getCreatedDate() { return createdDate; } /** * @param createdDateArg * the createdDate to set */ public void setCreatedDate(LocalDateTime createdDateArg) { createdDate = createdDateArg; } /** * @return the creator */ @CreatedBy public String getCreator() { return creator; } /** * @param creatorArg * the creator to set */ public void setCreator(String creatorArg) { creator = creatorArg; } /** * @return the weakness */ public String getWeakness() { return weakness; } /** * @param weaknessArg * the weakness to set */ public void setWeakness(String weaknessArg) { weakness = weaknessArg; } @Override public String toString() { /* Evil */ String evilStr = "good"; if (isEvil) { evilStr = "evil"; } /* Creation date */ String dateStr = ""; if (createdDate != null) { dateStr = createdDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); } return "[" + dateStr + "][" + creator + "] My name is <b>" + name + "</b> and my super power is <b>" + power + "</b>. Do not panic, I am " + evilStr + ". I am afraid to " + weakness; } } |
La única anotación JPA, aparte de la antes mencionada @Entity, que se usa en el POJO sirve para indicar cuál es la clave primaria de nuestra entidad (@Id). Como no se usan anotaciones para indicar mapeos a columnas, se mapean los atributos a columnas con el mismo nombre que los atributos.
Las anotaciones @CreatedDate y @CreateBy son anotaciones de la parte de auditoría de Spring Data. Simplemente indican que campos se usarán para generar información de auditoría sobre la entidad, en este caso la fecha de creación y el usuario que lo creo. Más adelante se completará la auditoría de Spring Data con un listener que detectará los cambios o la creación de las entidades y completará los valores de los campos de auditoría, en particular el usuario que crea o modifica el registro. El campo de fechas es compatible con un tipo DateTime estándar de Java 8.
2. Los DAOs
Creamos dos tipos de DAOs diferentes. La forma tradicional y recomendable de crear estos componentes es codificar su interfaz de métodos públicos y posteriormente desarrollar una clase que implemente dicha interfaz. De esta manera desacoplamos la funcionalidad pública del DAO respecto de su implementación. Por comodidad y rapidez, y porque la implementación del DAO usando Spring Data solo necesita de la interfaz, en este caso usaremos una interfaz para el DAO hecho con Spring Data y una clase estándar para el DAO implementado con JPA, pero este último podria tener su propia interfaz o implementar la interfaz de Spring Data con sus propios métodos.
- DAO de Spring Data
1 2 3 4 5 6 |
/** * @author lagarcia * */ public interface SuperheroDAO extends CrudRepository<Superhero, String> { } |
Basta con crear una interfaz y extenderla de la interfaz Repositoy o CrudRespository. Ademas se indica en el genérico la clase que implementa la entidad que se procesa con esta interfaz (Superhero). El String es el tipo del ID de la entidad (el nombre en este caso).
- DAO de JPA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/** * @author lagarcia * */ @Repository public class OtherSuperheroDAO { /** A logger reference */ private static Logger logger = Logger.getLogger(OtherSuperheroDAO.class); /** JPA entity manager. */ @PersistenceContext private EntityManager entityManager; /** * Find all superheros. * * @return A superhero list */ public List<Superhero> findAll() { logger.info("En Other supherhero DAO"); /* Create query */ Query query = getEntityManager().createQuery( "select entity from Superhero entity"); return query.getResultList(); } /** * @return the entityManager */ public EntityManager getEntityManager() { return entityManager; } /** * @param entityManagerArg * the entityManager to set */ public void setEntityManager(EntityManager entityManagerArg) { entityManager = entityManagerArg; } } |
El DAO implementado con JPA desde Spring es una implementación clásica de este tipo de desarrollos. Primero se anota la clase con @Repository. Esto indica a Spring que la clase implementa un DAO. Dentro del DAO se añade una referencia al EntityManager de JPA que se usa para acceder a los métodos de acceso a datos. Para vincular este EntityManager exclusivo de este DAO con el EntityManagerFactory y la estructura de acceso a datos de JPA necesaria, se anota con @PersistenceContext. De esta manera Spring se encargará de crear estas conexiones de forma automática (y se añaden los métodos set/get correspondientes).
Una vez hecho esto, se puede usar el entityManager para acceder a los métodos JPA que se pueden usar para manipular los datos, como por ejemplo para acceder a ellos mediante una query en JPQL, usando entidades y lenguaje orientado a objetos, como en el ejemplo. En este caso, la implementación de JPA corre a cargo de Hibernate, ya que se incluye automaticamente con Spring Boot y Spring Data.
3. Auditoría
En el punto 1, en relación al POJO y al modelo de datos, incluimos dentro del mismo unos atributos y unas anotaciones especiales de Spring Data que permiten generar información auditable a la hora de determinar las fechas de creación, modificación y los usuarios que crean o modifican los registros.
La parte que falta en relación a la auditoría sería la clase que detecta estas modificaciones e incorpora la información que falta respecto al usuario que crear o modifica el registro (ya que las fechas de creación y modificación se rellenarían de forma automática). Esta clase sería un listener y se implementaría tal que así:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * @author lagarcia * */ public class AuditorAwareImpl implements AuditorAware<String> { /** * Current auditor. */ @Override public String getCurrentAuditor() { return "Dr. Maligno"; } } |
La clase en cuestion implementa la interfaz AuditorAware y devuelve un String en el método getCurrentAuditor(). Será la implementación de este método la que habra que adaptar a cada caso, en este ejemplo, lo más sencillo es devolver siempre el mismo String pero lo normal es que se devuelva el login o el id del usuario que este logado o que se encuentre activo en ese momento.
Aún falta añadir este listener a la configuración de la aplicación. Esto lo vemos a continucación.
4. Configuración
Tal y como se hace con Spring Boot de forma habitual, en este caso la configuración es via Java, mediante una clase anotada a tal efecto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/** * @author lagarcia * */ @Configuration @EnableAutoConfiguration @EnableJpaAuditing public class SuperheroConfig { /** * Store some superheros for testing. * * @param sheroDAO * - The superhero DAO * @return The command line runner */ @Bean public CommandLineRunner demo(SuperheroDAO sheroDAO) { return (args) -> { sheroDAO.save(new Superhero("Superman", "Super strengh", false)); sheroDAO.save(new Superhero("Dr. Magneto", "Super mutant", true)); sheroDAO.save(new Superhero("Flash", "Super speed", false)); }; } /** * Spring JPA auditor aware implementation. * * @return The auditor aware bean */ @Bean public AuditorAware<String> auditorProvider() { return new AuditorAwareImpl(); } } |
Se indica que la clase se usará para configurar (@Configuration). Se activa la configuración automática que habilitará la conexión a la base de datos embebida y el entityManagerFactory asociado a la misma (@EnableAutoConfiguration). Por último, se habilira la auditoría de Spring Data con @EnableJpaAuditing. Además declaramos un bean (@Bean) con nombre auditorProvider que devuelve una instancia del listener de auditoría que se expone en el punto anterior.
Además usamos el bean “demo” para crear 3 registros nada más arrancar el aplicativo. Hacemos uso del DAO creado con Spring Data que contiene métodos para insertar nuevas entidades.
4. Servicios web
Para probar el uso de estos DAOs utilizamos un controlador web como el que se ha visto en post anteriores. Se usa una URL para invocar al método que devuelve todos los registros de Superhero del DAO implementado con Spring Data, y otra URL para invocar al mismo método del DAO implementado con JPA. El resultado debería ser el mismo y ambos deberían retornar registros.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
/** * Example controller class. * * @author lagarcia * */ @RestController public final class SuperheroController { /** A logger reference */ private static Logger logger = Logger.getLogger(SuperheroController.class); /** The superhero DAO */ @Autowired private SuperheroDAO heroDAO; /** The other superhero DAO */ @Autowired private OtherSuperheroDAO otherHeroDAO; /** * Find all superheros. * * @return The superheros list. */ @RequestMapping("/superheros") public String findAllSuperheros() { String list = ""; List<Superhero> sheros = (List<Superhero>) this.getHeroDAO().findAll(); if (sheros != null && !sheros.isEmpty()) { for (Superhero hero : sheros) { list += hero + "<br/>"; } } return list; } /** * Find all superheros. * * @return The superheros list. */ @RequestMapping("/superheros2") public String findAllSuperheros2() { String list = ""; List<Superhero> sheros = this.getOtherHeroDAO().findAll(); if (sheros != null && !sheros.isEmpty()) { for (Superhero hero : sheros) { list += hero + "<br/>"; } } return list; } /** * @return the heroDAO */ public SuperheroDAO getHeroDAO() { return heroDAO; } /** * @param heroDAOArg * the heroDAO to set */ public void setHeroDAO(SuperheroDAO heroDAOArg) { heroDAO = heroDAOArg; } /** * @return the otherHeroDAO */ public OtherSuperheroDAO getOtherHeroDAO() { return otherHeroDAO; } /** * @param otherHeroDAOArg * the otherHeroDAO to set */ public void setOtherHeroDAO(OtherSuperheroDAO otherHeroDAOArg) { otherHeroDAO = otherHeroDAOArg; } } |
5. Conclusión
Lanzando la aplicación y accediendo a ambas URLs se ve como se retornar los mismos registros y además en los logs se aprecia como en cada caso se entra por un DAO diferente.
Se puede preparar una aplicación con acceso a datos con Spring Boot y Spring Data y/o JPA rápida y facilmente y respetando los principios de diseño y patrones estándar más habituales. Este es el enlace para descargar el proyecto entero (Spring Data + Spring Batch). Y también en Github.