Accés a dades amb Spring i Hibernate

Java té una API que ens facilita la feina d’escriure codi que interactuï amb una base de dades. Aquesta API és la Java Persistance API (JPA), i és simplement una especificació que defineix les interfícies per fer operacions amb la BD. JPA no consisteix en cap implementació de les dades i per si sola no ens servirà de res. Necessitem unes llibreries que implementin aquesta especificació i que siguin capaces de transformar objectes Java en registres a la BD (ORM, Object-Relational Mapping) a més d’implementar totes les operacions amb la BD. Hi ha moltes implementacions, com ara Eclipselink (www.eclipse.org/eclipselink), Open JPA (openjpa.apache.org) o TopLink (bit.ly/2lAMG8o), però una de les més populars és Hibernate (hibernate.org).

Hibernate és, doncs, un framework ORM per a Java que té un gran rendiment i velocitat i facilita considerablement la feina de programació amb BD. Amb una senzilla configuració, permet establir una relació directa entre classes Java amb taules i tipus de dades SQL i facilita fins i tot la creació automàtica de les taules. Hibernate s’encarregarà aproximadament del 90% de la feina que s’ha de fer per treballar amb una BD automatitzant tasques repetitives. Una altra característica que fa de Hibernate un framework molt popular és el fet que té un llenguatge propi de consulta amb la BD que es diu Hibernate Query Language (HQL). Això permet utilitzar qualsevol de les 10 BD suportades per Hibernate sense haver de canviar ni una sola línia de codi, ja que Hibernate s’encarregarà de traduir les consultes HQL que fem a un llenguatge SQL específic per a cadascuna de les BD. Altres característiques són:

  • És un projecte Opensource amb llicència LGPL.
  • Alt rendiment: utilitza una memòria cache interna que fa que les operacions de lectura de la BD (normalment sempre hi ha moltes més operacions de lectura que d’escriptura) siguin molt ràpides.
  • Creació automàtica de les taules.
  • Simplifica l’obtenció de dades de múltiples taules.

Hibernate ens ajudarà pel que fa a les bases de dades, i Spring, per la seva banda, ens ajudarà en el disseny de la nostra aplicació. Spring és un framework que utilitza injecció de dependències (en anglès, DI, Dependency Injection) o la inversió del control (en anglès, IoC, Inversion of Control). IoC es refereix al fet que una classe no s’encarregarà de crear instàncies de les seves dependències, sinó que el contenidor DI s’encarregarà de crear els objectes amb la configuració necessària i injectar-los on faci falta. Aquest fet, que sembla tan simple, té grans implicacions a l’hora de desenvolupar una aplicació. Afavoreix la composició sobre l’herència de classes, la qual cosa fa que hi hagi menys dependències entre les classes. I menys dependències implica que les classes faran menys coses i més específiques, per la qual cosa el codi serà més fàcilment reutilitzable i més fàcilment testejable.

La combinació de Spring i Hibernate ens permetrà dissenyar i desenvolupar un codi que pugui treballar fàcilment amb diferents tipus de BD (Hibernate) i on Spring ens donarà flexibilitat per canviar si és necessari Hibernate per qualsevol altre framework ORM.

Continuarem amb el desenvolupament de l’aplicació Java “SocIoc”. Explicarem:

  • Hibernate
  • Spring i IoC: inversió del control o injecció de dependències
  • HQL
  • Com configurar Spring i Hibernate
  • Anotacions
  • Relacions 1..M i N..M
  • Validació
  • Tests unitaris

"SocIoc". Dialogant amb usuaris amb Spring i Hibernate

Les classes Java haurien de ser al més independents possible d’altres classes. Això augmenta la possibilitat de reutilitzar aquestes classes i simplifica els tests unitaris. Per aconseguir aquesta separació o desacoblament, la dependència que una classe tingui amb d’altres s’ha d’injectar, més que fer que la classe creï o busqui la dependència. Això representa el principi d’inversió de control o principi de Hollywood: “no ens truquis” (crear/buscar objectes), “nosaltres et trucarem” (injectarem els objectes). Per exemple, la classe A dependrà de B si usa la classe B com a variable. Si usem injecció de dependències, la classe B serà passada a la A via el constructor (injecció de construcció) o a través d’un mètode setter (injecció setter). Vegem un exemple:

  1. public interface UserDAO {
  2. public void create(User user);
  3. public User edit(User user);
  4. public void remove(User user);
  5. public User findUserByUsername(String username);
  6. public User findUserWithHighestRank();
  7. public List<User> findActiveUsers();
  8. }
  9.  
  10. @Stateless
  11. public class UserDAOJPA implements UserDAO {
  12. @PersistenceContext
  13. private EntityManager entityManager;
  14.  
  15. @Override
  16. public void create(User user) {
  17. entityManager.persist(user);
  18. }
  19.  
  20. @Override
  21. public User edit(User user) {
  22. return entityManager.merge(user);
  23. }
  24.  
  25. @Override
  26. public void remove(User user) {
  27. user = entityManager.merge(user);
  28. entityManager.remove(user);
  29. }
  30.  
  31. @Override
  32. public User findUserByUsername(String username) {
  33. try {
  34. return (User) entityManager.createQuery("select object(o) from User o " +
  35. "where o.username = :username")
  36. .setParameter("username", username)
  37. .getSingleResult();
  38. } catch (NoResultException e) {
  39. return null;
  40. }
  41.  
  42. }
  43.  
  44. @Override
  45. public List<User> findActiveUsers() {
  46. try {
  47. return (List<User>) entityManager.createQuery("select object(o) from User o " +
  48. "where o.active= true")
  49. .getResultList();
  50. } catch (NoResultException e) {
  51. return null;
  52. }
  53. }
  54.  
  55. @Override
  56. public User findUserWithHighestRank() {
  57. try {
  58. return (User) entityManager.createQuery("select object(o) from User o order by o.rank DESC")
  59. .setMaxResults(1)
  60. .getSingleResult();
  61. } catch (NoResultException e) {
  62. return null;
  63. }
  64. }
  65. }
  66.  
  67. public class UserController {
  68. private UserDAO userDAO;
  69.  
  70. public User getUser(String username){
  71. return userDAO.findUserByUsername(username);
  72. }
  73. }

Podeu veure que la classe UserController utilitza la interfície UserDAO. Si us hi fixeu, el codi no fa referència a cap implementació de la interfície. Hem de modificar el codi de UserController per fer referència a una implementació.

  1. public class UserService {
  2. private UserDAO userDAO;
  3.  
  4. public UserService() {
  5. this.userDAO = new UserDAOJPA();
  6. }
  7.  
  8. public User getUser(String username) {
  9. return userDAO.findUserByUsername(username);
  10. }
  11. }

En aquest cas, fem que la implementació de la interfície sigui la de la classe UserDAOJPA, que ens proporciona la funcionalitat per accedir a la BD utilitzant JPA. Aquest codi té diversos problemes. La classe UserService té la configuració de la implementació de la interfície UserDAO. Això farà que per testejar la classe UserService utilitzem un objecte real UserDAOJPA, que establirà una connexió amb una BD. Un altre problema és que si canviem la implementació de UserDAO i fem una implementació que usa Hibernate, haurem de venir a la classe UserController i canviar el codi. El que ens permet la injecció de dependències (DI) és que la classe UserController no conegui els detalls de la configuració de UserDAO, sinó que aquests es passin. Si anéssim a utilitzar DI podríem refactoritzar el codi:

  1. public class UserController {
  2. private UserDAO userDAO;
  3.  
  4. public UserController(UserDAO userDAO) {
  5. this.userDAO = userDAO;
  6. }
  7.  
  8. public User getUser(String username){
  9. return userDAO.findUserByUsername(username);
  10. }
  11. }

D’aquesta manera, la classe UserController està totalment desacoblada de la implementació de UserDAO. El framework de DI s’encarregarà de passar la versió correcta de UserDAO a la classe UserController. La injecció de dependències es pot aconseguir amb Java Standard. Spring, però, simplifica el procés mitjançant una forma estàndard de configurar les dependències entre els objectes.

Integrant l'aplicació "SocIoc" amb Spring

Spring és un framework d’inversió del control (IoC) format d’una sèrie de mòduls que faciliten la feina de desenvolupar aplicacions Java. En ser modular, ens permet escollir quins mòduls utilitzar segons les necessitats de l’aplicació que estem desenvolupant. Hi ha uns 20 mòduls, i en la figura podeu veure la seva arquitectura.

Figura Mòduls Spring

Core container: aquest és el mòdul fonamental de Spring, i permet fer IoC i DI. També defineix el context de l’aplicació que indicarà quins beans (objectes gestionats pel container IoC) hi ha i com es relacionen entre si. Està format per quatre mòduls:

  • Core proporciona les parts fonamentals del framework, incloent IoC i injecció de dependències.
  • Beans proporciona BeanFactory, que és una classe que segueix el patró de disseny de software anomenat factory pattern i que serveix per crear objectes a partir d’una classe.
  • Context proporciona la funcionalitat per accedir als objectes gestionats per Spring (beans). La interfície ApplicationContext interface és la part central d’aquest mòdul.
  • SpEL proporciona un llenguatge potent i flexible per manipular un bean i les seves dependències mentre l’aplicació s’està executant.

Data Access/Integration: aquest és el mòdul que proporciona accés a dades i sistemes d’integració. Està format pels següents mòduls:

  • JDBC proporciona la funcionalitat JDBC per accedir a bases de dades.
  • ORM proporciona capes d’integració per a API de mapeig d’objectes Java amb taules de les BD. Entre les API suportades hi ha JPA, JDO, Hibernate i iBatis.
  • OXM proporciona la funcionalitat per a la transformació d’objectes a XML, i viceversa.
  • JMS (Java Messaging Service) és un mòdul que conté la funcionalitat per consumir i produir missatges JMS.
  • Transaction és un mòdul que facilita la gestió de transaccions.

Web: la capa web està formada pels següents mòduls:

  • Web proporciona funcionalitats típiques que s’utilitzen a les aplicacions web, com pujada de fitxers o la inicialització del contenidor IoC utilitzant servlets.
  • Web-MVC: conté una implementació de MVC (Model View Controller) per a aplicacions web.
  • Web-Socket proporciona suport per a comunicacions client-servidor en aplicacions web.
  • Web-Portlet proporciona una implementació de MVC per ser utilitzada en entorns portlet.

Hi ha altres mòduls importants:

  • AOP (Aspect-Oriented Programming): consisteix en una implementació de programació orientada a aspectes. AOP permet interceptar crides a funcions i injectar codi que s’executarà abans o després d’executar el codi de la funció.
  • Aspects: aquest mòdul permet la integració d’AspectJ, que és un framework d’AOP.
  • Instrumentation: proporciona instrumentació de classes i funcionalitat per carregar classes que són necessàries a certs servidors d’aplicacions.
  • Test: permet testejar aplicacions Spring utilitzant frameworks de testeig, com JUnit o TestNG.

No tots aquests mòduls seran necessaris quan desenvolupem una aplicació. El que serà fonamental serà afegir el core, que inclou el contenidor IoC, que serà l’encarregat de crear instàncies dels objectes, configurar-los i crear les dependències necessàries. El contenidor IoC obté aquesta informació de la configuració de Spring que es pot definir amb fitxers XML o amb classes de configuració Java.

L’arxiu de partida per treballar el descrit en aquest apartat el teniu disponble als annexos de la unitat.

El primer que haurem de fer és afegir les dependències. A partir del fitxer proporcionat als annexos afegirem les següents dependències:

  1. <properties>
  2. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  3. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  4. <springframework.version>4.3.4.RELEASE</springframework.version>
  5. <mysql.connector.version>5.1.40</mysql.connector.version>
  6. <junit.version>4.12</junit.version>
  7. <mockito.version>1.10.19</mockito.version>
  8. <h2.version>1.4.190</h2.version>
  9. </properties>
  10. <dependencies>
  11. ...
  12. <dependency>
  13. <groupId>org.springframework</groupId>
  14. <artifactId>spring-core</artifactId>
  15. <version>${springframework.version}</version>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.springframework</groupId>
  19. <artifactId>spring-web</artifactId>
  20. <version>${springframework.version}</version>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework</groupId>
  24. <artifactId>spring-webmvc</artifactId>
  25. <version>${springframework.version}</version>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework</groupId>
  29. <artifactId>spring-tx</artifactId>
  30. <version>${springframework.version}</version>
  31. </dependency>
  32. <dependency>
  33. <groupId>org.springframework</groupId>
  34. <artifactId>spring-orm</artifactId>
  35. <version>${springframework.version}</version>
  36. </dependency>
  37. <dependency>
  38. <groupId>org.springframework</groupId>
  39. <artifactId>spring-test</artifactId>
  40. <version>${springframework.version}</version>
  41. <scope>test</scope>
  42. </dependency>
  43. <dependency>
  44. <groupId>org.mockito</groupId>
  45. <artifactId>mockito-all</artifactId>
  46. <version>${mockito.version}</version>
  47. <scope>test</scope>
  48. </dependency>
  49. ...
  50. </dependencies>

Fixeu-vos que hem definit una sèrie de propietats al pom.xml que permeten centralitzar la versió utilitzada en una variable. D’aquesta manera, quan hi hagi disponible una nova versió de Spring, enlloc de canviar el valor en sis dependències, només ho haurem de fer en un lloc.

  1. <properties>
  2. <springframework.version>4.3.4.RELEASE</springframework.version>
  3. </properties>

A la implementació que tenim de UserService, la classe crea una instància de UserDAO. El que volem ara és que Spring s’encarregui d’aquesta configuració. Refactoritzarem UserService de manera que no creï una instància de UserDAO.

  1. public class UserService {
  2. private UserDAO userDAO;
  3.  
  4. public UserService(UserDAO userDAO) {
  5. this.userDAO = userDAO;
  6. }
  7.  
  8. public User getUser(String username) {
  9. return userDAO.findUserByUsername(username);
  10. }
  11. }

El que farem serà que Spring s’encarregui d’injectar la configuració de UserDAO necessària. Això vol dir que podrem testejar la classe UserService sense necessitar tenir cap informació de com serà la implementació de UserDAO. Per poder fer-ho necessitem configurar la nostra aplicació per tal que utilitzi Spring. Spring permet escollir quin serà el context a utilitzar en els tests. El que nosaltres volem testejar ara és que la classe UserService i les seves dependències estan gestionades per Spring. El que faci la implementació de la classe UserDAO realment no ens interessa en aquest moment, per la qual cosa utilitzarem un mock de la classe. Definim llavors la classe que tindrà la configuració del context de Spring a src/test/java al paquet package org.ioc.daw.config;.

  1. package org.ioc.daw.config;
  2.  
  3. import org.ioc.daw.user.UserDAO;
  4. import org.ioc.daw.user.UserService;
  5. import org.mockito.Mockito;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration
  8.  
  9. @Configuration
  10. public class SpringTestConfig {
  11. @Bean
  12. public UserDAO userDAO() {
  13. return Mockito.mock(UserDAO.class);
  14. }
  15.  
  16. @Bean
  17. public UserService userService(UserDAO userDAO) {
  18. return new UserService(userDAO);
  19. }
  20. }

@Configuration indica que la classe conté un o més mètodes anotats amb @Bean i que produeixen beans gestionats pel contenidor de Spring. Aquesta configuració defineix dos beans que estaran gestionats pel contenidor IoC de Spring. Això permetrà injectar aquests dos beans quan ens faci falta.

Fixeu-vos que l’objecte que retorna userDAO() no és cap implementació real de la interfície. Utilitzant Mockito.mock(UserDAO.class) retornem un mock, és a dir, un objecte que fa de proxy amb la interfície però que no té cap codi. El que això permet és oblidar-se completament del funcionament de les classes que implementen UserDAO i focalitzar el test en UserService. Si l’objecte mock no té cap implementació, com es pot usar en el codi? Al test veureu que podeu definir què retornen els diferents mètodes quan són invocats. Creeu la classe de test UserServiceTest al paquet package org.ioc.daw.user;.

Mockito és un framework de testeig que facilita la creació d’objectes de test (mock) que amaguen la implementació real i que faciliten els tests unitaris.

  1. import org.ioc.daw.config.SpringTestConfig;
  2. import org.junit.Test;
  3. import org.junit.runner.RunWith;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.test.context.ContextConfiguration;
  6. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  7.  
  8. import static org.junit.Assert.assertEquals;
  9. import static org.mockito.Mockito.times;
  10. import static org.mockito.Mockito.verify;
  11. import static org.mockito.Mockito.when;
  12.  
  13. @RunWith(SpringJUnit4ClassRunner.class)
  14. @ContextConfiguration(classes = {SpringTestConfig.class})
  15. public class UserServiceTest {
  16. @Autowired
  17. private UserDAO userDAO;
  18.  
  19. @Autowired
  20. private UserService userService;
  21.  
  22. @Test
  23. public void getUserByUsername() {
  24. String username = "test";
  25. User user = new User();
  26. user.setUsername(username);
  27. user.setUserId(1L);
  28.  
  29. when(userDAO.findUserByUsername(username)).thenReturn(user);
  30.  
  31. User userResult = userService.getUser(username);
  32. assertEquals(username, userResult.getUsername());
  33. assertEquals(new Long(1), userResult.getUserId());
  34. verify(userDAO, times(1)).findUserByUsername(username);
  35. }
  36. }

@RunWith(SpringJUnit4ClassRunner.class) especifica que el test carregarà un context de Spring per ser utilitzat en un test JUnit, i @ContextConfiguration(classes = {SpringTestConfig.class}) especifica quines classes tenen la configuració del context. Com que hem definit que SpringTestConfig serà la configuració a utilitzar, podem injectar (amb la notació @Autowired) els beans definits. Fixeu-vos que com que UserDAO és un mock, podem dir el que retornaran els seus mètodes:

  1. when(userService.findUserByUsername(username)).thenReturn(user);

Estem dient que quan es cridi al mètode findUserByUsername retornarem l’objecte user. Les línies que utilitzen assertEquals fan les comprovacions del test, comproven que l’objecte retornat pel mètode findUserByUsername de UserService és el mateix que el que retorna UserDAO. Finalment, verify(userDAO, times(1)).findUserByUsername(username); comprova que el mètode findUserByUsername només és invocat un cop. Podeu trobar el codi en el fitxer

uf4-a3-01.zip ( 12 KB )
.

Integrant l'aplicació "SocIoc" amb Hibernate

Hibernate és un framework ORM (Object-Relational Mapping). És a dir, Hibernate s’encarrega de relacionar taules d’una base de dades amb objectes Java. Com es pot veure en la figura, Hibernate crea una capa entre la BD i l’aplicació. S’encarregarà de gestionar la configuració de com accedir a la BD, el tipus de BD, com fer el mapeig entre les classes i taules, i establir les relacions entre diferents taules.

Figura Arquitectura d’Hibernate

En la figura es representa amb més detall com funciona Hibernate. A l’hora de guardar les dades a la BD, Hibernate crea una instància de la classe de tipus entitat (una classe Java mapejada amb una taula). Aquest objecte s’anomena objecte transient, ja que no està associat amb cap sessió i no està guardat a la BD. Per guardar un objecte a la BD s’utilitza una instància de la interfície SessionFactory, un objecte de tipus singleton (només hi ha una instància de l’objecte a l’aplicació) que implementa el patró de disseny factory. SessionFactory carrega la configuració d’Hibernate i s’encarrega de gestionar la configuració de la connexió amb la BD.

Figura Arquitectura detallada d’Hibernate

Cada connexió amb la BD a Hibernate es fa creant una instància d’una implementació de la interfície Session. Hibernate també disposa d’una API per gestionar les transaccions i que permet utilitzar transaccions JDBC o JTA. Una transacció representa una única unitat de treball amb la base de dades. Vegem amb una mica més de detall els diferents blocs de la figura:

  • SessionFactory: és una classe encarregada de produir objectes de tipus Session. Opcionalment, manté una memòria cache de segon nivell que guarda dades de la connexió amb la BD perquè siguin reutilitzades entre diferents transaccions.
  • Session: s’encarreguen de la conversa entre les aplicacions i la BD. Manté una cache de primer nivell dels objectes de l’aplicació. Aquesta cache s’utilitza a l’hora de recuperar objectes utilitzant el seu identificador o a l’hora de navegar a través de les dependències de l’objecte.
  • Objectes persistents: són objectes que contenen la funcionalitat de l’aplicació. Cada objecte està associat amb una única sessió d’Hibernate. Un cop la sessió associada a un objecte es tanca, els objectes passen a estar a l’estat detached (separat).
  • Objectes tipus transient i detached: són les instàncies de classes de tipus Entity que no estan associades a cap sessió d’Hibernate. Poden haver estat creades per l’aplicació i no haver estat guardades, o poden ser el resultat que s’hagi tancat una sessió d’Hibernate.
  • Proveïdor de connexions (ConnectionProvider): és opcional i permet crear un pool de connexions JDBC.
  • TransactionFactory: permet crear instàncies d’objectes de tipus Transaction.

Per poder treballar amb Hibernate i que formi part de la vostra aplicació, el primer que fareu serà afegir les dependències necessàries. Importeu a Netbeans el codi descarregat dels annexos. Modificareu el fitxer pom.xml per afegir les dependències d’Hibernate.

Als annexos de la unitat trobareu un arxiu amb el codi per importar a Netbeans i treballar amb Hibernate.

  1. <properties>
  2. ..
  3. <hibernate.version>5.2.5.Final</hibernate.version>
  4. ..
  5. </properties>
  6. <dependency>
  7. <groupId>org.hibernate</groupId>
  8. <artifactId>hibernate-validator</artifactId>
  9. <version>5.3.4.Final</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.hibernate</groupId>
  13. <artifactId>hibernate-core</artifactId>
  14. <version>${hibernate.version}</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.jadira.usertype</groupId>
  18. <artifactId>usertype.core</artifactId>
  19. <version>6.0.1.GA</version>
  20. </dependency>

Hibernate a la llibreria hibernate-core porta l’especificació JPA 2.1 i la seva implementació. Per tant, no és necessari incloure les següents dependències.

  <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>persistence-api</artifactId>
        <version>1.0.2</version>
    </dependency>

Hibernate utilitza el concepte de sessions per gestionar les connexions amb la base de dades. A cada sessió s’obre una única connexió amb la BD i s’utilitza fins que la sessió es tanca. Cada objecte que es carrega en memòria per Hibernate estarà associat amb la sessió. Això permet a Hibernate persistir automàticament els objectes que s’han modificat. Quan la sessió persisteix canvis a la BD es diu flushing. Cada objecte associat amb la sessió es comprova per veure si ha canviat d’estat. Qualsevol objecte amb canvi d’estat es conservarà a la base de dades, amb independència que els objectes modificats es guardin o no explícitament. Aquesta característica es pot configurar, però per defecte podeu configurar el comportament arran d’Hibernate, es farà automàticament. Hibernate fa flushing en les següents situacions:

  • Quan s’executa directament el mètode flush().
  • Abans que Hibernate faci una consulta, si creu que és necessari per obtenir un resultat precís.
  • Quan es confirma una transacció.
  • Quan es tanca la sessió.

