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:
public interface UserDAO { public void create(User user); public User edit(User user); public void remove(User user); public User findUserWithHighestRank(); public List<User> findActiveUsers(); } @Stateless public class UserDAOJPA implements UserDAO { @PersistenceContext private EntityManager entityManager; @Override public void create(User user) { entityManager.persist(user); } @Override public User edit(User user) { return entityManager.merge(user); } @Override public void remove(User user) { user = entityManager.merge(user); entityManager.remove(user); } @Override try { return (User) entityManager.createQuery("select object(o) from User o " + "where o.username = :username") .setParameter("username", username) .getSingleResult(); } catch (NoResultException e) { return null; } } @Override public List<User> findActiveUsers() { try { return (List<User>) entityManager.createQuery("select object(o) from User o " + "where o.active= true") .getResultList(); } catch (NoResultException e) { return null; } } @Override public User findUserWithHighestRank() { try { return (User) entityManager.createQuery("select object(o) from User o order by o.rank DESC") .setMaxResults(1) .getSingleResult(); } catch (NoResultException e) { return null; } } } public class UserController { private UserDAO userDAO; return userDAO.findUserByUsername(username); } }
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ó.
public class UserService { private UserDAO userDAO; public UserService() { this.userDAO = new UserDAOJPA(); } return userDAO.findUserByUsername(username); } }
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:
public class UserController { private UserDAO userDAO; public UserController(UserDAO userDAO) { this.userDAO = userDAO; } return userDAO.findUserByUsername(username); } }
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.
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:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <springframework.version>4.3.4.RELEASE</springframework.version> <mysql.connector.version>5.1.40</mysql.connector.version> <junit.version>4.12</junit.version> <mockito.version>1.10.19</mockito.version> <h2.version>1.4.190</h2.version> </properties> <dependencies> ... <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${springframework.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> ... </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.
<properties> <springframework.version>4.3.4.RELEASE</springframework.version> </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
.
public class UserService { private UserDAO userDAO; public UserService(UserDAO userDAO) { this.userDAO = userDAO; } return userDAO.findUserByUsername(username); } }
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;
.
package org.ioc.daw.config; import org.ioc.daw.user.UserDAO; import org.ioc.daw.user.UserService; import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration @Configuration public class SpringTestConfig { @Bean public UserDAO userDAO() { return Mockito.mock(UserDAO.class); } @Bean public UserService userService(UserDAO userDAO) { return new UserService(userDAO); } }
@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.
import org.ioc.daw.config.SpringTestConfig; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {SpringTestConfig.class}) public class UserServiceTest { @Autowired private UserDAO userDAO; @Autowired private UserService userService; @Test public void getUserByUsername() { User user = new User(); user.setUsername(username); user.setUserId(1L); when(userDAO.findUserByUsername(username)).thenReturn(user); User userResult = userService.getUser(username); assertEquals(username, userResult.getUsername()); verify(userDAO, times(1)).findUserByUsername(username); } }
@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:
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
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.
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.
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 tipusTransaction
.
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.
<properties> .. <hibernate.version>5.2.5.Final</hibernate.version> .. </properties> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.3.4.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.jadira.usertype</groupId> <artifactId>usertype.core</artifactId> <version>6.0.1.GA</version> </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
.
import org.hibernate.Criteria; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import javax.transaction.Transactional; import java.util.List; @Transactional public class UserHibernateDAO implements UserDAO { @Autowired private SessionFactory sessionFactory; @Override public void create(User user) { getSession().saveOrUpdate(user); } @Override public User edit(User user) { return (User) getSession().merge(user); } @Override public void remove(User user) { getSession().delete(user); } @Override Criteria criteria = createEntityCriteria(); criteria.add(Restrictions.eq("username", username)); return (User) criteria.uniqueResult(); } @Override public User findUserWithHighestRank() { Criteria criteria = createEntityCriteria(); criteria.addOrder(Order.desc("rank")); return (User) criteria.uniqueResult(); } @Override public List<User> findActiveUsers() { Criteria criteria = createEntityCriteria(); criteria.add(Restrictions.eq("active", true)); return (List<User>) criteria.list(); } protected Session getSession() { return sessionFactory.getCurrentSession(); } private Criteria createEntityCriteria() { return getSession().createCriteria(User.class); } }
@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.
import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; import java.util.Properties; @Configuration @EnableTransactionManagement @PropertySource(value = {"application-test.properties"}) public class EmbeddedDatabaseTestConfig { @Autowired @Bean public UserDAO userDAO() { return new UserHibernateDAO(); } @Bean public DataSource dataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2).build(); return db; } @Bean @Autowired public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setPackagesToScan("org.ioc.daw"); sessionFactory.setHibernateProperties(hibernateProperties()); return sessionFactory; } properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect")); properties.put("hibernate.hbm2ddl.auto", environment.getRequiredProperty("hibernate.hbm2ddl")); properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql")); properties.put("hibernate.format_sql", environment.getRequiredProperty("hibernate.format_sql")); return properties; } @Bean @Autowired public HibernateTransactionManager transactionManager(SessionFactory s) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(s); return txManager; } }
@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.
@Bean public UserDAO userDAO() { return new UserHibernateDAO(); }
Defineix que com el Bean de tipus UserDAO
que utilitzarem serà del tipus UserHibernateDAO
.
@Bean public DataSource dataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2).build(); return db; }
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.
@Bean @Autowired public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setPackagesToScan("org.ioc.daw"); sessionFactory.setHibernateProperties(hibernateProperties()); return sessionFactory; }
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.
@Bean @Autowired public HibernateTransactionManager transactionManager(SessionFactory s) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(s); return txManager; }
É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.
import org.ioc.daw.config.EmbeddedDatabaseTestConfig; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.sql.Timestamp; import java.util.Date; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {EmbeddedDatabaseTestConfig.class}) public class UserDAOTest { @Autowired private UserDAO userDAO; @Test public void saveUser() { User user = new User(); user.setUsername("test"); user.setActive(true); user.setEmail("email@test.com"); user.setPassword("password"); user.setName("name"); user.setRank(10); assertNull(user.getUserId()); userDAO.create(user); assertNotNull(user.getUserId()); User userFromDb = userDAO.findUserByUsername("test"); assertEquals(user.getUserId(), userFromDb.getUserId()); } }
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.
@Test public void saveUser(){ User user = new User(); user.setUsername("test"); user.setActive(true); user.setEmail("email@test.com"); user.setPassword("password"); user.setName("name"); user.setRank(10); assertNull(user.getUserId()); userDAO.create(user); assertNotNull(user.getUserId()); user.setEmail("new-email@test.com"); User userFromDb = userDAO.findUserByUsername("test"); assertEquals(user.getUserId(), userFromDb.getUserId()); assertEquals("new-email@test.com", userFromDb.getEmail()); }
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
.
package org.ioc.daw.config; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.naming.NamingException; import javax.sql.DataSource; import java.util.Properties; @Configuration @EnableTransactionManagement @ComponentScan({"org.ioc.daw.user", "org.ioc.daw.question", "org.ioc.daw.answer", "org.ioc.daw.vote", "org.ioc.daw.rank"}) @PropertySource(value = {"jdbc.properties", "hibernate.properties"}) @Import(value = {HibernateConfiguration.class}) public class HibernateMysqlConfiguration { @Autowired @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName")); dataSource.setUrl(environment.getRequiredProperty("jdbc.url")); dataSource.setUsername(environment.getRequiredProperty("jdbc.username")); dataSource.setPassword(environment.getRequiredProperty("jdbc.password")); return dataSource; } }
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
.
import org.ioc.daw.user.UserDAO; import org.ioc.daw.user.UserHibernateDAO; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DAOConfig { @Bean public UserDAO userDAO() { return new UserHibernateDAO(); } }
HibernateConfiguration conté la configuració comú:
import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.naming.NamingException; import javax.sql.DataSource; import java.util.Properties; @Configuration @EnableTransactionManagement @Import(value = {DAOConfig.class}) public class HibernateConfiguration { @Autowired @Bean @Autowired LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setPackagesToScan("org.ioc.daw.user"); sessionFactory.setHibernateProperties(hibernateProperties()); return sessionFactory; } properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect")); properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql")); properties.put("hibernate.format_sql", environment.getRequiredProperty("hibernate.format_sql")); properties.put("hibernate.hbm2ddl.auto", environment.getRequiredProperty("hibernate.hbm2ddl")); return properties; } @Bean @Autowired public HibernateTransactionManager transactionManager(SessionFactory s) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(s); return txManager; } }
HibernateMysqlConfiguration
tindrà la configuració específica per connectar amb la BD MySQL.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; @Configuration @EnableTransactionManagement @PropertySource(value = {"application.properties"}) @Import(value = {HibernateConfiguration.class}) public class HibernateMysqlConfiguration { @Autowired @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName")); dataSource.setUrl(environment.getRequiredProperty("jdbc.url")); dataSource.setUsername(environment.getRequiredProperty("jdbc.username")); dataSource.setPassword(environment.getRequiredProperty("jdbc.password")); return dataSource; } }
I finalment, EmbeddedDatabaseTestConfig
tindrà la configuració per connectar-se a la BD en memòria H2.
@Configuration @EnableTransactionManagement @PropertySource(value = {"application-test.properties"}) @Import(value = {HibernateConfiguration.class}) public class EmbeddedDatabaseTestConfig { @Bean public DataSource dataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2).build(); return db; } }
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.
A continuació canvieu el context del test UserDAOTest
perquè utilitzi la classe de configuració de MySQL i executeu el test UserDAOTest.saveUser
.
@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.
@Id @NotNull @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") return userId; } this.userId = userId; }
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:
@Test @Transactional @Rollback(false) public void saveUser() {
Ara sí que veureu que hi ha l’usuari que el test ha creat (vegeu la figura).
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:
<dependency> <groupId>org.glassfish.main.common</groupId> <artifactId>glassfish-naming</artifactId> <version>4.1.1</version> </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.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; @Configuration @EnableTransactionManagement @PropertySource(value = {"glassfish-application.properties"}) @Import(value = {HibernateConfiguration.class}) public class GlassfishPoolConfiguration { @Autowired @Bean(name = "jndiDataSource") public DataSource dataSource() { JndiDataSourceLookup lookup = new JndiDataSourceLookup(); lookup.setResourceRef(true); return lookup.getDataSource(jndiName); } }
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.
@PropertySource(value = {"glassfish.properties","hibernate.properties" }) @Import(value = {HibernateConfiguration.class}) public class GlassfishPoolConfiguration { @PropertySource(value = {"jdbc.properties", "hibernate.properties"}) @Import(value = {HibernateConfiguration.class}) 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.
package org.ioc.daw; import javax.naming.NamingException; import org.ioc.daw.config.HibernateMysqlConfiguration; import org.junit.runners.model.InitializationError; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.mock.jndi.SimpleNamingContextBuilder; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; public class SpringJNDIRunner extends SpringJUnit4ClassRunner { public static boolean isJNDIactive; public SpringJNDIRunner(Class<?> klass) throws InitializationError, IllegalStateException, NamingException { super(klass); synchronized (SpringJNDIRunner.class) { if (!isJNDIactive) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(HibernateMysqlConfiguration.class); SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder(); builder.bind("jdbc/socioc", applicationContext.getBean("dataSource")); builder.activate(); isJNDIactive = true; } } } }
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.
@RunWith(SpringJNDIRunner.class) @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.
Començareu creant les classes que representen les preguntes i respostes (vegeu la figura).
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable; @Table(name = "answers") private static final long serialVersionUID = 1L; @Id @NotNull @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @NotNull @Size(max = 45) @Column(name = "text") return answerId; } this.answerId = answerId; } return text; } this.text = text; } } import org.ioc.daw.answer.Answer; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable; import java.util.Set; @Table(name = "questions") private static final long serialVersionUID = 1L; @Id @NotNull @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @NotNull @Size(max = 800) @Column(name = "text") @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) private Set<Answer> answers; return questionId; } this.questionId = questionId; } return text; } this.text = text; } public Set<Answer> getAnswers() { return answers; } public void setAnswers(Set<Answer> answers) { this.answers = answers; } }
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.
@OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) 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
.
@OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) private Set<Question> questions; @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) private Set<Answer> answers; public Set<Question> getQuestions() { return questions; } public void setQuestions(Set<Question> questions) { this.questions = questions; } public Set<Answer> getAnswers() { return answers; } public void setAnswers(Set<Answer> answers) { this.answers = answers; }
Un cop definides les noves entitats, heu de canviar la configuració de HibernateConfiguration
per tal que Hibernate escanegi els nous paquets.
public class HibernateConfiguration { .... @Bean @Autowired LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setPackagesToScan("org.ioc.daw.user", "org.ioc.daw.question", "org.ioc.daw.answer"); sessionFactory.setHibernateProperties(hibernateProperties()); return sessionFactory; } ....
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
.
public interface QuestionDAO { void save(Question question); Question update(Question question); } import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import javax.transaction.Transactional; @Transactional public class QuestionHibernateDAO implements QuestionDAO { @Autowired private SessionFactory sessionFactory; @Override return getSession().get(Question.class, questionId); } @Override public void save(Question question) { getSession().saveOrUpdate(question); } @Override public Question update(Question question) { return (Question) getSession().merge(question); } protected Session getSession() { return sessionFactory.getCurrentSession(); } }
I declareu el nou bean a org.ioc.daw.config.DAOConfig
.
@Bean public QuestionDAO questionDAO(){ return new QuestionHibernateDAO(); }
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.
public interface UserDAO { .... ... } public class UserDAOJPA implements UserDAO { ..... @Override try { return (User) entityManager.createQuery("select object(o) from User o " + "where o.id = :id") .setParameter("id", id) .getSingleResult(); } catch (NoResultException e) { return null; } } ..... } public class UserHibernateDAO implements UserDAO { ..... @Override return getSession().get(User.class, userId); } ..... }
public interface QuestionService { } import org.ioc.daw.answer.Answer; import org.ioc.daw.user.User; import org.ioc.daw.user.UserDAO; import org.springframework.beans.factory.annotation.Autowired; import javax.transaction.Transactional; import java.util.HashSet; import java.util.Set; @Transactional public class QuestionServiceImpl implements QuestionService { @Autowired private UserDAO userDAO; @Autowired private QuestionDAO questionDAO; @Override User user = userDAO.getById(userId); return user.getQuestions(); } @Override User user = userDAO.getById(userId); Set<Answer> userAnswers = user.getAnswers(); addAnswerToCollection(answer, userAnswers); user.setAnswers(userAnswers); userDAO.create(user); Question question = questionDAO.getById(questionId); Set<Answer> answers = question.getAnswers(); answers = addAnswerToCollection(answer, answers); question.setAnswers(answers); return questionDAO.update(question); } private Set<Answer> addAnswerToCollection(Answer answer, Set<Answer> answers) { if (answers != null) { answers.add(answer); } else { answers = new HashSet<Answer>(); answers.add(answer); } return answers; } @Override User user = userDAO.getById(userId); Set<Question> questions = user.getQuestions(); if (questions != null) { questions.add(question); } else { questions = new HashSet<>(); questions.add(question); user.setQuestions(questions); } userDAO.create(user); } }
Creeu un nou fitxer de configuracions org.ioc.daw.config.ServicesConfig i declareu el now bean.
@Configuration public class ServicesConfig { @Bean public QuestionService questionService(){ return new QuestionServiceImpl(); } @Bean public UserService userService(UserDAO userDAO) { return new UserService(userDAO); } }
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:
@Configuration public class ServicesConfig { @Bean public QuestionService questionService(UserDAO userDAO, QuestionDAO questionDAO) { return new QuestionServiceImpl(userDAO, questionDAO); } @Bean public UserService userService(UserDAO userDAO) { return new UserService(userDAO); } }
public class QuestionServiceImpl implements QuestionService { private UserDAO userDAO; private QuestionDAO questionDAO; public QuestionServiceImpl(UserDAO userDAO, QuestionDAO questionDAO) { this.userDAO = userDAO; this.questionDAO = questionDAO; }
A continuació creareu el test QuestionDAOTest
per comprovar que les preguntes es guarden correctament a la base de dades.
import org.ioc.daw.config.EmbeddedDatabaseTestConfig; import org.ioc.daw.config.ServicesConfig; import org.ioc.daw.user.User; import org.ioc.daw.user.UserDAO; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.sql.Timestamp; import java.util.Date; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {ServicesConfig.class, EmbeddedDatabaseTestConfig.class}) public class QuestionDAOTest { @Autowired private QuestionService questionService; @Autowired private UserDAO userDAO; @Test public void createQuestion() { User user = new User(); user.setUsername("test"); user.setActive(true); user.setEmail("email@test.com"); user.setPassword("password"); user.setName("name"); user.setRank(10); userDAO.create(user); Question question = new Question(); question.setText("This is a question"); questionService.create(question, user.getUserId()); assertNotNull(question.getQuestionId()); Set<Question> questions = questionService.getAllQuestions(user.getUserId()); assertEquals(1, questions.size()); } }
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.
@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.
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”.
Amb les noves taules, les dades que relacionen preguntes i usuaris quedarien tal com es pot veure en la taula i la taula.
user_id | question_id |
---|---|
2 | 1 |
2 | 2 |
4 | 3 |
2 | 4 |
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.
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.
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.
@Test public void addAnswer() { User user = new User(); user.setUsername("test"); user.setActive(true); user.setEmail("email@test.com"); user.setPassword("password"); user.setName("name"); userDAO.create(user); Question question = new Question(); question.setText("This is a question"); questionService.create(question, user.getUserId()); Answer answer = new Answer(); answer.setText("This is an answer"); question = questionService.addAnswer(answer, question.getQuestionId(), user.getUserId()); assertEquals(1, question.getAnswers().size()); }
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
.
@Configuration @EnableTransactionManagement @Import(value = {ServiceConfig.class}) public class SpringTestConfig { @Bean public UserDAO userDAO() { return Mockito.mock(UserDAO.class); } @Bean public QuestionDAO questionDAO() { return Mockito.mock(QuestionDAO.class); } @Bean public PlatformTransactionManager transactionManager() { return Mockito.mock(PlatformTransactionManager.class); } }
Un cop la configuració dels beans està preparada, ja podeu escriure el test.
import org.ioc.daw.answer.Answer; import org.ioc.daw.config.SpringTestConfig; import org.ioc.daw.user.User; import org.ioc.daw.user.UserDAO; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {SpringTestConfig.class}) public class QuestionServiceTest { @Autowired private UserDAO userDAOMock; @Autowired private QuestionDAO questionDAOMock; @Autowired private QuestionService questionService; @Test public void getAllQUestions() { Question question1 = getDummyQuestion(1); Set<Question> questions = new HashSet<>(); questions.add(question1); int userId = 1; User userMock = Mockito.mock(User.class); when(userMock.getQuestions()).thenReturn(questions); when(userDAOMock.getById(userId)).thenReturn(userMock); Set<Question> questionsResponse = questionService.getAllQuestions(userId); assertEquals(1, questionsResponse.size()); } @Test public void addTheFirstAnswerToAQuestion() { int userID = 1; int questionId = 1; int answerId = 1; Answer answer = getDummyAnswer(answerId); Question question1 = getDummyQuestion(questionId); User user1 = getDummyUser(userID); when(userDAOMock.getById(userID)).thenReturn(user1); when(questionDAOMock.getById(questionId)).thenReturn(question1); when(questionDAOMock.update(question1)).thenReturn(question1); Question questionResponse = questionService.addAnswer(answer, questionId, userID); assertEquals(1, questionResponse.getAnswers().size()); verify(userDAOMock, Mockito.times(1)).create(user1); } @Test public void addTheAnswerToAQuestionOnceItHasSomeAnswers() { int questionId = 1; int userID = 1; Answer answer1 = getDummyAnswer(1); Answer answer2 = getDummyAnswer(2); Question question1 = getDummyQuestion(questionId); Set<Answer> answers = new HashSet<>(); answers.add(answer1); question1.setAnswers(answers); List<Question> questions = new ArrayList<>(); questions.add(question1); User user1 = getDummyUser(userID); when(userDAOMock.getById(userID)).thenReturn(user1); when(questionDAOMock.getById(questionId)).thenReturn(question1); when(questionDAOMock.update(question1)).thenReturn(question1); questionService.addAnswer(answer2, questionId, userID); verify(userDAOMock, Mockito.times(1)).create(user1); assertEquals(2, answers.size()); } @Test public void createFirstUserQuestion() { int userId = 1; Question question = getDummyQuestion(1); User user = getDummyUser(1); when(userDAOMock.getById(userId)).thenReturn(user); questionService.create(question, userId); verify(userDAOMock, timeout(1)).create(user); assertEquals(1, user.getQuestions().size()); } @Test public void createUserQuestionWhenItsNotTheFirstOne() { int userId = 1; Question question1 = getDummyQuestion(1); Question question2 = getDummyQuestion(2); User user = getDummyUser(1); Set<Question> questions = new HashSet<>(); questions.add(question1); user.setQuestions(questions); when(userDAOMock.getById(userId)).thenReturn(user); questionService.create(question2, userId); verify(userDAOMock, timeout(1)).create(user); assertEquals(2, questions.size()); } private Question getDummyQuestion(int questionId) { Question question1 = new Question(); question1.setQuestionId(questionId); question1.setText("Some question"); Set<Question> questions = new HashSet<>(); questions.add(question1); return question1; } private Answer getDummyAnswer(int answerId) { Answer answer = new Answer(); answer.setAnswerId(answerId); answer.setText("This is an answer"); return answer; } private User getDummyUser(int userId) { User user = new User(); user.setUsername(username); user.setUserId(userId); return user; } }
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.
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.
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.NotNull; import java.io.Serializable; @Table(name = "ranks") private static final long serialVersionUID = 1L; @Id @NotNull @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @Column(name = "positive_votes") private int positive; @Column(name = "negative_votes") private int negative; @Column(name = "total") private int total; public int getPositive() { return positive; } public void setPositive(int positive) { this.positive = positive; this.total = positive - getNegative(); } public int getNegative() { return negative; } public void setNegative(int negative) { this.negative = negative; this.total = getPositive() - negative; } return rankId; } this.rankId = rankId; } public int getTotal() { return total; } public void setTotal(int total) { this.total = total; } }
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.
@OneToOne(cascade = {CascadeType.ALL}) private Rank rank; public Rank getRank() { return rank; } public void setRank(Rank rank) { this.rank = rank; }
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.
@Override public User findUserWithHighestRank() { Criteria criteria = createEntityCriteria(); criteria.addOrder(Order.desc("rank")); return (User) criteria.uniqueResult(); }
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
- @Override
- public User findUserWithHighestRank() {
- CriteriaBuilder cb = createCriteriaBuilder();
- CriteriaQuery<User> criteriaQuery = cb.createQuery(User.class);
- Root<User> root = criteriaQuery.from(User.class);
- criteriaQuery.select(root);
- criteriaQuery.orderBy(cb.desc(root.join("rank").get("total")));
- EntityManager em = createEntityManager();
- return em.createQuery(criteriaQuery).setMaxResults(1).getSingleResult();
- }
- private CriteriaBuilder createCriteriaBuilder() {
- return getSession().getEntityManagerFactory().getCriteriaBuilder();
- }
- private EntityManager createEntityManager() {
- return getSession().getEntityManagerFactory().createEntityManager();
- }
A continuació creeu la classe org.ioc.daw.vote.Vote
.
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.NotNull; import java.io.Serializable; @Table(name = "votes") private static final long serialVersionUID = 1L; @Id @NotNull @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @Column(name = "vote") return voteId; } this.voteId = voteId; } return vote; } this.vote = vote; } }
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
.
.... @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) private Set<Vote> votes; public Set<Vote> getVotes() { return votes; } public void setVotes(Set<Vote> votes) { this.votes = votes; } .... } public interface AnswerDAO { void save(Answer question); } @Transactional public class AnswerHibernateDAO implements AnswerDAO { @Autowired private SessionFactory sessionFactory; @Override return getSession().get(Answer.class, questionId); } @Override public void save(Answer answer) { getSession().saveOrUpdate(answer); } protected Session getSession() { return sessionFactory.getCurrentSession(); } } @Configuration public class DAOConfig { ....... @Bean public AnswerDAO answerDAO(){ return new AnswerHibernateDAO(); } ....... }
Un usuari votarà, així que heu d’afegir la relació dels usuaris amb els vots.
.... @OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) private Set<Vote> votes; public Set<Vote> getVotes() { return votes; } public void setVotes(Set<Vote> votes) { this.votes = votes; } ....
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.
public interface VoteService { } import org.ioc.daw.answer.Answer; import org.ioc.daw.answer.AnswerDAO; import javax.transaction.Transactional; import java.util.HashSet; import java.util.Set; @Transactional public class VoteServiceImpl implements VoteService { private AnswerDAO answerDAO; public VoteServiceImpl(AnswerDAO answerDAO, UserDAO userDAO){ this.answerDAO = answerDAO; this.userDAO = userDAO; } @Override vote(answerId, userId, true); } @Override vote(answerId, userId, false); } User user = userDAO.getById(userId); Set<Vote> userVotes = user.getVotes(); Vote vote = new Vote(); vote.setVote(value); userVotes = getVotes(vote, userVotes); user.setVotes(userVotes); userDAO.create(user); Answer answer = answerDAO.getById(answerId); Set<Vote> votes = answer.getVotes(); votes = getVotes(vote, votes); answer.setVotes(votes); answerDAO.save(answer); return vote; } private Set<Vote> getVotes(Vote vote, Set<Vote> votes) { if (votes != null) { votes.add(vote); } else { votes = new HashSet<Vote>(); votes.add(vote); } return votes; }
No us heu d’oblidar d’afegir VoteService
a ServiceConfig
per tal que Spring creï el bean.
@Bean public VoteService voteService(AnswerDAO answerDAO, UserDAO userDAO){ return new VoteServiceImpl(answerDAO, userDAO); }
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.
import org.ioc.daw.answer.Answer; import org.ioc.daw.answer.AnswerDAO; import org.ioc.daw.config.EmbeddedDatabaseTestConfig; import org.ioc.daw.config.ServicesConfig; import org.ioc.daw.question.Question; import org.ioc.daw.question.QuestionService; import org.ioc.daw.user.User; import org.ioc.daw.user.UserDAO; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.sql.Timestamp; import java.util.Date; import static org.junit.Assert.assertEquals; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {ServicesConfig.class, EmbeddedDatabaseTestConfig.class}) public class VoteDAOTest { @Autowired private QuestionService questionService; @Autowired private UserDAO userDAO; @Autowired private AnswerDAO answerDAO; @Autowired private VoteService voteService; @Test public void votePositive() { User user1 = getUser("test", "test@email.com"); User user2 = getUser("test1", "test1@email.com"); User user3 = getUser("test2", "test2@email.com"); userDAO.create(user1); userDAO.create(user2); userDAO.create(user3); Question question = new Question(); question.setText("This is a question"); questionService.create(question, user1.getUserId()); Answer answer = new Answer(); answer.setText("This is an answer"); question = questionService.addAnswer(answer, question.getQuestionId(), user2.getUserId()); int answerId = question.getAnswers().iterator().next().getAnswerId(); voteService.votePositive(user3.getUserId(), answerId); User userDB = userDAO.getById(user3.getUserId()); Answer answerDB = answerDAO.getById(answerId); assertEquals(1, userDB.getVotes().size()); assertEquals(1, answerDB.getVotes().size()); assertEquals(userDB.getVotes().iterator().next().getVoteId(), answerDB.getVotes().iterator().next().getVoteId()); } User user = new User(); user.setUsername(username); user.setActive(true); user.setEmail(email); user.setPassword("password"); user.setName("name"); return user; } }
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.