Skip to content

17. Aplicação Web MVC numa arquitetura de 3 camadas – Exemplo 3 – SGBD Firebird

17.1. A base de dados Firebird

Nesta nova versão, iremos armazenar a lista de pessoas numa tabela da base de dados Firebird. Informações sobre a instalação e gestão deste SGBD podem ser encontradas no documento [http://tahe.developpez.com/divers/sql-firebird/]. As capturas de ecrã abaixo são do IBExpert, um cliente de administração para os SGBDs Interbase e Firebird.

A base de dados tem o nome [dbpersonnes.gdb]. Contém uma tabela chamada [PERSONNES]:

Image

A tabela [PERSONNES] conterá a lista de pessoas geridas pela aplicação web. Foi criada utilizando as seguintes instruções SQL:

CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);


ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);
  • Linhas 2–10: A estrutura da tabela [PERSONNES], concebida para armazenar objetos do tipo [Person], reflete a estrutura desse objeto. Uma vez que o tipo booleano não existe no Firebird, o campo [MARRIED] (linha 8) foi declarado como sendo do tipo [SMALLINT], um inteiro. O seu valor será 0 (solteiro) ou 1 (casado).
  • Linhas 13–16: restrições de integridade que espelham as do validador de dados [ValidatePerson].
  • Linha 19: O campo ID é a chave primária da tabela [PERSONNES]

A tabela [PERSONNES] poderia ter o seguinte conteúdo:

Image

A base de dados [dbpersonnes.gdb] contém, além da tabela [PERSONNES], um objeto chamado gerador denominado [GEN_PERSONNES_ID]. Este gerador produz números inteiros sequenciais que utilizaremos para atribuir um valor à chave primária [ID] da tabela [PERSONNES]. Vejamos um exemplo para ilustrar como funciona:

Podemos ver que o valor do gerador [GEN_PERSONNES_ID] mudou (clique duas vezes nele + F5 para atualizar):

 

A instrução SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

retorna, portanto, o seguinte valor para o gerador [GEN_PERSONNES_ID]. GEN_ID é uma função interna do Firebird e [RDB$DATABASE] é uma tabela de sistema neste SGBD.

17.2. O projeto Eclipse para as camadas [dao] e [service]

Para desenvolver as camadas [dao] e [service] da nossa aplicação de base de dados, utilizaremos o seguinte projeto Eclipse [mvc-personnes-03]:

Image

O projeto é um projeto Java simples, não um projeto web Tomcat. Lembre-se de que a versão 2 da nossa aplicação utilizará a camada [web] da versão 1. Esta camada, portanto, não precisa de ser escrita.


Pasta [src]


Esta pasta contém o código-fonte das camadas [dao] e [service]:

Image

Contém vários pacotes:

  • [istia.st.mvc.personnes.dao]: contém a camada [dao]
  • [istia.st.mvc.personnes.entites]: contém a classe [Person]
  • [istia.st.mvc.people.service]: contém a classe [service]
  • [istia.st.mvc.personnes.tests]: contém os testes JUnit para as camadas [dao] e [service]

bem como ficheiros de configuração que devem estar no ClassPath da aplicação.


pasta [database]


Esta pasta contém a base de dados Firebird para pessoas:

Image

  • [dbpersonnes.gdb] é a base de dados.
  • [dbpersonnes.sql] é o script SQL para gerar a base de dados:
/******************************************************************************/
/*** Generated by IBExpert 2006.03.07 27/04/2006 10:27:11 ***/
/******************************************************************************/

SET SQL DIALECT 3;

SET NAMES NONE;

CREATE DATABASE 'C:\data\2005-2006\webjava\dvp-spring-mvc\mvc-38\database\DBPERSONNES.GDB'
USER 'SYSDBA' PASSWORD 'masterkey'
PAGE_SIZE 16384
DEFAULT CHARACTER SET NONE;



/******************************************************************************/
/*** Generators ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/*** Tables ***/
/******************************************************************************/



CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);

INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (1, 1, 'Major', 'Joachim', '1984-11-13', 1, 2);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (2, 1, 'Humbort', 'Mélanie', '1985-02-12', 0, 1);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (3, 1, 'Lemarchand', 'Charles', '1986-03-01', 0, 0);

COMMIT WORK;



/* Check constraints definition */

ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


/******************************************************************************/
/*** Primary Keys ***/
/******************************************************************************/

ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);

Pasta [lib]


Esta pasta contém os ficheiros necessários para a aplicação:

Repare na presença do controlador JDBC [firebirdsql-full.jar] para o SGBD Firebird, bem como de vários ficheiros [spring-*.jar]. Poderíamos ter utilizado o único ficheiro [spring.jar] encontrado na pasta [dist] da distribuição, que contém todas as classes do Spring. Também podemos utilizar apenas os arquivos necessários para o projeto. Foi isso que fizemos aqui, guiados pelos erros de classes em falta relatados pelo Eclipse e pelos nomes dos arquivos parciais do Spring. Todos estes arquivos da pasta [lib] foram colocados no Classpath do projeto.


Pasta [dist]


Esta pasta conterá os arquivos resultantes da compilação das classes da aplicação:

Image

  • [personnes-dao.jar]: arquivo da camada [dao]
  • [personnes-service.jar]: arquivo da camada [service]

17.3. A camada [dao]

17.3.1. Componentes da camada [dao]

A camada [dao] é composta pelas seguintes classes e interfaces:

Image

  • [IDao] é a interface fornecida pela camada [dao]
  • [DaoImplCommon] é uma implementação desta interface, na qual o grupo de pessoas é armazenado numa tabela de base de dados. [DaoImplCommon] agrupa funcionalidades independentes do SGBD.
  • [DaoImplFirebird] é uma classe derivada de [DaoImplCommon] para gerir especificamente uma base de dados Firebird.
  • [DaoException] é o tipo de exceções não tratadas lançadas pela camada [dao]. Esta classe é da versão 1.

A interface [IDao] é a seguinte:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
    // list of all persons
    Collection getAll();
    // find a specific person
    Personne getOne(int id);
    // add/modify a person
    void saveOne(Personne personne);
    // delete a person
    void deleteOne(int id);
}
  • A interface tem os mesmos quatro métodos da versão anterior.

A classe [DaoImplCommon] que implementa esta interface será a seguinte:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

    // list of persons
    public Collection getAll() {
...
    }

    // get a specific person
    public Personne getOne(int id) {
...
    }

    // deleting a person
    public void deleteOne(int id) {
...
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

    // add a person
    protected void insertPersonne(Personne personne) {
...
    }

    // edit a person
    protected void updatePersonne(Personne personne) {
...
    }

    // person validity check
    private void check(Personne p) {
...
    }

...
}
  • linhas 8–9: a classe [DaoImpl] implementa a interface [IDao] e, portanto, os quatro métodos [getAll, getOne, saveOne, deleteOne].
  • linhas 27–37: O método [saveOne] utiliza dois métodos internos, [insertPerson] e [updatePerson], dependendo se é necessário adicionar ou modificar uma pessoa.
  • linha 50: o método privado [check] é o mesmo da versão anterior. Não o abordaremos novamente aqui.
  • Linha 8: Para implementar a interface [IDao], a classe [DaoImpl] estende a classe Spring [SqlMapClientDaoSupport].

17.3.2. A camada de acesso a dados [iBATIS]