El que volem fer a continuació és començar a utilitzar Hibernate. Per fer-ho definireu una classe que implementi la interfície UserDAO i que utilitzi la funcionalitat d’Hibernate. Creareu la classe org.ioc.daw.user.UserHibernateDAO.

  1. import org.hibernate.Criteria;
  2. import org.hibernate.Session;
  3. import org.hibernate.SessionFactory;
  4. import org.hibernate.criterion.Order;
  5. import org.hibernate.criterion.Restrictions;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.stereotype.Repository;
  8. import javax.transaction.Transactional;
  9. import java.util.List;
  10.  
  11. @Transactional
  12. @Repository("userHibernateDAO")
  13. public class UserHibernateDAO implements UserDAO {
  14.  
  15. @Autowired
  16. private SessionFactory sessionFactory;
  17.  
  18. @Override
  19. public void create(User user) {
  20. getSession().saveOrUpdate(user);
  21. }
  22.  
  23. @Override
  24. public User edit(User user) {
  25. return (User) getSession().merge(user);
  26. }
  27.  
  28. @Override
  29. public void remove(User user) {
  30. getSession().delete(user);
  31. }
  32.  
  33. @Override
  34. public User findUserByUsername(String username) {
  35. Criteria criteria = createEntityCriteria();
  36. criteria.add(Restrictions.eq("username", username));
  37. return (User) criteria.uniqueResult();
  38. }
  39.  
  40. @Override
  41. public User findUserWithHighestRank() {
  42. Criteria criteria = createEntityCriteria();
  43. criteria.addOrder(Order.desc("rank"));
  44. return (User) criteria.uniqueResult();
  45. }
  46.  
  47. @Override
  48. public List<User> findActiveUsers() {
  49. Criteria criteria = createEntityCriteria();
  50. criteria.add(Restrictions.eq("active", true));
  51. return (List<User>) criteria.list();
  52. }
  53.  
  54. protected Session getSession() {
  55. return sessionFactory.getCurrentSession();
  56. }
  57.  
  58. private Criteria createEntityCriteria() {
  59. return getSession().createCriteria(User.class);
  60. }
  61. }

@Repository(“userHibernateDAO”) indica que quan la classe sigui escanejada per Spring es crearà un bean anomenat userHibernateDAO. La notació @Transactional indica que els mètodes definits a la classe utilitzaran transaccions, és a dir, que quan el mètode es comença a executar s’obre una transacció i abans d’acabar es tanca. A les línies 4-5 injecteu l’objecte que permetrà fer totes les operacions utilitzant Hibernate SessionFactory i obtenir una sessió d’Hibernate. A la classe UserHibernateDAO heu vist com injectar les dependències necessàries per utilitzar Hibernate i com fer que els mètodes siguin transaccionals. El que heu de fer ara és crear la configuració que creï el bean SessionFactory, un altre bean que defineixi la classe que gestioni les transaccions, i finalment necessitareu definir un bean de tipus DataSource, que establirà com es farà la connexió amb la base de dades. El que interessa és testejar que Hibernate treballi amb la BD correctament, no tant la BD en si, i per això utilitzareu una BD en memòria. En aquest cas, H2. Al directori test/java creareu la classe org.ioc.daw.config.EmbeddedDatabaseTestConfig, i el fitxer de propietats al directori test/resources application-test.properties, tal com podeu veure en la figura.

Figura Hibernate

  1. import org.hibernate.SessionFactory;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.context.annotation.PropertySource;
  6. import org.springframework.core.env.Environment;
  7. import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
  8. import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
  9. import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
  10. import org.springframework.orm.hibernate5.HibernateTransactionManager;
  11. import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
  12. import org.springframework.transaction.annotation.EnableTransactionManagement;
  13. import javax.sql.DataSource;
  14. import java.util.Properties;
  15.  
  16. @Configuration
  17. @EnableTransactionManagement
  18. @PropertySource(value = {"application-test.properties"})
  19. public class EmbeddedDatabaseTestConfig {
  20.  
  21. @Autowired
  22. private Environment environment;
  23.  
  24. @Bean
  25. public UserDAO userDAO() {
  26. return new UserHibernateDAO();
  27. }
  28.  
  29. @Bean
  30. public DataSource dataSource() {
  31. EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
  32. EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2).build();
  33. return db;
  34. }
  35.  
  36. @Bean
  37. @Autowired
  38. public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
  39. LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
  40. sessionFactory.setDataSource(dataSource);
  41. sessionFactory.setPackagesToScan("org.ioc.daw");
  42. sessionFactory.setHibernateProperties(hibernateProperties());
  43. return sessionFactory;
  44. }
  45.  
  46. private Properties hibernateProperties() {
  47. Properties properties = new Properties();
  48. properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect"));
  49. properties.put("hibernate.hbm2ddl.auto", environment.getRequiredProperty("hibernate.hbm2ddl"));
  50. properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql"));
  51. properties.put("hibernate.format_sql", environment.getRequiredProperty("hibernate.format_sql"));
  52. return properties;
  53. }
  54.  
  55. @Bean
  56. @Autowired
  57. public HibernateTransactionManager transactionManager(SessionFactory s) {
  58. HibernateTransactionManager txManager = new HibernateTransactionManager();
  59. txManager.setSessionFactory(s);
  60. return txManager;
  61. }
  62. }

@EnableTransactionManagement habilita la capacitat de gestionar les transaccions amb la BD. @PropertySource(value = {“classpath:application-test.properties”}) permet definir propietats a un fitxer de propietats que serà accessible a través del component injectat Environment.

  1. @Bean
  2. public UserDAO userDAO() {
  3. return new UserHibernateDAO();
  4. }

Defineix que com el Bean de tipus UserDAO que utilitzarem serà del tipus UserHibernateDAO.

  1. @Bean
  2. public DataSource dataSource() {
  3. EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
  4. EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2).build();
  5. return db;
  6. }

Aquest codi crea un bean de tipus DataSource, que és el que establirà amb quina base de dades es connectarà Hibernate. En el vostre cas, indiqueu que serà una BD en memòria de tipus H2. Això serà l’únic que haureu de canviar en el codi si voleu utilitzar una BD diferent.

  1. @Bean
  2. @Autowired
  3. public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
  4. LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
  5. sessionFactory.setDataSource(dataSource);
  6. sessionFactory.setPackagesToScan("org.ioc.daw");
  7. sessionFactory.setHibernateProperties(hibernateProperties());
  8. return sessionFactory;
  9. }

El mètode sessionFactory() crea un bean de tipus LocalSessionFactoryBean que té informació de com connectar amb la base de dades a través de l’objecte DataSource. Fixeu-vos que el bean DataSource s’injecta utilitzant la notació @Autowired. A més de DataSource, es necessita definir les propietats d’Hibernate (hibernateProperties()). Gràcies a la notació @PropertySource es poden externalitzar les propietats a fitxers, fet que permet carregar diferents fitxers de propietats a diferents contextos. Un cop l’objecte SessionFactory s’ha creat s’injectarà al mètode transactionManager, que proporcionarà suport per a les transaccions amb la base de dades.

  1. @Bean
  2. @Autowired
  3. public HibernateTransactionManager transactionManager(SessionFactory s) {
  4. HibernateTransactionManager txManager = new HibernateTransactionManager();
  5. txManager.setSessionFactory(s);
  6. return txManager;
  7. }

És a dir, Spring crearà tres beans: un que té informació de la BD a utilitzar, un que usa aquest bean per crear una sessió que gestiona la connexió amb la BD i un altre que gestiona les transaccions. El fitxer de propietats application-test.properties d’Hibernate té el contingut mostrat a continuació. A més de definir que el dialecte SQL que usarà Hibernate és H2Dialect, estem indicant també amb la propietat hibernate.hbm2ddl que es creïn les taules automàticament a partir dels objectes.

hibernate.dialect = org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl = create
hibernate.show_sql = true
hibernate.format_sql = true

Un cop Hibernate i Spring estan configurats podem passar a crear el test UserDAOTest al paquet org.ioc.daw.user. De moment comprovareu que podeu escriure i llegir informació de la BD.

  1. import org.ioc.daw.config.EmbeddedDatabaseTestConfig;
  2. import static org.junit.Assert.*;
  3. import org.junit.Test;
  4. import org.junit.runner.RunWith;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.test.context.ContextConfiguration;
  7. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  8. import java.sql.Timestamp;
  9. import java.util.Date;
  10.  
  11.  
  12. @RunWith(SpringJUnit4ClassRunner.class)
  13. @ContextConfiguration(classes = {EmbeddedDatabaseTestConfig.class})
  14. public class UserDAOTest {
  15. @Autowired
  16. private UserDAO userDAO;
  17.  
  18. @Test
  19. public void saveUser() {
  20. User user = new User();
  21. user.setUsername("test");
  22. user.setActive(true);
  23. user.setEmail("email@test.com");
  24. user.setPassword("password");
  25. user.setName("name");
  26. user.setRank(10);
  27. user.setCreatedOn(new Timestamp(new Date().getTime()));
  28. assertNull(user.getUserId());
  29. userDAO.create(user);
  30. assertNotNull(user.getUserId());
  31.  
  32. User userFromDb = userDAO.findUserByUsername("test");
  33. assertEquals(user.getUserId(), userFromDb.getUserId());
  34. }
  35. }

Amb @ContextConfiguration(classes = {EmbeddedDatabaseTestConfig .class}) definiu quin serà el fitxer de propietats que utilitzareu al test; en el vostre cas és el fitxer de configuració que defineix un DataSource per connectar a la BD H2. Fixeu-vos que abans de guardar l’usuari el valor del seu userId és null, però que un cop Hibernate persisteix l’objecte, tal com es va definir a la classe User (@GeneratedValue(strategy = GenerationType.IDENTITY)), es genera un valor automàticament. El test consisteix a guardar l’usuari a la BD, recuperar-lo usant el nom d’usuari i comprovar que el userId dels dos objectes és el mateix.

Una de les característiques d’Hibernate és que un cop un objecte forma part d’una sessió si es fan canvis sobre alguna de les seves propietats, els canvis es persisteixen a la BD. Modificareu el test anterior per comprovar-ho.

  1. @Test
  2. public void saveUser(){
  3. User user = new User();
  4. user.setUsername("test");
  5. user.setActive(true);
  6. user.setEmail("email@test.com");
  7. user.setPassword("password");
  8. user.setName("name");
  9. user.setRank(10);
  10. user.setCreatedOn(new Timestamp(new Date().getTime()));
  11.  
  12. assertNull(user.getUserId());
  13. userDAO.create(user);
  14. assertNotNull(user.getUserId());
  15. user.setEmail("new-email@test.com");
  16.  
  17. User userFromDb = userDAO.findUserByUsername("test");
  18. assertEquals(user.getUserId(), userFromDb.getUserId());
  19. assertEquals("new-email@test.com", userFromDb.getEmail());
  20. }

Un cop s’ha guardat l’objecte user, modifiqueu el correu-e i comproveu que s’ha guardat correctament. Si executeu el test veureu que hi ha un error:

Failed tests:   saveUser(org.ioc.daw.user.UserDAOTest): expected:<[new-]email@test.com> but was:<[]email@test.com>

El problema és que necessiteu establir el context per a la transacció, és a dir, indicar on comença i on acaba la transacció. Si afegiu la notació @Transactional al mètode del test i torneu a executar el test veureu que aquest cop s’executa correctament.

Podeu accedir al codi al fitxer que trobareu als annexos de la unitat.

Com podríeu utilitzar el test que heu creat per comprovar que podeu escriure dades a la BD MySQL “SocIoc”?

La configuració està definida a la classe EmbeddedDatabaseTestConfig, així que tot el que haureu de canviar estarà aquí:

  • @PropertySource(value = {“classpath:application-test. properties”}) indica que s’han d’agafar els valors d’aquest fitxer de propietats per fer la connexió amb la BD.
  • El bean DataSource indica que utilitzareu una BD en memòria.

Llavors, per connectar-vos a la BD MySQL només haureu de canviar el fitxer de propietats i el tipus de DataSource. Si feu el canvi a EmbeddedDatabaseTestConfig, si voleu tornar a utilitzar la BD en memòria, com serà el cas, haureu de tornar a canviar un altre cop el fitxer. Creareu una altra classe de configuració al paquet org.ioc.daw.config que establirà com treballar amb la BD MySQL que anomenarem HibernateMysqlConfiguration.

  1. package org.ioc.daw.config;
  2.  
  3. import org.hibernate.SessionFactory;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.ComponentScan;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.context.annotation.PropertySource;
  9. import org.springframework.core.env.Environment;
  10. import org.springframework.jdbc.datasource.DriverManagerDataSource;
  11. import org.springframework.orm.hibernate5.HibernateTransactionManager;
  12. import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
  13. import org.springframework.transaction.annotation.EnableTransactionManagement;
  14. import javax.naming.NamingException;
  15. import javax.sql.DataSource;
  16. import java.util.Properties;
  17.  
  18. @Configuration
  19. @EnableTransactionManagement
  20. @ComponentScan({"org.ioc.daw.user", "org.ioc.daw.question",
  21. "org.ioc.daw.answer", "org.ioc.daw.vote", "org.ioc.daw.rank"})
  22. @PropertySource(value = {"jdbc.properties", "hibernate.properties"})
  23. @Import(value = {HibernateConfiguration.class})
  24. public class HibernateMysqlConfiguration {
  25. @Autowired
  26. private Environment environment;
  27.  
  28. @Bean
  29. public DataSource dataSource() {
  30. DriverManagerDataSource dataSource = new DriverManagerDataSource();
  31. dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName"));
  32. dataSource.setUrl(environment.getRequiredProperty("jdbc.url"));
  33. dataSource.setUsername(environment.getRequiredProperty("jdbc.username"));
  34. dataSource.setPassword(environment.getRequiredProperty("jdbc.password"));
  35. return dataSource;
  36. }
  37. }

Afegiu també un fitxer de propietats src/main/resources/application.properties. Fixeu-vos que haureu de canviar jdbc.url per la IP:PORT on estigui funcionant la vostra base de dades.

jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://192.168.99.100:32768/socioc?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
jdbc.username = root
jdbc.password = root
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.show_sql = true
hibernate.format_sql = true
hibernate.hbm2ddl = validate

A causa de possibles problemes amb la configuració de MySQL i la seva configuració horària, afegiu una sèrie de paràmetres (?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacy DatetimeCode=false&serverTimezone=UTC/) a la cadena de caracters de la connexió amb el servidor MySQL.

Si us hi fixeu, la classe de configuració HibernateMysqlConfiguration té repetit gairebé tot el codi respecte a EmbeddedDatabaseTestConfig. Per tant, si voleu fer algun canvi al DataSource o introduir una nova propietat d’Hibernate us haureu de recordar de canviar les dues classes. Això no és una bona pràctica de desenvolupament i és susceptible a errors. El que fareu serà separar les classes en tres: una classe de configuració que contindrà el codi comú i dues amb les configuracions específiques per a MySQL i H2, i afegir els DAO al seu propi fitxer de configuració. Creareu DAOConfig al paquet org.ioc.daw.config.

  1. import org.ioc.daw.user.UserDAO;
  2. import org.ioc.daw.user.UserHibernateDAO;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5.  
  6. @Configuration
  7. public class DAOConfig {
  8.  
  9. @Bean
  10. public UserDAO userDAO() {
  11. return new UserHibernateDAO();
  12. }
  13. }

HibernateConfiguration conté la configuració comú:

  1. import org.hibernate.SessionFactory;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.core.env.Environment;
  6. import org.springframework.orm.hibernate5.HibernateTransactionManager;
  7. import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
  8. import org.springframework.transaction.annotation.EnableTransactionManagement;
  9. import javax.naming.NamingException;
  10. import javax.sql.DataSource;
  11. import java.util.Properties;
  12. @Configuration
  13. @EnableTransactionManagement
  14. @Import(value = {DAOConfig.class})
  15. public class HibernateConfiguration {
  16. @Autowired
  17. private Environment environment;
  18.  
  19. @Bean
  20. @Autowired
  21. public LocalSessionFactoryBean sessionFactory(DataSource dataSource) throws NamingException {
  22. LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
  23. sessionFactory.setDataSource(dataSource);
  24. sessionFactory.setPackagesToScan("org.ioc.daw.user");
  25. sessionFactory.setHibernateProperties(hibernateProperties());
  26. return sessionFactory;
  27. }
  28.  
  29. private Properties hibernateProperties() {
  30. Properties properties = new Properties();
  31. properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect"));
  32. properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql"));
  33. properties.put("hibernate.format_sql", environment.getRequiredProperty("hibernate.format_sql"));
  34. properties.put("hibernate.hbm2ddl.auto", environment.getRequiredProperty("hibernate.hbm2ddl"));
  35. return properties;
  36. }
  37.  
  38. @Bean
  39. @Autowired
  40. public HibernateTransactionManager transactionManager(SessionFactory s) {
  41. HibernateTransactionManager txManager = new HibernateTransactionManager();
  42. txManager.setSessionFactory(s);
  43. return txManager;
  44. }
  45. }

HibernateMysqlConfiguration tindrà la configuració específica per connectar amb la BD MySQL.

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.context.annotation.Import;
  5. import org.springframework.context.annotation.PropertySource;
  6. import org.springframework.core.env.Environment;
  7. import org.springframework.jdbc.datasource.DriverManagerDataSource;
  8. import org.springframework.transaction.annotation.EnableTransactionManagement;
  9. import javax.sql.DataSource;
  10. @Configuration
  11. @EnableTransactionManagement
  12. @PropertySource(value = {"application.properties"})
  13. @Import(value = {HibernateConfiguration.class})
  14. public class HibernateMysqlConfiguration {
  15. @Autowired
  16. private Environment environment;
  17.  
  18. @Bean
  19. public DataSource dataSource() {
  20. DriverManagerDataSource dataSource = new DriverManagerDataSource();
  21. dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName"));
  22. dataSource.setUrl(environment.getRequiredProperty("jdbc.url"));
  23. dataSource.setUsername(environment.getRequiredProperty("jdbc.username"));
  24. dataSource.setPassword(environment.getRequiredProperty("jdbc.password"));
  25. return dataSource;
  26. }
  27. }

I finalment, EmbeddedDatabaseTestConfig tindrà la configuració per connectar-se a la BD en memòria H2.

  1. @Configuration
  2. @EnableTransactionManagement
  3. @PropertySource(value = {"application-test.properties"})
  4. @Import(value = {HibernateConfiguration.class})
  5. public class EmbeddedDatabaseTestConfig {
  6.  
  7. @Bean
  8. public DataSource dataSource() {
  9. EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
  10. EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2).build();
  11. return db;
  12. }
  13. }

Als annexos de la unitat trobareu un arxiu amb el codi refactoritzat, així com un altre arxiu de MySQL Workbench amb la base de dades “SocIoc” per importar-la.

Comproveu que amb el codi refactoritzat podeu executar tots els tests. A continuació modificareu EmbeddedDatabaseTestConfig per tal d’executar els tests contra la BD MySQL. Podeu importar la base de dades “SocIoc” del fitxer de MySQL Workbench descarregat dels annexos.

Per comprovar que el vostre codi i configuració pot escriure a la BD MySQL que vau definir a Workbench, podeu utilitzar l’editor de queries de Workbench per fer la comprovació. En la figura podeu veure que el contingut de la taula està buit.

Figura Contingut de la taula “Users”

A continuació canvieu el context del test UserDAOTest perquè utilitzi la classe de configuració de MySQL i executeu el test UserDAOTest.saveUser.

  1. @ContextConfiguration(classes = {HibernateMysqlConfiguration.class})

Si executeu el test hi haurà un altre error:

