Canigó - Servei de persistència 2.3.x
SERVEI DE PERSISTÈNCIA
IntroduccióPropòsitEl Servei de Persistència permet persistir i recuperar dades entre l'aplicatiu i el motor de base de dades. La base tecnològica està basada en Spring i Hibernate, i entre d'altres característiques ofereix:
Context i Escenaris d'ÚsEl Servei de Persistència es troba dins dels serveis de Propòsit General de Canigó.
Versions i DependènciesEn el present apartat es mostren quines són les versions i dependències necessàries per fer ús del Servei. Dependències BàsiquesLes dependències descrites a la següent url són requerides per tal de compilar i fer funcionar el projecte: Cal destacar que dintre de la dependència "Hibernate" s'inclouen totes les llibreries necessàries per al funcionament correcte d'aquesta capa de persistència. Per a veure quines són es pot consultar la web http://www.hibernate.org A qui va dirigitAquest document va dirigit als següents perfils:
Documents i Fonts de Referència
GlossariDAO POJO BO VO Descripció DetalladaArquitectura i ComponentsExisteixen tres tipus de components. Podem classificar-los en:
Es pot trobar tota la documentació JavaDoc i el codi font referent aquests components a les següents urls: JavaDoc: http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-persistence/apidocs/index.html Instal.lació i ConfiguracióInstal.lacióLa instal.lació del servei requereix de la utilització de la llibreria 'canigo-services-persistence' i les dependències indicades a l'apartat 'Introducció-Versions i Dependències'. ConfiguracióLa configuració del servei implica:
Definició del servei i de les seves dependències Fitxer de configuració: canigo-services-persistence.xml
Atributs:
També és necessari configurar les següents propietats:
Exemple:
... <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="configLocation" value="classpath:$\{sessionFactory.configLocation\}" /> <property name="hibernateProperties"> <props> <prop key="hibernate.connection.datasource">$\{dataSource.jndiName\}</prop> </props> </property> </bean> ... ![]() Fitxer de configuració: canigo-services-persistence.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring Delegat transaccional Atributs: || Atribut || Requerit || Descripció ||
També és necessari configurar les següents propietats:
Exemple:
... <!-- Transaction manager for a single Hibernate SessionFactory --> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> ... ![]() Fitxer de configuració: canigo-services-persistence.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring Proxy per a la definició declarativa de la transaccionalitat dels nostres DAO's. Servirà de bean "pare" d'altres beans. Atributs: || Atribut || Requerit || Descripció ||
També és necessari configurar les següents propietats:
Exemple:
... <bean id="baseDaoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref local="transactionManager" /> </property> <property name="target"> <bean class="java.lang.Object" /> </property> <property name="transactionAttributes"> <props> <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop> <prop key="save*">PROPAGATION_REQUIRED</prop> <prop key="delete*">PROPAGATION_REQUIRED</prop> </props> </property> </bean> ... Enllaç d'una acció amb el seu DAO ![]() Fitxer de configuració: action-servlet-XXX.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring Cada acció que implementi una búsqueda necessita la definició d'un bean que hereti del bean "pare" del servei i que tingui els següents atributs: Atributs: || Atribut || Requerit || Descripció ||
A més és necessari configurar les següents propietats
Exemple:
... <property name="dao" > <bean parent="baseDaoProxy"> <property name="target" ref="productDaoTarget"/> </bean> </property> ... ![]() Fitxer de configuració: action-servlet-XXX.xml Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring Cada acció que implementi una cerca necessita la definició d'un bean que hereti de la implementació DAO "pare" del servei (Spring amb Hibernate) i que tingui els següents atributs: Atributs: || Atribut || Requerit || Descripció ||
A més és necessari configurar les següents propietats
Exemple:
... <bean id="productDaoTarget" class="net.gencat.ctti.canigo.samples.jpetstore.model.dao. hibernate.impl.HibernateProductDAOImpl"> <property name="sessionFactory" ref="sessionFactory" /> </bean> ... Utilització del ServeiActorsEn un escenari típic d'utilització de Canigó hi ha aspectes importants a considerar. En concret són els derivats de la relació dels nostres DAO's amb les 'BeanFactory' __que proporciona Spring i les 'SessionFactory' que proporciona Hibernate. Aquests aspectes són bàsicament tres: 1. D'on treiem les instàncies dels DAO's. Una vegada tenim els DAO's ja els podem començar a utilitzar a qualsevol classe. Se'ns ocòrrer, per exemple, una classe 'Action' d'Struts a on tinguem una variable d'instància que sigui un dels nostres DAO's i que guardi un objecte de negoci
... public class CategoryAction extends ActionExtendedSupport { private CategoryDAO dao = null; ... public ActionForward save(Category vo, StrutsContext context) { ... dao.saveOrUpdate(vo); ... return context.getActionMapping().findForward("success"); } ... } A l'exemple, la instància del CategoryDAO no l'hem creat nosaltres si no que és Spring qui la injecta al nostre 'Action'. Això és aplicable sempre i és l'escenari correcte d'utilització dels DAO's, és a dir, no serem nosaltres qui instanciaran els DAO's si no que deixarem a Spring aquesta tasca mitjançant els fitxers de configuració (típicament applicationContext.xml; a l'apartat Configuració s'explica com fer-ho). La següent figura mostra la jerarquia de dependències dels 'beans' d'Spring per aquest cas senzill (sense introduir cap concepte de transaccionalitat): 2. Com es relacionen els nostres DAO's amb la 'SessionFactory' d'Hibernate que han de fer servir. A la figura anterior veiem que el nostre DAO es relaciona amb la 'SessionFactory' d'Hibernate a partir de la propietat 'sessionFactory'. Això es així perquè els nostres DAO's hereten tots de 'net.gencat.ctti.canigo.services.persistence.spring.dao.impl.SpringHibernateDAOImpl'. Aquesta herència la definim a nivell de configuració de projecte d'Hibernate Synchronizer. A l'apartat Configuració s'explica com. ![]() 3. Com saben els nostres DAO's les característiques de transaccionalitat dels seus mètodes. Aquest punt requereix un apartat separat que analitzem a continuació. TransaccionalitatL'escenari plantejat en l'apartat anterior es correspon amb un exemple senzill d'utilització d'un DAO qualsevol sense introduir en cap moment el concepte de transaccionalitat. Això no es correspon amb una aplicació complexe, on les operacions de base de dades es realitzen en bloc (transacció) i on el resultat pot ser que volguem fer enrera tota la transacció si es produeix un error. <bean id="categoryBOTarget" class="net.gencat.ctti.canigo.samples.prototip.model.bo.impl.CategoryBOImpl"> <property name="dao" ref="universalHibernateDAO"/> </bean> <bean name="/categories" class="net.gencat.ctti.canigo.samples.prototip.struts.action.CategoryAction"> ... <property name="bo" > <bean parent="baseDaoProxy"> <property name="target"> <ref bean="categoryBOTarget"/> </property> </bean> </property> ... </bean> El primer que destaca és l'aparició en escena d'un 'categoryDaoProxy' i un 'transactionManager'. El que abans era el 'categoryDao' ara és el 'categoryDaoTarget'. Ambdós beans són classes que ens proporciona Spring. L'explicació de com configurar-les es troba a l'apartat Configuració. La idea que hi ha darrere de tot això és ben senzilla. En lloc d'utilitzar directament el nostre DAO, utilitzem un proxy que ens intercepta les crides als nostres métodes i que afegeix la transaccionalitat amb el manager que li indiquem, en aquest cas un manager d'Hibernate que fa ús de la 'sessionFactory' configurada. A nivell de codi tot queda igual, és a dir, farem ús de les nostres interfícies DAO però per sota s'apliquen els conceptes necessaris per garantir la coherència de les nostres dades. Tot això és possible perquè és Spring qui ens proporciona les instàncies de les implementacions dels nostres DAO's, tal i com s'ha remarcat en l'apartat anterior. Important! Amb la transaccionalitat d'Hibernate activada només es fa rollback al llançar una UncheckedException. En el cas que també es vulgui fer rollback amb les CheckedExceptions cal configurar-ho manualment afegint el següent codi "-java.lang.Throwable" al fitxer de configuració canigo-service-persistence.xml: ... <property name="transactionAttributes"> <props> <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop> <prop key="save*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop> <prop key="add*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop> <prop key="delete*">PROPAGATION_REQUIRED,-java.lang.Throwable</prop> </props> </property> ... Eines de SuportHibernate ToolsA l'hora de configurar la persistència, recomanem la utilització d'Hibernate Tools, que consisteix en un conjunt de plugins per a Eclipse que faciliten el treball amb Hibernate (mappings, HQL, ...). En aquest enllaç podeu trobar tota informació al respecte: http://www.hibernate.org/255.html Us podeu descarregar l'eina aquí La utilització del servei de dades comporta el coneixement dels conceptes que es maneguen i els passos a seguir per a poder persistir les dades, més enllà de les classes disponibles en el propi framework (amb el suport de l'eina Hibernate Tools). Els passos a seguir en aquest ordre són:
A continuació es detalla cadascun dels passos a seguir per a completar amb èxit una operació de persistència: El primer que s'ha de fer és definir les propietats necessàries per al motor de persistència que farem servir: Hibernate 3. Típicament aquestes propietats es defineixen en un fitxer que es diu 'hibernate.cfg.xml'.
Cliquem a 'Finish' i ja tenim el fitxer 'hibernate.cfg.xml' al directori que hem indicat:
![]() Aquest és només el primer pas en la configuració del nostre motor de persistència. Realment falta afegir la informació rellevant: els 'mappings' dels nostres objectes de negoci. Cada taula presenta (en la majoria de casos) un objecte de negoci i té associat un fitxer de mapping. Aquest fitxer es diu igual que el nom de la taula (però amb la convenció Java) i té extensió 'hbm.xml'. Anem a crear doncs aquests fitxers. En el menú 'File/New/Other' seleccionem 'Hibernate/Hibernate Mapping File'. Aquesta opció està disponible gràcies a què ens em instal.lat l'Hibernate Tools. Se'ns obrirà una finestra on hem d'informar els següents camps:
Hibernate Tools també ens ofereix la possibilitat de generar codi automàticament. Podeu consultar el seu funcionament descarregant-vos aquest fitxer. Ara tenim, d'una banda els fitxers de mapeig i per l'altra el fitxer de configuració d'hibernate. Però com els lliguem? És tan fàcil com afegir els mappings al fitxer 'hibernate.cfg.xml' (Hibernate Tools dona suport a aquesta tasca)
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> ... <mapping resource="test/Category.hbm.xml" /> <mapping resource="test/Item.hbm.xml" /> <mapping resource="test/Product.hbm.xml" /> <mapping resource="test/Supplier.hbm.xml" /> </session-factory> </hibernate-configuration> El següent pas en l'utilització del servei de dades és crear les següents classes:
Per a aquesta creació seleccionem els fitxers de mapeig i en el menú contextual seleccionem 'Hibernate Synchronizer/Sincronize Files': Si tot va bé apareixan cinc directoris i l'estructura dels mateixos ha de ser alguna cosa similar a la figura següent:
Qualsevol canvi a la base de dades només implicaria una resincronització dels fitxers de mapeig per tornar a generar les classes implicades amb l'opció 'Synchronize And Overwrite'. S'ha de tenir en compte que una resincronització fa que es sobreescriguin els fitxers que ja tenim amb els nous. ExemplesTest unitarisCom a exemple d'utilització implementarem un test unitari que faci recull de tots els conceptes implicats. Resumint, necessitem:
Partim doncs d'una base de dades Hypersonic senzilla amb dues taules: CATEGORY i PRODUCT i una relació '1..n' entre Category i Product. També obviem la part de configuració de l'Hibernate Sync. ja que s'explica en detall en apartats posteriors.
A l'exemple anterior cal destacar:
i afegim un gestor transaccional sobre aquest objecte per indicar que volem una transacció sobre el métode que inicia la transacció i llença totes les operacions de base de dades:
<bean id="multiSaveProxy" class="org.springframework.transaction.interceptor. TransactionProxyFactoryBean"> <property name="transactionManager"> <ref local="transactionManager" /> </property> <property name="target"> <ref local="multiSaveTarget" /> </property> <property name="transactionAttributes"> <props> <prop key="save*">PROPAGATION_REQUIRED</prop> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> </props> </property> </bean>
i afegir un altre cas de test a la nostra classe DAOTest:
... public class DAOTest extends TestCase { ... public void testMultipleSave(){ /** * Request a the 'multisave object' */ MultiSave multiSave = (MultiSave)factory.getBean("multiSaveProxy"); ArrayList list = new ArrayList(); for(int i=0;i<3;i++)\{ /** * Request a new category instance */ Category newCategory = (Category)factory.getBean("category"); newCategory.setName("name at "+System.currentTimeMillis()); newCategory.setDescn("descn at "+System.currentTimeMillis()); list.add(newCategory); } try { multiSave.saveMultiple(list); } catch(PersistenceServiceException ex){ log.warn("Se ha producido un error: "+ex.getLocalizedMessage()); } Iterator it = list.iterator(); while(it.hasNext()){ Category c = (Category)it.next(); if(c.getId()!=null){ log.debug("Loading category with ID="+c.getId()); Category cLoaded = multiSave*.loadCategory( c.getId() ); log.debug("Loaded from BD a category "cLoaded" with Id="c.getId()"?"+(cLoaded!=null)); assertTrue("The category has been saved into DB and should not!",cLoaded==null); } } log.debug("End."); } ... } Si ens fixem, en el mètode saveMultiple( ... ) el que estem fent és provocar un error a l'hora de guardar la última Category, ja que estem cridant a update( ... ) quan la instància és nova i hauríem de cridar a save( ... ). Això provocarà un 'rollback' de totes les instàncies previament guardades. Com ho comprovem? Posteriorment en el codi intentem fem un load( ... ) per veure si s'ha desat algun registre. Això mateix ho podem mirar veient els logs generats: DEBUG [main] org.hibernate.transaction.JDBCTransaction - begin DEBUG [main] org.hibernate.transaction.JDBCTransaction - current autocommit status: false DEBUG [main] com.myapp.test.MultiSave - Trying to save category... DEBUG [main] com.myapp.test.MultiSave - Category saved with id 1 DEBUG [main] com.myapp.test.MultiSave - Trying to save category... DEBUG [main] com.myapp.test.MultiSave - Category saved with id 2 DEBUG [main] com.myapp.test.MultiSave - Trying to save category... DEBUG [main] com.myapp.test.MultiSave - Persistence error will occur... DEBUG [main] org.hibernate.transaction.JDBCTransaction - rollback DEBUG [main] org.hibernate.transaction.JDBCTransaction - rolled back JDBC Connection WARN [main] com.myapp.test.DAOTest - Se ha producido un error: org.springframework.dao.InvalidDataAccessApiUsageException: The given object has a null identifier: com.myapp.model.Category; nested exception is org.hibernate.TransientObjectException: The given object has a null identifier: com.myapp.model.Category DEBUG [main] com.myapp.test.DAOTest - Loading category with ID=1 DEBUG [main] org.hibernate.transaction.JDBCTransaction - begin DEBUG [main] org.hibernate.transaction.JDBCTransaction - current autocommit status: false DEBUG [main] org.hibernate.transaction.JDBCTransaction - commit DEBUG [main] org.hibernate.transaction.JDBCTransaction - committed JDBC Connection DEBUG [main] com.myapp.test.DAOTest - Loaded from BD a category null with Id=1?false DEBUG [main] com.myapp.test.DAOTest - Loading category with ID=2 DEBUG [main] org.hibernate.transaction.JDBCTransaction - begin DEBUG [main] org.hibernate.transaction.JDBCTransaction - current autocommit status: false DEBUG [main] org.hibernate.transaction.JDBCTransaction - commit DEBUG [main] org.hibernate.transaction.JDBCTransaction - committed JDBC Connection DEBUG [main] com.myapp.test.DAOTest - Loaded from BD a category null with Id=2?false DEBUG [main] com.myapp.test.DAOTest - End. La seqüència és la següent:
|