ItEr45S02AnaliseItEr44S02: Include git and development guide in doc

Includes git development guide and development guide in restructured
document.

Makes compile this document and includes logo image.
This commit is contained in:
Jose Maria Casanova Crespo 2010-01-26 22:49:33 +01:00
parent 02b9dadabe
commit 9f837198df
4 changed files with 954 additions and 2 deletions

View file

@ -1,2 +1,196 @@
Guía de Desenvolvemento con GIT
###############################
Guia de uso de GIT no proxecto de Xestión da Producción
#######################################################
Para a utilización do sistema de xestión da configuración distribuida GIT e recomendable instalar os seguinte paquetes nas distribicións Debian GNU/Linux e Ubuntu.
::
$sudo apt-get install git-core git-email giggle
Esta guía detalla o uso que deberá realizar o desenvolvedor que quere participar no proxecto a hora de remitir os parches e como organizarse para traballar, tamén se detalla a forma de traballar do integrador dos parches que será de utilidades para quen quera incorporar parches remitidos por outros desenvolvedores na súa versión do código fonte.
Perfil do desenvolvedor
=======================
O desenvolvedor traballará nun repositorio git local e non ten dereito de escritura sobre o repositorio git central do proxecto. No repositorio de referencia so poderá aplicar parches os integradores do proxecto. Unha vez que un desenvolvedor implemente unha funcionalidade no software deberá crear un parche e envialo a lista de desenvolvemento do proxecto. Para a súa revisión e subida a rama principal do proxecto (*master*).
Para comezar a traballar o primerio que terá que facer o desenvolvedor e descargar o repositorio GIT que se atopa accesible de forma anónima por http mediante o seguinte comando:
::
$ git clone http://git.igalia.com/xestion-producion.git
Esto creará unha copia local de trabajo, para empezar a trabajar se recomienda la creación de una rama para cada caso de uso a desarrollar o nombre de la rama é recomendable que teña realación coa funcionalidade que implemente ou o número de bug que trata de solucionar, e logo nos movemos a traballar na rama cun checkout.
::
$ git branch NomeDaRama
$ git checkout NomeDaRama
Unha vez dentro da rama, a medida que se vai avanzando coa implementación ou ao terminar coa implementación faise o commit do traballo feito. Debe terse en conta que as modificacións deben ter unha unidade de obxectivo e funcións.
::
$ git add <ficheiros novos e modificados>
$ git commit
E importate ter o habito de crear mensaxes de commit de calidad xa que permite facilitar o traballo aos intergradores. Como regla xleneral as mensaxes deben comezar cunha liña de non mais de 50 caracteres que describe o cambio de forma concisa, esta debe estar seguida dunha liña en blanco, seguida dunha explicación detallada. E recomendabe que a explicación detallada inclua a motivación do campo e contraste a implementación coa anterior comportamento. Outra recomedación e empregar o tempso verbais en presente. O emprego de puntos e secuencias é correcto.
A primeira liña da mensaxe dará lugar al nombre del fichero del parche.
Previamente a enviar o parche ao integrador, debemos comprobar o seu funcionamento coa última versión no upstream do master, polo que teremos que actualizar a última copia.
::
$ git checkout master
$ git pull
Neste momento coa última versión do master pasamos a facer un checkout a rama para facer un rebase.
::
git checkout NomeDaRamaFEA
git rebase master
Aquí temos un exemplo da mezcla e como aparece un conflicto:
::
chema@rohan:~/git/xestion-producion$ git status
# On branch master
nothing to commit (working directory clean)
chema@rohan:~/git/xestion-producion$ git co NomeDaRamaFEA
Switched to branch "NomeDaRamaFEA"
chema@rohan:~/git/xestion-producion$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Nova version sobre os conflictos
error: patch failed: HOWTO.GIT:1
error: HOWTO.GIT: patch does not apply
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merged HOWTO.GIT
CONFLICT (content): Merge conflict in HOWTO.GIT
Failed to merge in the changes.
Patch failed at 0001.
When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".
chema@rohan:~/git/xestion-producion$ git rebase --continue
You must edit all merge conflicts and then
mark them as resolved using git add
Como podese ver para solucionar os conflictos e preciso revisar os ficheiros que teñen conflicto e unha vez solucionado faise un add cos conflictos solucionados. E logo continuar co git rebase --continue , se é mellor abortar podese empregar o git rebase --abort.
Existe a opción de facer un git rebase -i que permite a realización do rebase de forma interactiva permitindo fusionar distintos commits en un ou otras operacións.
Para crear o parche unha vez rematado o rebase procedemos da seguinte forma, empregando o gomando git format-patch:
::
$ git format-patch master
Este comando crea os ficheiros .patch un por cada un dos commits.
Está e unha sintaxe máis completa para a creación dos parches do commits da rama en curso. O parametro --cover-letter crea unha portada coma os dos faxes que pode ser logo utilizada no envío dos mails unha vez editada, este comando só funciona a partires da versión 1.5 de git. Este comando xerará un directorio patches no que creará os emails para formatear
::
$ git format-patch -n --attach --thread --cover-letter -o patches/ master
Para facilitar o envío de parches por email e recomendable instalar o paquete git-email
::
$ apt-get install git-email
Para configurar os comandos no arquivo .git/config coa información do seu proveedor a internet.
::
[user]
email = direccion@desenvolvedor.com
name = Nome Desenvolvedor
[sendemail]
to = xestion-producion-patches@igalia.com
smtpserver = <smtp-servidor>
smtpencryption = tls
smtpserverport = 587
smtpuser = usuario
smtppass = <a contraseña do smtp>
Para enviar os parches xerados que se atopan nun directorio previamente xerados co comando format-patch podemos utilizar:
::
$ git send-email patches/
que enviará por email tódolos parches a lista definida que é o to definido por defecto.
Se queremos enviar un parche a outro desenvolvedor en particular empregaremos o parámetro --to:
::
$ git send-email --to desenvolvedor2@email.com patches/
O desenvolvedor poderá borrar a rama cando o integrador lle confirme que o parche foi aplicado correctamente.
O perfil de integrador
======================
A diferencia do resto dos usuarios debería descargar o repositorio da seguinte forma, tendo os permisos adecuados no repositorio:
::
$ git clone https://git.igalia.com/xestion-producion.git
Esta forma de acceso permite un acceso de escritura ao repositorio:
Sobre o repositorio debería facer o seguinte para aplicar un parche, primeiro deberá descargar os parches a un ficheiro con formato mailbox co email completo enviado polo desenvolvedor.
O recomendable sería crear unha nova rama para probar o parche:
::
$ git branch test-patch-xxxx
$ git checkout test-patch-xxxx
$ git am ficheiro.mbox
Se se quere aplicar un parche en formato .patch simplemente tense que executar o seguinte comando,
::
$ git apply ficheiro.patch
A diferencia de git am, git apply non manten a identidade do commiter orixinal, polo que e recomendable empregar o git am para a importación dos commits.
Se o parche funciona debería procederse a aplicalo no master. E borraríase a rama temporal de aplicación. E se publicaría cun push.
::
$ git rebase master
$ git checkout master
$ git merge test-patch-xxxx
$ git branch -d test-patch-xxxx
$ git push
Logo o integrador deberá responder a lista confirmando a aplicación do parche.
Trucos con GIT
==============
Cando estase resolvendo conflictos podense ver os distintos pais do merge. Empregar =git show :1:filename= para ver o ancestro comun, =git show :2:filename= para ver a version local, =git show :3:filename= para ver a version remota. Para escoller unha das versions empregar =git checkout --ours file= e =git checkout --theirs file=. (Solo en git 1.6.1 e posteriores)
Podese usar un =git mergetool= para resolver os conflictos empregando unha ferramenta visual.

View file