.HibernateConfiguration: Invocation of init method failed; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: wrong column type encountered in column [id] in table [users]; found [int (Types#INTEGER)], but expecting [bigint (Types#BIGINT)]

El problema és que vau definir la taula “Users” com a tipus int, i Hibernate el que espera és un tipus bigint. Això passa perquè heu definit userId com a Long, que Hibernate mapeja amb el tipus bigint per tal d’acomodar a tots els possibles valors que pot guardar una variable de tipus Long. Per solucionar el problema modifiqueu la classe User canviant el tipus de userId de Long a Integer.

  1. @Id
  2. @NotNull
  3. @GeneratedValue(strategy = GenerationType.IDENTITY)
  4. @Column(name = "id")
  5. private Integer userId;
  6. public Integer getUserId() {
  7. return userId;
  8. }
  9.  
  10. public void setUserId(Integer userId) {
  11. this.userId = userId;
  12. }

Ara sí, executeu el test i mireu el contingut de la taula “Users”; veureu està buida. Què ha passat? Doncs que com que és un test, Hibernate fa un rollback (desfer els canvis d’una transacció) abans de tancar el test i esborra les dades guardades a la BD. Per tal que es guardin les dades a la BD és necessari indicar al test que no faci rollback amb la notació @Rollback(false). El mètode amb el test tindrà llavors les següents anotacions:

  1. @Test
  2. @Transactional
  3. @Rollback(false)
  4. public void saveUser() {

Ara sí que veureu que hi ha l’usuari que el test ha creat (vegeu la figura).

Figura Contingut de la taula “Users”

Aquests canvis que heu fet són només per veure que podeu guardar dades a la BD només canviant un fitxer de configuració. Desfeu els canvis (treure la notació @Rollback i utilitzar la configuració EmbeddedDatabaseTestConfig) abans de continuar endavant.

A continuació veurem com podreu configurar Spring i Hibernate per treballar amb el servidor d’aplicacions Glassfish. Primer afegireu al fitxer pom.xml una dependència que us permetrà cercar recursos (en el vostre cas, un recurs JDBC) utilitzant JNDI. Java Naming and Directory Interface és una API que permet trobar recursos, serveis i components EJB que estan distribuïts. Afegiu el següent al pom.xml:

  1. <dependency>
  2. <groupId>org.glassfish.main.common</groupId>
  3. <artifactId>glassfish-naming</artifactId>
  4. <version>4.1.1</version>
  5. </dependency>

A continuació heu de crear una classe de configuració amb els detalls específics de Glassfish al paquet org.ioc.daw.config. Fixeu-vos que l’únic que heu d’especificar és el tipus de DataSource. En aquest cas utilitzeu JndiDataSourceLookup per buscar el recurs JDBC especificat per a la propietat jndi.socioc del fitxer de propietats glassfish-application.properties.

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.context.annotation.Import;
  5. import org.springframework.context.annotation.PropertySource;
  6. import org.springframework.core.env.Environment;
  7. import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
  8. import org.springframework.transaction.annotation.EnableTransactionManagement;
  9. import javax.sql.DataSource;
  10.  
  11. @Configuration
  12. @EnableTransactionManagement
  13. @PropertySource(value = {"glassfish-application.properties"})
  14. @Import(value = {HibernateConfiguration.class})
  15.  
  16. public class GlassfishPoolConfiguration {
  17.  
  18. @Autowired
  19. private Environment environment;
  20.  
  21. @Bean(name = "jndiDataSource")
  22. public DataSource dataSource() {
  23. String jndiName = environment.getRequiredProperty("jndi.socioc");
  24. JndiDataSourceLookup lookup = new JndiDataSourceLookup();
  25. lookup.setResourceRef(true);
  26. return lookup.getDataSource(jndiName);
  27. }
  28. }

El fitxer de propietats només ha de contenir les propietats que configuren Hibernate i la propietat que indica el nom del recurs JNDI.

jndi.socioc = jdbc/socioc
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.show_sql = true
hibernate.format_sql = true
hibernate.hbm2ddl = validate

Si us hi fixeu, les propietats d’Hibernate són les mateixes que les que vau utilitzar per a la connexió MySQL. Com que no voleu tenir codi ni configuració repetides, refactoritzareu els fitxers de propietats. Creareu el fitxer hibernate.properties amb les propietats d’Hibernate, jdbc.properties amb les propietats específiques per a JDBC i glassfish.properties amb les propietats de Glassfish.

hibernate.properties:

hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.show_sql = true
hibernate.format_sql = true
hibernate.hbm2ddl = validate

jdbc.properties:

jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://192.168.99.100:32768/socioc
jdbc.username = root
jdbc.password = root

glassfish.properties:

jndi.socioc = jdbc/socioc

A continuació us heu d’assegurar que les classes de configuració inclouen els fitxers de propietats adients.

  1. @PropertySource(value = {"glassfish.properties","hibernate.properties" })
  2. @Import(value = {HibernateConfiguration.class})
  3. public class GlassfishPoolConfiguration {
  4.  
  5. @PropertySource(value = {"jdbc.properties", "hibernate.properties"})
  6. @Import(value = {HibernateConfiguration.class})
  7. public class HibernateMysqlConfiguration {

Finalment, voleu testejar que la nova configuració funciona i que podeu escriure a la BD MySQL. El problema és que el bean de tipus DataSource configurat per utilitzar el recurs JDBC a Glassfish, si l’aplicació no està desplegada al servidor Glassfish, no el trobarà. Hi diverses alternatives: una podria ser utilitzar un servidor Glassfish que s’executi com a part del test utilitzant Glassfish-embedded. Un altra aproximació, que és molt més flexible, ràpida i que ens permet testejar la funcionalitat de Spring, Hibernate i la nostra aplicació, és ampliar la funcionalitat de la classe Spring que executa els tests JUnit perquè sigui ella la que s’encarregui que el test tingui disponible recursos JNDI. La idea és molt senzilla: crear un context JNDI on registrareu un bean que després utilitzareu al test. Això ho podeu fer amb la classe org.ioc.daw.SpringJNDIRunner, que formarà part dels tests.

  1. package org.ioc.daw;
  2.  
  3. import javax.naming.NamingException;
  4. import org.ioc.daw.config.HibernateMysqlConfiguration;
  5. import org.junit.runners.model.InitializationError;
  6. import org.springframework.context.ApplicationContext;
  7. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  8. import org.springframework.mock.jndi.SimpleNamingContextBuilder;
  9. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  10.  
  11. public class SpringJNDIRunner extends SpringJUnit4ClassRunner {
  12. public static boolean isJNDIactive;
  13. public SpringJNDIRunner(Class<?> klass) throws InitializationError, IllegalStateException, NamingException {
  14. super(klass);
  15.  
  16. synchronized (SpringJNDIRunner.class) {
  17. if (!isJNDIactive) {
  18.  
  19. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(HibernateMysqlConfiguration.class);
  20. SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
  21. builder.bind("jdbc/socioc", applicationContext.getBean("dataSource"));
  22. builder.activate();
  23.  
  24. isJNDIactive = true;
  25. }
  26. }
  27. }
  28. }

Amb new AnnotationConfigApplicationContext definiu un context de Spring que carrega el bean definit a la classe de configuració HibernateMysqlConfiguration. A continuació registreu el bean anomenat dataSource amb el nom JNDI jdbc/socioc. El bean dataSource és el definit a HibernateMysqlConfiguration, que permet la connexió amb la BD MySQL. Per executar el test només heu d’especificar la nova classe que utilitzareu per definir el context de Spring en el qual correrà el test i el fitxer on heu configurat el DataSource que busca el recurs JDBC utilitzant JNDI.

Assegureu-vos de buidar el contingut de la taula “Users” abans d’executar el test. Aquest és el problema de treballar amb BD reals amb els tests, que mai es pot saber en quin es troba la BD i quines dades conté abans d’executar els tests. Això pot fer que els tests fallin, no per un error en el codi, sinó per un error en les dades.

  1. @RunWith(SpringJNDIRunner.class)
  2. @ContextConfiguration(classes = {GlassfishPoolConfiguration.class})

Heu vist els avantatges d’utilitzar Spring i Hibernate, així com una breu introducció al seu funcionament. Després heu vist com configurar i integrar l’aplicació “SocIoc” amb aquests dos frameworks i com treballar amb diferents recursos JDBC, ja sigui en memòria, connectant directament amb MySQL o utilitzant Glassfish i l’accés als seus recursos utilitzant JNDI.

Podeu trobar el codi al fitxer que teniu disponible als annexos de la unitat.

"SocIoc". Dialogant amb preguntes i respostes

A contnuació aprofundirem més a fons en Hibernate i explorar les relacions entre els objectes i com es guardaran en la BD. Per fer-ho treballarem amb les preguntes, respostes i vots de l’aplicació SocIoc. Recordeu les taules de la base de dades “SocIoc” i com es relacionen. En la figura podeu veure que les preguntes (taula “Questions”) poden tenir més d’una resposta (taula “Answers”), i que un usuari pot fer més d’una pregunta i contestar moltes altres.

Figura Contingut de la taula “Users”

Començareu creant les classes que representen les preguntes i respostes (vegeu la figura).

Figura Classes Question i Answer

  1. import javax.persistence.Column;
  2. import javax.persistence.Entity;
  3. import javax.persistence.GeneratedValue;
  4. import javax.persistence.GenerationType;
  5. import javax.persistence.Id;
  6. import javax.persistence.Table;
  7. import javax.validation.constraints.NotNull;
  8. import javax.validation.constraints.Size;
  9. import java.io.Serializable;
  10. @Table(name = "answers")
  11. public class Answer implements Serializable {
  12. private static final long serialVersionUID = 1L;
  13. @Id
  14. @NotNull
  15. @GeneratedValue(strategy = GenerationType.IDENTITY)
  16. @Column(name = "id")
  17. private Integer answerId;
  18.  
  19. @NotNull
  20. @Size(max = 45)
  21. @Column(name = "text")
  22. private String text;
  23.  
  24. public Integer getAnswerId() {
  25. return answerId;
  26. }
  27.  
  28. public void setAnswerId(Integer answerId) {
  29. this.answerId = answerId;
  30. }
  31.  
  32. public String getText() {
  33. return text;
  34. }
  35.  
  36. public void setText(String text) {
  37. this.text = text;
  38. }
  39. }
  40.  
  41. import org.ioc.daw.answer.Answer;
  42. import javax.persistence.CascadeType;
  43. import javax.persistence.Column;
  44. import javax.persistence.Entity;
  45. import javax.persistence.FetchType;
  46. import javax.persistence.GeneratedValue;
  47. import javax.persistence.GenerationType;
  48. import javax.persistence.Id;
  49. import javax.persistence.OneToMany;
  50. import javax.persistence.Table;
  51. import javax.validation.constraints.NotNull;
  52. import javax.validation.constraints.Size;
  53. import java.io.Serializable;
  54. import java.util.Set;
  55. @Table(name = "questions")
  56. public class Question implements Serializable {
  57. private static final long serialVersionUID = 1L;
  58. @Id
  59. @NotNull
  60. @GeneratedValue(strategy = GenerationType.IDENTITY)
  61. @Column(name = "id")
  62. private Integer questionId;
  63.  
  64. @NotNull
  65. @Size(max = 800)
  66. @Column(name = "text")
  67. private String text;
  68.  
  69. @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
  70. private Set<Answer> answers;
  71.  
  72. public Integer getQuestionId() {
  73. return questionId;
  74. }
  75.  
  76. public void setQuestionId(Integer questionId) {
  77. this.questionId = questionId;
  78. }
  79.  
  80. public String getText() {
  81. return text;
  82. }
  83.  
  84. public void setText(String text) {
  85. this.text = text;
  86. }
  87.  
  88. public Set<Answer> getAnswers() {
  89. return answers;
  90. }
  91.  
  92. public void setAnswers(Set<Answer> answers) {
  93. this.answers = answers;
  94. }
  95. }

Com podeu veure, a la classe Question s’ha definit la relació amb les respostes amb la notació @OneToMany, que indica que una pregunta podrà tenir moltes respostes. CascadeType.ALL indica que si volem persistir un objecte que té com a atribut algun objecte que no està persistit, i per tant gestionat per Hibernate, el persistirem. Per exemple, si voleu guardar un objecte User que té una pregunta que no està guardada, la guardarà. De la mateixa manera, si esborreu un usuari que té preguntes, totes les preguntes s’esborraran. FetchType indica l’estratègia que s’utilitzarà per carregar les dades, FetchType.EAGER recuperarà les dades immediatament i FetchType.LAZY ho farà quan faci falta. S’ha de tenir present que per poder utilitzar FetchType.LAZY la sessió ha d’estar encara oberta quan s’intenti accedir a les dades. En el vostre cas FetchType.EAGER és suficient, però s’ha d’estudiar cada cas per veure quina estratègia de càrrega de dades s’utilitza.

  1. @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
  2. private Set<Answer> answers;

Fixeu-vos que a la classe Question no hi ha cap referència als usuaris; com es fa llavors per indicar la relació? Les preguntes no tenen usuaris, una pregunta estarà formulada per un usuari, però serà l’entitat de tipus usuari la que pot tenir múltiples preguntes. Definiu aquesta relació a la classe User. De la mateixa manera, afegiu la relació amb Answer.

  1. @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
  2. private Set<Question> questions;
  3.  
  4. @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
  5. private Set<Answer> answers;
  6.  
  7. public Set<Question> getQuestions() {
  8. return questions;
  9. }
  10.  
  11. public void setQuestions(Set<Question> questions) {
  12. this.questions = questions;
  13. }
  14.  
  15. public Set<Answer> getAnswers() {
  16. return answers;
  17. }
  18.  
  19. public void setAnswers(Set<Answer> answers) {
  20. this.answers = answers;
  21. }

Un cop definides les noves entitats, heu de canviar la configuració de HibernateConfiguration per tal que Hibernate escanegi els nous paquets.

  1. public class HibernateConfiguration {
  2. ....
  3. @Bean
  4. @Autowired
  5. public LocalSessionFactoryBean sessionFactory(DataSource dataSource) throws NamingException {
  6. LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
  7. sessionFactory.setDataSource(dataSource);
  8. sessionFactory.setPackagesToScan("org.ioc.daw.user", "org.ioc.daw.question", "org.ioc.daw.answer");
  9. sessionFactory.setHibernateProperties(hibernateProperties());
  10. return sessionFactory;
  11. }
  12. ....

Un cop teniu definides les relacions, definireu els objectes per accedir a les dades (DAO). Creeu org.ioc.daw.question.QuestionDAO i la seva implementació org.ioc.daw.question.QuestionHibernateDAO.

  1. public interface QuestionDAO {
  2. Question getById(Integer questionId);
  3. void save(Question question);
  4. Question update(Question question);
  5. }
  6.  
  7. import org.hibernate.Session;
  8. import org.hibernate.SessionFactory;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.stereotype.Repository;
  11. import javax.transaction.Transactional;
  12.  
  13. @Transactional
  14. @Repository("questionHibernateDAO")
  15. public class QuestionHibernateDAO implements QuestionDAO {
  16.  
  17. @Autowired
  18. private SessionFactory sessionFactory;
  19.  
  20. @Override
  21. public Question getById(Integer questionId) {
  22. return getSession().get(Question.class, questionId);
  23. }
  24.  
  25. @Override
  26. public void save(Question question) {
  27. getSession().saveOrUpdate(question);
  28. }
  29.  
  30. @Override
  31. public Question update(Question question) {
  32. return (Question) getSession().merge(question);
  33. }
  34.  
  35. protected Session getSession() {
  36. return sessionFactory.getCurrentSession();
  37. }
  38.  
  39. }

I declareu el nou bean a org.ioc.daw.config.DAOConfig.

  1. @Bean
  2. public QuestionDAO questionDAO(){
  3. return new QuestionHibernateDAO();
  4. }

Amb aquest DAO que només permet guardar i trobar preguntes pel seu identificador, com podreu trobar per exemple totes les preguntes d’un usuari o afegir una resposta a una pregunta? Com que els objectes estan relacionats, per fer alguna d’aquestes operacions utilitzareu una combinació de diferents DAO. Creareu, al paquet org.ioc.daw.question, la interfície QuestionService i la seva implementació QuestionServiceImpl, però primer afegireu un mètode a UserDAO que permeti obtenir un usuari utilitzant el seu id.

  1. public interface UserDAO {
  2. ....
  3.  
  4. public User getById(Integer id);
  5. ...
  6. }
  7. public class UserDAOJPA implements UserDAO {
  8. .....
  9.  
  10. @Override
  11. public User getById(Integer id) {
  12. try {
  13. return (User) entityManager.createQuery("select object(o) from User o " +
  14. "where o.id = :id")
  15. .setParameter("id", id)
  16. .getSingleResult();
  17. } catch (NoResultException e) {
  18. return null;
  19. }
  20. }
  21. .....
  22. }
  23.  
  24. @Repository("userHibernateDAO")
  25. public class UserHibernateDAO implements UserDAO {
  26. .....
  27. @Override
  28. public User getById(Integer userId) {
  29. return getSession().get(User.class, userId);
  30. }
  31. .....
  32. }
  1. public interface QuestionService {
  2. public Set<Question> getAllQuestions(Integer userId);
  3. public void addAnswer(Answer answer, Integer questionId, Integer userId);
  4. public void create(Question question, Integer userId);
  5. }
  6.  
  7. import org.ioc.daw.answer.Answer;
  8. import org.ioc.daw.user.User;
  9. import org.ioc.daw.user.UserDAO;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import javax.transaction.Transactional;
  12. import java.util.HashSet;
  13. import java.util.Set;
  14.  
  15. @Transactional
  16. public class QuestionServiceImpl implements QuestionService {
  17. @Autowired
  18. private UserDAO userDAO;
  19.  
  20. @Autowired
  21. private QuestionDAO questionDAO;
  22.  
  23.  
  24. @Override
  25. public Set<Question> getAllQuestions(Integer userId) {
  26. User user = userDAO.getById(userId);
  27. return user.getQuestions();
  28. }
  29.  
  30. @Override
  31. public Question addAnswer(Answer answer, Integer questionId, Integer userId) {
  32. User user = userDAO.getById(userId);
  33. Set<Answer> userAnswers = user.getAnswers();
  34. addAnswerToCollection(answer, userAnswers);
  35. user.setAnswers(userAnswers);
  36. userDAO.create(user);
  37.  
  38. Question question = questionDAO.getById(questionId);
  39. Set<Answer> answers = question.getAnswers();
  40. answers = addAnswerToCollection(answer, answers);
  41. question.setAnswers(answers);
  42. return questionDAO.update(question);
  43. }
  44.  
  45. private Set<Answer> addAnswerToCollection(Answer answer, Set<Answer> answers) {
  46. if (answers != null) {
  47. answers.add(answer);
  48. } else {
  49. answers = new HashSet<Answer>();
  50. answers.add(answer);
  51. }
  52. return answers;
  53. }
  54.  
  55. @Override
  56. public void create(Question question, Integer userId) {
  57. User user = userDAO.getById(userId);
  58. Set<Question> questions = user.getQuestions();
  59. if (questions != null) {
  60. questions.add(question);
  61. } else {
  62. questions = new HashSet<>();
  63. questions.add(question);
  64. user.setQuestions(questions);
  65. }
  66. userDAO.create(user);
  67. }
  68. }

Creeu un nou fitxer de configuracions org.ioc.daw.config.ServicesConfig i declareu el now bean.

  1. @Configuration
  2. public class ServicesConfig {
  3. @Bean
  4. public QuestionService questionService(){
  5. return new QuestionServiceImpl();
  6. }
  7. @Bean
  8. public UserService userService(UserDAO userDAO) {
  9. return new UserService(userDAO);
  10. }
  11. }

Comproveu que estem injectant dependències de forma diferent als beans QuestionService i UserController. A UserController esteu injectant UserDAO al constructor, mentre que a QuestionService ho feu amb la notació @Autowired, que injectarà els beans utilitzant els setters. Què és millor, utilitzar injecció per a setters o per a constructors? No és qüestió de millor ni pitjor, i realment depèn de cada cas. De forma general, si les dependències que esteu injectant són imprescindibles per al funcionament de la classe es recomana utilitzar injecció mitjançant constructors; per tant, refactoritzarem la classe QuestionServiceImpl. La classe que configura els beans de servei quedarà de la següent forma:

  1. @Configuration
  2. public class ServicesConfig {
  3. @Bean
  4. public QuestionService questionService(UserDAO userDAO, QuestionDAO questionDAO) {
  5. return new QuestionServiceImpl(userDAO, questionDAO);
  6. }
  7.  
  8. @Bean
  9. public UserService userService(UserDAO userDAO) {
  10. return new UserService(userDAO);
  11. }
  12. }
  1. public class QuestionServiceImpl implements QuestionService {
  2. private UserDAO userDAO;
  3. private QuestionDAO questionDAO;
  4.  
  5. public QuestionServiceImpl(UserDAO userDAO, QuestionDAO questionDAO) {
  6. this.userDAO = userDAO;
  7. this.questionDAO = questionDAO;
  8. }

A continuació creareu el test QuestionDAOTest per comprovar que les preguntes es guarden correctament a la base de dades.

  1. import org.ioc.daw.config.EmbeddedDatabaseTestConfig;
  2. import org.ioc.daw.config.ServicesConfig;
  3. import org.ioc.daw.user.User;
  4. import org.ioc.daw.user.UserDAO;
  5. import org.junit.Test;
  6. import org.junit.runner.RunWith;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.test.context.ContextConfiguration;
  9. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  10. import java.sql.Timestamp;
  11. import java.util.Date;
  12. import java.util.Set;
  13. import static org.junit.Assert.assertEquals;
  14. import static org.junit.Assert.assertNotNull;
  15.  
  16. @RunWith(SpringJUnit4ClassRunner.class)
  17. @ContextConfiguration(classes = {ServicesConfig.class, EmbeddedDatabaseTestConfig.class})
  18. public class QuestionDAOTest {
  19. @Autowired
  20. private QuestionService questionService;
  21.  
  22. @Autowired
  23. private UserDAO userDAO;
  24.  
  25. @Test
  26. public void createQuestion() {
  27. User user = new User();
  28. user.setUsername("test");
  29. user.setActive(true);
  30. user.setEmail("email@test.com");
  31. user.setPassword("password");
  32. user.setName("name");
  33. user.setRank(10);
  34. user.setCreatedOn(new Timestamp(new Date().getTime()));
  35. userDAO.create(user);
  36. Question question = new Question();
  37. question.setText("This is a question");
  38.  
  39. questionService.create(question, user.getUserId());
  40. assertNotNull(question.getQuestionId());
  41.  
  42. Set<Question> questions = questionService.getAllQuestions(user.getUserId());
  43. assertEquals(1, questions.size());
  44. }
  45. }

Podeu trobar aquest codi al fitxer que teniu disponible als annexos de la unitat.

A continuació comprovareu si la vostra aplicació seria capaç de guardar les dades a la base de dades “SocIoc” que vau crear a MySQL. Per fer-ho només fa falta canviar el ContextConfiguration del test.

  1. @ContextConfiguration(classes = {ServicesConfig.class, HibernateMysqlConfiguration.class})

Si executeu el test veureu el següent error on s’indica que la taula “questions_answers” no existeix.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory' defined in org.ioc.daw.config.HibernateConfiguration: Invocation of init method failed; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing table [users_answers]

D’on surt aquesta taula? Per defecte, Hibernate utilitza taules intermèdies per desnormalitzar la base de dades. Això no té gaire a veure amb Hibernate per se, però sí amb el correcte disseny de la BD. En la taula podeu veure com quedarien les dades de la taula “Questions” amb l’exemple d’un usuari que hagués fet diverses preguntes.

Taula Taula “Questions”
id text user_id
1 pregunta 1 2
2 pregunta 2 2
3 pregunta 3 4
4 pregunta 4 2

Podeu veure que el fet que la taula tingui el camp “user_id” fa que no representi només les dades d’una pregunta, sinó que hi ha també informació dels usuaris. Això trenca la segona norma de normalització de les base de dades, que diu que totes les columnes han de ser dependents de la clau principal. És a dir, si a la pregunta “aquesta columna descriu el que la clau principal identifica?” la resposta és no, llavors vol dir que la columna no és dependent de la clau principal. En el vostre cas, “la columna ‘user_id’ descriu el que la clau principal ‘id’ identifica (una pregunta)?”, clarament no. Això implica que aquesta taula no està normalitzada. En la figura podeu veure com es podria aconseguir la normalització per a les taules “Questions”, “Users” i “Answers”.

Figura Normalització de les taules

Amb les noves taules, les dades que relacionen preguntes i usuaris quedarien tal com es pot veure en la taula i la taula.

Taula Taula “Users questions”
user_id question_id
2 1
2 2
4 3
2 4
Taula Taula “Questions”
id text
1 pregunta 1
2 pregunta 2
3 pregunta 3
4 pregunta 4

Un dels beneficis d’utilitzar Hibernate és que es pot encarregar d’això. El que fareu serà crear un nou esquema a MySQL que utilitzareu per fer que Hibernate s’encarregui de la generació de les taules necessàries. Creeu un nou esquema utilitzant MySQL Workbench anomenat “soc_ioc”, tal com es mostra en la figura.

Figura Schema soc_ioc

Canvieu l’URL de connexió JDBC per utilitzar el nou esquema modificant el paràmetre jdbc.url del fitxer jdbc.properties i modifiqueu la propietat hibernate.hbm2ddl de hibernate.properties perquè creï les taules automàticament, però que no modifiqui les taules si hi ha canvis a les entitats.

jdbc.url = jdbc:mysql://192.168.99.100:32768/soc_ioc
hibernate.hbm2ddl = update

Si ara executeu el test org.ioc.daw.question.QuestionDAOTest#createQuestion comprovareu que no hi ha cap error. Si comproveu a MySQL Worbench veureu que Hibernate haurà creat les taules a partir de les entitats que hem definit, tal com podeu veure en la figura.

Figura Taules creades per Hibernate

Ara que sabeu que el codi és capaç d’escriure i llegir dades correctament a la BD MySQL, tornareu a canviar la BD utilitzada als tests per a la BD en memòria H2 i hi afegireu més tests.

  1. @Test
  2. public void addAnswer() {
  3. User user = new User();
  4. user.setUsername("test");
  5. user.setActive(true);
  6. user.setEmail("email@test.com");
  7. user.setPassword("password");
  8. user.setName("name");
  9. user.setCreatedOn(new Timestamp(new Date().getTime()));
  10. userDAO.create(user);
  11. Question question = new Question();
  12. question.setText("This is a question");
  13.  
  14. questionService.create(question, user.getUserId());
  15.  
  16. Answer answer = new Answer();
  17. answer.setText("This is an answer");
  18. question = questionService.addAnswer(answer, question.getQuestionId(), user.getUserId());
  19.  
  20. assertEquals(1, question.getAnswers().size());
  21. }

Fixeu-vos en un detall: no heu definit cap objecte AnswersDAO i heu pogut persistir una resposta. Recordeu que això és possible gràcies al fet que hem utilitzat CascadeType.ALL, que automàticament s’encarregarà de persistir els objectes que no estiguin associats a una sessió d’Hibernate.

A continuació escriureu el test per a la classe QuestionService. En aquest cas el que voldrem fer serà injectar objectes mock per a UserDAO i QuestionDAO i que el test comprovi que la lògica és correcta. La configuració dels objectes mocks és a la classe SpringTestConfig.

  1. @Configuration
  2. @EnableTransactionManagement
  3. @Import(value = {ServiceConfig.class})
  4. public class SpringTestConfig {
  5. @Bean
  6. public UserDAO userDAO() {
  7. return Mockito.mock(UserDAO.class);
  8. }
  9.  
  10. @Bean
  11. public QuestionDAO questionDAO() {
  12. return Mockito.mock(QuestionDAO.class);
  13. }
  14.  
  15. @Bean
  16. public PlatformTransactionManager transactionManager() {
  17. return Mockito.mock(PlatformTransactionManager.class);
  18. }
  19. }

Un cop la configuració dels beans està preparada, ja podeu escriure el test.

  1. import org.ioc.daw.answer.Answer;
  2. import org.ioc.daw.config.SpringTestConfig;
  3. import org.ioc.daw.user.User;
  4. import org.ioc.daw.user.UserDAO;
  5. import org.junit.Test;
  6. import org.junit.runner.RunWith;
  7. import org.mockito.Mockito;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.test.context.ContextConfiguration;
  10. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  11.  
  12. import java.util.ArrayList;
  13. import java.util.HashSet;
  14. import java.util.List;
  15. import java.util.Set;
  16. import static org.junit.Assert.assertEquals;
  17. import static org.junit.Assert.assertNotNull;
  18. import static org.junit.Assert.assertEquals;
  19. import static org.mockito.Mockito.*;
  20.  
  21.  
  22. @RunWith(SpringJUnit4ClassRunner.class)
  23. @ContextConfiguration(classes = {SpringTestConfig.class})
  24. public class QuestionServiceTest {
  25. @Autowired
  26. private UserDAO userDAOMock;
  27.  
  28. @Autowired
  29. private QuestionDAO questionDAOMock;
  30.  
  31. @Autowired
  32. private QuestionService questionService;
  33.  
  34. @Test
  35. public void getAllQUestions() {
  36. Question question1 = getDummyQuestion(1);
  37. Set<Question> questions = new HashSet<>();
  38. questions.add(question1);
  39.  
  40. int userId = 1;
  41. User userMock = Mockito.mock(User.class);
  42. when(userMock.getQuestions()).thenReturn(questions);
  43.  
  44. when(userDAOMock.getById(userId)).thenReturn(userMock);
  45.  
  46. Set<Question> questionsResponse = questionService.getAllQuestions(userId);
  47. assertEquals(1, questionsResponse.size());
  48. }
  49.  
  50. @Test
  51. public void addTheFirstAnswerToAQuestion() {
  52. int userID = 1;
  53. int questionId = 1;
  54. int answerId = 1;
  55. Answer answer = getDummyAnswer(answerId);
  56. Question question1 = getDummyQuestion(questionId);
  57. User user1 = getDummyUser(userID);
  58.  
  59. when(userDAOMock.getById(userID)).thenReturn(user1);
  60. when(questionDAOMock.getById(questionId)).thenReturn(question1);
  61. when(questionDAOMock.update(question1)).thenReturn(question1);
  62.  
  63. Question questionResponse = questionService.addAnswer(answer, questionId, userID);
  64. assertEquals(1, questionResponse.getAnswers().size());
  65. verify(userDAOMock, Mockito.times(1)).create(user1);
  66. }
  67.  
  68.  
  69. @Test
  70. public void addTheAnswerToAQuestionOnceItHasSomeAnswers() {
  71. int questionId = 1;
  72. int userID = 1;
  73.  
  74. Answer answer1 = getDummyAnswer(1);
  75. Answer answer2 = getDummyAnswer(2);
  76. Question question1 = getDummyQuestion(questionId);
  77. Set<Answer> answers = new HashSet<>();
  78. answers.add(answer1);
  79. question1.setAnswers(answers);
  80.  
  81. List<Question> questions = new ArrayList<>();
  82. questions.add(question1);
  83.  
  84. User user1 = getDummyUser(userID);
  85. when(userDAOMock.getById(userID)).thenReturn(user1);
  86. when(questionDAOMock.getById(questionId)).thenReturn(question1);
  87. when(questionDAOMock.update(question1)).thenReturn(question1);
  88.  
  89. questionService.addAnswer(answer2, questionId, userID);
  90. verify(userDAOMock, Mockito.times(1)).create(user1);
  91. assertEquals(2, answers.size());
  92. }
  93.  
  94. @Test
  95. public void createFirstUserQuestion() {
  96. int userId = 1;
  97. Question question = getDummyQuestion(1);
  98. User user = getDummyUser(1);
  99. when(userDAOMock.getById(userId)).thenReturn(user);
  100.  
  101. questionService.create(question, userId);
  102.  
  103. verify(userDAOMock, timeout(1)).create(user);
  104. assertEquals(1, user.getQuestions().size());
  105. }
  106.  
  107.  
  108. @Test
  109. public void createUserQuestionWhenItsNotTheFirstOne() {
  110. int userId = 1;
  111. Question question1 = getDummyQuestion(1);
  112. Question question2 = getDummyQuestion(2);
  113. User user = getDummyUser(1);
  114. Set<Question> questions = new HashSet<>();
  115. questions.add(question1);
  116. user.setQuestions(questions);
  117. when(userDAOMock.getById(userId)).thenReturn(user);
  118.  
  119. questionService.create(question2, userId);
  120.  
  121. verify(userDAOMock, timeout(1)).create(user);
  122. assertEquals(2, questions.size());
  123. }
  124.  
  125. private Question getDummyQuestion(int questionId) {
  126. Question question1 = new Question();
  127. question1.setQuestionId(questionId);
  128. question1.setText("Some question");
  129. Set<Question> questions = new HashSet<>();
  130. questions.add(question1);
  131. return question1;
  132. }
  133.  
  134. private Answer getDummyAnswer(int answerId) {
  135. Answer answer = new Answer();
  136. answer.setAnswerId(answerId);
  137. answer.setText("This is an answer");
  138. return answer;
  139. }
  140.  
  141. private User getDummyUser(int userId) {
  142. String username = "test";
  143. User user = new User();
  144. user.setUsername(username);
  145. user.setUserId(userId);
  146. return user;
  147. }
  148. }

Una cosa que heu de comprovar és la diferència de què passa quan creem preguntes o respostes per primer cop. Si és el primer cop, la col·lecció s’ha de crear. Agafem el test createUserQuestionWhenItsNotTheFirstOne com a exemple. A la línia 86 afegim una llista de preguntes a l’objecte usuari. Com que la llista existirà, el que farà QuestionService serà afegir un element nou, però l’objecte llista serà el mateix que tenia l’usuari. Per això podem comprovar que la llista creada té un objecte més. Al test createFirstUserQuestion, l’usuari no té cap pregunta, llavors un nou objecte de tipus llista es crearà i s’associarà a l’objecte user, que és el que comprovem a la línia 74.

Al fitxer que teniu disponible als annexos de la unitat podeu trobar el codi d’aquest apartat.

"SocIoc". Dialogant amb usuaris, rangs i vots

Un usuari tindrà associat un rang, que serà el resultat d’un càlcul dels vots rebuts per les respostes d’un usuari. Els vots poden ser positius o negatius. Així, un usuari tindrà associada una sèrie de vots, que estaran associats a diferents preguntes. D’aquesta manera, un usuari tindrà múltiples vots i una resposta també pot tenir múltiples vots. Vegeu en la figura com serà la relació entre les taules.

Figura Taules normalitzades

La taula “Votes” està relacionada amb “Answers” i “Users” no directament, però amb unes taules intermèdies . Fixeu-vos també en com la taula “Users” es relaciona amb rank, hi ha un camp “user_id” i no hi ha cap taula intermèdia. El motiu és que la relació d’un usuari amb el seu rank és 1 a 1, és a dir, cada usuari tindrà només un rank i cada rank pertany només a un usuari. Vegeu com es representa això en el codi Java de la vostra aplicació. Creeu la classe Rank al paquet org.ioc.daw.rank. Com podeu veure, l’atribut total el calcularem a partir dels vots positius i negatius. Cada cop que s’actualitzi el valor dels vots, s’actualitzarà el total.

  1. import javax.persistence.Column;
  2. import javax.persistence.Entity;
  3. import javax.persistence.GeneratedValue;
  4. import javax.persistence.GenerationType;
  5. import javax.persistence.Id;
  6. import javax.persistence.Table;
  7. import javax.validation.constraints.NotNull;
  8. import java.io.Serializable;
  9.  
  10. @Table(name = "ranks")
  11. public class Rank implements Serializable {
  12. private static final long serialVersionUID = 1L;
  13.  
  14. @Id
  15. @NotNull
  16. @GeneratedValue(strategy = GenerationType.IDENTITY)
  17. @Column(name = "id")
  18. private Integer rankId;
  19.  
  20. @Column(name = "positive_votes")
  21. private int positive;
  22.  
  23. @Column(name = "negative_votes")
  24. private int negative;
  25.  
  26. @Column(name = "total")
  27. private int total;
  28.  
  29. public int getPositive() {
  30. return positive;
  31. }
  32.  
  33. public void setPositive(int positive) {
  34. this.positive = positive;
  35. this.total = positive - getNegative();
  36. }
  37.  
  38. public int getNegative() {
  39. return negative;
  40. }
  41.  
  42. public void setNegative(int negative) {
  43. this.negative = negative;
  44. this.total = getPositive() - negative;
  45. }
  46.  
  47. public Integer getRankId() {
  48. return rankId;
  49. }
  50.  
  51. public void setRankId(Integer rankId) {
  52. this.rankId = rankId;
  53. }
  54.  
  55. public int getTotal() {
  56. return total;
  57. }
  58.  
  59. public void setTotal(int total) {
  60. this.total = total;
  61. }
  62. }

A la classe “User” hi afegiu l’atribut rank amb el seu getter i setter, i també el nou paquet a escanejar per Hibernate a HibernateConfiguration. En la definició de la interfície UserDAO hi ha un mètode relacionat amb el rang. L’implementareu i hi afegireu un test.

  1. @OneToOne(cascade = {CascadeType.ALL})
  2. private Rank rank;
  3. public Rank getRank() {
  4. return rank;
  5. }
  6. public void setRank(Rank rank) {
  7. this.rank = rank;
  8. }
 sessionFactory.setPackagesToScan("org.ioc.daw.user", "org.ioc.daw.question",
                "org.ioc.daw.answer", "org.ioc.daw.rank");

La implementació que tenim de UserHibernateDAO#findActiveUsers està basada en quan la classe User tenia un atribut de tipus enter que tenia el rang de l’usuari. Ara aquesta implementació no funcionarà, ja que l’atribut que conté el rang d’un usuari és una classe, i a la BD, una altra taula. Per obtenir quin és l’usuari que té un major rang ho podríeu fer mitjançant el llenguatge de consultes d’Hibernate (HQL); el problema és que si canvieu d’implementació de JPA haureu de tornar a escriure totes les consultes de nou. Una solució és utilitzar consultes JPA.

  1. @Override
  2. public User findUserWithHighestRank() {
  3. Criteria criteria = createEntityCriteria();
  4. criteria.addOrder(Order.desc("rank"));
  5. return (User) criteria.uniqueResult();
  6. }

JPA permet crear consultes amb la classe CriteriaBuilder. El que primer indicareu serà de quina classe voldreu fer la consulta (línia 5) i què és el que retornarà la consulta (línia 4) i com s’ordenaran els resultats retornats (línia 7). En aquest cas, l’ordre serà descendent (cb.desc) i estarà ordenat per l’atribut total de la classe Rank, que és l’atribut rank a la classe User. Finalment, indiqueu que només voleu retornar el primer resultat (setMaxResults(1)) i que per tant aquesta consulta només ha de retornar un únic resultat (getSingleResult()).

1
  1. @Override
  2. public User findUserWithHighestRank() {
  3. CriteriaBuilder cb = createCriteriaBuilder();
  4. CriteriaQuery<User> criteriaQuery = cb.createQuery(User.class);
  5. Root<User> root = criteriaQuery.from(User.class);
  6. criteriaQuery.select(root);
  7. criteriaQuery.orderBy(cb.desc(root.join("rank").get("total")));
  8. EntityManager em = createEntityManager();
  9. return em.createQuery(criteriaQuery).setMaxResults(1).getSingleResult();
  10. }
  11. private CriteriaBuilder createCriteriaBuilder() {
  12. return getSession().getEntityManagerFactory().getCriteriaBuilder();
  13. }
  14.  
  15. private EntityManager createEntityManager() {
  16. return getSession().getEntityManagerFactory().createEntityManager();
  17. }

A continuació creeu la classe org.ioc.daw.vote.Vote.

  1. import javax.persistence.Column;
  2. import javax.persistence.Entity;
  3. import javax.persistence.GeneratedValue;
  4. import javax.persistence.GenerationType;
  5. import javax.persistence.Id;
  6. import javax.persistence.Table;
  7. import javax.validation.constraints.NotNull;
  8. import java.io.Serializable;
  9.  
  10. @Table(name = "votes")
  11. public class Vote implements Serializable {
  12. private static final long serialVersionUID = 1L;
  13.  
  14. @Id
  15. @NotNull
  16. @GeneratedValue(strategy = GenerationType.IDENTITY)
  17. @Column(name = "id")
  18. private Integer voteId;
  19.  
  20. @Column(name = "vote")
  21. private Boolean vote;
  22.  
  23. public Integer getVoteId() {
  24. return voteId;
  25. }
  26.  
  27. public void setVoteId(Integer voteId) {
  28. this.voteId = voteId;
  29. }
  30.  
  31. public Boolean getVote() {
  32. return vote;
  33. }
  34.  
  35. public void setVote(Boolean vote) {
  36. this.vote = vote;
  37. }
  38. }

Afegiu el paquet a la configuració d’Hibernate (HibernateConfiguration) per tal que l’escanegi.

 sessionFactory.setPackagesToScan("org.ioc.daw.user", "org.ioc.daw.question",
                "org.ioc.daw.answer", "org.ioc.daw.rank", "org.ioc.daw.vote");

Abans de crear el les classes relacionades amb els vots creareu un objecte DAO per a “Answers” i afegireu la relació entre respostes i vots a la classe Answer. Creareu la interfície AnswersDAO i la seva implementació AnswerHibernateDAO, i afegireu el nou DAO a DAOConfig i el mock a SpringTestConfig.

  1. public class Answer implements Serializable {
  2. ....
  3. @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
  4. private Set<Vote> votes;
  5.  
  6. public Set<Vote> getVotes() {
  7. return votes;
  8. }
  9.  
  10. public void setVotes(Set<Vote> votes) {
  11. this.votes = votes;
  12. }
  13. ....
  14. }
  15.  
  16.  
  17. public interface AnswerDAO {
  18. Answer getById(Integer questionId);
  19. void save(Answer question);
  20. }
  21.  
  22. @Transactional
  23. @Repository("answerHibernateDAO")
  24. public class AnswerHibernateDAO implements AnswerDAO {
  25. @Autowired
  26. private SessionFactory sessionFactory;
  27.  
  28. @Override
  29. public Answer getById(Integer questionId) {
  30. return getSession().get(Answer.class, questionId);
  31. }
  32.  
  33. @Override
  34. public void save(Answer answer) {
  35. getSession().saveOrUpdate(answer);
  36. }
  37.  
  38. protected Session getSession() {
  39. return sessionFactory.getCurrentSession();
  40. }
  41.  
  42. }
  43.  
  44. @Configuration
  45. public class DAOConfig {
  46. .......
  47. @Bean
  48. public AnswerDAO answerDAO(){
  49. return new AnswerHibernateDAO();
  50. }
  51. .......
  52. }

Un usuari votarà, així que heu d’afegir la relació dels usuaris amb els vots.

  1. public class User implements Serializable {
  2. ....
  3. @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
  4. private Set<Vote> votes;
  5. public Set<Vote> getVotes() {
  6. return votes;
  7. }
  8. public void setVotes(Set<Vote> votes) {
  9. this.votes = votes;
  10. }
  11. ....

Com sempre, ara heu de testejar que podem treballar amb l’entitat Votes. Però què és el que realment volem testejar? Realment no volem testejar que podem guardar un vot a la base de dades, ni mai guardarem un vot de forma aïllada. Els vots sempre estaran relacionats amb les respostes i els usuaris. Llavors, el que volem testejar és que un usuari té la capacitat de votar una pregunta i que, si ho fa, aquesta informació es guardarà a la base de dades. Per fer-ho creareu el test VoteDAOTest, encara que no hem creat la classe VoteDAO; el que volem testejar és que els vots es guarden a la base de dades correctament. Abans implementarem la funcionalitat per votar negativament i positivament. Creem la interfície org.ioc.daw.vote.VoteService i la seva implementació. A partir del l’identificador d’una pregunta, recupereu les dades de la BD i creeu el nou objecte de tipus Vote, i en guardar l’objecte pregunta es guardarà la informació del vot.

  1. public interface VoteService {
  2. void votePositive(Integer answerId, Integer userId);
  3. void voteNegative(Integer answerId, Integer userId);
  4. }
  5.  
  6. import org.ioc.daw.answer.Answer;
  7. import org.ioc.daw.answer.AnswerDAO;
  8. import javax.transaction.Transactional;
  9. import java.util.HashSet;
  10. import java.util.Set;
  11.  
  12. @Transactional
  13. public class VoteServiceImpl implements VoteService {
  14. private AnswerDAO answerDAO;
  15.  
  16. public VoteServiceImpl(AnswerDAO answerDAO, UserDAO userDAO){
  17. this.answerDAO = answerDAO;
  18. this.userDAO = userDAO;
  19. }
  20.  
  21. @Override
  22. public void votePositive(Integer answerId, Integer userId) {
  23. vote(answerId, userId, true);
  24. }
  25.  
  26. @Override
  27. public void voteNegative(Integer answerId, Integer userId) {
  28. vote(answerId, userId, false);
  29. }
  30.  
  31. private Vote vote(Integer userId, Integer answerId, Boolean value) {
  32. User user = userDAO.getById(userId);
  33. Set<Vote> userVotes = user.getVotes();
  34. Vote vote = new Vote();
  35. vote.setVote(value);
  36. userVotes = getVotes(vote, userVotes);
  37. user.setVotes(userVotes);
  38. userDAO.create(user);
  39.  
  40. Answer answer = answerDAO.getById(answerId);
  41. Set<Vote> votes = answer.getVotes();
  42. votes = getVotes(vote, votes);
  43. answer.setVotes(votes);
  44. answerDAO.save(answer);
  45. return vote;
  46. }
  47.  
  48. private Set<Vote> getVotes(Vote vote, Set<Vote> votes) {
  49. if (votes != null) {
  50. votes.add(vote);
  51. } else {
  52. votes = new HashSet<Vote>();
  53. votes.add(vote);
  54.  
  55. }
  56. return votes;
  57. }

No us heu d’oblidar d’afegir VoteService a ServiceConfig per tal que Spring creï el bean.

  1. @Bean
  2. public VoteService voteService(AnswerDAO answerDAO, UserDAO userDAO){
  3. return new VoteServiceImpl(answerDAO, userDAO);
  4. }

Com sempre, ara heu de testejar que podeu treballar amb l’entitat Votes. Però què és el que realment volem testejar? Realment no volem testejar que podem guardar un vot a la base de dades, ni mai guardarem un vot de forma aïllada. Els vots sempre estaran relacionats amb les respostes i els usuaris. Llavors, el que volem testejar és que un usuari té la capacitat de votar una pregunta i que, si ho fa, aquesta informació es guardarà a la base de dades. Per fer-ho creareu el test VoteDAOTest, encara que no hem creat la classe VoteDAO; el que volem testejar és que els vots es guarden a la base de dades correctament.

En el test fareu diverses coses. Primer creeu i persistiu tres usuaris, després l’usuari “user1” crea una pregunta, l’usuari “user2” crea una resposta per a la pregunta i feu que l’usuari “user3” voti de forma positiva la pregunta. Finalment, comproveu que la resposta i l’usuari “user3” tenen un vot i que l’identificador del vot és el mateix en els dos casos.

  1. import org.ioc.daw.answer.Answer;
  2. import org.ioc.daw.answer.AnswerDAO;
  3. import org.ioc.daw.config.EmbeddedDatabaseTestConfig;
  4. import org.ioc.daw.config.ServicesConfig;
  5. import org.ioc.daw.question.Question;
  6. import org.ioc.daw.question.QuestionService;
  7. import org.ioc.daw.user.User;
  8. import org.ioc.daw.user.UserDAO;
  9. import org.junit.Test;
  10. import org.junit.runner.RunWith;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.test.context.ContextConfiguration;
  13. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  14. import java.sql.Timestamp;
  15. import java.util.Date;
  16. import static org.junit.Assert.assertEquals;
  17.  
  18.  
  19. @RunWith(SpringJUnit4ClassRunner.class)
  20. @ContextConfiguration(classes = {ServicesConfig.class, EmbeddedDatabaseTestConfig.class})
  21. public class VoteDAOTest {
  22. @Autowired
  23. private QuestionService questionService;
  24.  
  25. @Autowired
  26. private UserDAO userDAO;
  27.  
  28. @Autowired
  29. private AnswerDAO answerDAO;
  30.  
  31. @Autowired
  32. private VoteService voteService;
  33.  
  34.  
  35. @Test
  36. public void votePositive() {
  37. User user1 = getUser("test", "test@email.com");
  38. User user2 = getUser("test1", "test1@email.com");
  39. User user3 = getUser("test2", "test2@email.com");
  40. userDAO.create(user1);
  41. userDAO.create(user2);
  42. userDAO.create(user3);
  43.  
  44. Question question = new Question();
  45. question.setText("This is a question");
  46. questionService.create(question, user1.getUserId());
  47.  
  48. Answer answer = new Answer();
  49. answer.setText("This is an answer");
  50. question = questionService.addAnswer(answer, question.getQuestionId(), user2.getUserId());
  51.  
  52. int answerId = question.getAnswers().iterator().next().getAnswerId();
  53. voteService.votePositive(user3.getUserId(), answerId);
  54.  
  55. User userDB = userDAO.getById(user3.getUserId());
  56. Answer answerDB = answerDAO.getById(answerId);
  57. assertEquals(1, userDB.getVotes().size());
  58. assertEquals(1, answerDB.getVotes().size());
  59. assertEquals(userDB.getVotes().iterator().next().getVoteId(), answerDB.getVotes().iterator().next().getVoteId());
  60.  
  61. }
  62.  
  63. private User getUser(String username, String email) {
  64. User user = new User();
  65. user.setUsername(username);
  66. user.setActive(true);
  67. user.setEmail(email);
  68. user.setPassword("password");
  69. user.setName("name");
  70. user.setCreatedOn(new Timestamp(new Date().getTime()));
  71. return user;
  72. }
  73. }

Podeu trobar el fitxer amb aquest codi als annexos de la unitat.

Què s'ha après?

Heu après que hi ha diferents frameworks que ens poden ajudar a l’hora de desenvolupar aplicacions. Spring ens ajuda a crear un codi més modular, reutilitzable i fàcil de testejar. Hem vist com podem canviar fàcilment quina base de dades utilitzar o els beans a injectar a una classe. Per una altra banda, Hibernate ens dóna les eines per treballar amb bases de dades focalitzant els esforços en el desenvolupament del codi i no en el disseny de la BD. Això no vol dir que el disseny de la BD no tingui importància; al contrari, serà fonamental per al correcte comportament de l’aplicació quan estigui a producció, però aquesta tasca serà responsabilitat de l’administrador de la BD. Hibernate ens farà més fàcil la tasca de relacionar els objectes de l’aplicació amb les taules de la base dades. També heu après a fer tests unitaris i a testejar l’aplicació utilitzant una BD en memòria emprant els frameworks JUnit i Mockito.

Anar a la pàgina anterior:
Annexos
Anar a la pàgina següent:
Activitats