A classe Spring [SqlMapClientDaoSupport] utiliza uma estrutura de terceiros [Ibatis SqlMap] disponível no URL [http://ibatis.apache.org/]:

Image

[iBATIS] é um projeto Apache que facilita a construção de camadas [DAO] orientadas por bases de dados. Com o [iBATIS], a arquitetura da camada de acesso aos dados é a seguinte:

O [iBATIS] situa-se entre a camada [DAO] da aplicação e o controlador JDBC da base de dados. Existem alternativas ao [iBATIS], tais como o [Hibernate]:

Image

A utilização da estrutura [iBATIS] requer dois arquivos [ibatis-common, ibatis-sqlmap], ambos colocados na pasta [lib] do projeto:

A classe [SqlMapClientDaoSupport] encapsula a parte genérica da utilização da estrutura [iBATIS], ou seja, os segmentos de código presentes em todas as camadas [DAO] que utilizam a ferramenta [iBATIS]. Para escrever a parte não genérica do código — ou seja, o código específico da camada [DAO] que estamos a escrever — basta derivar a classe [SqlMapClientDaoSupport]. É isso que estamos a fazer aqui.

A classe [SqlMapClientDaoSupport] é definida da seguinte forma:

Image

Entre os métodos desta classe, um deles permite-nos configurar o cliente [iBATIS] com o qual iremos operar a base de dados:

Image

O objeto [SqlMapClient sqlMapClient] é o objeto [iBATIS] utilizado para aceder a uma base de dados. Por si só, implementa a camada [iBATIS] da nossa arquitetura:

Uma sequência típica de ações com este objeto é a seguinte:

  1. solicitar uma ligação a partir de um conjunto de ligações
  2. abrir uma transação
  3. executar uma série de instruções SQL armazenadas num ficheiro de configuração
  4. fechar a transação
  5. devolver a ligação ao conjunto

Se a nossa implementação [DaoImplCommon] funcionasse diretamente com o [iBATIS], teria de executar esta sequência repetidamente. Apenas a operação 3 é específica da camada [dao]; as outras operações são genéricas. A classe Spring [SqlMapClientDaoSupport] irá tratar das operações 1, 2, 4 e 5 por si própria, delegando a operação 3 à sua classe derivada, neste caso a classe [DaoImplCommon].

Para funcionar, a classe [SqlMapClientDaoSupport] requer uma referência ao objeto iBATIS [SqlMapClient sqlMapClient], que irá tratar da comunicação com a base de dados. Este objeto requer duas coisas para funcionar:

  • um objeto [DataSource] ligado à base de dados a partir da qual solicitará ligações
  • um (ou mais) ficheiros de configuração onde as instruções SQL a executar são externalizadas. De facto, estas não se encontram no código Java. São identificadas por um código num ficheiro de configuração, e o objeto [SqlMapClient sqlMapClient] utiliza este código para executar uma instrução SQL específica.

Uma configuração preliminar da nossa camada [dao] que refletisse a arquitetura acima seria a seguinte:


    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
</bean>

Aqui, a propriedade [sqlMapClient] (linha 3) da classe [DaoImplCommon] (linha 2) é inicializada. É inicializada pelo método [setSqlMapClient] da classe [DaoImpl]. Esta classe não possui este método. É a sua classe pai [SqlMapClientDaoSupport] que o possui. Portanto, é, na verdade, esta classe que é inicializada aqui.

Agora, na linha 4, fazemos referência a um objeto chamado "sqlMapClient" que ainda não foi criado. Como mencionado, este objeto é do tipo [SqlMapClient], um tipo [iBATIS]:

Image

[SqlMapClient] é uma interface. O Spring fornece a classe [SqlMapClientFactoryBean] para obter um objeto que implemente esta interface:

Image

Lembre-se de que pretendemos instanciar um objeto que implemente a interface [SqlMapClient]. Este não parece ser o caso da classe [SqlMapClientFactoryBean]. Esta classe implementa a interface [FactoryBean] (ver acima). Possui o seguinte método [getObject()]:

Image

Quando é solicitado ao Spring uma instância de um objeto que implemente a interface [FactoryBean], ele:

  • cria uma instância [I] da classe — neste caso, cria uma instância do tipo [SqlMapClientFactoryBean].
  • retorna ao método de chamada o resultado do método [I].getObject() — o método [SqlMapClientFactoryBean].getObject() irá retornar um objeto que implementa a interface [SqlMapClient].

Para devolver um objeto que implementa a interface [SqlMapClient], a classe [SqlMapClientFactoryBean] necessita de duas informações necessárias para esse objeto:

  • um objeto [DataSource] conectado à base de dados da qual solicitará conexões
  • um (ou mais) ficheiros de configuração onde estão armazenadas as instruções SQL a executar

A classe [SqlMapClientFactoryBean] possui métodos set para inicializar estas duas propriedades:

Image

Estamos a fazer progressos... O nosso ficheiro de configuração está a tomar forma e fica assim:


<!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
  • linhas 2-3: o bean "sqlMapClient" é do tipo [SqlMapClientFactoryBean]. Pelo que acabámos de explicar, sabemos que quando solicitamos ao Spring uma instância deste bean, obtemos um objeto que implementa a interface iBATIS [SqlMapClient]. É este objeto que será, portanto, obtido na linha 14.
  • linhas 7-9: especificamos que o ficheiro de configuração exigido pelo objeto [SqlMapClient] do iBATIS se chama «sql-map-config-firebird.xml» e que deve estar localizado no ClassPath da aplicação. O método [SqlMapClientFactoryBean].setConfigLocation é utilizado aqui.
  • Linhas 4–6: Inicializamos a propriedade [dataSource] do [SqlMapClientFactoryBean] utilizando o seu método [setDataSource].

Linha 5: Referenciamos um bean chamado "dataSource" que ainda não foi criado. Se analisarmos o parâmetro esperado pelo método [setDataSource] de [SqlMapClientFactoryBean], vemos que é do tipo [DataSource]:

Image

Mais uma vez, estamos a lidar com uma interface para a qual precisamos de encontrar uma classe de implementação. O papel dessa classe é fornecer de forma eficiente a uma aplicação ligações a uma base de dados específica. Um SGBD não pode manter um grande número de ligações abertas simultaneamente. Para reduzir o número de ligações abertas em qualquer momento, para cada interação com a base de dados, devemos:

  • abrir uma ligação
  • iniciar uma transação
  • executar instruções SQL
  • fechar a transação
  • fechar a ligação

Abrir e fechar ligações repetidamente é demorado. Para resolver estas duas questões — limitar o número de ligações abertas em qualquer momento e reduzir a sobrecarga de as abrir e fechar — as classes que implementam a interface [DataSource] procedem frequentemente da seguinte forma:

  • Após a instanciação, elas abrem N ligações à base de dados de destino. N tem geralmente um valor predefinido e pode normalmente ser definido num ficheiro de configuração. Estas N ligações permanecem abertas em todos os momentos e formam um conjunto de ligações disponíveis para os threads da aplicação.
  • Quando uma thread da aplicação solicita uma ligação, o objeto [DataSource] fornece-lhe uma das N ligações abertas no arranque, caso ainda haja alguma disponível. Quando a aplicação fecha a ligação, esta não é realmente fechada, mas simplesmente devolvida ao conjunto de ligações disponíveis.

Existem várias implementações disponíveis gratuitamente da interface [DataSource]. Aqui, utilizaremos a implementação [commons DBCP] disponível no URL [http://jakarta.apache.org/commons/dbcp/]:

Image

A utilização da ferramenta [commons DBCP] requer dois arquivos [commons-dbcp, commons-pool], ambos colocados na pasta [lib] do projeto:

A classe [BasicDataSource] do [commons DBCP] fornece a implementação [DataSource] de que precisamos:

Image

Esta classe irá fornecer-nos um conjunto de ligações para aceder à base de dados Firebird da nossa aplicação [dbpersonnes.gdb]. Para tal, temos de lhe fornecer as informações necessárias para criar as ligações nesse conjunto:

  1. o nome do controlador JDBC a utilizar – inicializado com [setDriverClassName]
  2. o URL da base de dados a utilizar – inicializado com [setUrl]
  3. o nome de utilizador do utilizador proprietário da ligação – inicializado com [setUsername] (e não setUserName, como seria de esperar)
  4. a sua palavra-passe – inicializada com [setPassword]

O ficheiro de configuração para a nossa camada [dao] poderia ter o seguinte aspeto:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- warning: do not leave spaces between the two <value> tags in the url -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • linhas 7–9: o nome do controlador JDBC para o SGBD Firebird
  • linhas 11-13: o URL da base de dados Firebird [dbpersonnes.gdb]. Preste muita atenção à forma como isto está escrito. Não deve haver espaços entre as tags <value> e o URL.
  • linhas 14-16: o proprietário da ligação – neste caso, [sysdba], que é o administrador predefinido para as distribuições Firebird
  • Linhas 17–19: a sua palavra-passe [masterkey] — também o valor predefinido

Fizemos progressos significativos, mas ainda há alguns pontos de configuração a esclarecer: a linha 28 faz referência ao ficheiro [sql-map-config-firebird.xml], que deve configurar o iBATIS [SqlMapClient]. Antes de examinar o seu conteúdo, vamos mostrar a localização destes ficheiros de configuração no nosso projeto Eclipse:

Image

  • [spring-config-test-dao-firebird.xml] é o ficheiro de configuração para a camada [dao] que acabámos de examinar
  • [sql-map-config-firebird.xml] é referenciado por [spring-config-test-dao-firebird.xml]. Vamos examiná-lo.
  • [personnes-firebird.xml] é referenciado por [sql-map-config-firebird.xml]. Vamos examiná-lo.

Os três ficheiros acima mencionados estão localizados na pasta [src]. No Eclipse, isto significa que, em tempo de execução, estarão presentes na pasta [bin] do projeto (não mostrada acima). Esta pasta faz parte do ClassPath da aplicação. Por conseguinte, os três ficheiros acima mencionados estarão presentes no ClassPath da aplicação. Isto é necessário.

O ficheiro [sql-map-config-firebird.xml] é o seguinte:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">
 
<sqlMapConfig>
    <sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
  • Este ficheiro deve ter <sqlMapConfig> como sua tag raiz (linhas 6 e 8)
  • Linha 7: A tag <sqlMap> é utilizada para especificar os ficheiros que contêm as instruções SQL a executar. Frequentemente — embora não necessariamente — existe um ficheiro por tabela. Isto permite que as instruções SQL para uma determinada tabela sejam agrupadas num único ficheiro. No entanto, são comuns instruções SQL que envolvem várias tabelas. Nesses casos, a estrutura anterior não se aplica. É simplesmente importante lembrar que todos os ficheiros designados pelas tags <sqlMap> serão fundidos. Estes ficheiros são procurados no ClassPath da aplicação.

O ficheiro [personnes-firebird.xml] descreve as instruções SQL que serão executadas na tabela [PERSONNES] na base de dados Firebird [dbpersonnes.gdb]. O seu conteúdo é o seguinte:


<?xml version="1.0" encoding="UTF-8" ?>
 
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
 
<sqlMap>
    <!-- alias class [Person] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - object [Person] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- list of all persons -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- get a specific person -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#value#</select>
    <!-- add a person -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        <selectKey keyProperty="id">
            SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
        </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
        #nbEnfants#) </insert>
    <!-- update a person -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
        VERSION=#version#</update>
    <!-- delete a person -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
</sqlMap>
  • O ficheiro deve ter <sqlMap> como tag raiz (linhas 7 e 45)
  • Linhas 9–10: Para facilitar a escrita do ficheiro, atribuímos o alias [Person.class] à classe [istia.st.springmvc.personnes.entites.Person].
  • Linhas 12–21: definem os mapeamentos entre as colunas da tabela [PERSONNES] e os campos do objeto [Personne].
  • Linhas 23–24: A instrução SQL [SELECT] para recuperar todas as pessoas da tabela [PERSONNES]
  • linhas 26–27: a instrução SQL [SELECT] para recuperar uma pessoa específica da tabela [PERSONNES]
  • Linhas 29–36: a instrução SQL [insert] que insere uma pessoa na tabela [PERSONS]
  • linhas 38-41: a instrução SQL [update] que atualiza uma pessoa na tabela [PERSONS]
  • linhas 42–44: o comando SQL [delete] que elimina uma pessoa da tabela [PERSONS]

O papel e o significado do conteúdo do ficheiro [people-firebird.xml] serão explicados através de uma análise da classe [DaoImplCommon], que implementa a camada [dao].

17.3.3. A classe [DaoImplCommon]

Vamos rever a arquitetura de acesso aos dados:

A classe [DaoImplCommon] é a seguinte:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

    // list of persons
    public Collection getAll() {
...
    }

    // get a specific person
    public Personne getOne(int id) {
...
    }

    // deleting a person
    public void deleteOne(int id) {
...
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

    // add a person
    protected void insertPersonne(Personne personne) {
...
    }

    // edit a person
    protected void updatePersonne(Personne personne) {
...
    }

    // person validity check
    private void check(Personne p) {
...
    }

...
}

Vamos analisar os métodos um por um.


getAll


Este método recupera todas as pessoas da lista. O seu código é o seguinte:

1
2
3
4
    // list of persons
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

Primeiro, recordemos que a classe [DaoImplCommon] deriva da classe [SqlMapClientDaoSupport] do Spring. É esta classe que fornece o método [getSqlMapClientTemplate()] utilizado na linha 3 acima. Este método tem a seguinte assinatura:

Image

O tipo [SqlMapClientTemplate] encapsula o objeto [SqlMapClient] da camada [iBATIS]. É através deste que acederemos à base de dados. O tipo SqlMapClient do [iBATIS] poderia ser utilizado diretamente, uma vez que a classe [SqlMapClientDaoSupport] tem acesso ao mesmo:

Image

A desvantagem da classe [iBATIS] SqlMapClient é que ela lança exceções [SQLException], um tipo de exceção controlada, ou seja, uma que deve ser tratada por um bloco try/catch ou declarada na assinatura dos métodos que a lançam. No entanto, lembremo-nos de que a camada [dao] implementa uma interface [IDao] cujos métodos não incluem exceções nas suas assinaturas. Os métodos das classes que implementam a interface [IDao] também não podem, portanto, ter exceções nas suas assinaturas. Temos, portanto, de interceptar todas as [SQLException] lançadas pela camada [iBATIS] e encapsulá-las numa exceção não verificada. O tipo [DaoException] do nosso projeto seria adequado para esta encapsulação.

Em vez de tratarmos estas exceções nós próprios, iremos confiá-las ao tipo Spring [SqlMapClientTemplate], que encapsula o objeto [SqlMapClient] da camada [iBATIS]. De facto, o [SqlMapClientTemplate] foi concebido para interceptar exceções [SQLException] lançadas pela camada [SqlMapClient] e encapsulá-las num tipo de exceção não tratada [ DataAccessException]. Este comportamento convém-nos. Basta lembrarmo-nos de que a camada [dao] é agora capaz de lançar dois tipos de exceções não tratadas:

  • o nosso tipo personalizado [DaoException]
  • o tipo [DataAccessException] do Spring

O tipo [SqlMapClientTemplate] é definido da seguinte forma:

Image

Ele implementa a seguinte interface [SqlMapClientOperations]:

Image

Esta interface define métodos capazes de utilizar o conteúdo do ficheiro [people-firebird.xml]:

[queryForList]

Image

Este método permite emitir uma instrução [SELECT] e recuperar o resultado como uma lista de objetos:

  • [statementName]: o identificador (id) da instrução [select] no ficheiro de configuração
  • [parameterObject]: o objeto «parameter» para um [SELECT] parametrizado. O objeto «parameter» pode assumir duas formas:
    • um objeto em conformidade com o padrão JavaBean: os parâmetros da instrução [SELECT] são, então, os nomes dos campos do JavaBean. Quando a instrução [SELECT] é executada, estes são substituídos pelos valores desses campos.
    • um dicionário: os parâmetros da instrução [select] são, então, as chaves do dicionário. Quando a instrução [select] é executada, estas são substituídas pelos seus valores associados no dicionário.
  • Se a instrução [SELECT] não devolver nenhuma linha, o resultado [List] é um objeto vazio, mas não nulo (a verificar).

[queryForObject]

Image

Este método é conceptualmente idêntico ao anterior, mas devolve apenas um único objeto. Se o [SELECT] não devolver nenhuma linha, o resultado é o ponteiro nulo.

[insert]

Image

Este método executa uma instrução SQL [insert] configurada pelo segundo parâmetro. O objeto devolvido é a chave primária da linha que foi inserida. Não é necessário utilizar este resultado.

[update]

Image

Este método executa uma instrução SQL [update] configurada pelo segundo parâmetro. O resultado é o número de linhas modificadas pela instrução SQL [update].

[delete]

Image

Este método executa uma instrução SQL [delete] configurada pelo segundo parâmetro. O resultado é o número de linhas eliminadas pela instrução SQL [delete].

Voltemos ao método [getAll] da classe [DaoImplCommon]:

1
2
3
4
    // list of persons
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • Linha 4: A instrução [select] denominada "Person.getAll" é executada. Não tem parâmetros, pelo que o objeto "parameter" é nulo.

Em [people-firebird.xml], a instrução [select] denominada "Person.getAll" é a seguinte:


<?xml version="1.0" encoding="UTF-8" ?>
 
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
 
<sqlMap>
    <!-- alias class [Person] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - object [Person] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- list of all persons -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • Linha 23: A instrução SQL "Person.getAll" não é parametrizada (não há parâmetros no texto da consulta).
  • A linha 3 do método [getAll] solicita a execução da consulta [select] denominada «Personne.getAll». Esta consulta será executada. O [iBATIS] baseia-se no JDBC. Sabemos, portanto, que o resultado da consulta será devolvido como um objeto [ResultSet]. Linha 23: o atributo [resultMap] da tag <select> indica ao [iBATIS] qual o «resultMap» a utilizar para converter cada linha do [ResultSet] obtido num objeto. É o "resultMap" [Person.map] definido nas linhas 12–21 que especifica como mapear uma linha da tabela [PERSONNES] para um objeto do tipo [Person]. O [iBATIS] utilizará estes mapeamentos para devolver uma lista de objetos [Person] com base nas linhas do [ResultSet].
  • A linha 3 do método [getAll] devolve então uma coleção de objetos [Person]
  • O método [queryForList] pode lançar uma [DataAccessException] do Spring. Deixamos que ela se propague.

Explicaremos os outros métodos da classe [AbstractDaoImpl] de forma mais sucinta, uma vez que os fundamentos da utilização do [iBATIS] já foram abordados na discussão do método [getAll].


getOne


Este método recupera uma pessoa identificada pelo seu [id]. O seu código é o seguinte:

        // get a specific person
    public Personne getOne(int id) {
        // it is retrieved from the BD
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
        // did we recover anything?
        if (personne == null) {
            // throw an exception
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
        // we return the person
        return personne;
    }
  • linha 4: solicita a execução da instrução [select] denominada "Person.getOne". Isto corresponde ao seguinte no ficheiro [people-firebird.xml]:

<!-- get a specific person -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#value#</select>

A consulta SQL é configurada pelo parâmetro #value# (linha 4). O atributo #value# especifica o valor do parâmetro passado para a consulta SQL quando esse parâmetro é de um tipo simples: Integer, Double, String, etc. Nos atributos da tag <select>, o atributo [parameterClass] indica que o parâmetro é do tipo Integer (linha 2). Na linha 5 de [getOne], vemos que este parâmetro é o ID da pessoa que está a ser pesquisada, na forma de um objeto Integer. Esta conversão de tipo é obrigatória, uma vez que o segundo parâmetro de [queryForList] deve ser do tipo [Object].

O resultado da consulta [select] será convertido num objeto através do atributo [resultMap="Personne.map"] (linha 2). Obteremos, portanto, um tipo [Personne].

  • Linhas 7–11: Se a consulta [select] não devolveu nenhuma linha, recuperamos o ponteiro nulo da linha 4. Isto significa que a pessoa procurada não foi encontrada. Neste caso, lançamos uma [DaoException] com o código 2 (linhas 9–10).
  • linha 13: se não ocorreu nenhuma exceção, então o objeto [Person] solicitado é devolvido.

deleteOne


Este método permite eliminar uma pessoa identificada pelo seu [id]. O seu código é o seguinte:

    // deleting a person
    public void deleteOne(int id) {
        // we delete the person
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
        // have we succeeded
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • linhas 4-5: solicita a execução do comando [delete] denominado "Person.deleteOne". Trata-se do seguinte no ficheiro [people-firebird.xml]:

<!-- delete a person -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>

O comando SQL é configurado pelo parâmetro #value# (linha 3) do tipo [parameterClass="int"] (linha 2). Este será o ID da pessoa que está a ser procurada (linha 5 de deleteOne)

  • linha 4: o resultado do método [SqlMapClientTemplate].delete é o número de linhas eliminadas.
  • Linhas 7–8: Se a consulta [delete] não eliminou nenhuma linha, isso significa que a pessoa não existe. É lançada uma [DaoException] com o código 2 (linha 8).

saveOne


Este método permite adicionar uma nova pessoa ou modificar uma já existente. O seu código é o seguinte:

        // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • linha 4: verificamos a validade da pessoa utilizando o método [check]. Este método já existia na versão anterior e tinha sido comentado na altura. Lança uma [DaoException] se a pessoa for inválida. Deixamos esta exceção propagar-se.
  • linha 6: se chegarmos a este ponto, significa que não ocorreu nenhuma exceção. A pessoa é, portanto, válida.
  • Linhas 6–11: Dependendo do ID da pessoa, trata-se de uma adição (ID = -1) ou de uma atualização (ID ≠ -1). Em ambos os casos, são chamados dois métodos internos da classe:
    • insertPersonne: para adicionar
    • updatePersonne: para a atualização

insertPerson


Este método permite adicionar uma nova pessoa. O seu código é o seguinte:

// add a person
    protected void insertPersonne(Personne personne) {
        // 1st version
        personne.setVersion(1);
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // insert the new person in the BD table
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • Linha 4: Defina o número da versão da pessoa que está a ser criada como 1
  • linha 9: insira o registo utilizando a consulta denominada "Person.insertOne", que é a seguinte:

        <insert id="Personne.insertOne" parameterClass="Personne.classe">
            <selectKey keyProperty="id">
                SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
            </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
    #nbEnfants#) </insert>

Esta é uma consulta parametrizada, e o parâmetro é do tipo [Person] (parameterClass="Person.class", linha 1). Os campos do objeto [Person] passado como parâmetro (linha 9 de insertPersonne) são utilizados para preencher as colunas da linha a ser inserida na tabela [PERSONS] (linhas 5–8). Temos um problema para resolver. Durante uma inserção, o objeto [Person] a ser inserido tem um ID igual a -1. Este valor deve ser substituído por uma chave primária válida. Para tal, utilizamos as linhas 2–4 da tag <selectKey> acima. Estas especificam:

  • (continuação)
    • a consulta SQL a executar para obter um valor de chave primária. A que aqui se apresenta é a que apresentámos na Secção 17.1. Há dois pontos que merecem destaque:
      • "as 'value'" é obrigatório. Também pode escrever "as value", mas "value" é uma palavra-chave do Firebird que deve ser colocada entre aspas.
      • A tabela Firebird chama-se, na verdade, [RDB$DATABASE]. No entanto, o caractere $ é interpretado pelo [iBATIS]. Foi escapado duplicando-o.
    • O campo do objeto [Person] que deve ser inicializado com o valor recuperado pela instrução [SELECT], neste caso o campo [id]. Este campo é especificado pelo atributo [keyProperty] na linha 2.
  • Linhas 6-7: Para fins de teste, vamos esperar 10 ms antes de realizar a inserção para verificar se há conflitos entre threads que tentam fazer adições simultaneamente.

updatePerson


Este método permite modificar uma pessoa já existente na tabela [PERSONNES]. O seu código é o seguinte:

// edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • Uma atualização pode falhar por pelo menos duas razões:
    1. a pessoa a ser atualizada não existe
    2. a pessoa a ser atualizada existe, mas o segmento que tenta modificá-la não possui a versão correta
  • linhas 7-8: é executada a consulta SQL [update] denominada «Person.updateOne». Tem o seguinte formato:

    <!-- update a person -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
VERSION=#version#</update>
  • (continuação)
    • Linha 2: A consulta é parametrizada e aceita um tipo [Person] como parâmetro (parameterClass="Person.class"). Esta é a pessoa a ser modificada (linha 8 – updatePerson).
    • Queremos apenas modificar a pessoa na tabela [PERSONS] que tenha o mesmo ID e versão que o parâmetro. É por isso que temos a restrição [WHERE ID=#id# and VERSION=#version#]. Se essa pessoa for encontrada, ela é atualizada com a pessoa do parâmetro e a sua versão é incrementada em 1 (linha 3 acima).
  • Linha 9: Recuperamos o número de linhas atualizadas.
  • Linhas 10–11: Se este número for zero, é lançada uma [DaoException] com o código 2, indicando que ou a pessoa a ser atualizada não existe, ou a sua versão mudou entretanto.

17.4. Testes para a camada [dao]

17.4.1. Testar a implementação [DaoImplCommon]

Agora que escrevemos a camada [dao], propomos testá-la com testes JUnit:

Image

Antes de realizar testes exaustivos, podemos começar com um programa [main] simples que irá apresentar o conteúdo da tabela [PERSONNES]. Esta é a classe [MainTestDaoFirebird]:

package istia.st.mvc.personnes.tests;

import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;
import java.util.Iterator;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class MainTestDaoFirebird {
    public static void main(String[] args) {
        IDao dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
        // current list
        Collection personnes = dao.getAll();
        // console display
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

O ficheiro de configuração [spring-config-test-dao-firebird.xml] para a camada [dao], utilizado nas linhas 13–14, é o seguinte:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- warning: do not leave spaces between the two <value> tags -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

Este ficheiro é o que foi abordado na Secção 17.3.2.

Para efeitos de teste, o SGBD Firebird é iniciado. O conteúdo da tabela [PERSONNES] é o seguinte:

Image

A execução do programa [MainTestDaoFirebird] produz a seguinte saída no ecrã:

Image

Conseguimos obter a lista de pessoas com sucesso. Podemos agora prosseguir para o teste JUnit.

O teste JUnit [TestDaoFirebird] é o seguinte:

package istia.st.mvc.personnes.tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;
import junit.framework.TestCase;

public class TestDaoFirebird extends TestCase {

    // layer [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // manufacturer
    public void setUp() {
        dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
    }

    // list of persons
    private void doListe(Collection personnes) {
...
    }

    // test1
    public void test1() throws ParseException {
...
    }

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
..
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
....
    }

    // multi-threaded insertions
    public void test6() throws ParseException, InterruptedException{
...
}
  • Os testes [test1] a [test5] são os mesmos da versão 1, exceto o [test4], que sofreu uma ligeira alteração. O teste [test6] é novo. Iremos comentar apenas estes dois testes.

[test4]


O [test4] tem como objetivo testar o método [updatePersonne - DaoImplCommon]. Aqui está o código desse método:

// edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • Linhas 4-5: Esperamos 10 ms. Isto força o thread que está a executar [updatePerson] a perder a CPU, o que pode aumentar as nossas hipóteses de observar conflitos de acesso entre threads simultâneas.

[test4] lança N=100 threads encarregadas de incrementar simultaneamente o número de filhos da mesma pessoa em 1. Queremos ver como os conflitos de versão e os conflitos de acesso são tratados.

    public void test4() throws Exception {
        // add a person
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
        // creation of N child update threads
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // we pick up the person
        p1 = dao.getOne(id1);
        // she must have N children
        assertEquals(N, p1.getNbEnfants());
        // delete person p1
        dao.deleteOne(p1.getId());
        // check
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

Os threads são criados nas linhas 8–13. Cada um deles incrementará em 1 o número de filhos da pessoa criada nas linhas 3–5. Os threads de atualização [ThreadDaoMajEnfants] são os seguintes:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
    // thread name
    private String name;

    // reference on the [dao] layer
    private IDao dao;

    // the id of the person we're going to work on
    private int idPersonne;

    // manufacturer
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // we loop until we have succeeded in incrementing by 1
        // person's number of children idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
            // follow-up
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
            // 10 ms wait to abandon processor
            try {
                // follow-up
                suivi("début attente");
                // we pause to let the processor
                Thread.sleep(10);
                // follow-up
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            int codeErreur = 0;
            try {
                // increments by 1 the number of children in this copy
                personne.setNbEnfants(nbEnfants + 1);
                // we try to modify the original
                dao.saveOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (DaoException ex) {
                // we retrieve the error code
                codeErreur = ex.getCode();
                // if a ID or error code 2 version error occurs, retry the update
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                    // unhandled exception - left to rise
                    throw ex;
                }
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // follow-up
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}

A atualização de uma pessoa pode falhar porque a pessoa que pretendemos modificar não existe ou porque foi previamente atualizada por outro segmento de execução. Estes dois casos são tratados aqui nas linhas 67–69. Em ambos os casos, o método [updatePersonne] lança uma [DaoException] com o código 2. O segmento de execução será então forçado a reiniciar o procedimento de atualização desde o início (loop while, linha 34).


[test6]


[test6] destina-se a testar o método [insertPersonne - DaoImplCommon]. Aqui está o código desse método:

// add a person
    protected void insertPersonne(Personne personne) {
        // 1st version
        personne.setVersion(1);
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // insert the new person in the BD table
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • Linhas 6-7: Esperamos 10 ms para forçar o thread que executa [insertPerson] a perder a CPU, aumentando assim as nossas hipóteses de observar conflitos causados por threads a realizar inserções ao mesmo tempo.

O código para [test6] é o seguinte:

    // multi-threaded insertions
    public void test6() throws ParseException, InterruptedException{
        // creation of a person
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        // duplicated N times in an array
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
        // creation of N insertion threads - each thread inserts 1 person
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoInsertPersonne("thread n° " + i, dao, personnes[i]);
            taches[i].start();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            // thread n° i
            taches[i].join();
            // supression personne
            dao.deleteOne(personnes[i].getId());
        }
}

Criamos 100 threads que irão inserir simultaneamente 100 pessoas diferentes. Estas 100 threads irão todas obter uma chave primária para a pessoa que precisam de inserir, sendo depois pausadas durante 10 ms (linha 10 – insertPerson) antes de poderem realizar a inserção. Queremos verificar se tudo corre bem e, em particular, se obtêm efetivamente valores de chave primária diferentes.

  • Linhas 7–11: É criada uma matriz com 100 pessoas. Todas estas pessoas são cópias da pessoa p criada nas linhas 4–5.
  • Linhas 14–17: São lançadas as 100 threads de inserção. Cada uma é responsável por inserir uma das 100 pessoas criadas anteriormente.
  • Linhas 19–23: [test6] aguarda que cada uma das 100 threads que lançou termine. Quando deteta que a thread número i terminou, elimina a pessoa que essa thread acabou de inserir.

A thread de inserção [ThreadDaoInsertPersonne] é a seguinte:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoInsertPersonne extends Thread {
    // thread name
    private String name;

    // reference on the [dao] layer
    private IDao dao;

    // the id of the person we're going to work on
    private Personne personne;

    // manufacturer
    public ThreadDaoInsertPersonne(String name, IDao dao, Personne personne) {
        this.name = name;
        this.dao = dao;
        this.personne = personne;
    }

    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // insertion
        dao.saveOne(personne);
        // follow-up
        suivi("a terminé");
    }

    // follow-up
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • Linhas 19–22: O construtor da thread armazena a pessoa a ser inserida e a camada [DAO] a ser utilizada para a inserção.
  • linha 30: a pessoa é inserida. Se ocorrer uma exceção, esta é propagada para [test6].

Testes


Durante os testes, obtemos os seguintes resultados:

O teste [test4] falha, portanto. O número de filhos baixou para 69, em vez dos 100 esperados. O que aconteceu? Vamos examinar os registos de ecrã. Estes mostram a existência de exceções lançadas pelo Firebird:


Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];   
--- The error occurred in personnes-firebird.xml.  
--- The error occurred while applying a parameter map.  
--- Check the Personne.updateOne-InlineParameterMap.  
--- Check the statement (update failed).  
--- Cause: org.firebirdsql.jdbc.FBSQLException: GDS Exception. 335544336. deadlock
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--- The error occurred in personnes-firebird.xml.  
--- The error occurred while applying a parameter map.  
  • Linha 1 – Ocorreu uma exceção Spring [org.springframework.jdbc.UncategorizedSQLException]. Esta é uma exceção não capturada que foi utilizada para envolver uma exceção lançada pelo controlador JDBC do Firebird, descrita na linha 6.
  • Linha 6 – O controlador JDBC do Firebird lançou uma exceção do tipo [org.firebirdsql.jdbc.FBSQLException] com o código de erro 335544336.
  • Linha 7: indica que houve um conflito de concorrência entre duas threads que estavam a tentar atualizar a mesma linha na tabela [PERSONNES] ao mesmo tempo.

Este não é um erro fatal. A thread que captura esta exceção pode tentar novamente a atualização. Para tal, modifique o código em [ThreadDaoMajEnfants]:

            try {
                // incrémente de 1 le nbre d'enfants de cette copie
                personne.setNbEnfants(nbEnfants + 1);
                // on essaie de modifier l'original
                dao.saveOne(personne);
                // on est passé - l'original a été modifié
                fini = true;
            } catch (DaoException ex) {
                // on récupère le code erreur
                codeErreur = ex.getCode();
                // si une erreur d'ID ou de version de code ereur 2, on réessaie la mise à jour
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                    // exception non gérée - on laisse remonter
                    throw ex;
                }
  • Linha 8: Tratamos uma exceção do tipo [DaoException]. Com base no que foi dito, devemos tratar a exceção que surgiu durante os testes, do tipo [org.springframework.jdbc.UncategorizedSQLException]. No entanto, não podemos simplesmente tratar este tipo, que é um tipo genérico do Spring destinado a encapsular exceções que não reconhece. O Spring reconhece exceções lançadas pelos controladores JDBC de vários SGBDs, como Oracle, MySQL, Postgres, DB2, SQL Server, ... mas não o Firebird. Portanto, qualquer exceção lançada pelo controlador JDBC do Firebird é encapsulada no tipo Spring [org.springframework.jdbc.UncategorizedSQLException]:

Image

Como mostrado acima, a classe [UncategorizedSQLException] deriva da classe [DataAccessException] mencionada na secção 17.3.3. É possível determinar qual a exceção que foi encapsulada na [UncategorizedSQLException] utilizando o seu método [getSQLException]:

Image

Esta [SQLException] é a que é lançada pela camada [iBATIS], que por sua vez encapsula a exceção lançada pelo controlador JDBC da base de dados. A causa exata da [SQLException] pode ser obtida utilizando o método:

Image

Obtemos o objeto do tipo [Throwable] que foi lançado pelo controlador JDBC:

Image

O tipo [Throwable] é a classe pai de [Exception].

Aqui, precisamos de verificar se o objeto [Throwable] lançado pelo controlador JDBC do Firebird — que causou a [SQLException] lançada pela camada [iBATIS] — é, de facto, uma exceção do tipo [org.firebirdsql.gds.GDSException] com o código de erro 335544336. Para recuperar o código de erro, podemos utilizar o método [getErrorCode()] da classe [org.firebirdsql.gds.GDSException].

Se utilizarmos a exceção [org.firebirdsql.gds.GDSException] no código [ThreadDaoMajEnfants], então este thread só funcionará com o SGBD Firebird. O mesmo se aplica ao teste [test4] que utiliza este thread. Queremos evitar isso. Na verdade, queremos que os nossos testes JUnit permaneçam válidos independentemente do SGBD utilizado. Para conseguir isso, decidimos que a camada [dao] lançará uma [DaoException] com o código 4 sempre que for detetada uma exceção de «conflito de atualização», independentemente do SGBD subjacente. Assim, o thread [ThreadDaoMajEnfants] pode ser reescrito da seguinte forma:

package istia.st.mvc.personnes.tests;
...

public class ThreadDaoMajEnfants extends Thread {
...

    // thread core
    public void run() {
...
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            int codeErreur = 0;
            try {
                // increments by 1 the number of children in this copy
                personne.setNbEnfants(nbEnfants + 1);
                // we try to modify the original
                dao.saveOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (DaoException ex) {
                // we retrieve the error code
                codeErreur = ex.getCode();
                // if a ID or version 2 error or a deadlock 4 occurs, we
                // try the update again
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                    // unhandled exception - left to rise
                    throw ex;
                }
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • linhas 34-36: a exceção [DaoException] com o código 4 é capturada. O segmento [ThreadDaoMajEnfants] será forçado a reiniciar o procedimento de atualização desde o início (linha 10)

A nossa camada [dao] deve, portanto, ser capaz de reconhecer uma exceção de «conflito de atualização». Esta exceção é lançada por um controlador JDBC e é específica deste. Esta exceção deve ser tratada no método [updatePerson] da classe [DaoImplCommon]:

// edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }

As linhas 7–11 devem estar incluídas num bloco try/catch. Para o SGBD Firebird, precisamos de verificar se a exceção que causou a falha na atualização é do tipo [org.firebirdsql.gds.GDSException] e tem o código de erro 335544336. Se colocarmos este tipo de teste em [DaoImplCommon], iremos vincular esta classe ao SGBD Firebird, o que é obviamente indesejável. Se quisermos manter a classe [DaoImplCommon] de uso geral, precisamos de a derivar e tratar a exceção numa classe específica do Firebird. É isso que estamos a fazer agora.

17.4.2. A classe [DaoImplFirebird]

O seu código é o seguinte:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

public class DaoImplFirebird extends DaoImplCommon {

    // edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        try {
            // we modify the person who has the correct version
            int n = getSqlMapClientTemplate().update("Personne.updateOne",
                    personne);
            if (n == 0)
                throw new DaoException("La personne d'Id [" + personne.getId()
                        + "] n'existe pas ou bien a été modifiée", 2);
        } catch (org.springframework.jdbc.UncategorizedSQLException ex) {
            if (ex.getSQLException().getCause().getClass().isAssignableFrom(
                    org.firebirdsql.jdbc.FBSQLException.class)) {
                org.firebirdsql.jdbc.FBSQLException cause = (org.firebirdsql.jdbc.FBSQLException) ex
                        .getSQLException().getCause();
                if (cause.getErrorCode() == 335544336) {
                    throw new DaoException(
                            "Conflit d'accès au même enregistrement", 4);
                }
            } else {
                throw ex;
            }
        }
    }

    // waiting
    private void wait(int N) {
        // we wait for N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
            // the exception trace is displayed
            e.printStackTrace();
            return;
        }
    }

}
  • Linha 5: A classe [DaoImplFirebird] deriva de [DaoImplCommon], a classe que acabámos de estudar. Ela redefine, nas linhas 8–33, o método [updatePersonne] que nos está a causar problemas.
  • linhas 20: capturamos a exceção Spring do tipo [UncategorizedSQLException]
  • linhas 21–22: verificamos que a exceção subjacente do tipo [SQLException], lançada pela camada [iBATIS], é causada por uma exceção do tipo [org.firebirdsql.jdbc.FBSQLException]
  • Linha 25: verificamos também que o código de erro para esta exceção do Firebird é 335544336, o código de erro «deadlock».
  • linhas 26-27: se todas estas condições forem satisfeitas, é lançada uma [DaoException] com o código 4.
  • linhas 36-44: o método [wait] pausa o thread atual por N milissegundos. É útil apenas para testes.

Estamos prontos para testar a nova camada [dao].

17.4.3. Testar a implementação [DaoImplFirebird]

O ficheiro de configuração de teste [spring-config-test-dao-firebird.xml] é modificado para utilizar a implementação [DaoImplFirebird]:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- warning: do not leave spaces between the two <value> tags -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • Linha 32: a nova implementação [DaoImplFirebird] da camada [dao].

Os resultados do teste [test4], que anteriormente tinha falhado, são os seguintes:

Image

[test4] aprovado. As últimas linhas dos registos de ecrã são as seguintes:

1
2
3
4
5
6
7
thread n° 36 [1145977145984] : fin attente
thread n° 75 [1145977145984] : a terminé et passé le nombre d'enfants à 99
thread n° 36 [1145977146000] : version corrompue ou personne inexistante
thread n° 36 [1145977146000] : 99 -> 100 pour la version 100
thread n° 36 [1145977146000] : début attente
thread n° 36 [1145977146015] : fin attente
thread n° 36 [1145977146031] : a terminé et passé le nombre d'enfants à 100

A última linha indica que o thread n.º 36 foi o último a terminar. A linha 3 mostra um conflito de versão que obrigou o thread n.º 36 a reiniciar o seu procedimento de atualização de pessoas (linha 4). Outros registos mostram conflitos de acesso durante as atualizações:

1
2
3
thread n° 52 [1145977145765] : version corrompue ou personne inexistante
thread n° 75 [1145977145765] : conflit de mise à jour
thread n° 36 [1145977145765] : version corrompue ou personne inexistante

A linha 2 mostra que o thread n.º 75 falhou durante a sua atualização devido a um conflito de atualização: quando o comando SQL [update] foi emitido na tabela [PERSONNES], a linha que precisava de ser atualizada estava bloqueada por outro thread. Este conflito de acesso forçará o thread n.º 75 a tentar novamente a sua atualização.

Para concluir com o [test4], observamos uma diferença significativa em relação aos resultados do mesmo teste na versão 1, onde falhou devido a problemas de sincronização. Uma vez que os métodos na camada [dao] da versão 1 não estavam sincronizados, ocorreram conflitos de acesso. Aqui, não foi necessário sincronizar a camada [dao]. Simplesmente tratámos os conflitos de acesso reportados pelo Firebird.

Vamos agora executar todo o teste JUnit para a camada [dao]:

Image

Parece, portanto, que temos uma camada [dao] válida. Para a declarar válida com um elevado grau de certeza, precisaríamos de realizar mais testes. No entanto, vamos considerá-la operacional.

17.5. A camada [service]

17.5.1. Os componentes da camada [service]

A camada [service] é composta pelas seguintes classes e interfaces:

Image

  • [IService] é a interface apresentada pela camada [serviço]
  • [ServiceImpl] é uma implementação desta interface

A interface [IService] é a seguinte:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
    // list of all persons
    Collection getAll();

    // find a specific person
    Personne getOne(int id);

    // add/modify a person
    void saveOne(Personne personne);

    // delete a person
    void deleteOne(int id);

    // save multiple people
    void saveMany(Personne[] personnes);

    // delete several people
    void deleteMany(int ids[]);
}
  • A interface possui os mesmos quatro métodos da versão 1, mas tem dois adicionais:
    • saveMany: permite guardar várias pessoas ao mesmo tempo de forma atómica. Ou são todas guardadas, ou nenhuma é.
    • deleteMany: permite eliminar várias pessoas ao mesmo tempo de forma atómica. Ou são todas eliminadas, ou nenhuma é.

Estes dois métodos não serão utilizados pela aplicação web. Adicionámo-los para ilustrar o conceito de transação de base de dados. Ambos os métodos devem ser executados dentro de uma transação para alcançar a atomicidade desejada.

A classe [ServiceImpl] que implementa esta interface será a seguinte:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;

public class ServiceImpl implements IService {

    // the [dao] layer
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // list of persons
    public Collection getAll() {
        return dao.getAll();
    }

    // get a specific person
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

    // deleting a person
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

    // save a collection of people
    public void saveMany(Personne[] personnes) {
        // we loop over the people table
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

    // delete a collection of people
    public void deleteMany(int[] ids) {
        // ids: the ids of the people to be deleted
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • Os métodos [getAll, getOne, insertOne, saveOne] chamam os métodos da camada [dao] com os mesmos nomes.
  • Linhas 42–47: O método [saveMany] guarda, um a um, as pessoas na matriz passada como parâmetro.
  • Linhas 50–55: O método [deleteMany] elimina, uma a uma, as pessoas cujos IDs são passados como um parâmetro de matriz.

Mencionámos que os métodos [saveMany] e [deleteMany] devem ser executados dentro de uma transação para garantir a natureza «tudo ou nada» destes métodos. Podemos ver que o código acima ignora completamente este conceito de transações. Isto só aparecerá no ficheiro de configuração da camada [service].

17.5.2. Configuração da camada [ service]

Acima, na linha 11, vemos que a implementação [ServiceImpl] mantém uma referência à camada [dao]. Esta, tal como na versão 1, será inicializada pelo Spring quando a camada [service - ServiceImpl] for instanciada. O ficheiro de configuração que permitirá a instanciação da camada [service] é o seguinte:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- warning: do not leave spaces between the two <value> tags -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- the [dao] layer access class -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- transaction manager -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- access classes to the [service] layer -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • linhas 1–36: configuração da camada [dao]. Esta configuração foi explicada ao abordar a camada [dao] na secção 17.3.2.
  • Linhas 38–64: configurar a camada [service]

Na linha 46, podemos ver que a camada [service] é implementada pelo tipo [TransactionProxyFactoryBean]. Esperávamos encontrar o tipo [ServiceImpl]. [TransactionProxyFactoryBean] é um tipo predefinido do Spring. Como é possível que um tipo predefinido implemente a interface [IService], que é específica da nossa aplicação?

Vamos primeiro dar uma olhada na classe [TransactionProxyFactoryBean]:

Image

Vemos que ela implementa a interface [FactoryBean]. Já nos deparámos com esta interface. Sabemos que, quando uma aplicação solicita uma instância de um tipo que implementa [FactoryBean] ao Spring, o Spring não devolve uma instância [I] desse tipo, mas sim o objeto devolvido pelo método [I].getObject():

Image

No nosso caso, a camada [service] será implementada pelo objeto devolvido por [TransactionProxyFactoryBean].getObject(). Qual é a natureza deste objeto? Não entraremos em detalhes, pois são complexos. Estes enquadram-se no que é conhecido como Spring AOP (Programação Orientada a Aspectos). Tentaremos esclarecer as coisas com alguns diagramas simples. A AOP permite o seguinte:

  • Temos duas classes, C1 e C2, em que C1 utiliza a interface [I2] fornecida por C2:
  • Graças à AOP, podemos colocar um interceptor entre as classes C1 e C2 de uma forma transparente para ambas as classes:

A classe [C1] foi compilada para funcionar com a interface [I2] que [C2] implementa. Em tempo de execução, o AOP coloca a classe [interceptor] entre [C1] e [C2]. Para que isso seja possível, a classe [interceptor] deve, naturalmente, apresentar a mesma interface [I2] à [C1] que [C2] apresenta.

Para que serve isto? A documentação do Spring fornece alguns exemplos. Por exemplo, poderá querer registar chamadas a um método específico M de [C2] para auditar esse método. Em [interceptor], escreveria então um método [M] que executa esses registos. A chamada de [C1] para [C2].M decorrerá da seguinte forma (ver diagrama acima):

  1. [C1] chama o método M de [C2]. Na verdade, é o método M de [interceptor] que será chamado. Isto é possível se [C1] se dirigir a uma interface [I2] em vez de a uma implementação específica de [I2]. Tudo o que é necessário é que [interceptor] implemente [I2].
  2. O método M de [interceptor] regista a informação e chama o método M de [C2] que foi inicialmente visado por [C1].
  3. O método M de [C2] é executado e devolve o seu resultado ao método M de [interceptor], que pode, opcionalmente, adicionar algo ao que foi feito no passo 2.
  4. O método M de [interceptor] devolve um resultado ao método de chamada de [C1]

Vemos que o método M de [interceptor] pode fazer algo antes e depois da chamada ao método M de [C2]. Do ponto de vista de [C1], isso enriquece, portanto, o método M de [C2]. Podemos, assim, ver a tecnologia AOP como uma forma de enriquecer a interface apresentada por uma classe.

Como é que este conceito se aplica à nossa camada [service]? Se implementarmos a camada [service] diretamente com uma instância [ServiceImpl], a nossa aplicação web terá a seguinte arquitetura:

Se implementarmos a camada [service] com uma instância [TransactionProxyFactoryBean], teremos a seguinte arquitetura:

Podemos dizer que a camada [service] é instanciada com dois objetos:

  • o objeto a que nos referimos acima como [proxy transacional], que é, na verdade, o objeto devolvido pelo método [getObject] de [TransactionProxyFactoryBean]. Este objeto atua como interface entre a camada [serviço] e a camada [web]. Por definição, implementa a interface [IService].
  • uma instância de [ServiceImpl], que também implementa a interface [IService]. Só ela sabe como interagir com a camada [dao], pelo que é necessária.

Imaginemos que a camada [web] chama o método [saveMany] da interface [IService]. Sabemos que, funcionalmente, as inserções/atualizações realizadas por este método devem ser feitas dentro de uma transação. Ou todas são bem-sucedidas, ou nenhuma é executada. Apresentámos o método [saveMany] da classe [ServiceImpl] e observámos que lhe faltava o conceito de transação. O método [saveMany] do [proxy transacional] irá melhorar o método [saveMany] da classe [ServiceImpl] com este conceito de transação. Vamos seguir o diagrama acima:

  1. A camada [web] chama o método [saveMany] da interface [IService].
  2. O método [saveMany] do [proxy transacional] é executado. Ele inicia uma transação. Para tal, deve dispor de informação suficiente, nomeadamente um objeto [DataSource] para estabelecer uma ligação ao SGBD. Em seguida, chama o método [saveMany] de [ServiceImpl].
  3. Este método é executado. Ele chama repetidamente a camada [dao] para realizar as inserções ou atualizações. As instruções SQL executadas neste momento são executadas dentro da transação iniciada no passo 2.
  4. Suponha que uma destas operações falhe. A camada [dao] propagará uma exceção até à camada [service], especificamente ao método [saveMany] da instância [ServiceImpl].
  5. Este método não faz nada e permite que a exceção se propague até ao método [saveMany] do [transactional proxy].
  6. Ao receber a exceção, o método [saveMany] do [transactional proxy], que é o proprietário da transação, executa um [rollback] para cancelar todas as atualizações e, em seguida, permite que a exceção se propague até à camada [web], que será responsável por tratá-la.

Na etapa 4, assumimos que uma das inserções ou atualizações falhou. Se não for esse o caso, em [5] nenhuma exceção é propagada. O mesmo se aplica em [6]. Neste caso, o método [saveMany] do [transactional proxy] confirma a transação para validar todas as atualizações.

Temos agora uma visão mais clara da arquitetura implementada pelo bean [TransactionProxyFactoryBean]. Vamos rever a sua configuração:


    <!-- transaction manager -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- access classes to the [service] layer -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

Vamos examinar esta configuração à luz da arquitetura que está definida:

  • O [proxy transacional] irá gerir as transações. O Spring oferece várias estratégias de gestão de transações. O [proxy transacional] requer uma referência ao gestor de transações escolhido.
  • Linhas 11–13: definem o atributo [transactionManager] do bean [TransactionProxyFactoryBean] com uma referência a um gestor de transações. Este é definido nas linhas 2–7.
  • Linhas 2–7: O gestor de transações é do tipo [DataSourceTransactionManager]:

Image

[DataSourceTransactionManager] é um gestor de transações adequado para SGBDs acedidos através de um objeto [DataSource]. Só pode gerir transações num único SGBD. Não pode gerir transações distribuídas por vários SGBDs. Aqui, temos apenas um SGBD. Portanto, este gestor de transações é adequado. Quando o [proxy transacional] inicia uma transação, fá-lo numa ligação associada ao thread. Esta ligação será utilizada em todas as camadas que conduzem à base de dados: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

A classe [DataSourceTransactionManager] precisa de saber a fonte de dados à qual deve solicitar uma ligação para associar à thread. Isto é definido nas linhas 4–6: é a mesma fonte de dados que a utilizada pela camada [dao] (ver secção 17.5.2).

  • Linhas 14–19: O atributo «target» especifica a classe a ser interceptada, neste caso a classe [ServiceImpl]. Esta informação é necessária por duas razões:
    • a classe [ServiceImpl] deve ser instanciada, uma vez que lida com a comunicação com a camada [dao]
    • o [TransactionProxyFactoryBean] deve gerar um proxy que apresente a mesma interface que [ServiceImpl] à camada [web].
  • Linhas 21–27: especificam quais métodos de [ServiceImpl] o proxy deve interceptar. O atributo [transactionAttributes] na linha 21 indica quais métodos de [ServiceImpl] requerem uma transação e quais são os atributos da transação:
  • linha 23: os métodos cujos nomes começam por get [getOne, getAll] são executados dentro de uma transação com os atributos [PROPAGATION_REQUIRED, readOnly]:
    • PROPAGATION_REQUIRED: o método é executado numa transação se já existir uma associada ao segmento; caso contrário, é criada uma nova e o método é executado dentro dela.
    • readOnly: transação de leitura apenas

Aqui, os métodos [getOne] e [getAll] de [ServiceImpl] serão executados dentro de uma transação, mesmo que isso não seja realmente necessário. Cada operação consiste numa única instrução SELECT. Não vemos sentido em colocar esta instrução SELECT dentro de uma transação.

  • Linha 24: Os métodos cujos nomes começam por «save» — [saveOne] e [saveMany] — são executados dentro de uma transação com o atributo [PROPAGATION_REQUIRED].
  • Linha 25: Os métodos [deleteOne] e [deleteMany] de [ServiceImpl] estão configurados de forma idêntica aos métodos [saveOne] e [saveMany].

Na nossa camada [service], apenas os métodos [saveMany] e [deleteMany] precisam de ser executados dentro de uma transação. A configuração poderia ter sido reduzida às seguintes linhas:


        <property name="transactionAttributes">
            <props>
                <prop key="saveMany">PROPAGATION_REQUIRED</prop>
                <prop key="deleteMany">PROPAGATION_REQUIRED</prop>
            </props>
</property>

17.6. Testar a camada [service]

Agora que escrevemos e configuramos a camada [service], vamos testá-la utilizando testes JUnit:

Image

O ficheiro de configuração da camada [service], [spring-config-test-service-firebird.xml], é o descrito na Secção 17.5.2.

O teste JUnit [TestServiceFirebird] é o seguinte:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

    // service] layer
    private IService service;

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

    // setup
    public void setUp() {
        service = (IService) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-service-firebird.xml"))).getBean("service");
    }

    // list of persons
    private void doListe(Collection personnes) {
...
    }

    // test1
    public void test1() throws ParseException {
...
    }

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
...
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
...
    }

        // multi-threaded insertions
    public void test6() throws ParseException, InterruptedException{
...
    }

    // tests of the deleteMany method
    public void test7() throws ParseException {
        // current list
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
        // display
        doListe(personnes);
        // creation of three people
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        Personne p2 = new Personne(-1, "Y", "Y", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/03/2006"), false, 0);
        Personne p3 = new Personne(-2, "Z", "Z", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/04/2006"), true, 2);
        // add 3 people - person p3 with id -2 will cause
        // an exception
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
        // check
        assertTrue(erreur);
        // new list - the number of elements must not have changed
        // because of automatic transaction rollback
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
        // addition of two able-bodied people
        // reset their id to -1
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
        // we retrieve their id
        int id1 = p1.getId();
        int id2 = p2.getId();
        // checks
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
        // new list - must have 2 + elements
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
        // deletion of p1 and p2 and a non-existent person
        // an exception must occur
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
        // check
        assertTrue(erreur);
        // new list
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
        // no person had to be deleted (rollback
        // automatic transaction)
        assertEquals(nbPersonnes4, nbPersonnes3);
        // we remove the two able-bodied people
        service.deleteMany(new int[] { id1, id2 });
        // checks
        // person p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // person p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // new list
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
        // verification - we must be back at the starting point
        assertEquals(nbPersonnes5, nbPersonnes1);
        // display
        doListe(personnes);
    }

}
  • linhas 19–22: o programa testa as camadas [dao] e [service] configuradas pelo ficheiro [spring-config-test-service-firebird.xml], que foi discutido na secção anterior.
  • Os testes [test1] a [test6] são conceptualmente idênticos aos seus homólogos com o mesmo nome na classe de teste [TestDaoFirebird] da camada [dao]. A única diferença é que, por configuração, os métodos [saveOne] e [deleteOne] agora são executados dentro de uma transação.
  • O objetivo do método [test7] é testar os métodos [saveMany] e [deleteMany]. Queremos verificar se eles são de facto executados dentro de uma transação. Vamos comentar o código deste método:
  • linhas 62–63: contamos o número de pessoas [nbPersonnes1] atualmente na lista
  • linhas 67–72: criamos três pessoas
  • linhas 73–83: estas três pessoas são guardadas pelo método [saveMany] – linha 77. As duas primeiras pessoas, p1 e p2, com um ID igual a -1, serão adicionadas à tabela [PERSONNES]. A pessoa p3 tem um ID igual a -2. Portanto, não se trata de uma inserção, mas de uma atualização. Esta atualização irá falhar porque não existe nenhuma pessoa com um ID de -2 na tabela [PERSONS]. A camada [dao] irá, portanto, lançar uma exceção que se propagará até à camada [service]. A existência desta exceção é verificada na linha 83.
  • Devido à exceção anterior, a camada [service] deve reverter todas as instruções SQL emitidas durante a execução do método [saveMany], porque este método é executado dentro de uma transação. Linhas 86–87: Verificamos que o número de pessoas na lista não se alterou, o que significa que as inserções de p1 e p2 não ocorreram.
  • Linhas 88–103: Adicionamos apenas p1 e p2 e verificamos que agora há mais duas pessoas na lista.
  • Linhas 106–114: Eliminamos um grupo de pessoas composto pelas pessoas p1 e p2 que acabámos de adicionar e por uma pessoa inexistente (id = -1). O método [deleteMany] é utilizado para este efeito, linha 108. Este método irá falhar porque não existe nenhuma pessoa com um id igual a –1 na tabela [PERSONNES]. A camada [dao] irá, portanto, lançar uma exceção que se propagará até à camada [service]. A existência desta exceção é verificada na linha 114.
  • Devido à exceção anterior, a camada [service] deve efetuar um [rollback] de todas as instruções SQL emitidas durante a execução do método [deleteMany], uma vez que este método é executado no âmbito de uma transação. Linhas 116–117: Verificamos se o número de pessoas na lista não se alterou e se, por conseguinte, as eliminações de p1 e p2 não ocorreram.
  • Linha 122: Eliminamos um grupo composto exclusivamente pelas pessoas p1 e p2. Isto deve ser bem-sucedido. O resto do método verifica se é realmente esse o caso.

A execução dos testes produz os seguintes resultados:

Image

Todos os sete testes foram bem-sucedidos. Consideraremos a nossa camada [service] como operacional.

17.7. A camada [w eb]

Vamos rever a arquitetura geral da aplicação web a ser construída:

Acabámos de criar as camadas [dao] e [service] para trabalhar com uma base de dados Firebird. Escrevemos uma versão 1 desta aplicação em que as camadas [dao] e [service] trabalhavam com uma lista de pessoas na memória. A camada [web] escrita nessa altura continua válida. Com efeito, interagia com uma camada [service] que implementava a interface [IService]. Uma vez que a nova camada [service] implementa esta mesma interface, a camada [web] não precisa de ser modificada.

No artigo anterior, a versão 1 da aplicação foi testada com o projeto Eclipse [mvc-personnes-02B], onde as camadas [web, service, dao, entities] foram empacotadas em ficheiros .jar:

A pasta [src] estava vazia. As classes das camadas estavam nos arquivos [people-*.jar]:

Para testar a versão 2, no Eclipse duplicamos a pasta [mvc-personnes-02B] para [mvc-personnes-03B] (copiar/colar):

Image

No projeto [mvc-personnes-03], exportamos [Arquivo / Exportar / Arquivo Jar] as camadas [DAO] e [service], respetivamente, para os arquivos [personnes-dao.jar] e [personnes-service.jar] na pasta [dist] do projeto:

Image

Copiamos estes dois ficheiros e, em seguida, no Eclipse, colamo-los na pasta [WEB-INF/lib] do projeto [mvc-personnes-03B], onde substituirão os ficheiros com o mesmo nome da versão anterior.

Copiamos e colamos também os arquivos [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] da pasta [lib] do projeto [mvc-personnes-03] para a pasta [WEB-INF/lib] do projeto [mvc-personnes-03B]. Estes ficheiros JAR são necessários para as novas camadas [dao] e [service].

Depois de fazer isso, incluímos os novos ficheiros JAR no Classpath do projeto: [clique com o botão direito do rato no projeto -> Propriedades -> Java Build Path -> Adicionar JARs].

A pasta [src] contém os ficheiros de configuração para as camadas [dao] e [service]:

Image

O ficheiro [spring-config.xml] configura as camadas [dao] e [service] da aplicação web. Na nova versão, é idêntico ao ficheiro [spring-config-test-service-firebird.xml] utilizado para configurar o teste da camada de serviço no projeto [mvc-personnes-03]. Por isso, copiamos e colamos de um para o outro:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- warning: do not leave spaces between the two <value> tags -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- the [dao] layer access class -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- transaction manager -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- access classes to the [service] layer -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • Linha 12: o URL da base de dados Firebird. Continuamos a utilizar a base de dados utilizada para testar as camadas [dao] e [service]

Implementamos o projeto web [mvc-personnes-03B] no Tomcat:

Estamos prontos para o teste . O SGBD Firebird está em execução. O conteúdo da tabela [PERSONNES] é o seguinte:

Image

O Tomcat é então iniciado. Utilizando um navegador, solicitamos a URL [http://localhost:8080/mvc-personnes-03B]:

Image

Adicionamos uma nova pessoa utilizando o link [Add]:

Verificamos a adição na base de dados:

Image

Convidamos o leitor a realizar outros testes [editar, eliminar].

Agora vamos realizar o teste de conflito de versões que foi feito na Versão 1. O [Firefox] será o navegador do Utilizador U1. O Utilizador U1 solicita o URL [http://localhost:8080/mvc-personnes-03B]:

Image

O [IE] será o navegador do utilizador U2. O utilizador U2 solicita a mesma URL:

Image

O utilizador U1 introduz os dados da pessoa [Perrichon]:

Image

O utilizador U2 faz o mesmo:

Image

O utilizador U1 faz alterações e submete:

O utilizador U2 faz o mesmo:

O utilizador U2 volta à lista de pessoas utilizando o link [Cancelar] no formulário:

Image

Encontra a pessoa [Perrichon] tal como foi alterada pelo U1 (nome em maiúsculas).

E quanto à base de dados? Vamos dar uma olhadela:

Image

O nome da Pessoa n.º 899 está, de facto, em maiúsculas, na sequência da modificação feita por U1.

17.8. Conclusão

Vamos recapitular o que pretendíamos fazer. Tínhamos uma aplicação web com a seguinte arquitetura de três camadas:

onde as camadas [dao] e [service] trabalhavam com uma lista de dados na memória que se perdia quando o servidor web era desligado. Essa era a versão 1. Na versão 2, as camadas [service] e [dao] foram reescritas para que a lista de pessoas fosse armazenada numa tabela de base de dados. Agora é persistente. Propomos agora examinar o impacto que a mudança do SGBD tem na nossa aplicação. Para tal, iremos construir três novas versões da nossa aplicação web:

  • Versão 3: o SGBD é o Postgres
  • Versão 4: o SGBD é o MySQL
  • Versão 5: o SGBD é o SQL Server Express 2005

As alterações são feitas nos seguintes locais:

  • A classe [DaoImplFirebird] implementa a funcionalidade da camada [dao] relacionada com o SGBD Firebird. Se este requisito persistir, será substituída pelas classes [DaoImplPostgres], [DaoImplMySQL] e [DaoImplSqlExpress], respetivamente.
  • O ficheiro de mapeamento iBATIS [personnes-firebird.xml] para o SGBD Firebird será substituído pelos ficheiros de mapeamento [personnes-postgres.xml], [personnes-mysql.xml] e [personnes-sqlexpress.xml], respetivamente.
  • A configuração do objeto [DataSource] na camada [dao] é específica de um SGBD. Por conseguinte, irá mudar com cada versão.
  • O controlador JDBC do SGBD também muda com cada versão

Para além destes pontos, tudo o resto permanece igual. Nas secções seguintes, descrevemos estas novas versões, focando-nos exclusivamente nas novas funcionalidades introduzidas por cada uma delas.