@ -1,2 +1,760 @@
Guía de Desenvolvemento
#######################
Introdución
===========
Arquitectura Model-View-Controller
==================================
Capa de lóxica de negocio
=========================
* FIXME[¿Recomendar usar sempre tipo de acceso property para as propiedades persistentes das entidades? - Importante para id (para que getId non inicialice o proxy) e version (para poder definir getVersion da maneira que requiren os reattachments en pasos intermedios)]
* FIXME[¿recomendar que setId e setVersion sexan privados, para que só Hibernate lles poida dar valor?]
* FIXME[Comenta-lo uso de DTOs]
* FIXME[Insistir en que na realización dos métodos dos DAOs (xenérico e concretos) é preciso facer try-catch para converti-las excepcións de runtime de Hibernate á xerarquía de Spring]
Capa Web
========
Conversacións
=============
Conceptos xerais
----------------
Unha **conversación** representa un caso de uso no que o usuario emprega unha serie de **pasos* para editar/crear un ou varios obxectos persistentes, e finalmente executa un paso para confirmar ou cancela-los cambios. Como exemplo de conversación, **podemos** considera-la edición/creación dos datos dun traballador, na que o usuario emprega unha serie de pasos para modifica-la información básica do traballador (e.g. nome) e modifica/elimina/engade datos da súa historia laboral (e.g. data de contratación) ou das súas localizacións de traballo.
Dende o punto de vista dun usuario final, unha conversación ten que cumprir dúas propiedades:
* **Detección de información obsoleta**. Mentres o usuario está a editar un obxecto persistente, outro usuario concorrentemente pode estar a editar ese mesmo obxecto. Nesta situación, se non se toma ningunha medida, o usuario que confirme máis tarde, sobrescribe os cambios feitos polo primeiro. A detección de información obsoleta ten que acadarse como mínimo no paso de confirmación.
* **Atomicidade**. Se o usuario elixe cancela-los cambios, ningún deles se debe materializar na base de datos. Se elixe confirmalos, todos eles se deben materializar na base de datos. A maneira máis sinxela e práctica de acadar esta semántica consiste en que só se modifique a base de datos no paso de confirmación.
Dende o punto de vista do desenvolvedor, a realización dunha conversación:
* Ten que garanti-la semántica (detección de información obsoleta e atomicidade) esperada polo usuario.
* Contén como **estado** os datos que o usuario está a editar ou crear.
* Normalmente dispón dun conxunto de operacións (pasos), que se poden invocar de acordo o seguinte **protocolo**:
# Paso inicial: unha operación (de varias posibles) para inicia-la conversación (e.g. carga-lo obxecto persistente ou crear unha versión inicial, dependendo de se edita ou se crea).
# Pasos intermedios: un conxunto de operacións que manipulan o estado.
# Paso final: unha operación (de varias posibles) para remata-la conversación, normalmente: confirmación (para materializa-los cambios na base de datos) ou cancelación (para descarta-los cambios).
Conceptos específicos a Hibernate
---------------------------------
Existen varias estratexias para realizar conversacións con Hibernate: *session-per-request-withdetached-objects*, *session-per-conversation* e extende-lo contexto de persistencia con JPA (con ou sen EJB 3.0). No contexto do proxecto, a opción mais viable é a estratexia *session-per-requestwith-detached-objects*, que se basea en modela-lo estado da conversación como obxectos en estado *detached*. Esta estratexia é a que se asumirá no resto do documento.
Para logra-la propiedade de detección de información obsoleta, as entidades que conforman o estado da conversación empregarán o campo *version*, de tipo *long/Long*. A `figura 1`_ amosa a definición do campo *version* na entidade *Resource*. A `figura 2`_ amosa a especificación do campo *version* no ficheiro de mapping (o tipo de acceso por defecto as propiedades persistentes é *property*). O método *setVersion* definiuse como privado para que só Hibernate o poida invocar, evitando así posibles erros de programación.
::
public abstract class Resource {
...
private long version;
...
public Long getVersion() {
return version;
}
private void setVersion(Long version) {
this.version = version;
}
...
}
_`Figura 1`. Definición do campo version nunha entidade.
Deste xeito, cando se actualicen en base de datos os obxectos que conforman o estado da conversación (*no* commit do paso de confirmación), Hibernate pode detectar se a versión do obxecto é igual que a que hai en base de datos. Se é a mesma, o obxecto actualízase en base de datos e increméntase a versión. Noutro caso, Hibernate lanza a excepción *org.hibernate.StaleObjectStateException*, que Spring converte a *org.springframework.dao.OptimisticLockingFailureException* (en adiante, *OptimisticLockingFailureException*). Esta última excepción debe capturarse dende a capa Web (seguramente de maneira xenérica, é dicir, común a tódolos casos de uso) para informar ó usuario de que outro usuario estaba a actualiza-los mesmos datos e confirmou antes que el.
::
<class name="Resource">
...
<!-IMPORTANT: type="java.lang.Long" must be specified (otherwise,
Hibernate infers type="integer".
-->
<version name="version" type="java.lang.Long"/>
...
</class>
_`Figura 2`. Especificación do campo version no ficheiro de mapping.
Para logra-la propiedade de atomicidade, só o último paso debe modifica-la base de datos. Dependendo da complexidade da conversación, esta propiedade require mais o menos esforzo de codificación. O caso mais sinxelo corresponde a unha conversación que teña a seguinte estrutura:
* Paso inicial: carga o obxecto persistente de base de datos en caso de edición, ou crea una versión inicial en memoria no caso de creación. En ambos casos, o obxecto quedará en memoria e forma parte do estado da conversación. No primeiro caso, o obxecto estará en estado *detached* e no segundo en estado *transient*.
* Pasos intermedios: validan as modificacións feitas polo usuario e modifican o estado en memoria (se é posible; en outro caso, infórmase ó usuario). No caso mais sinxelo, non é necesario pasa-lo obxecto de estado *detached*/*transient* a *persistent*, é dicir, engadilo na sesión de Hibernate.
* Paso de confirmación: valida todo o estado é materialízao na base de datos. Para materializa-lo estado na base de datos, é preciso pasa-lo obxecto de estado *detached*/*transient* a *persistent*, é dicir engadilo na sesión de Hibernate. O paso de *detached* a *transient* coñécese có nome de *reattachment*. Existen varias formas de lograr tanto o *reattachment* coma o paso de *transient* a *persistent*. A forma mais consolidada e xeral consiste no uso da operación *Session::saveOrUpdate*, que contempla ambos casos. A operación *IGenericDao::save* (e a súa realización por defecto *GenericDaoHibernate::save*) proporciona esta semántica. En consecuencia, e para simplifica-lo texto, referirémonos sempre como reattachment ó uso de *IGenericDao::save* (tanto si o obxecto era *detached* coma se era *transient*). É importante darse conta que có modo normal de traballo con Spring e Hibernate, Hibernate non lanza as consultas de actualización/inserción/eliminación no momento de invocar a *IGenericDao::save*, senón que o obxecto corresponde engádese á sesión e planifícase para actualización/inserción na base de de datos cando se faga o *commit* (Spring) da transacción.
O apartado `Un exemplo inicial`_ explica como realizar unha conversación na que é posible emprega-la anterior estrutura. As conversacións máis complexas requiren emprega-la técnica de reattachment en pasos intermedios. O apartado `Técnicas avanzadas`_ discute as técnicas **necesarias**.
Un exemplo inicial
------------------
Nesta sección preséntase un exemplo do caso máis sinxelo de realización dunha conversación, é dicir, unha na que non hai que facer *reattachment* en pasos intermedios. Para elo considerarase o caso de uso de edición dos datos dun traballador. Este caso de uso está **inspirado** nun caso de uso real do proxecto, simplificado e modificado con respecto á realización real para explicar só os aspectos relativos á realización dunha conversación, sen complexidades adicionais.
.. image:: images/01-listadotraballadores.png
:alt: Listado de traballadores
:width: 200
_`Figura 3` : Listado de traballadores.
Neste exemplo, para crear ou edita-los datos dun traballador, o usuario accede a unha pantalla que lle amosa un listado cós traballadores existentes (`figura 3`_). A continuación, utiliza o botón Create para crear un novo traballador, ou o botón Edit dun traballador existente para edita-los seus datos.
Ambos botóns inician a conversación de creación ou edición do datos dun traballador. En ambos casos, o usuario accede a un wizard que contén tres pestanas para edita-los datos do traballador, un botón Save para confirmar tódolos cambios e un botón Cancel para cancela-los cambios. A primeira das pestanas (`figura 4`_) permite edita-los datos básicos do traballador. A segunda pestana (`figura 5`_) dispón de botóns para engadir ou eliminar localizacións do traballador. A terceira pestana (`figura 6`_) dispón de botóns para engadir, eliminar ou editar items da historia laboral (data de contratación, baixas laborais, etc.) do traballador. A `figura 7`_ amosa a edición/creación dun item da historia laboral do traballador. Unha vez que o usuario completa a edición dos datos, remata a conversación pulsando en Save (para materializa-los datos na base de datos) ou en Cancel (para descartalos). En ambos casos, vólvese ó listado xeral de traballadores (`figura 3`_).
_`Figura 4`: Edición dos datos básicos dun traballador.
_`Figura 5`: Edición das localizacións dun traballador.
_`Figura 6`: Edición da historia laboral dun traballador.
_`Figura 7`: Edición/creación dun item da historia laboral dun traballador.
A `figura 8`_ amosa as entidades que modelan os datos dun traballador. Un traballador é un tipo de recurso (Resource). A clase Worker contén os datos básicos do traballador. Cada traballador ten un conxunto de CriterionSatisfaction asociados (relación bidireccional 1:N). Unha instancia de CriterionSatisfaction modela de maneira xenérica algún tipo de situación laboral dun recurso nun intervalo temporal. En particular, cada localización dun traballador e cada item da súa historia laboral modélase coma una instancia de CriterionSatisfaction. En consecuencia, os CriterionSatisfaction dun recurso representan a súa situación laboral ó longo do tempo. Cada CriterionSatisfaction está asociado cunha instancia dun Criterion. Un Criterion representan unha clase de situación laboral, coma por exemplo, contratación ou baixa médica, no caso de relacións laborais, ou Coruña ou Pontevedra no caso de localizacións. Finalmente, é importante entender que os datos dun traballador veñen dados por unha instancia da clase Worker e os seus CriterionSatisfaction asociados. As instancias de Criterion son globais a tódolos traballadores, dado que representan clases de situacións laborais.
_`Figura 8`: Entidades que modelan os datos dun traballador.
A `figura 9`_ amosa a estrutura do modelo (sección 4) que representa a interacción do usuario para crear/edita-los datos dun traballador. A interface do modelo ven dada por IWorkerModel. WorkerModelImpl realiza a interface IWorkerModel. A clase WorkerModelImpl modélase como un servizo de Spring con estado, que entre outras cousas, contén o traballador en creación/edición.
_`Figura 9`: Modelo para a edición dos datos dun traballador.
A continuación coméntase o protocolo da conversación. Cando o usuario desexa visualiza-lo listado de traballadores (`figura 3`_), o controlador invoca a operación IWorkerModel::getWorkers. Se o usuario pulsa o botón Create ou o botón Edit dun traballador, o controlador invoca o método IWorkerModel::initCreate ou IWorkerModel::initEdit, respectivamente, iniciándose así a conversación para crear ou edita-los datos dun traballador. No primeiro caso, inicialízase o atributo worker de WorkerModelImpl creándose un Worker en memoria que non ten ningún CriterionSatisfaction asociado. No segundo caso, inicialízase o atributo worker recuperándose o Worker da base de datos mediante o DAO IWorkerDao. Sempre que o controlador precisa mostralos datos (e.g. datos básicos, localizacións e items da historia laboral) do worker en edición/creación, invoca IWorkerModel::getWorker, que devolve o worker almacenado en WorkerModelImpl. Resource dispón de métodos para devolve-los conxuntos de localizacións actuais e pasadas (`figura 5`_), e os items da historia laboral (`figura 6`_). Estes métodos son simples operacións de conveniencia que retornan o subconxunto [#]_ apropiado dos CriterionSatisfaction do recurso (traballador).
.. [#] O controlador pode ordenar (e.g. por data de inicio) este subconxunto empregando un java.util.TreeSet.
Para mostra-la lista ordenada de localizacións non asignadas actualmente ó traballador (`figura 5`_), o controlador invoca a IWorkerModel:getNonAssignedLocationCriteria. Cada vez que o usuario selecciona un conxunto de localizacións para asignar ou des-asignar, o controlador invoca a IWorkerModel::assignLocations ou IWorkerModel::unassignLocations. Cada vez que o usuario engade, modifica ou elimina un item da historia laboral (`figura 6`_), o controlador invoca IWorkerModel::addLaboralHistoryItem, IWorkerModel::updateLaboralHistoryItem ou IWorkerModel::removeLaboralHistoryItem. Para poder mostra-la lista desplegable (ordenada)
do formulario da `figura 7`_, o controlador invoca IWorkerModel::getLaboralCriteria. A edición dos datos básicos (`figura 4`_) non supón ningunha interacción especial con IWorkerModel (os datos básicos non se validan ata o paso de confirmación). Finalmente, cando o usuario remata o proceso de edición de datos, se pulsa o botón Save, o controlador invoca a operación IWorkerModel::confirm. Esta operación materializa o atributo worker de WorkerModelImpl na base de datos. Se decide pulsa-lo botón Cancel, o controlador invoca a operación IWorkerModel::cancel para resetea-lo estado. En ambos casos (confirmación ou cancelación), o controlador invoca posteriormente a operación IWorkerModel::getWorkers para amosa-lo listado de traballadores.
A continuación coméntase a realización dalgunhas operacións de IWorkerModel na clase WorkerModelImpl. A `figura 10`_ amosa a realización da operación getWorkers. Esta operación simplemente delega no método homólogo do DAO IWorkerDao. Obsérvese que se empregou a anotación @Transactional có atributo readOnly = true. Tódalas operacións de WorkerModelImpl correspondentes a un paso inicial ou intermedio, e que usen directa ou indirectamente DAOs, empregarán esta mesma anotación, dado que: (1) os métodos dos DAOs só se poden empregar no contexto dunha transacción (@Transactional), e (2) as operacións correspondes a un paso inicial ou intermedio non modifican a base de datos (readOnly = true).
::
@Transactional(readOnly = true)
public List<Worker> getWorkers() {
return workerDao.getWorkers();
}
_`Figura 10`: WorkerModelImpl::getWorkers.
A `figura 11`_ amosa a operación WorkerModelImpl::initEdit. A operación inicializa o atributo worker cargando a instancia de Worker a partir do identificador do traballador. Dado que as relacións en Hibernate por defecto son lazy (independentemente da cardinalidade), o worker non terá inicializada a colección (java.util.Set) de CriterionSatisfaction. Sen embargo, as operacións correspondentes ós pasos intermedios precisan traballar cós CriterionSatisfaction do traballador, e cós Criterion asociados a cada CriterionSatisfaction. Do mesmo, xeito, a capa vista tamén asume que pode navegar polos CriterionSatisfaction (e Criterion) do traballador que devolve o método WorkerModelImpl::getWorker. Por este motivo, o código recorre a lista de CriterionSatifaction (a primeira iteración causa que a lista se inicialice) e invoca un método que non sexa get<<Clave>> sobre cada Criterion asociado, para inicializa-lo proxy do Criterion asociado ó CriterionSatisfaction desa iteración. O método get<<Clave>> é o único método que non causa a inicalización dun proxy, suxeito a que se empregue o tipo de acceso property para a clave (que é o tipo de acceso por defecto para tódalas propiedadespersistentes). É importante observar que esta incialización faise dende WorkerModelImpl no canto de facela dende un método de negocio da entidade Resource, dado que son as clase modelo as que coñecen as relacións que é preciso inicializar para levar a cabo a conversación.
::
@Transactional(readOnly = true)
public void initEdit(Long workerId) {
worker = (Worker) workerDao.findExistingEntity(workerId);
for (CriterionSatisfaction c : worker.getCriterionSatisfactions()) {
c.getCriterion().getName();
}
}
_`Figura 11`: WorkerModelImpl::initEdit.
O método getWorker (`figura 12`_) simplemente devolve o atributo worker. A `figura 13`_ amosa a realización do método updateLaboralHistoryItem. O método recibe un CriterionSatisfaction que actúa como Data Transfer Object (DTO) [#]_ , é dicir contén os datos do formulario de edición (`figura 7`_) así como o identificador do CriterionSatisfaction (o valor do atributo version é irrelevante). Obsérvese que non sería correcto que a operación recibise o CriterionSatisfaction destino modificado polo controlador, dado que se a modificación non é correcta e o usuario na corrixe, o estado do worker quedaría inconsistente e podería afectar a validacións de interaccións posteriores. A operación Resource::updateCriterionSatisfactioncomproba se é posible modifica-lo CriterionSatisfaction do worker cós datos recibidos no DTO. En caso de selo, copia os atributos do DTO no CriterionSatisfaction do worker, excepto os atributos id e version. Neste exemplo, a comprobación que realiza o método updateCriterionSatisfaction só se reduce a iterar por tódolos CriterionSatisfaction do worker (excepto o CriterionSatisfaction obxectivo) e comprobar que non haxa ningunha incompatibilidade cós datos recibidos no DTO (e.g. o rango temporal do DTO non debe solaparse con outros CriterionSatisfaction). En consecuencia, non se require facer un reattachment do worker na sesión de Hibernate, dado que a lóxica do método updateCriterionSatisfaction só itera polo conxunto CriterionSatisfaction asociados, que xa están cargados en memoria (no caso da edición do traballador, WorkerModelImpl::initEdit provoca a inicialización do conxunto de CriterionSatisfaction inicial, e tanto na edición coma na creación, os items da historia laboral vanse engadindo ó conxunto CriterionSatisfaction con WorkerModelImpl::addLaboralHistoryItem). Finalmente, tamén e importante darse conta que o método Resource::updateCriterionSatisfaction levanta a excepción ResourceConstraintException en caso de que non sexa posible actualiza-lo item da historia laboral (a capas vista/controlador deben notificar este error ó usuario final).
.. [#] Neste caso é posible reusa-la entidade CriterionSatisfaction coma un DTO, dado que esta entidade contén tódolos datos necesarios. Noutro caso, sería necesario definir unha clase pura DTO, é dicir, unha clase non persistente que contén os datos necesarios.
::
public Worker getWorker() {
return worker;
}
_`Figura 12`: WorkerModelImpl::getWorker.
::
public void updateLaboralHistoryItem(CriterionSatisfaction newDataDto)
throws ResourceConstraintException {
worker.updateCriterionSatisfaction(newDataDto);
}
_`Figura 13`: WorkerModelImpl::updateLaboralHistoryItem.
A `figura 14`_ mostra o código da operación WorkerModelImpl::confirm. O código encárgase de tres aspectos: (1) valida-los datos básicos do traballador mediante Worker::validateBasicData (que pode devolver ResourceConstraintException), (2) actualiza-lo worker e os seus CriterionSatisfaction asociados en base de datos e (3) resetea-lo estado. Con respecto ó primeiro aspecto, é importante recordar que a edición dos datos básicos do traballador non supónningunha interacción con IWorkerModel. Por contra, durante a edición dos datos básicos do
traballador, o controlador modifica directamente o obxecto retornado por
IWorkerModel::getWorker (o estado da conversación). En consecuencia, no paso de confirmacióné preciso valida-los datos básicos. É importante observar que se os valores dos datos básicos influíran no resto de validacións (e.g. nos items da historia laboral), o controlador non debería traballar directamente có obxecto retornado por IWorkerModel::getWorker, senón empregar un DTO cós datos básicos do traballador e pasalo como parámetro en IWorkerModel::confirm (dado que valores erróneos nos datos básicos poderían influír negativamente noutras validacións mentres o usuario non os corrixa), de maneira similar a como se fixo con IWorkerModel::updateLaboralHistoryItem (`figura 13`_). Así mesmo, tamén sería preciso facer unha validación de tódolos datos do traballador (datos básicos e conxunto de CriterionSatisfaction) en WorkerModelImpl::confirm.
Con respecto ó segundo aspecto, o código invoca a operación IGenericDao::save, que como se explicou anteriormente (apartado `Conceptos específicos a Hibernate`_), causa un reattachment do worker na sesión de Hibernate, de maneira que cando se faga o commit da transacción, Hibernate engadirá ou actualizará o worker na base de datos. Dado que se pretende que tamén se actualicen en base de datos (actualizándose/engadíndose/elimándose) os CriterionSatisfaction asociados ó worker, é preciso facer que o reattachment do worker se propague en cascada ós CriterionSatisfaction asociados. Para elo, basta empregar unha opción de cascada no mapping da relación entre Resource e CriterionSatisfaction que como mínimo inclúa save-update. Ademais, como cada instancia dun CriterionSatisfaction só se referenciada dende un só recurso, e dende ningunha outra entidade, para provocar que os CriterionSatisfaction eliminados (items da historia laboral) no só se desasocien do traballador en edición, senón que tamén se eliminen da base de datos automaticamente, é posible emprega-la opción delete-orphan. A `figura 15`_ amosa o uso da opción all-delete-orphan, que é sinónima de tódalas opcións de cascada (concretamente é sinónimo de “all, delete-orphan”). Obsérvese tamén que o paso de confirmación non emprega o elemento *readOnly = true* na anotación *@Transactional*.
::
@Transactional
public void confirm() throws ResourceConstraintException {
worker.validateBasicData();
workerDao.save(worker);
resetState();
}
private void resetState() {
worker = null;
}
_`Figura 14`: WorkerModelImpl::confirm.
::
<class name="Resource">
...
<set name="criterionSatisfactions" inverse="true"
cascade="all-delete-orphan">
<key column="resourceId" not-null="true"/>
<one-to-many class="org.navalplanner...CriterionSatisfaction"/>
</set>
...
</class>
_`Figura 15`: Uso de opcións de cascada na relación entre Resource e CriterionSatisfaction.
A `figura 16`_ amosa a operación WorkerModelImpl::cancel, que simplemente resetea o estado de WorkerModelImpl, descartando os cambios feitos.
::
public void cancel() {
resetState();
}
_`Figura 16`: WorkerModelImpl::cancel.
Coma se explicou no apartado `Conceptos específicos a Hibernate`_, se dous usuarios están editando os datos dun mesmo traballador concorrentemente, un confirmará (IWorkerModel::confirm) os datos antes que o outro. O paso de confirmación (`figura 14`_), provoca un reattachment do worker en edición, que se propaga en cascada ós CriterionSatisfaction asociados debido ó uso da opción de cascada ilustrada na `figura 15`_ (en particular, debido a semántica asociada a save-update). É importante entender que Hibernate sempre considera como dirty os obxectos dos que se fai un reattachment, tanto se se modificaron coma se non (cando se fai un reattachment dun obxecto, Hibernate non sabe se foi modificando antes de facelo reattachment, polo que sempre o considera dirty). Neste caso, isto quere dicir que o paso de confirmación sempre provoca unha actualización dos datos básicos do traballador e a actualización/inserción/eliminación dos CriterionSatisfaction asociados, cós conseguintes incrementos de versión no worker e nos CriterionSatisfaction que xa existían, tanto se se modificaron coma senón. En consecuencia, o usuario que execute o paso de confirmación en segundo lugar, recibirá a excepción OptimisticLockingFailureException, dado que o número de versión do seu worker e menor que o actual en base de datos. Esta excepción debe capturarse dende a capa Web (seguramente de maneira xenérica) para informar ó usuario de que outro usuario estaba a actualiza-los datos do mesmo traballador e confirmou os cambios antes que ela/el.
::
@Transactional(readOnly = true)
public void updateLaboralHistoryItem(CriterionSatisfaction newDataDto)
throws ResourceConstraintException {
workerDao.checkVersion(worker);
worker.updateCriterionSatisfaction(newDataDto);
}
_`Figura 17`: Mellora de WorkerModelImpl::updateLaboralHistoryItem.
IGenericDao proporciona a operación checkVersion que permite detecta-la anterior situación de concorrencia sen esperar ó paso de confirmación. A `figura 17`_ amosa unha mellora ó método WorkerModelImpl::updateLoaboralHistoryItem, que a diferencia da versión anterior (`figura 13`_), invoca primeiramente ó método IGenericDao::checkVersion. Este método lanza unha consulta á base de datos para comprobar se o número de versión do obxecto recibido como parámetro é o mesmo que en base de datos. En caso negativo, lanza a excepción OptimisticLockingFailureException. Se a clave ou a versión da entidade é null, ou a entidade non existen en base de datos, o método considera a comprobación correcta. Isto permite trata-la creación e a edición de obxectos de maneira unificada (e.g. na `figura 17`_ o traballador pasado a IGenericDao::checkVersion pode ser un traballador en creación ou un en edición). O método require que a entidade pasada como parámetro dispoña dos métodos getId (para devolve-la clave) e getVersion (para devolve-lo campo version). Esta mesma idea pódese aplicar ó resto de métodos correspondentes a pasos intermedios de modificación (e.g. addLaboralHistoryItem, removeLaboralHistoryItem, etc.), ou incluso, a calquera paso intermedio. Desta maneira, cada vez que se intenta executar un paso intermedio, compróbase se ten sentido permitir que o usuario continúe editando os datos.
Finalmente queda por tratar un aspecto ó que aínda non se lle prestou atención: a asociación dunha instancia de Criterion cada vez que o controlador ten que crear unha instancia de CriterionSatisfaction, é dicir, cada vez que invoca a IWorkerModel::assignLocations, IWorkerModel::addLaboralHistoryItem ou IWorkerModel::updateLaboralHistoryItem. O encargado de facer esta asociación é o controlador, dado que é este quen se encarga de crea-las instancias de CriterionSatisfaction que se pasan ás anteriores operacións. O controlador asigna o Criterion apropiado a partir dos Criterion retornados por IWorkerModel::getLaboralCriteria e IWorkerModel::getNonAssignedLocationCriteria. A `figura 18`_ amosa a realización destes dous métodos na clase WorkerModelImpl. Os métodos fan uso dos atributos privados laboralCriteria e locationCriteria. O primeiro representa co conxunto total de Criterion que modelan clases de situacións laborais (e.g. contratación, baixa médica, etc.). O segundo representa o conxunto total de Criterion que modelan localizacións. Ambos atributos son inicializados en cada un dos pasos iniciais da conversación mediante o método privado initCriteria, que recupera os dous conxuntos de Criterion da base de datos. O método WorkerModelImpl::getLaboralCriteria simplemente devolve o atributo laboralCriteria, mentres que o método WorkerModelImpl.getNonAssignedLocationCriteria devolve o subconxunto de localizacións actualmente non asignadas.
É importante darse conta que o código da `figura 18`_ garante que tódolos CriterionSatisfaction do traballador usarán a mesma instancia de Criterion por cada tipo de Criterion que empreguen. Neste caso, esta garantía non é necesaria. Sen embargo, é unha boa práctica non ter obxectos duplicados en memoria, e ademais algúns mappings de Hibernate requiren esta garantía á hora de executa-lo paso de confirmación. Por exemplo, se a relación entre CriterionSatisfaction e Criterion fose 1:N e unidireccional (un CriterionSatisfaction dispón dun conxunto de Criterion) e se empregase unha táboa intermedia para mapea-la relación [#]_, o paso de confirmación produciría org.hibernate.NonUniqueObjectException (en adiante NonUniqueObjectException) se hai máis dunha instancia dun mesmo tipo de Criterion entre os CriterionSatisfaction do traballador. Existen casos máis complexos, nos que incluso é necesario garantir non só que non haxa instancias repetidas, senón tamén que estean engadidas na sesión de Hibernate. No apartado `Técnicas avanzadas`_ presentarase unha mellora á solución da `figura 18`_ para garantir adicionalmente este último aspecto.
.. [#] Sería preciso empregar unha táboa intermedia se as instancias de Criterion puideran ter relacións exclusivas con varias entidades.
::
private Set<Criterion> laboralCriteria;
private Set<Criterion> locationCriteria;
@Transactional(readOnly = true)
public void initEdit(Long workerId) {
worker = (Worker) workerDao.findExistingEntity(workerId);
for (CriterionSatisfaction c : worker.getCriterionSatisfactions()) {
c.getCriterion().getName();
}
initCriteria();
}
@Transactional(readOnly = true)
public void initCreate() {
worker = new Worker();
initCriteria();
}
public Set<Criterion> getLaboralCriteria() {
return laboralCriteria;
}
public Set<Criterion> getNonAssignedLocationCriteria() {
Set<Criterion> nonAssignedLocationCriteria =
new HashSet<Criterion>(locationCriteria);
nonAssignedLocationCriteria.removeAll(worker.getCurrentLocationCriteria());
return nonAssignedLocationCriteria;
}
private void initCriteria() {
laboralCriteria = criterionDao.findLaboralCriteria();
locationCriteria = criterionDao.findLocationCriteria();
}
private void resetState() {
worker = null;
laboralCriteria = null;
locationCriteria = null;
}
_`Figura 18`: Xestión do conxunto total de Criterion da historia laboral e localizacións en
WorkerModelImpl.
Técnicas avanzadas
------------------
Reattachment de obxectos modificados en pasos intermedios
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Para ilustra-la necesidade de facer reattachments en pasos intermedios, así como unha serie de problemas asociados que poder ser necesario ter en conta, a `figura 19`_ engade unha entidade con respecto ó diagrama que amosa a `figura 8`_. A nova entidade, ResourceGroup, representa un grupo de recursos e ten unha relación bidireccional 1:N con Resource (un grupo de recursos está formado por varios recursos e un recurso só pode pertencer a un grupo). Cada grupo de recursos impón unha serie de localizacións actuais obrigatorias ós seus membros en total, é dicir, o grupo debe ter “presencia” nesas localizacións. En consecuencia, o conxunto de localizacións obrigatorias do grupo debe estar contido no conxunto total das localizacións actuais dos seus recursos. As localizacións obrigatorias están modeladas inicialmente [#]_ coma un conxunto de nomes (e.g. A Coruña, Pontevedra, etc.), e corresponden ó atributo name dos Criterion que representan localizacións.
.. [#] Máis adiante, as localizacións obrigatorias modelaranse coma un conxunto de Criterion.
_`Figura 19`: Adición da entidade ResourceGroup o diagrama da `figura 8`_.
Por simplicidade e consistencia coa interface gráfica do apartado `Un exemplo inicial`_ (figuras 3-7), asumirase que a creación dos datos dun traballador non ten que encargarse da asignación ó grupo de traballo, senón que isto último faise dende outro caso de uso (e.g. xestión de grupos). En consecuencia, a restrición de localizacións obrigatorias só afecta á des-asignación de localizacións (`figura 5`_) no caso de edición dos datos dun traballador. A `figura 20`_ amosa o código relevante dun “primeiro intento” para contemplar esta nova restrición. Por unha banda, a operación WorkerModelImpl::unassignLocations (`figura 20`_), despois de comprobar se a versión do worker segue sendo a mesma que en base de datos, elimina as localizacións recibidas por parámetro do conxunto de CriterionSatisfaction do worker mediante o método Resource:removeCriterionSatisfactions. Posteriormente, no caso de edición dos datos do traballador, compróbase se o grupo ó que pertence o traballador segue cumprindo a restrición de localizacións obrigatorias, e en caso negativo, vólvense a engadi-las localizacións que se pretendían des-asignar ó traballador. A comprobación da restrición de localizacións obrigatorias faise a través do método ResourceGroup::validateLocations (`figura 21`_). Este método calcula o conxunto de localizacións actuais (aquelas con data final non especificada ou superior á data actual) como o conxunto das localizacións actuais de tódolos recursos do grupo, e comproba se contén as localizacións obrigatorias do grupo.
::
@Transactional(readOnly = true)
public void unassignLocations(Set<CriterionSatisfaction> locations)
throws ResourceConstraintException {
workerDao.checkVersion(worker);
worker.removeCriterionSatisfactions(locations);
if (isEditing()) {
try {
worker.getResourceGroup().validateLocations();
} catch (ResourceConstraintException e) {
worker.addCriterionSatisfactions(locations);
throw e;
}
}
}
_`Figura 20`: WorkerModelImpl::unassignLocations (primeiro intento).
::
public void validateLocations() throws ResourceConstraintException {
Set<String> locationNames = new HashSet<String>();
for (Resource r : resources) {
locationNames.addAll(r.getCurrentLocationNames());
}
if (!locationNames.containsAll(requiredLocationNames)) {
throw new ResourceConstraintException(...);
}
}
_`Figura 21`: ResourceGroup::validateLocations.
A solución anterior non é correcta dado que cando se execute a liña worker.getResourceGroup().validateLocations() no método WorkerModelImpl::unassignLocations (`figura 20`_), produciríase a excepción org.hibernate.LazyInitializationException (en adiante LazyInitializationException), dado que, por exemplo, o método WorkerModelImpl:initEdit non inicializa a referencia ó ResourceGroup (é un proxy sen inicializar). Poderíase pensar que unha solución a este problema consiste en facer que WorkerModelImpl::initEdit inicialice tamén a referencia ó ResourceGroup. Aínda así, cando se executase a iteración sobre os recursos do grupo no método ResourceGroup::validateLocations (`figura 21`_), produciríase outra vez LazyInitializationException, dado que o ResourceGroup non ten inicializada a colección (java.util.Set) de recursos. De novo, podería pensarse en modificar WorkerModelImpl::initEdit para que tamén inicializase os recursos do grupo (e os CriterionSatisfaction/Criterion de cada un deles). No obstante, esta solución non é correcta dado que tanto o ResourceGroup coma os seus recursos (cós seus CriterionSatisfaction) acabarán en memoria, como parte do estado de WorkerModelImpl, de maneira que as validacións faranse contra as localizacións dos traballadores en memoria, que poden ser distintas ás que hai en base de datos. Para elo, basta pensar en dúas conversacións concorrentes que modifican as localizacións de dous traballadores distintos dun mesmo grupo. Se unha conversación elimina a localización A do traballador 1 e outra a mesma localización A do traballador 2, de maneira que a localización A era obrigatoria e ningún outro recurso do grupo tiña esa localización, ambas conversacións confirman os cambios (porque en memoria sempre hai un traballador que está na localización A), deixando a base de datos nun estado inconsistente (ningún traballador do grupo estará na localización A). Outra solución alternativa a forza-la carga do ResourceGroup, e os seus recursos asociados, consiste en facer un reattachment (e aplica-las técnicas que se comentan mais adiante) do worker no método WorkerModelImpl::unassignLocations antes de navegar ó ResourceGroup. No obstante, isto conduce ó mesmo problema: o ResourceGroup e os seus recursos asociados rematan formando parte do estado da conversación (o que se poría de manifesto nos seguintes pasos da conversación). Finalmente, tamén é importante darse conta de que ambas alternativas conducen a ter como estado da conversacións obxectos persistentes que non son editados pola conversación, o que conceptualmente quizais non é correcto.
::
@Transactional(readOnly = true)
public void unassignLocations(Set<CriterionSatisfaction> locations)
throws ResourceConstraintException {
workerDao.checkVersion(worker);
worker.removeCriterionSatisfactions(locations);
if (isEditing()) {
workerDao.save(worker);
ResourceGroup resourceGroupFromDb =
resourceGroupDao.findExistingEntity(
worker.getResourceGroup().getId());
try {
resourceGroupFromDb.validateLocations();
} catch (ResourceConstraintException e) {
worker.addCriterionSatisfactions(locations);
throw e;
}
}
}
_`Figura 22`: WorkerModelImpl::unassignLocations (segundo intento).
As `figura 22`_ presenta unha nova versión de WorkerModelImpl::unassignLocations que solventa os problemas anteriores. A diferencia da solución anterior, o ResourceGroup non se obtén por navegación a partir do worker, senón que se recupera da base de datos a partir do seu identificador. En consecuencia, a comprobación faise sempre contra os datos reais que hai na base de datos. Con respecto ó código da `figura 22`_, é importante destacar varios aspectos:
* A invocación worker.getResourceGroup().getId() non provoca que o proxy do ResourceGroup asociado ó worker se inicialize, dado que o método get<<Clave>> é o único método que non provoca a inicialización dun proxy (sempre que o tipo de acceso da clave sexa property).
* Para que o método ResourceGroup::validateLocations (`figura 21`_) funcione correctamente, no caso de edición dos datos do traballador, o worker que se está a editar debe ser un dos recursos do atributo resources. Para que isto sexa posible, é preciso que cando Hibernate lance a consulta correspondente á inicialización do conxunto resources, o worker xa estea engadido na sesión de Hibernate (de maneira que Hibernate descarte o obxecto coa mesma clave retornado pola consulta), é dicir, é preciso facer un reattachment (IGenericDao::save) do worker. Por este motivo, o método WorkerModelImpl::unassignResources fai un reattachment do worker antes de face-la validación.
* En xeral, o reattachment deberíase facer dende as clases modelo, dado que son elas as que definen o estado da conversación, maximizando así a reusabilidade dos métodos de negocio das entidades.
* Emprégase a anotación @Transactional có elemento readOnly = true. Por unha banda, o uso desta anotación é conceptualmente correcto dado que se accede á base de datos e non se pretende realizar ningunha modificación. Por outra banda, o uso desta anotación é necesario, dado que a operación IGenericDao::save causa a execución de Session::saveOrUpdate. Como xa se dixo, esta operación provoca un reattachment do obxecto pasado como parámetro. Como mínimo, isto provoca que Hibernate considere ó obxecto pasado como parámetro como dirty, de maneira que cando Spring faiga o commit da transacción, Hibernate, en principio, faría un flush de sesión, o que provocaría que os obxectos dirty (e os novos) se actualicen en base de datos. Dado que un paso intermedio dunha conversación non debería modifica-la base de datos, é preciso indicarlle a Hibernate que non faga o flush. Para elo, basta emprega-lo elemento readOnly = true. Este elemento provoca que Spring fixe o modo de flush da sesión de Hibernate a FlashMode.MANUAL, de maneira o flush non ocorre a menos que o desenvolvedor o provoque explicitamente (e.g. Session::flush).
Finalmente, na `figura 23`_ móstrase a nova versión do paso de confirmación. A diferencia da versión anterior (`figura 14`_), agora é preciso valida-las localizacións do worker, dado que aínda que se validaron en WorkerModelImpl::unassignLocations, entre a última execución deste paso e a execución do paso de confirmación, outra conversación concorrente puido altera-las localizacións doutro traballador do mesmo grupo, de maneira que a condición de validación das localizacións xa non se cumpra. Neste punto, o lector pode pensar en que, dado que é preciso facer esta comprobación no paso de confirmación, pódese simplifica-la realización de WorkerModelImpl::unassignLocations de maneira que no se faga ningunha comprobación nese paso. Desta forma, non sería preciso facer un reattachment en WorkerModelImpl::unassignLocations, e o código non tería que tratar coas complexidades asociadas ós reattachments en pasos intermedios (que se comentan a continuación). Como contrapartida, non se advertiría ó usuario de posibles erros nas localizacións ata o paso de confirmación. En calquera caso, a efectos de ilustra-los posibles problemas (e as solucións) que poden provoca-los reattachments en pasos intermedios, no resto do apartado, asumirase que é preciso face-lo reattachment en WorkerModelImpl::unassignLocations (e.g. como parte dos requisitos é preciso informar de posibles errores canto antes ó usuario final).
Tamén é importante observa-lo uso do elemento rollbackFor na anotación @Transactional. Por defecto, Spring fai un rollback cando un método anotado con @Transactional levanta unha excepción de runtime, pero non cando se produce unha excepción checked. Asumindo que a excepción ResourceConstraintException sexa checked, o comportamento por defecto, non é suficiente para o correcto funcionamento de WorkerModelImpl::confirm, dado que se o método ResourceGroup::validateLocations levanta a excepción ResourceConstraintException, o worker e os seus CriterionSatisfaction actualizaranse en base de datos, dado que antes fíxose un reattachment do worker (recórdese que no paso de confirmación non se emprega readOnly = true). Para provocar que Spring faga un rollback se se produce a excepción ResourceConstraintException, emprégase o elemento rollbackFor, que permite especificar unha ou varias excepcións para as que Spring debe facer un rollback (adicionalmente, Spring segue facendo rollback das excepcións de runtime, a menos que se exclúan có elemento noRollbackFor).
::
@Transactional(rollbackFor = {ResourceConstraintException.class})
public void confirm() throws ResourceConstraintException {
worker.validateBasicData();
workerDao.save(worker);
if (isEditing()) {
ResourceGroup resourceGroupFromDb =
resourceGroupDao.findExistingEntity(
worker.getResourceGroup().getId());
resourceGroupFromDb.validateLocations();
}
resetState();
}
_`Figura 23`: WorkerModelImpl::confirm.
Desafortunadamente, o uso de readOnly = true para facer reattachments en pasos intermedios non é suficiente en determinados escenarios, coma o caso deste exemplo. Neste caso, o reattachment do worker ocorre en cascada ós CriterionSatisfaction, o que da lugar ós seguintes problemas:
* Pode haber novos CriterionSatisfaction, correspondentes a localizacións ou items da historia laboral engadidos polo usuario. Estes CriterionSatisfaction non terán o identificador xerado, polo que IGenericDao::save (Sesssion::saveOrUpdate) terá que xeralo. Asumino que as entidades empreguen a estratexia native, Hibernate usará unha secuencia ou fará unha inserción na táboa na que se mapea a entidade CriterionSatisfaction, dependendo de se a base de datos soporta secuencias ou columnas contador, respectivamente. No primeiro caso, o único efecto lateral consiste en “n” incrementos na secuencia correspondente. Este efecto lateral pódese considerar irrelevante. No segundo caso, os novos CriterionSatisfaction teríanse que engadir á base de datos. Este efecto lateral, por contra, é incompatible coa semántica asociada a un paso intermedio (ademais, provoca unha excepción, dado que a inserción de obxectos é incompatible coa semántica asociada a readOnly = true).
* Aínda que se consiga xera-lo identificador dos obxectos novos nos pasos intermedios que fagan reattachments (e.g. empregando unha base de datos que dispoña de secuencias), independentemente do tipo de base de datos, o paso de confirmación (`figura 23`_) provocará unha excepción. Para entende-lo motivo, é preciso recordar que cando se invoca Session::saveOrUpdate, Hibernate sincroniza os obxectos que xa existían coa base de datos mediante sentencias UPDATE ... WHERE id = ? AND version = ? (e os novos mediante INSERT), é dicir, engade a comprobación do número de versión na sentenza UPDATE. Se o obxecto que vai a sincronizar coa base de datos é un obxecto novo (e.g. un CriterionSatisfaction) có identificador xerado, Hibernate non tentará engadilo, senón actualizalo, dado que dispón de identificador, e en consecuencia, asume que é un obxecto que existe en base de datos. Consecuentemente, lanzará unha sentencia UPDATE, que fallará porque non hai ningún obxecto na base de datos con ese identificador.
As `figura 24`_, `figura 25`_ e `figura 26`_ amosan unha posible solución ós problemas anteriores. Por unha banda, no mapping de CriterionSatisfaction emprégase a estratexia hilo (`figura 24`_) para a xeración de identificadores. Conceptualmente, esta estratexia é similar a sequence, pero funciona con calquera base de datos. A realización de hilo emprega unha táboa (table) que conceptualmente emula unha secuencia, e ademais emprega un algoritmo que non necesita acceder á base de datos cada vez que é preciso xerar un identificador (usa un rango, de tamaño max_lo, de identificadores en memoria, e cando se acaba, colle outro rango da táboa subxacente). No mapping de CriterionSatisfaction tamén se define version coma java.lang.Long (no canto de long) para que poida toma-lo valor null. Hibernate considera que as entidades con clave ou version con valor null son transient.
::
<class name="CriterionSatisfaction">
...
<id access="field" name="id">
<generator class="hilo">
<param name="table">crisat_hilo</param>
<param name="max_lo">100</param>
</generator>
</id>
<version name="version" type="java.lang.Long"/>
...
</class>
_`Figura 24`: Uso da estratexia hilo para a xeración de identificadores.
A `figura 25`_ amosa os cambios feitos sobre a entidade Resource. Por unha banda, defínese o atributo booleano newObject, que especifica se o obxecto é transient ou non. Este atributo non é persistente e pódeselle dar valor mediante o método setNewObject. Os métodos getVersion e setVersion permiten acceder á propiedade version, que agora ten tipo de acceso property(`figura 24`_). O método getVersion devolve null se o atributo newObject é true, de maneira que Hibernate asumirá que o obxecto é transient [#]_ aínda que a clave non sexa null.
.. [#] Hibernate considera que unha entidade é transient se a clave ou o campo version son null.
A `figura 26`_ amosa a operación WorkerModelImpl::addLaboralHistoryItem. Antes de engadi-lo item da historia laboral, a operación establece o atributo newObject do item a true. Na operación WorkerModelImpl::assignLocations sería preciso face-lo mesmo para as novas localizacións. A invocación a setNewObject en WorkerModelImpl::addLaboralHistoryItem e WorkerModelImpl::assignLocations farán que as instancias novas (é dicir, transient) de CriterionSatisfaction teñan o atributo newObject a true, e en consecuencia, o método getVersion sobre elas devolverá null.
::
private boolean newObject;
private Long version;
public void setNewObject(boolean newObject) {
this.newObject = newObject;
}
public Long getVersion() {
if (newObject) {
return null;
} else {
return version;
}
}
private void setVersion(Long version) {
this.version = version;
}
_`Figura 25`: Estratexia de version == null para instancias “transient”. (CriterionSatisfaction).
::
@Transactional(readOnly = true)
public void addLaboralHistoryItem(CriterionSatisfaction item)
throws ResourceConstraintException {
workerDao.checkVersion(worker);
item.setNewObject(true);
worker.addCriterionSatisfaction(item);
}
_`Figura 26`: Nova versión de WorkerModelImpl::addLaboralHistoryItem.
Cada vez que se fai un reattachment do worker (recordemos que se propaga en cascada ós CriterionSatisfaction) en WorkerModelImpl::unassignLocations (`figura 22`_), xenéranse os identificadores dos CriterionSatisfaction transient, aínda que xa foran xerados como consecuencia dun reattachment anterior, dado que o método getVersion devolve null (e en consecuencia, Hibernate asume que os obxectos son transient). Tendo en conta que cada o identificador é de tipo java.lang.Long (64 bits) e que cada entidade pode usa-la súa propia táboa para a estratexia hilo, o posible desaproveitamento de identificadores non parece un problema [#]_.
.. [#] Unha maneira de minimiza-lo desaproveitamento de identificadores consiste en engadi-lo atributo booleano beforeSave e o método setBeforeSave, de maneira que getVersion só devolva null se newObject e beforeSave son true, e que o paso de confirmación invoque a setBeforeSave sobre cada obxecto transient antes de face-lo reattachment (no exemplo, o desenvolvedor, no paso de confirmación, tería que iterar polos CriterionSatisfaction e invocar a setBeforeSave sobre tódolos obxectos con newObject a true).
No paso de confirmación (`figura 23`_), cando se fai o reattachment do worker, Hibernate volve a considera-los CriterionSatisfaction novos como transient (dado que getVersion segue devolvendo null), de maneira que xenera os identificadores destes obxectos outra vez, e planifica a inserción dos obxectos na base de datos para o momento no que se faga o commit da transacción.
A estratexia hilo e a técnica de redefinición de getVersion tamén é preciso aplicalas sobre a clase Resource, dado que no caso de creación dos datos dun traballador, o worker é un obxecto transient durante os pasos intermedios. En consecuencia, a operación WorkerModel::initCreate ten que invocar a setNewObject sobre o worker creado. A `figura 27`_ amosa esta operación.
::
public void initCreate() {
worker = new Worker();
worker.setNewObject(true);
initCriteria();
}
_`Figura 27`: WorkerModelImpl::initCreate.
Finalmente, é importante coñecer que Hibernate ofrece un alternativa a técnica de reattachment mediante o método Session::merge. O uso deste método coñécese con nome de merging. A técnica de merging é necesaria cando non é posible saber no código se o obxecto do que se vai a facer un reattachment xa está engadido na sesión de Hibernate. Se efectivamente o estivese, e se intenta facer un reattachment, Hiberante lanza a excepción NonUniqueObjectException. Nestes caso, é preciso emprega-la técnica de merging mediante a operación Session::merge. Este método recibe o obxecto detached (ou transient) e devolve a referencia que se debe de empregar a partir dese momento para actuar sobre el. O método copia o estado do obxecto pasado como parámetro á sesión de Hibernate. A referencia pasada como parámetro débese descartar e a referencia retornada refírese o obxecto que está na sesión de Hibernate. O feito de ter que traballar coa referencia retornada pode complica-lo código, polo que unha boa estratexia é intentar face-lo reattachment dende as clases modelo antes de cargar, directa (mediante un DAO) ou indirectamente (navegación a través dunha relación lazy), calquera obxecto da base de datos, como se amosou no exemplo usado nesta sección.
Reattachment de obxectos non modificados en pasos intermedios
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Na sección anterior, o conxunto de localizacións obrigatorias do grupo modelouse coma un conxunto de nomes de localizacións (`figura 19`_ e `figura 21`_). Sen embargo, a opción máis coherente có modelo de obxectos sería modela-lo conxunto de localizacións obrigatorias coma un conxunto de Criterion (Set<Criterion>). A `figura 28`_ amosa a nova versión de ResourceGroup::validateLocations asumindo esta nova (e mellor) opción de modelado.
::
public void validateLocations() throws ResourceConstraintException {
Set<Criterion> locations = new HashSet<Criterion>();
for (Resource r : resources) {
locations.addAll(r.getCurrentLocationCriteria());
}
if (!locations.containsAll(requiredLocations)) {
throw new ResourceConstraintException();
}
}
_`Figura 28`: Versión de ResourceGroup::validateLocations, modelando as localización obrigatorias coma Set<Criterion>.
É importante observar que para que o código mostrado na `figura 28`_ funcione é vital que as referencias que manteñen os CriterionSatisfaction ós Criterion de tódolos traballadores do grupo sexan consistentes, é dicir, non debe haber máis dunha instancia dun Criterion dun mesmo tipo. Na sección `Un exemplo inicial`_ empregouse unha técnica (`figura 18`_) para tentar acadar esta semántica. Sen embargo, a solución alí presentada só é válida para garantir esta semántica nos CriterionSatisfaction do traballador en edición. Para entendelo basta examina-la realización da operación WorkerModelImpl::unassignLocations (`figura 22`_). Esta operación recupera da base de datos ó ResourceGroup do traballador, e posteriormente invoca a ResourceGroup::validateLocations (`figura 28`_). Cando ResourceGroup::validateLocationscomeza a iterar polos recursos do grupo, estes obtéñense da base de datos (nunha soa consulta), excepto o traballador en edición/creación (dado que se lle fixo un reattachment en WorkerModelImpl::unassignLocations). Sobre cada recurso, invócase Resource::getCurrentLocationCriteria, que itera por tódolos CriterionSatisfaction do recurso da iteración actual para devolve-los Criterion correspondentes ás localización actuais do recurso. En cada iteración, excepto para o traballador en edición, os CriterionSatisfaction do recurso da iteración, así coma os seus Criterion asociados, vanse recuperando da base de datos e engadindo á sesión de Hibernate (dado que a sesión actúa como caché de primeiro nivel, cada tipo de Criterion só se recupera unha vez da base de datos). En consecuencia, os Criterion recuperados da base de datos non serán consistentes cós Criterion do traballador en edición, dado que estes estaban en memoria, pero non na sesión de Hibernate. Este problema tamén se pon de manifesto en WorkerModelImpl::confirm (`figura 23`_).
Para solucionar este problema é preciso que antes de que se invoque ResourceGroup::validateLocations en WorkerModelImpl::unassignLocations e WorkerModelImpl::confirm, tódolos Criterion do traballador en edición estean na sesión de Hibernate. É dicir, é preciso facer un reattachment destes Criterion. A `figura 29`_ presenta unha nova realización das operacións WorkerModelImpl::unassignLocations e WorkerModelImpl::confirm. A diferencia das versións anteriores, agora o reattachment do worker faise mediante o método privado WorkerModelImpl::reattachWorker, que ademais de facer un reattachment do worker (e os seus CriterionSatisfaction asociados), tamén fai un reattachment dos Criterion correspondentes a localizacións e situacións laborais. En realidade, chegaría con facer un reattachment de aqueles que usa o traballador en edición. Para elo, bastaría con iterar polos CriterionSatisfaction do traballador e facer un reattachment de cada Criterion. Certamente, esta posibilidade é correcta. No obstante, dado que a clase WorkerModelImpl cachea como parte do estado tódolos Criterion correspondentes ás situacións laborais e localizacións nos atributos laboralCriteria e locationCriteria, por consistencia, optouse por facer un reattachment de todos eles.
Para face-lo reattachment dos Criterion non se emprega IGenericDao::save, dado que este método causa a execución de Sesssion.saveOrUpdate, que deixa o obxecto en estado dirty (aínda que non se modifique). En consecuencia, se se empregase IGenericDao::save sobre os Criterion no paso de confirmación, os Criterion actualizaríanse na base de datos (facendo que os seus números de versión se incrementen). Para evitar este problema, emprégase IGenericDao::reattachUnmodifiedEntity, que causa a execución de Sesssion.lock(LockMode.NONE, entity). Este método causa o reattachment da entidade pasada como parámetro, pero a diferencia de Session.saveOrUpdate, a entidade non se considera dirty.
Como alternativa a LockMode.NONE, é posible empregar LockMode.READ, que adicionalmente lanza unha consulta para comproba-lo número de versión do obxecto pasado como parámetro a Session.lock (só a primeira vez que se fai un reattachment). No obstante, o uso de LockMode.NONE, en xeral, parece suficiente e máis eficiente (no se lanza ningunha consulta).
Con respecto á semántica da operación Session.lock, convén destacar:
* É posible invocar esta operación máis dunha vez co mesmo obxecto (o reattachment só ocorre a primeira vez).
* O reattachment que causa Session.lock pódese propagar ás entidades relacionadas coa opción de cascada lock ou unha que a inclúa (e.g. all, all-delete-orphan). Sen embargo, a semántica do modo de bloqueo (LockMode.XXX) só se aplica a entidade pasada como parámetro.
* A entidade pasada como parámetro non debe estar modificada (pero se se modifica unha vez que se lle fixo o reattachment, considérase dirty).
Como se indicou anterioremente, o método WorkerModelImpl::reattachWorker non precisa facer un reattachment de tódolos Criterion correspondentes ás situacións laborais e localizacións, senón que chega con facer un reattachment de aqueles que usa o traballador en creación/edición. Esta observación conduce de maneira natural a outra solución alternativa que garante a unicidade de instancias de Criterion do mesmo tipo. A `figura 30`_ amosa a nova solución. Agora non se precisa manter como estado da conversación os atributos laboralCriteria e locationCriteria, de maneira que desaparece o método initCriteria e simplifícase o método resetState (`figura 18`_) .
Os métodos WorkerModelImpl::getLaboralCriteria e WorkerModelImpl::getNonAssignedLocationCriteria recuperan da base de datos tódolos Criterion correspondentes a situacións laborais e localizacións. Para garanti-la consistencia cós Criterion do traballador en edición/creación, antes de recupera-los Criterion da base de datos, ambos métodos fan un rettachment (WorkerModelImpl::reattachWorkerCriteria) dos Criterion que ten o traballador en edición/creación. Para garanti-la consistencia de Criterion que demanda o método Resource::validateLocations (`figura 28`_) entre o traballador en edición e o resto de traballadores do grupo, o método WorkerModelImpl::reattachWorker, fai o reattachment do traballador e os seus Criterion. Ademais, esta alternativa, con respecto á da `figura 29`_, non cachea o conxunto total de Criterion durante a conversación, de maneira que permitiría detectar cambios concorrentes (novos Criterion, Criterion modificados ou Criterion eliminados) durante o transcurso da conversación.
::
@Transactional(readOnly = true)
public void unassignLocations(
Set<CriterionSatisfaction> locations)
throws ResourceConstraintException {
workerDao.checkVersion(worker);
worker.removeLocations(locations);
if (isEditing()) {
reattachWorker();
ResourceGroup resourceGroupFromDb =
resourceGroupDao.findExistingEntity(
worker.getResourceGroup().getId());
try {
resourceGroupFromDb.validateLocations();
} catch (ResourceConstraintException e) {
worker.addLocations(locations);
throw e;
}
}
}
@Transactional(rollbackFor = {ResourceConstraintException.class})
public void confirm() throws ResourceConstraintException {
worker.validateBasicData();
reattachWorker();
if (isEditing()) {
ResourceGroup resourceGroupFromDb =
resourceGroupDao.findExistingEntity(
worker.getResourceGroup().getId());
resourceGroupFromDb.validateLocations();
}
resetState();
}
private void reattachWorker() {
reattachCriteria(laboralCriteria);
reattachCriteria(locationCriteria);
workerDao.save(worker);
}
private void reattachCriteria(Set<Criterion> criteria) {
for (Criterion c : criteria) {
criterionDao.reattachUnmodifiedEntity(c);
}
}
_`Figura 29`: Reattachment do worker (inclusive os CriterionSatisfaction) e os Criterion.
::
public Set<Criterion> getLaboralCriteria() {
reattachWorkerCriteria();
return criterionDao.findLaboralCriteria();
}
public Set<Criterion> getNonAssignedLocationCriteria() {
reattachWorkerCriteria();
Set<Criterion> nonAssignedLocationCriteria =
new HashSet<Criterion>(criterionDao.findLocationCriteria());
nonAssignedLocationCriteria.removeAll(worker.getCurrentLocationCriteria());
return nonAssignedLocationCriteria;
}
private void reattachWorker() {
workerDao.save(worker);
reattachWorkerCriteria();
}
private void reattachWorkerCriteria() {
for (CriterionSatisfaction c : worker.getCriterionSatisfactions()) {
criterionDao.reattachUnmodifiedEntity(c.getCriterion());
}
}
private void resetState() {
worker = null;
}
_`Figura 30`: Consistencia de instancias de Criterion sen necesidade de manter como estado tódalas instancias de Criterion.
Bloqueos pesimistas
~~~~~~~~~~~~~~~~~~~
Na sección `Técnicas Avanzadas`_ fíxose énfase en garantir que a validación das localizacións do traballador en edición fose compatible coa edición concorrente de outros membros do mesmo grupo, recuperando o ResourceGroup (e os seus recursos) da base de datos antes de valida-las localizacións, tanto en WorkerModelImpl::unassignLocations coma en WorkerModelImpl::confirm. Esta técnica é suficiente para facer fronte a edición concorrente de varios traballadores dun mesmo grupo, sempre e cando non ocorra que dúas o máis conversacións executen **xunto á vez** o método WorkerModelImpl::confirm. Para entendelo basta pensar en dúas conversacións (conversación 1 e conversación 2) que modifican os datos de dous traballadores (traballador 1 e traballador 2) dun mesmo grupo concorrentemente, de maneira que ambas tentan quita-la localización obrigatoria A dos respectivos traballadores, sendo eles os únicos que teñen esta localización actualmente.
Imaxinemos a seguinte secuencia de pasos:
* A conversación 1 elimina a localización A do traballador 1 (WorkerModelImpl::unassignLocations).
* A conversación 2 fai o mesmo co traballador 2 (pódeo facer porque en base de datos, o traballador 1 aínda ten a localización A).
* As dúas conversacións executan xunto á vez o método WorkerModelImpl::confirm(`figura 29`_). Neste caso ambas verían aínda hai outro traballador coa localización A, de maneira que ambas poderían confirma-los cambios, quedando o grupo sen ningún recurso na localización A.
Para solucionar este problema, pódese empregar IGenericDao::lock sobre o grupo do traballador en edición na operación WorkerModelImpl::confirm (`figura 31`_). Esta operación causa a execución de Session.lock(LockMode.UPGRADE, entity). O modo LockMode.UPGRADE provoca que se lance unha sentencia SELECT ... WHERE id = ? AND version = ? FOR UPDATE. A cláusula FOR UPDATE establece un bloqueo de escritura sobre as filas afectadas, o que provoca que calquera outra transacción concorrente non poia escribir ou establecer outro bloqueo de escritura sobre esas filas. En consecuencia, se dúas conversacións executan a operación WorkerModelImpl::confirm concorrentemente, a primeira que execute o método IGenericDao::lock bloqueará á outra no mesmo método ata que remate a primeira transacción. Deste xeito, na secuencia de pasos plantexada anteriormente, cando as dúas conversacións executen o paso de confirmación á vez, unha executará antes o método IGenericDao::lock (impedindo que a outra avance ó chegar a este mesmo método) e superará con éxito a validación (porque hai outro traballador en base de datos que ten a localización A). Cando a transacción do paso de confirmación desta conversación remate, continúase coa execución do paso de confirmación da outra conversación, que xa non superará a validación, dado que non hai outro traballador en base de datos coa localización A.
::
@Transactional(rollbackFor = {ResourceConstraintException.class})
public void confirm() throws ResourceConstraintException {
worker.validateBasicData();
reattachWorker();
if (isEditing()) {
ResourceGroup resourceGroupFromDb =
resourceGroupDao.findExistingEntity(
worker.getResourceGroup().getId());
resourceGroupDao.lock(resourceGroupFromDb);
resourceGroupFromDb.validateLocations();
}
resetState();
}
_`Figura 31`: Versión de WorkerModelImpl::confirm que contempla a posible concorrencia na confirmación de varias conversacións que actúan sobre traballadores dun mesmo grupo.
Finalmente, é importante resaltar dous aspectos:
* O uso de LockMode.UPGRADE é incompatible coa semántica de readOnly = true en @Transactional, de maneira que IGenericDao::lock só se pode empregar nun paso de confirmación.
* O uso de IGenericDao::lock non é específico ó concepto de conversación, senón que é un mecanismo que se pode aplicar para soluciona-los problemas de concorrencia que non son abordables có control de versións (no exemplo, cando se edita un traballador, non se modifica o ResourceGroup asociado, e en consecuencia, non se incrementa o número de versión do ResourceGroup).
Convencións de nomeado
----------------------
Nomeado das operacións da conversación
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Para nomea-las operacións dunha clase modelo que represente unha conversación, empregaranse as seguintes convencións:
* Se só hai unha operación para inicia-la conversación, empregarase o nome init (e.g. IWorkerModel::init). Se a conversación se pode iniciar con distintas operacións, empregaranse nomes con prefixo init (e.g. IWorkerModel::initCreate, IWorkerModel::initEdit, etc.).
* Se só hai unha operación para finaliza-la conversación con éxito, empregarase o nome confirm (e.g. IWorkerModel::confirm). Se é posible remata-la conversación con éxito con distintas operacións, empregaranse nomes con prefixo confirm.
* A operación para cancela-los cambios chamarase cancel (e.g. IWorkerModel::cancel).
Documentación do protocolo da conversación
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Na interface do modelo débense documenta-los seguintes aspectos relativos á semántica da conversación:
* Estado da conversación. Refírese a entidade ou entidades manipuladas na conversación. Unha realización particular da interface pode manter adicionalmente calquera outro obxecto que precise.
* Pasos independentes da conversación: especifica as operacións que non están involucradas na conversación.
* Protocolo da conversación:
* Paso inicial: indica as operacións (exclusivas) que permiten inicia-la conversación.
* Pasos intermedios: especifica os métodos que se invocan unha vez iniciada a conversación e antes de que se execute o paso final.
* Paso final: conxunto de operacións (exclusivas) que rematan a conversación.
A `figura 32`_ ilustra o estilo de documentación que se deberá seguir para documenta-lo protocolo dunha conversación, aplicado ó exemplo de *IWorkerModel*.
::
/**
* This interface contains the operations to create/edit a worker. The
* creation/edition process of a worker is conversational. <br/>
*
* <strong>Conversation state</strong>: the <code>Worker</code> instance and
* the associated <code>CriterionSatisfaction</code> instances. Some of the
* <code>CriterionSatisfaction</code> instances represent laboral history items
* (e.g. paternity leave) and others represent locations. <br/>
*
* <strong>Non conversational steps</strong>: <code>getWorkers</code> (to
* return all workers).
* <br/>
*
* <strong>Conversation protocol:</strong>
* <ul>
* <li>
* Initial conversation step: <code>initCreate</code> (to create
* a worker) or (exclusive) <code>initEdit</code> (to edit an existing
* worker).
* </li>
* <li>
* Intermediate conversation steps: <code>getWorker</code> (to return the
* worker being edited/created), <code>getNonAssignedLocationCriteria</code>
* (to return all non-assigned locations), <code>assignLocations</code> (to
* assign a set of locations), <code>unassignLocations</code> (to unassign a
* set of locations), <code>addLaboralHistoryItem</code> (to add a laboral
* history item), <code>updateLaboralHistoryItem</code> (to update a laboral
* history item), <code>removeLaboralHistoryItem</code> (to remove a laboral
* history item), <code>getLaboralCriteria</code> (to return all
* <code>Criterion</code> instances representing laboral relationships).
* </li>
* <li>
* Final conversational step: <code>confirm</code> (to save the worker being
* edited/created) or (exclusive) <code>cancel</code> (to discard changes).
* </li>
* </ul>
*
* @author ...
*/
_`Figura 32` : Exemplo de documentación da semántica dunha conversación (IWorkerModel).
Outros aspectos
~~~~~~~~~~~~~~~
* Cando unha operación reciba un parámetro cuxo tipo sexa unha entidade que actúa coma un DTO, o nome do parámetro usará o sufixo Dto (e.g. parámetro *newDataDo* na operación *updateLaboralHistoryItem* da `figura 13`_ ). FIXME[Quizais sexa preciso mover isto á sección 3 máis adiante]

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB