Skip to content

17. Aplicação web MVC numa arquitetura de três camadas – Exemplo 3 – SGBD Firebird

17.1. A base de dados Firebird

Nesta nova versão, vamos instalar a lista de pessoas numa tabela da base de dados Firebird. No documento [http://tahe.developpez.com/divers/sql-firebird/], encontrar-se-ão informações para instalar e gerir este SGBD. A seguir, as capturas de ecrã provêm do IBExpert, um cliente de administração dos SGBD Interbase e Firebird.

A base de dados chama-se [dbpersonnes.gdb]. Contém uma tabela [PERSONNES]:

Image

A tabela [PERSONNES] conterá a lista de pessoas geridas pela aplicação web. Foi criada com os seguintes comandos 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], destinada a guardar objetos do tipo [Personne], reflete a estrutura desse objeto. Como o tipo booleano não existe no Firebird, o campo [MARIE] (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 refletem as do validador de dados [ValidatePersonne].
  • 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] possui, além da tabela [PERSONNES], um objeto denominado gerador e denominado [GEN_PERSONNES_ID]. Este gerador fornece números inteiros sucessivos que utilizaremos para atribuir um valor à chave primária [ID] da classe [PERSONNES]. Vejamos um exemplo para ilustrar o seu funcionamento:

Pode-se verificar que o valor do gerador [GEN_PERSONNES_ID] mudou (clique duas vezes nele + F5 para atualizar):

 

A ordem e SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

permite, assim, obter o seguinte valor do gerador [GEN_PERSONNES_ID]. GEN_ID é uma função interna do Firebird e [RDB$DATABASE], uma tabela de sistema deste SGBD.

17.2. O projeto Eclipse das camadas [dao] e [service]

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

Image

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


Pasta [src]


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

Image

Encontram-se aqui vários pacotes:

  • [istia.st.mvc.personnes.dao]: contém a camada [dao]
  • [istia.st.mvc.personnes.entites]: contém a classe [Personne]
  • [istia.st.mvc.personnes.service]: contém a classe [service]
  • [istia.st.mvc.personnes.tests]: contém os testes JUnit das 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 das pessoas:

Image

  • [dbpersonnes.gdb] é a base de dados.
  • [dbpersonnes.sql] é o script SQL para a geração da base de dados:
/******************************************************************************/
/***           Gerado por IBExpert 07/03/2006 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;



/******************************************************************************/
/***                                                               Geradores ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/***                                                                   Tabelas ***/
/******************************************************************************/



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;



/* Verificar definição de restrições */

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);


/******************************************************************************/
/***                                                             Chaves primárias ***/
/******************************************************************************/

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

Pasta [lib]


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

De salientar a presença do ficheiro de configuração JDBC [firebirdsql-full.jar] do Firebird SGBD, bem como de vários ficheiros de arquivo [spring-*.jar]. Poderíamos ter utilizado o único arquivo [spring.jar] que se encontra na pasta [dist] da distribuição e que contém todas as classes do Spring. Também é possível utilizar apenas os arquivos necessários para o projeto. Foi isso que fizemos aqui, orientando-nos pelos erros de classes ausentes assinalados 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. Os componentes da camada [dao]

A camada [dao] é constituída pelas seguintes classes e interfaces:

Image

  • [IDao] é a interface apresentada pela camada [dao]
  • [DaoImplCommon] é uma implementação desta, em que o grupo de pessoas se encontra numa tabela de base de dados. [DaoImplCommon] agrupa funcionalidades independentes da SGBD.
  • [DaoImplFirebird] é uma classe derivada de [DaoImplCommon] para gerir especificamente uma base de dados Firebird.
  • [DaoException] é o tipo das exceções não controladas, lançadas pela camada [dao]. Esta classe corresponde à 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 {
     // lista de todas as pessoas
    Collection getAll();
     // obter uma pessoa específica
    Personne getOne(int id);
     // Adicionar/alterar uma pessoa
    void saveOne(Personne personne);
     // eliminar uma pessoa
    void deleteOne(int id);
}
  • A interface possui os mesmos quatro métodos que na 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 {

     // lista de pessoas
    public Collection getAll() {
...
    }

     // obter uma pessoa específica
    public Personne getOne(int id) {
...
    }

     // eliminação de uma pessoa
    public void deleteOne(int id) {
...
    }

     // adicionar ou alterar uma pessoa
    public void saveOne(Personne personne) {
         // O parâmetro «pessoa» é válido?
        check(personne);
         // Adição ou alteração?
        if (personne.getId() == -1) {
             // adição
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // Adicionar uma pessoa
    protected void insertPersonne(Personne personne) {
...
    }

     // alterar uma pessoa
    protected void updatePersonne(Personne personne) {
...
    }

     // verificação da validade de uma pessoa
    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, [insertPersonne] e [updatePersonne], consoante se trate de adicionar ou alterar um utilizador.
  • linha 50: o método privado [check] é o da versão anterior. Não voltaremos a abordá-lo.
  • linha 8: para implementar a interface [IDao], a classe [DaoImpl] deriva da classe Spring [SqlMapClientDaoSupport].

17.3.2. A camada de acesso aos dados [iBATIS]

A classe Spring [SqlMapClientDaoSupport] utiliza um framework de terceiros [Ibatis SqlMap] disponível na URL [http://ibatis.apache.org/]:

Image

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

O [iBATIS] insere-se entre a camada [dao] da aplicação e o controlador JDBC da base de dados. Existem alternativas ao [iBATIS], como, por exemplo, a alternativa [Hibernate]:

Image

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

A classe [SqlMapClientDaoSupport] encapsula a parte genérica da utilização do framework [iBATIS], c.a.d. das partes de código que se encontram em todas as camadas [dao] que utilizam a ferramenta [iBATIS]. Para escrever a parte não genérica do código, ou seja, aquilo que é específico da camada [dao] que estamos a escrever, basta derivar a classe [SqlMapClientDaoSupport]. É isso que fazemos aqui.

A classe [SqlMapClientDaoSupport] é definida da seguinte forma:

Image

Entre os métodos desta classe, um deles permite configurar o cliente [iBATIS] com o qual iremos explorar 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 um conjunto de ligações
  2. abrir uma transação
  3. executar uma série de ordens SQL armazenadas num ficheiro de configuração
  4. encerrar a transação
  5. devolver a ligação ao conjunto

Se a nossa implementação [DaoImplCommon] trabalhasse diretamente com [iBATIS], teria de repetir esta sequência continuamente. Apenas a operação 3 é específica da camada [dao], sendo as restantes operações genéricas. A classe Spring [SqlMapClientDaoSupport] irá assegurar ela própria as operações 1, 2, 4 e 5, delegando a operação 3 à sua classe derivada, neste caso a classe [DaoImplCommon].

Para poder funcionar, a classe [SqlMapClientDaoSupport] necessita de uma referência ao objeto iBATIS [SqlMapClient sqlMapClient], que irá assegurar a comunicação com a base de dados. Este objeto necessita de dois elementos para funcionar:

  • um objeto [DataSource] ligado à base de dados, ao qual irá solicitar ligações
  • um (ou mais) ficheiro(s) de configuração onde estão externalizadas as ordens SQL a executar. Com efeito, 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 esse código para executar uma ordem SQL específica.

Um esboço da configuração da nossa camada [dao], que refletiria a arquitetura acima, seria o seguinte:


    <!-- As classes de acesso à camada [dao] -->
    <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. A inicialização é feita pelo método [setSqlMapClient] da classe [DaoImpl]. Esta classe não possui esse método. É a sua classe pai, [SqlMapClientDaoSupport], que o possui. Por isso, é essa classe que, na realidade, está a ser inicializada aqui.

Agora, na linha 4, faz-se referência a um objeto denominado «sqlMapClient», que ainda está por construir. Este, como já foi referido, é do tipo [SqlMapClient], um tipo [iBATIS]:

Image

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

Image

Recorde-se que pretendemos instanciar um objeto que implemente a interface [SqlMapClient]. Aparentemente, não é esse o caso da classe [SqlMapClientFactoryBean]. Esta implementa a interface [FactoryBean] (ver acima). Esta possui o seguinte método [getObject()]:

Image

Quando se solicita ao Spring uma instância de um objeto que implemente a interface [FactoryBean], este:

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

Para poder devolver um objeto que implemente a interface [SqlMapClient], a classe [SqlMapClientFactoryBean] necessita de duas informações essenciais para esse objeto:

  • um objeto [DataSource] ligado à base de dados, ao qual irá solicitar ligações
  • um (ou mais) ficheiro(s) de configuração onde estão externalizadas as ordens SQL a executar

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

Image

Estamos a avançar... O nosso ficheiro de configuração vai-se concretizando e passa a ser:


<!-- 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>
    <!-- classes de acesso à camada [dao] -->
    <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 último objeto que será, portanto, obtido na linha 14.
  • linhas 7-9: indicamos que o ficheiro de configuração necessário para o objeto iBATIS [SqlMapClient] se chama «sql-map-config-firebird.xml» e que deve ser procurado no ClassPath da aplicação. Aqui é utilizado o método [SqlMapClientFactoryBean].setConfigLocation.
  • Linhas 4-6: inicializamos a propriedade [dataSource] de [SqlMapClientFactoryBean] com o seu método [setDataSource].

Na linha 5, fazemos referência a um bean chamado «dataSource», que ainda está por criar. Se analisarmos o parâmetro esperado pelo método [setDataSource] de [SqlMapClientFactoryBean], verificamos que é do tipo [DataSource]:

Image

Estamos novamente perante uma interface para a qual temos de encontrar uma classe de implementação. A função dessa classe é fornecer a uma aplicação, de forma eficiente, ligações a uma base de dados específica. Um SGBD não consegue manter abertas simultaneamente um grande número de ligações. Para reduzir o número de ligações abertas num determinado momento, somos levados, para cada interação com a base de dados, a:

  • abrir uma ligação
  • iniciar uma transação
  • emitir comandos SQL
  • encerrar a transação
  • fechar a ligação

Abrir e fechar ligações repetidamente é demorado. Para resolver estes dois problemas (limitar simultaneamente o número de ligações abertas num determinado momento e limitar o custo de abertura/encerramento das mesmas), as classes que implementam a interface [DataSource] procedem frequentemente da seguinte forma:

  • abrem, logo após a sua instanciação, N ligações à base de dados em questão. N tem, geralmente, um valor por predefinição e pode, na maioria das vezes, ser definido num ficheiro de configuração. Estas N ligações permanecerão sempre abertas e formam um conjunto de ligações disponíveis para os threads da aplicação.
  • Quando um thread da aplicação solicita a abertura de uma ligação, o objeto [DataSource] atribui-lhe uma das N ligações abertas no arranque, caso ainda haja alguma disponível. Quando a aplicação encerra a ligação, esta não é, na realidade, encerrada, mas simplesmente devolvida ao conjunto de ligações disponíveis.

Existem várias implementações da interface [DataSource] disponíveis gratuitamente. Vamos utilizar aqui 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 ficheiros [commons-dbcp, commons-pool], ambos colocados na pasta [lib] do projeto:

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

Image

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

  1. o nome do controlador JDBC a utilizar – inicializado com [setDriverClassName]
  2. o nome da URL da base de dados a utilizar – inicializado com [setUrl]
  3. o identificador 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 da nossa camada [dao] poderá ser o seguinte:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- a fonte de dados DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- atenção: não deixe espaços entre as duas balizas <value> do 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>
    <!-- a classe de acesso à camada [dao] -->
    <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 do Firebird SGBD
  • linhas 11-13: o URL da base de dados Firebird [dbpersonnes.gdb]. Deve prestar-se especial atenção à forma como este é escrito. Não deve haver nenhum espaço entre as balizas <value> e o URL.
  • linhas 14-16: o proprietário da ligação – neste caso, [sysdba], que é o administrador predefinido das distribuições Firebird
  • linhas 17-19: a sua palavra-passe [masterkey] – também o valor por predefinição

Já avançámos bastante, 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 cliente [SqlMapClient] de iBATIS. Antes de analisarmos o seu conteúdo, vamos mostrar a localização destes ficheiros de configuração no nosso projeto Eclipse:

Image

  • O [spring-config-test-dao-firebird.xml] é o ficheiro de configuração da camada [dao] que acabámos de analisar
  • O [sql-map-config-firebird.xml] é referenciado pelo [spring-config-test-dao-firebird.xml]. Vamos analisá-lo.
  • O [personnes-firebird.xml] é referenciado pelo [sql-map-config-firebird.xml]. Vamos analisá-lo.

Os três ficheiros anteriores encontram-se na pasta [src]. No Eclipse, isto significa que, durante a execução, estarão presentes na pasta [bin] do projeto (não representada acima). Esta pasta faz parte do ClassPath da aplicação. Por fim, os três ficheiros anteriores estarão, portanto, 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 tag raiz (linhas 6 e 8)
  • linha 7: a baliza <sqlMap> serve para indicar os ficheiros que contêm as ordens SQL a executar. Muitas vezes, embora não seja obrigatório, existe um ficheiro por tabela. Isto permite reunir as ordens SQL relativas a uma determinada tabela num único ficheiro. No entanto, é frequente encontrar ordens SQL que envolvem várias tabelas. Neste caso, a decomposição anterior não se aplica. Basta ter em conta que todos os ficheiros designados pelas balizas <sqlMap> serão fundidos. Estes ficheiros são procurados no ClassPath da aplicação.

O ficheiro [personnes-firebird.xml] descreve os comandos SQL que serão emitidos na tabela [PERSONNES] da 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 da classe [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabela de mapeamento [PERSONNES] - objeto [Personne] -->
    <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>
    <!-- lista de todas as pessoas -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- obter uma pessoa específica -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#valor#</select>
    <!-- adicionar uma pessoa -->
    <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#, #versão#, #apelido#, #nome próprio#, #dateNaissance#, #casada#, 
        #nbEnfants#) </insert>
    <!-- atualizar um utilizador -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#versão#+1, NOM=#apelido#, PRENOM=#nome próprio#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# e 
        VERSION=#versão#</update>
    <!-- eliminar uma pessoa -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#valor# </delete>
</sqlMap>
  • o ficheiro deve ter <sqlMap> como tag raiz (linhas 7 e 45)
  • linhas 9-10: para facilitar a criação do ficheiro, atribui-se o alias (sinónimo) [Personne.classe] à classe [istia.st.springmvc.personnes.entites.Personne].
  • linhas 12-21: define as correspondências entre as colunas da tabela [PERSONNES] e os campos do objeto [Personne].
  • linhas 23-24: a ordem SQL [select] para obter todas as pessoas da tabela [PERSONNES]
  • linhas 26-27: a ordem SQL [select] para obter uma pessoa específica da tabela [PERSONNES]
  • linhas 29-36: a ordem SQL [insert] que insere uma pessoa na tabela [PERSONNES]
  • linhas 38-41: a ordem SQL [update] que atualiza um indivíduo da tabela [PERSONNES]
  • linhas 42-44: a ordem SQL [delete] que elimina uma pessoa da tabela [PERSONNES]

A função e o significado do conteúdo do ficheiro [personnes-firebird.xml] serão explicados através da análise da classe [DaoImplCommon], que implementa a camada [dao].

17.3.3. A classe [DaoImplCommon]

Voltemos à 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 {

     // lista de pessoas
    public Collection getAll() {
...
    }

     // obter uma pessoa específica
    public Personne getOne(int id) {
...
    }

     // eliminar uma pessoa
    public void deleteOne(int id) {
...
    }

     // adicionar ou alterar uma pessoa
    public void saveOne(Personne personne) {
         // o parâmetro «pessoa» é válido?
        check(personne);
         // adição ou alteração?
        if (personne.getId() == -1) {
             // adição
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // adicionar uma pessoa
    protected void insertPersonne(Personne personne) {
...
    }

     // alterar uma pessoa
    protected void updatePersonne(Personne personne) {
...
    }

     // verificação da validade de uma pessoa
    private void check(Personne p) {
...
    }

...
}

Vamos analisar os métodos um a um.


getAll


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

1
2
3
4
     // lista de pessoas
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

Recordemos, em primeiro lugar, que a classe [DaoImplCommon] deriva da classe Spring [SqlMapClientDaoSupport]. É esta classe que possui 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 dele que se terá acesso à base de dados. O tipo [iBATIS] SqlMapClient poderia ser utilizado diretamente, uma vez que a classe [SqlMapClientDaoSupport] tem acesso a ele:

Image

A desvantagem da classe [iBATIS] SqlMapClient é que ela lança exceções do tipo [SQLException], um tipo de exceção controlada, c.a.d. que deve ser tratada por um try/catch ou declarada na assinatura dos métodos que a lançam. No entanto, lembremos que a camada [dao] implementa uma interface [IDao] cujos métodos não incluem exceções nas suas assinaturas. Os métodos das classes de implementação da interface [IDao] também não podem, portanto, conter exceções nas suas assinaturas. Temos, portanto, de interceptar cada exceção [SQLException] lançada pela camada [iBATIS] e encapsulá-la numa exceção não controlada. O tipo [DaoException] do nosso projeto serviria para este encapsulamento.

Em vez de gerirmos nós próprios estas exceções, vamos confiá-las ao tipo Spring [SqlMapClientTemplate], que encapsula o objeto [SqlMapClient] da camada [iBATIS]. Com efeito, o [SqlMapClientTemplate] foi concebido para interceptar as exceções [SQLException] lançadas pela camada [SqlMapClient] e encapsulá-las num tipo [DataAccessException] não controlado. Este comportamento é do nosso agrado. Basta ter em conta que a camada [dao] pode agora lançar dois tipos de exceções não controladas:

  • o nosso tipo proprietário [DaoException]
  • o tipo Spring [DataAccessException]

O tipo [SqlMapClientTemplate] é definido da seguinte forma:

Image

Implementa a seguinte interface [SqlMapClientOperations]:

Image

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

[queryForList]

Image

Este método permite emitir uma ordem [SELECT] e recuperar o resultado na forma de uma lista de objetos:

  • [statementName]: o identificador (id) da ordem [select] no ficheiro de configuração
  • [parameterObject]: o objeto «parâmetro» para um [select] configurado. O objeto «parâmetro» pode assumir duas formas:
    • um objeto que cumpre a norma JavaBean: os parâmetros da ordem [select] são, nesse caso, os nomes dos campos do JavaBean. Na execução da ordem [select], estes são substituídos pelos valores desses campos.
    • um dicionário: os parâmetros da ordem [select] são, nesse caso, as chaves do dicionário. Na execução da ordem [select], estas são substituídas pelos seus valores associados no dicionário.
  • Se o [SELECT] não devolver nenhuma linha, o resultado [List] é um objeto vazio de elementos, mas não o null (a verificar).

[queryForObject]

Image

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

[insert]

Image

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

[update]

Image

Este método permite executar uma ordem SQL [update] definida pelo segundo parâmetro. O resultado é o número de linhas alteradas pela ordem SQL [update].

[delete]

Image

Este método permite executar uma ordem SQL [delete] configurada pelo segundo parâmetro. O resultado é o número de linhas eliminadas pela ordem SQL [delete].

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

1
2
3
4
     // lista de pessoas
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • linha 4: a ordem [select] denominada «Personne.getAll» é executada. Não está configurada e, por isso, o objeto «parâmetro» é null.

Em [personnes-firebird.xml], a ordem [select], denominada «Personne.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 da classe [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabela de mapeamento [PERSONNES] - objeto [Personne] -->
    <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>
    <!-- lista de todas as pessoas -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • linha 23: a ordem SQL «Personne.getAll» não está parametrizada (ausência de parâmetros no texto da consulta).
  • A linha 3 do método [getAll] solicita a execução da consulta [select] denominada «Personne.getAll». Esta será executada. O [iBATIS] baseia-se no JDBC. Sabe-se, portanto, que o resultado da consulta será obtido sob a forma de um objeto [ResultSet]. Na linha 23, o atributo [resultMap] da baliza <select> indica ao [iBATIS] qual o "resultMap " deve utilizar para transformar cada linha do [ResultSet] obtido num objeto. É o «resultMap» [Personne.map], definido nas linhas 12-21, que indica como passar de uma linha da tabela [PERSONNES] para um objeto do tipo [Personne]. O [iBATIS] utilizará estas correspondências para fornecer uma lista de objetos [Personne] a partir das linhas do objeto [ResultSet].
  • A linha 3 do método [getAll] devolve então uma coleção de objetos [Personne]
  • o método [queryForList] pode lançar uma exceção Spring [DataAccessException]. Deixamos que esta seja propagada.

Explicamos os restantes métodos da classe [AbstractDaoImpl] de forma mais sucinta, uma vez que o essencial sobre a utilização de [iBATIS] já foi abordado na análise do método [getAll].


getOne


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

         // obter uma pessoa em particular
    public Personne getOne(int id) {
         // recupera-se na BD
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
         // recuperou-se alguma coisa?
        if (personne == null) {
             // lança-se uma exceção
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
         // devolve-se a pessoa
        return personne;
    }
  • linha 4: solicita a execução da ordem [select] denominada «Personne.getOne». Esta é a seguinte no ficheiro [personnes-firebird.xml]:

<!-- obter uma pessoa em particular -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#valor#</select>

A ordem SQL é configurada pelo parâmetro #value# (linha 4). O atributo #value# designa o valor do parâmetro passado para a ordem SQL, quando esse parâmetro é de tipo simples: Integer, Double, String, ... Nos atributos da baliza <select>, o atributo [parameterClass] indica que o parâmetro é do tipo inteiro (linha 2). Na linha 5 de [getOne], verifica-se que este parâmetro é o identificador da pessoa procurada, na forma de um objeto Integer. Esta alteração de tipo é obrigatória, uma vez que o segundo parâmetro de [queryForList] deve ser do tipo [Object].

O resultado da consulta [select] deverá ser transformado num objeto através do atributo [resultMap="Personne.map"] (linha 2). Obter-se-á, assim, um tipo [Personne].

  • linhas 7-11: se a consulta [select] não tiver devolvido nenhuma linha, recupera-se então o ponteiro null na linha 4. Isto significa que não foi encontrada a pessoa procurada. Neste caso, lança-se uma consulta [DaoException] com o código 2 (linhas 9-10).
  • linha 13: se não tiver ocorrido nenhuma exceção, então devolve-se o objeto [Personne] solicitado.

deleteOne


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

     // eliminação de uma pessoa
    public void deleteOne(int id) {
         // elimina-se a pessoa
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
         // conseguiu-se
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • linhas 4-5: solicita a execução da ordem [delete] denominada «Personne.deleteOne». Esta é a seguinte no ficheiro [personnes-firebird.xml]:

<!-- eliminar uma pessoa -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#valor# </delete>

A ordem SQL é configurada pelo parâmetro #value# (linha 3) do tipo [parameterClass="int"] (linha 2). Este será o identificador da pessoa 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 tiver eliminado nenhuma linha, isso significa que a pessoa não existe. Executa-se um [DaoException] com o código 2 (linha 8).

saveOne


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

         // adicionar ou alterar uma pessoa
    public void saveOne(Personne personne) {
         // o parâmetro «pessoa» é válido?
        check(personne);
         // adição ou alteração?
        if (personne.getId() == -1) {
             // adição
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • linha 4: verifica-se a validade da pessoa com o método [check]. Este método já existia na versão anterior e tinha sido, na altura, comentado. Lança um [DaoException] se a pessoa for inválida. Deixa-se que este seja reenviado.
  • linha 6: se chegarmos até aqui, significa que não houve 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 a adição
    • updatePersonne: para a atualização

insertPersonne


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

// adicionar uma pessoa
    protected void insertPersonne(Personne personne) {
         // 1.ª versão
        personne.setVersion(1);
         // aguarda-se 10 ms — para os testes, definir como «true» em vez de «false»
        if (true)
            wait(10);
         // inserimos a nova pessoa na tabela do BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • linha 4: define-se como 1 o número de versão da pessoa que se está a criar
  • linha 9: efetua-se a inserção através da consulta denominada «Personne.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#, #versão#, #apelido#, #nome próprio#, #dateNaissance#, #marie#, 
    #nbEnfants#) </insert>

Trata-se de uma consulta parametrizada e o parâmetro é do tipo [Personne] (parameterClass="Personne.classe", linha 1). Os campos do objeto [Personne] passados como parâmetro (linha 9 de insertPersonne) são utilizados para preencher as colunas da linha que vai ser inserida na tabela [PERSONNES] (linhas 5-8). Há um problema a resolver. Durante uma inserção, o objeto [Personne] a inserir tem o seu ID igual a -1. É necessário substituir este valor por uma chave primária válida. Para tal, utilizam-se as linhas 2 a 4 da baliza <selectKey> acima referida. Estas indicam:

  • (continuação)
    • a consulta SQL a executar para obter um valor de chave primária. A consulta aqui indicada é a que apresentámos no parágrafo 17.1. Há dois pontos a destacar:
      • «as " value"» é obrigatório. Também se pode escrever «as value», mas «value» é uma palavra-chave do Firebird que teve de ser protegida por aspas.
      • A tabela do Firebird chama-se, na realidade, [RDB$DATABASE]. No entanto, o carácter $ é interpretado como [iBATIS]. Foi protegida duplicando-a.
    • O campo do objeto [Personne] que deve ser inicializado com o valor recuperado pela ordem [SELECT], neste caso o campo [id]. É o atributo [keyProperty] da linha 2 que indica este campo.
  • linhas 6-7: para efeitos de teste, teremos de aguardar 10 ms antes de efetuar a inserção, para verificar se existem conflitos entre threads que pretendam efetuar adições em simultâneo.

updatePersonne


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

// editar um utilizador
    protected void updatePersonne(Personne personne) {
         // aguarda-se 10 ms — para os testes, colocar «true» em vez de «false»
        if (true)
            wait(10);
         // alteração
        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 atualizar não existe
    2. a pessoa a atualizar existe, mas o thread que pretende alterá-la não tem a versão correta
  • linhas 7-8: a consulta SQL [update] denominada «Personne.updateOne» é executada. É a seguinte:

    <!-- atualizar um utilizador -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#versão#+1, NOM=#apelido#, PRENOM=#nome próprio#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# e 
VERSION=#versão#</update>
  • (continuação)
    • linha 2: a consulta é configurada e aceita como parâmetro um tipo [Personne] (parameterClass = «Personne.classe»). Este é o registo a modificar (linha 8 – updatePersonne).
    • pretende-se alterar apenas a pessoa da tabela [PERSONNES] que tenha o mesmo n.º [id] e a mesma versão [version] que o parâmetro. É por isso que existe a restrição [WHERE ID=#id# and VERSION=#version#]. Se essa pessoa for encontrada, é atualizada com a pessoa do parâmetro e a sua versão é incrementada em 1 (linha 3 acima).
  • linha 9: recupera-se o número de linhas atualizadas.
  • linhas 10-11: se esse número for nulo, é lançado um [DaoException] com código 2, indicando que, ou a pessoa a atualizar não existe, ou mudou de versão entretanto.

17.4. Testes da camada [dao]

17.4.1. Testes da implementação [DaoImplCommon]

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

Image

Antes de realizarmos testes intensivos, podemos começar com um programa simples do tipo [main] que irá apresentar o conteúdo da tabela [PERSONNES]. Trata-se da 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");
         // lista atual
        Collection personnes = dao.getAll();
         // visualização da consola
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

O ficheiro de configuração [spring-config-test-dao-firebird.xml] da 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>
    <!-- a fonte de dados DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- atenção: não deixe espaços entre as duas balizas <value> -->
        <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>
    <!-- a classe de acesso à camada [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

Este ficheiro é o que foi analisado no parágrafo 17.3.2.

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

Image

A execução do programa [MainTestDaoFirebird] apresenta os seguintes resultados no ecrã:

Image

Conseguimos, de facto, obter a lista de pessoas. Podemos passar ao 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 {

     // camada [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

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

     // lista de pessoas
    private void doListe(Collection personnes) {
...
    }

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

     // alteração-eliminação de um elemento inexistente
    public void test2() throws ParseException {
..
    }

     // gestão de versões de pessoas
    public void test3() throws ParseException, InterruptedException {
...
    }

     // bloqueio otimista - acesso multithread
    public void test4() throws Exception {
...
    }

     // testes de validade de saveOne
    public void test5() throws ParseException {
....
    }

     // inserções multithread
    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] é, por sua vez, novo. Iremos comentar apenas estes dois testes.

[test4]


O [test4] tem como objetivo testar o método [updatePersonne - DaoImplCommon]. Recordamos o código deste último:

// alterar um utilizador
    protected void updatePersonne(Personne personne) {
         // espera de 10 ms — para os testes, definir como «true» em vez de «false»
        if (true)
            wait(10);
         // alteração
        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: aguarda-se 10 ms. Desta forma, força-se o thread que executa o [updatePersonne] a perder o acesso ao processador, o que pode aumentar as nossas hipóteses de observar conflitos de acesso entre threads concorrentes.

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

    public void test4() throws Exception {
         // adição de uma pessoa
        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();
         // criação de N threads para atualizar o número de filhos
        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();
        }
         // aguarda-se o fim dos threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // recuperar a pessoa
        p1 = dao.getOne(id1);
         // ela deve ter N filhos
        assertEquals(N, p1.getNbEnfants());
         // eliminação da pessoa p1
        dao.deleteOne(p1.getId());
         // verificação
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve haver um erro de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

Os threads são criados nas linhas 8-13. Cada um irá aumentar 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 {
     // nome do thread
    private String name;

     // referência na camada [dao]
    private IDao dao;

     // o ID da pessoa em que vamos trabalhar
    private int idPersonne;

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

     // núcleo do tópico
    public void run() {
         // acompanhamento
        suivi("lancé");
         // repetimos o ciclo até conseguirmos incrementar em 1
         // o número de filhos da pessoa idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // recupera-se uma cópia da pessoa de idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // acompanhamento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
             // espera de 10 ms para libertar o processador
            try {
                 // seguimento
                suivi("début attente");
                 // interrompe-se para libertar o processador
                Thread.sleep(10);
                 // acompanhamento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // espera concluída — tenta-se validar a cópia
             // entretanto, outras threads podem ter alterado o original
            int codeErreur = 0;
            try {
                 // incrementa em 1 o número de instâncias desta cópia
                personne.setNbEnfants(nbEnfants + 1);
                 // está a tentar alterar o original
                dao.saveOne(personne);
                 // conseguimos — o original foi alterado
                fini = true;
            } catch (DaoException ex) {
                 // recuperamos o código de erro
                codeErreur = ex.getCode();
                 // se ocorrer um erro de ID ou de versão com código de erro 2, tenta-se novamente a atualização
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // exceção não tratada — deixa-se que seja reportada
                    throw ex;
                }
            }
        }
         // acompanhamento
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

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

Uma atualização de pessoa pode falhar porque a pessoa que se pretende modificar não existe ou porque já foi atualizada anteriormente por outro thread. Estes dois casos são aqui tratados nas linhas 67-69. De facto, nestes dois casos, o método [updatePersonne] lança um [DaoException] com o código 2. O thread será então levado a reiniciar o procedimento de atualização desde o início (laço while, linha 34).


[test6]


O [test6] tem como objetivo testar o método [insertPersonne - DaoImplCommon]. Recorde-se o código deste último:

// adicionar uma pessoa
    protected void insertPersonne(Personne personne) {
         // 1.ª versão
        personne.setVersion(1);
         // aguarda-se 10 ms — para os testes, definir como «true» em vez de «false»
        if (true)
            wait(10);
         // inserimos a nova pessoa na tabela do BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • linhas 6-7: aguarda-se 10 ms para forçar o thread que executa o [insertPersonne] a perder o processador e, assim, aumentar as nossas hipóteses de observar conflitos decorrentes de threads que efetuam inserções em simultâneo.

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

     // inserções multithread
    public void test6() throws ParseException, InterruptedException{
         // criação de uma pessoa
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
         // que se duplica N vezes numa matriz
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
         // criação de N threads de inserção — cada thread insere 1 pessoa
        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();
        }
         // aguarda-se o fim dos threads
        for (int i = 0; i < taches.length; i++) {
             // thread n.º i
            taches[i].join();
             // eliminação de pessoa
            dao.deleteOne(personnes[i].getId());
        }
}

Criamos 100 threads que irão inserir, ao mesmo tempo, 100 pessoas diferentes. Estas 100 threads irão todas obter uma chave primária para a pessoa que devem inserir e, em seguida, serão interrompidas durante 10 ms (linha 10 – insertPersonne) antes de poderem efetuar a sua 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 tabela com 100 pessoas. Todas estas pessoas são cópias da pessoa p criada nas linhas 4-5.
  • linhas 14-17: são lançados os 100 threads de inserção. Cada um deles é responsável por inserir uma das 100 pessoas criadas anteriormente.
  • linhas 19-23: [test6] aguarda a conclusão de cada um dos 100 threads que lançou. Quando deteta a conclusão do thread n.º i, elimina a pessoa que esse thread acabou de inserir.

O thread de inserção [ThreadDaoInsertPersonne] é o 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 {
     // nome do thread
    private String name;

     // referência na camada [dao]
    private IDao dao;

     // o ID da pessoa em que se vai trabalhar
    private Personne personne;

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

     // núcleo do tópico
    public void run() {
         // acompanhamento
        suivi("lancé");
         // inserção
        dao.saveOne(personne);
         // acompanhamento
        suivi("a terminé");
    }

     // acompanhamento
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • linhas 19-22: o construtor do thread armazena a pessoa que deve inserir e a camada [dao] que deve utilizar para efetuar essa inserção.
  • linha 30: a pessoa é inserida. Se ocorrer uma exceção, esta é propagada para [test6].

Testes


Nos testes, obtêm-se os seguintes resultados:

O teste [test4] falha, portanto. O número de filhos passou para 69, em vez dos 100 esperados. O que aconteceu? Vamos analisar 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];   
--- O erro ocorreu em personnes-firebird.xml.  
--- O erro ocorreu durante a aplicação de um mapa de parâmetros.  
--- Verifique o Personne.updateOne-InlineParameterMap.  
--- Verifique a instrução (atualização falhada).  
--- Causa: org.firebirdsql.jdbc.FBSQLException: Exceção GDS. 335544336. impasse
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--- O erro ocorreu em personnes-firebird.xml.  
--- O erro ocorreu durante a aplicação de um mapa de parâmetros.  
  • linha 1 – ocorreu uma exceção Spring [org.springframework.jdbc.UncategorizedSQLException]. Trata-se de uma exceção não controlada que foi utilizada para encapsular 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 ocorreu um conflito de acesso entre duas threads que pretendiam atualizar simultaneamente a mesma linha da tabela [PERSONNES].

Não se trata de um erro irrecuperável. O thread que intercepta esta exceção pode tentar novamente a atualização. Para tal, é necessário alterar o código de [ThreadDaoMajEnfants]:

            try {
                 // incrementa em 1 o número de filhos desta cópia
                personne.setNbEnfants(nbEnfants + 1);
                 // está-se a tentar modificar o original
                dao.saveOne(personne);
                 // concluído  o original foi alterado
                fini = true;
            } catch (DaoException ex) {
                 // recupera-se o código de erro
                codeErreur = ex.getCode();
                 // se ocorrer um erro de ID ou de versão do código de erro 2, tenta-se novamente a atualização
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // exceção não tratada - deixa-se que seja reportada
                    throw ex;
                }
  • linha 8: trata-se de uma exceção do tipo [DaoException]. De acordo com o que foi referido, deveríamos tratar a exceção que surgiu nos testes, do tipo [org.springframework.jdbc.UncategorizedSQLException]. No entanto, não podemos limitar-nos a tratar este tipo, que é um tipo genérico do Spring destinado a encapsular exceções que ele não reconhece. O Spring reconhece as exceções emitidas pelos controladores JDBC de uma série de SGBD, tais como Oracle, MySQL, Postgres, DB2, SQL Server, ... mas não o Firebird. Assim, qualquer exceção lançada pelo controlador JDBC do Firebird é encapsulada no tipo Spring [org.springframework.jdbc.UncategorizedSQLException]:

Image

Como se pode ver acima, a classe [UncategorizedSQLException] deriva da classe [DataAccessException] que mencionámos no parágrafo 17.3.3. É possível identificar a exceção que foi encapsulada em [UncategorizedSQLException] através do seu método [getSQLException]:

Image

Esta exceção do tipo [SQLException] é a 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 exceção do tipo [SQLException] pode ser obtida através do método:

Image

Obtém-se o objeto do tipo [Throwable], que foi lançado pelo controlador JDBC:

Image

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

Aqui, teremos de verificar se o objeto do tipo [Throwable], lançado pelo controlador JDBC do Firebird e causa daexceção [SQLException] lançada pela camada [iBATIS] é, de facto, uma exceção do tipo [org.firebirdsql.gds.GDSException] e 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 no código de [ThreadDaoMajEnfants] a exceção [org.firebirdsql.gds.GDSException], então este thread só poderá funcionar com o Firebird SGBD. O mesmo se aplica ao teste [test4] que utiliza esta thread. Queremos evitar isso. Com efeito, pretendemos que os nossos testes JUnit permaneçam válidos independentemente do SGBD utilizado. Para alcançar este resultado, decidimos que a camada [dao] irá iniciar um [DaoException] de código 4 quando for detetada uma exceção do tipo «conflito de atualização», independentemente do SGBD subjacente. Assim, o thread [ThreadDaoMajEnfants] poderá ser reescrito da seguinte forma:

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

public class ThreadDaoMajEnfants extends Thread {
...

     // núcleo do thread
    public void run() {
...
        while (!fini) {
             // recuperar uma cópia do registo de idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
             // espera concluída - tenta-se validar a cópia
             // entretanto, outras threads podem ter alterado o original
            int codeErreur = 0;
            try {
                 // incrementa em 1 o número de filhos desta cópia
                personne.setNbEnfants(nbEnfants + 1);
                 // está a tentar alterar o original
                dao.saveOne(personne);
                 // já foi processado — o original foi alterado
                fini = true;
            } catch (DaoException ex) {
                 // recuperamos o código de erro
                codeErreur = ex.getCode();
                 // se ocorrer um erro de ID, da versão 2 ou um impasse 4, então
                 // tenta novamente a atualização
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                     // exceção não tratada — deixa-se que seja reportada
                    throw ex;
                }
            }
        }
         // acompanhamento
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • linhas 34-36: a exceção do tipo [DaoException] com código 4 é interceptada. O thread [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 do tipo «conflito de atualização». Esta é emitida por um controlador JDBC e é-lhe específica. Esta exceção deve ser tratada no método [updatePersonne] da classe [DaoImplCommon]:

// alterar um utilizador
    protected void updatePersonne(Personne personne) {
         // aguarda-se 10 ms — para os testes, definir como «true» em vez de «false»
        if (true)
            wait(10);
         // alteração
        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 a 11 devem estar entre um try e um catch. Para o SGBD Firebird, é necessário verificar se a exceção que causou a falha na atualização é do tipo [org.firebirdsql.gds.GDSException] e tem como código de erro 335544336. Se colocarmos este tipo de teste no [DaoImplCommon], iremos associar esta classe ao Firebird SGBD, o que obviamente não é desejável. Se quisermos manter o caráter genérico da classe [DaoImplCommon], temos de a derivar e gerir a exceção numa classe específica para o Firebird. É isso que vamos 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 {

     // alterar um utilizador
    protected void updatePersonne(Personne personne) {
         // aguarda 10 ms — para os testes, coloque «true» em vez de «false»
        if (true)
            wait(10);
         // alteração
        try {
             // alteramos a pessoa que tem a versão correta
            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;
            }
        }
    }

     // espera
    private void wait(int N) {
         // aguarda N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // exibe-se o registo da exceção
            e.printStackTrace();
            return;
        }
    }

}
  • linha 5: a classe [DaoImplFirebird] deriva de [DaoImplCommon], a classe que acabámos de analisar. Ela redefine, nas linhas 8 a 33, o método [updatePersonne] que nos está a causar problemas.
  • linhas 20: interceptamos a exceção Spring do tipo [UncategorizedSQLException]
  • linhas 21-22: verificamos se a exceção subjacente do tipo [SQLException], lançada pela camada [iBATIS], tem como causa uma exceção do tipo [org.firebirdsql.jdbc.FBSQLException]
  • linha 25: verifica-se ainda que o código de erro desta exceção do Firebird é 335544336, o código de erro do «deadlock».
  • linhas 26-27: se todas estas condições estiverem reunidas, é lançada uma [DaoException] com código 4.
  • linhas 36-44: o método [wait] permite suspender o thread atual durante N milissegundos. Só tem utilidade para fins de teste.

Estamos prontos para os testes da nova camada [dao].

17.4.3. Testes da implementação [DaoImplFirebird]

O ficheiro de configuração dos testes [spring-config-test-dao-firebird.xml] é alterado 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>
    <!-- a fonte de dados DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- atenção: não deixar espaços entre as duas balizas <value> -->
        <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>
    <!-- a classe de acesso à camada [dao] -->
    <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

O [test4] foi bem-sucedido. 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 foi o thread n.º 36 que terminou em último lugar. A linha 3 mostra um conflito de versão que obrigou o thread n.º 36 a reiniciar o seu procedimento de atualização do utilizador (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 devia ser atualizada estava bloqueada por outro thread. Este conflito de acesso obrigará o thread n.º 75 a tentar novamente a atualização.

Para concluir com o [test4], nota-se uma diferença significativa em relação aos resultados do mesmo teste na versão 1, onde este falhou devido a problemas de sincronização. Como os métodos da camada [dao] da versão 1 não estavam sincronizados, surgiram conflitos de acesso. Aqui, não foi necessário sincronizar a camada [dao]. Limitámo-nos a gerir os conflitos de acesso sinalizados pelo Firebird.

Vamos agora executar o teste JUnit na íntegra a partir da camada [dao]:

Image

Parece, portanto, que temos uma camada [dao] válida. Para a declarar válida com elevada probabilidade, seria necessário 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] é constituída pelas seguintes classes e interfaces:

Image

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

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 {
     // lista de todas as pessoas
    Collection getAll();

     // obter uma pessoa específica
    Personne getOne(int id);

     // adicionar/alterar uma pessoa
    void saveOne(Personne personne);

     // eliminar uma pessoa
    void deleteOne(int id);

     // guardar várias pessoas
    void saveMany(Personne[] personnes);

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

Estes dois métodos não serão utilizados pela aplicação web. Adicionámo-los para ilustrar o conceito de transação numa base de dados. De facto, ambos os métodos terão de ser executados no âmbito de uma transação para se obter 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 {

     // a camada [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // lista de pessoas
    public Collection getAll() {
        return dao.getAll();
    }

     // obter uma pessoa específica
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

     // adicionar ou alterar uma pessoa
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // eliminar uma pessoa
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

     // guardar uma coleção de pessoas
    public void saveMany(Personne[] personnes) {
         // percorrer a tabela de pessoas
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

     // eliminar uma coleção de pessoas
    public void deleteMany(int[] ids) {
         // ids: os IDs das pessoas a eliminar
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • Os métodos [getAll, getOne, insertOne, saveOne] recorrem aos métodos da camada [dao] com o mesmo nome.
  • linhas 42-47: o método [saveMany] guarda, uma a uma, as pessoas da tabela passada como parâmetro.
  • linhas 50-55: o método [deleteMany] elimina, uma a uma, as pessoas cuja tabela foi passada como parâmetro pelo método id

Já referimos que os métodos [saveMany] e [deleteMany] devem ser executados no âmbito de uma transação para garantir o caráter «tudo ou nada» destes métodos. Podemos constatar que o código acima ignora totalmente este conceito de transação. Este só aparecerá no ficheiro de configuração da camada [service].

17.5.2. Configuração da camada [service]

Na linha 11 acima, vemos que a implementação [ServiceImpl] possui uma referência à camada [dao]. Esta, tal como na versão 1, será inicializada pelo Spring no momento da instanciação da camada [service - ServiceImpl]. O ficheiro de configuração que permitirá a instanciação da camada [service] será o seguinte:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- a fonte de dados 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">
            <!-- atenção: não deixar espaços entre as duas balizas <value> -->
            <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>
    <!-- a classe de acesso à camada [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- gestor de transações -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- a classe de acesso à camada [service] -->
    <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 durante a análise da camada [dao] no parágrafo 17.3.2.
  • linhas 38-64: configuram a camada [service]

Na linha 46, pode-se ver que a implementação da camada [service] é feita pelo tipo [TransactionProxyFactoryBean]. Esperávamos encontrar o tipo [ServiceImpl]. O [TransactionProxyFactoryBean] é um tipo predefinido do Spring. Como é possível que um tipo predefinido possa implementar a interface [IService], que, por sua vez, é específica da nossa aplicação?

Vamos, em primeiro lugar, analisar a classe [TransactionProxyFactoryBean]:

Image

Vemos que ela implementa a interface [FactoryBean]. Já nos deparámos com esta interface. Sabemos que, quando uma aplicação solicita ao Spring uma instância de um tipo que implemente [FactoryBean], 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 pelo método [TransactionProxyFactoryBean].getObject(). Qual é a natureza deste objeto? Não vamos entrar em pormenores, pois são complexos. Estão relacionados com o que se denomina Spring AOP (Programação Orientada a Aspectos). Vamos tentar esclarecer as coisas com esquemas simples. O AOP permite o seguinte:

  • temos duas classes, C1 e C2, sendo que C1 utiliza a interface [I2] apresentada por C2:
  • graças ao AOP, é possível inserir, de forma transparente para ambas as classes, um interceptor entre as classes C1 e C2:

A classe [C1] foi compilada para funcionar com a interface [I2], que é implementada pela [C2]. No momento da execução, a AOP insere a classe [intercepteur] entre a [C1] e a [C2]. Para que isso seja possível, é claro que a classe [intercepteur] tem de apresentar à [C1] a mesma interface [I2] que a [C2].

Para que é que isto pode servir? A documentação do Spring apresenta alguns exemplos. Podemos querer, por exemplo, registar logs aquando das chamadas a um método M específico de [C2], para realizar uma auditoria a esse método. No [intercepteur], escrever-se-á então um método [M] que efetua esses registos. A chamada do [C1] ao [C2].M decorrerá da seguinte forma (ver esquema acima):

  1. O [C1] chama o método M do [C2]. Na verdade, será chamado o método M de [intercepteur]. Isto é possível se [C1] se dirigir a uma interface [I2] em vez de a uma implementação específica de [I2]. Basta, então, que [intercepteur] implemente [I2].
  2. O método M de [intercepteur] gera os registos e chama o método M de [C2], inicialmente visado por [C1].
  3. O método M de [C2] é executado e devolve o seu resultado ao método M de [intercepteur], que pode, eventualmente, acrescentar algo ao que foi feito no ponto 2.
  4. O método M de [intercepteur] devolve um resultado ao método chamador de [C1]

Vê-se que o método M de [intercepteur] pode realizar alguma ação antes e depois da chamada ao método M de [C2]. Em relação ao [C1], ela enriquece, portanto, o método M do [C2]. Podemos, assim, considerar 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:

Pode dizer-se que a camada [service] é instanciada com dois objetos:

  • o objeto a que nos referimos acima como [proxy transactionnel] e que é, na verdade, o objeto devolvido pelo método [getObject] de [TransactionProxyFactoryBean]. É este objeto que fará a interface da camada [service] com a camada [web]. Por definição, ele implementa a interface [IService].
  • uma instância [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, do ponto de vista funcional, as adições/atualizações efetuadas por este método devem ser realizadas numa transação. Ou todas são bem-sucedidas, ou nenhuma é efetuada. Apresentámos o método [saveMany] da classe [ServiceImpl] e salientámos o facto de este não incluir o conceito de transação. O método [saveMany] da classe [proxy transactionnel] irá enriquecer o método [saveMany] da classe [ServiceImpl] com este conceito de transação. Sigamos o esquema acima:

  1. a camada [web] chama o método [saveMany] da interface [IService].
  2. O método [saveMany] de [proxy transactionnel] é executado. Este inicia uma transação. É necessário que disponha de informações suficientes para o fazer, nomeadamente um objeto [DataSource] para estabelecer uma ligação ao SGBD. Em seguida, invoca o método [saveMany] de [ServiceImpl].
  3. Esta é executada. Chama repetidamente a camada [dao] para executar as inserções ou atualizações. As ordens SQL executadas nesta ocasião são executadas na transação iniciada no ponto 2.
  4. Suponhamos que uma destas operações falhe. A camada [dao] permitirá que uma exceção seja propagada para a camada [service], neste caso, o método [saveMany] da instância [ServiceImpl].
  5. Esta instância não faz nada e permite que a exceção seja propagada até ao método [saveMany] de [proxy transactionnel].
  6. Ao receber a exceção, o método [saveMany] de [proxy transactionnel], que é o proprietário da transação, executa um [rollback] sobre a mesma para anular todas as atualizações, depois permite que a exceção seja encaminhada até à camada [web], que ficará encarregada de a gerir.

Na etapa 4, partimos do princípio de que uma das inserções ou atualizações falhava. Se não for esse o caso, em [5] não é propagada qualquer exceção. O mesmo se aplica a [6]. Nesse caso, o método [saveMany] de [proxy transactionnel] executa um [commit] da transação para validar todas as atualizações.

Temos agora uma ideia mais precisa da arquitetura implementada pelo bean [TransactionProxyFactoryBean]. Voltemos à sua configuração:


    <!-- gestor de transações -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- classes de acesso à camada [service] -->
    <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 analisar esta configuração à luz da arquitetura que está definida:

  • O [proxy transactionnel] irá gerir as transações. O Spring oferece várias estratégias para a gestão das mesmas. O [proxy transactionnel] necessita de 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

O [DataSourceTransactionManager] é um gestor de transações adaptado aos SGBD acedidos através de um objeto [DataSource]. Este gestor só consegue gerir transações num único SGBD. Não consegue gerir transações distribuídas por vários SGBD. Neste caso, temos apenas um único SGBD. Por isso, este gestor de transações é adequado. Quando o [proxy transactionnel] iniciar uma transação, fá-lo-á numa ligação associada ao thread. É esta ligação que será utilizada em todas as camadas que conduzem à base de dados: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

A classe [DataSourceTransactionManager] precisa de saber qual é a fonte de dados à qual deve solicitar uma ligação para a associar ao thread. Esta é definida nas linhas 4-6: trata-se da mesma fonte de dados utilizada pela camada [dao] (ver parágrafo 17.5.2).

  • linhas 14-19: o atributo «target» indica a classe que deve 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 é ela que assegura a comunicação com a camada [dao]
    • a [TransactionProxyFactoryBean] deve gerar um proxy que apresente à camada [web] a mesma interface que a [ServiceImpl].
  • linhas 21-27: indicam quais os métodos de [ServiceImpl] que o proxy deve interceptar. O atributo [transactionAttributes], na linha 21, indica quais os métodos de [ServiceImpl] que requerem uma transação e quais são os atributos dessa transação:
  • linha 23: os métodos cujo nome começa por «get» [getOne, getAll] são executados numa transação com o atributo [PROPAGATION_REQUIRED,readOnly]:
    • PROPAGATION_REQUIRED: o método é executado numa transação se já existir uma associada ao thread; caso contrário, é criada uma nova transação e o método é executado nessa transação.
    • readOnly: transação de leitura única

Aqui, os métodos [getOne] e [getAll] de [ServiceImpl] serão executados numa transação, quando, na verdade, isso não é necessário. Trata-se, em todos os casos, de uma operação constituída por uma única ordem SELECT. Não se percebe a utilidade de colocar este SELECT numa transação.

  • linha 24: os métodos cujo nome começa por «save», [saveOne, saveMany], são executados numa 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, saveMany].

Na nossa camada [service], apenas os métodos [saveMany] e [deleteMany] precisam de ser executados numa 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. Testes da camada [service]

Agora que escrevemos e configurámos a camada [service], propomos testá-la com os testes JUnit:

Image

O ficheiro de configuração [spring-config-test-service-firebird.xml] da camada [service] é o que foi descrito no parágrafo 17.5.2.

O teste JUnit [TestServiceFirebird] é o seguinte:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

     // camada [service]
    private IService service;

    public IService getService() {
        return service;
    }

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

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

     // lista de pessoas
    private void doListe(Collection personnes) {
...
    }

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

     // alteração-eliminação de um elemento inexistente
    public void test2() throws ParseException {
...
    }

     // gestão de versões de pessoas
    public void test3() throws ParseException, InterruptedException {
...
    }

     // bloqueio otimista - acesso multithread
    public void test4() throws Exception {
...
    }

     // testes de validade de saveOne
    public void test5() throws ParseException {
...
    }

         // inserções multithread
    public void test6() throws ParseException, InterruptedException{
...
    }

     // testes do método deleteMany
    public void test7() throws ParseException {
         // lista atual
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
         // visualização
        doListe(personnes);
         // criação de três pessoas
        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);
         // adição das 3 pessoas — a pessoa p3 com o ID -2 va i provocar
         // uma exceção
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // verificação
        assertTrue(erreur);
         // nova lista — o número de elementos não deve ter mudado
         // devido ao rollback automático da transação
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
         // adição das duas pessoas válidas
         // redefine-se o ID delas para -1
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
         // recuperamos os seus IDs
        int id1 = p1.getId();
        int id2 = p2.getId();
         // verificações
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
         // nova lista — deve ter mais 2 elementos
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
         // eliminação de p1 e p2 e de uma pessoa inexistente
         // deve ocorrer uma exceção
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // verificação
        assertTrue(erreur);
         // nova lista
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
         // nenhuma pessoa deveria ter sido eliminada (reversão
         // automático da transação)
        assertEquals(nbPersonnes4, nbPersonnes3);
         // eliminam-se as duas pessoas válidas
        service.deleteMany(new int[] { id1, id2 });
         // verificações
         // pessoa p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve haver um erro com o código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // pessoa p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve haver um erro de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // nova lista
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
         // verificação — devemos ter voltado ao ponto de partida
        assertEquals(nbPersonnes5, nbPersonnes1);
         // visualização
        doListe(personnes);
    }

}
  • linhas 19-22: o programa testa as camadas [dao] e [service] configuradas pelo ficheiro [spring-config-test-service-firebird.xml], o mesmo analisado na secção anterior.
  • Os testes de [test1] a [test6] são idênticos, em termos de conceito, 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] são agora executados numa transação.
  • O método [test7] tem como objetivo testar os métodos [saveMany] e [deleteMany]. Pretende-se verificar se estes são efetivamente executados numa transação. Vamos comentar o código deste método:
  • linhas 62-63: conta-se o número de pessoas [nbPersonnes1] atualmente na lista
  • linhas 67-72: criam-se 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, vão ser adicionadas à tabela [PERSONNES]. A pessoa p3, por sua vez, tem um id igual a -2. Não se trata, portanto, de uma inserção, mas sim de uma atualização. Esta irá falhar, uma vez que não existe nenhuma pessoa com um id igual a -2 na tabela [PERSONNES]. A camada [dao] irá, portanto, lançar uma exceção que será propagada até à camada [service]. A existência desta exceção é verificada na linha 83.
  • Devido à exceção anterior, a camada [service] deverá executar um [rollback] sobre o conjunto de ordens SQL emitidas durante a execução do método [saveMany], isto porque este método é executado numa transação. Linhas 86-87: verifica-se se o número de pessoas na lista não se alterou e, portanto, se as inserções de p1 e p2 não ocorreram.
  • linhas 88-103: adicionam-se apenas as pessoas p1 e p2 e verifica-se que, em seguida, há mais duas pessoas na lista.
  • linhas 106-114: elimina-se um grupo de pessoas constituído pelas pessoas p1 e p2 que acabámos de adicionar e por uma pessoa inexistente (id= -1). Para tal, é utilizado o método [deleteMany], na linha 108. Este método irá falhar, uma vez que 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 será propagada até à camada [service]. A existência desta exceção é verificada na linha 114.
  • Devido à exceção anterior, a camada [service] deverá executar um [rollback] sobre o conjunto de ordens SQL emitidas durante a execução do método [deleteMany], isto porque este método é executado numa transação. Linhas 116-117: verifica-se se o número de pessoas da lista não se alterou e, portanto, se as eliminações de p1 e p2 não ocorreram.
  • linha 122: elimina-se um grupo constituído apenas pelas pessoas p1 e p2. Isto deverá ser bem-sucedido. O resto do método verifica se é efetivamente esse o caso.

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

Image

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

17.7. A camada [web]

Recorde-se a arquitetura geral da aplicação web a construir:

Acabámos de construir as camadas [dao] e [service], que permitem trabalhar com uma base de dados Firebird. Escrevemos uma versão 1 desta aplicação, na qual as camadas [dao] e [service] trabalhavam com uma lista de pessoas na memória. A camada [web], criada nessa ocasião, continua válida. Com efeito, destinava-se a uma camada [service] que implementava a interface [IService]. Uma vez que a nova camada [service] implementa essa mesma interface, a camada [web] não precisa de ser alterada.

No artigo anterior, a versão 1 da aplicação tinha sido testada com o projeto Eclipse [mvc-personnes-02B], em que as camadas [web, service, dao, entites] tinham sido colocadas em arquivos .jar:

A pasta [src] estava vazia. As classes das camadas encontravam-se nos arquivos [personnes-*.jar ]:

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

Image

No projeto [mvc-personnes-03], exportamos as camadas [dao] e [service], respetivamente, para os arquivos [personnes-dao.jar] e [personnes-service.jar] da 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 irão substituir os ficheiros com o mesmo nome da versão anterior.

Copiamos e colamos também os ficheiros [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 arquivos são necessários para as novas camadas [dao] e [service].

Feito isto, incluímos os novos arquivos no Classpath do projeto: [clic droit sur projet -> Properties -> Java Build Path -> Add Jars].

A pasta [src] contém os ficheiros de configuração das 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], que foi utilizado para configurar o teste da camada de serviço no projeto [mvc-personnes-03]. Por isso, faz-se um copiar/colar 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>
    <!-- a fonte de dados 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">
            <!-- atenção: não deixe espaços entre as duas balizas <value> -->
            <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>
    <!-- a classe de acesso à camada [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- gestor de transações -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- classes de acesso à camada [service] -->
    <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 que serviu para os testes das camadas [dao] e [service]

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

Estamos prontos para os testes de « ». O Firebird SGBD é iniciado. O conteúdo da tabela [PERSONNES] é então o seguinte:

Image

O Tomcat é, por sua vez, iniciado. Através de um navegador, acedemos à URL [http://localhost:8080/mvc-personnes-03B]:

Image

Adicionamos uma nova pessoa através do link [Ajout]:

Verificamos a adição na base de dados:

Image

O leitor é convidado a realizar outros testes com o [modification, suppression].

Vamos agora realizar o teste de conflitos de versão que tinha sido feito na versão 1. [Firefox] será o navegador do utilizador U1. Este solicita o URL [http://localhost:8080/mvc-personnes-03B]:

Image

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

Image

O utilizador U1 acede à edição do perfil da pessoa [Perrichon]:

Image

O utilizador U2 faz o mesmo:

Image

O utilizador U1 efetua alterações e valida:

O utilizador U2 faz o mesmo:

O utilizador U2 volta à lista de pessoas através do link [Annuler] do formulário:

Image

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

E a base de dados nisto tudo? Vejamos:

Image

A pessoa n.º 899 tem, de facto, o nome em maiúsculas na sequência da alteração efetuada por U1.

17.8. Conclusion

Recorde-se 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, por isso, 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 ficasse numa tabela da base de dados. Assim, passa a ser persistente. Vamos agora analisar o impacto que a alteração em SGBD tem na nossa aplicação. Para tal, vamos criar três novas versões da nossa aplicação web:

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

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

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

Para além destes pontos, tudo permanece inalterado. A seguir, descrevemos estas novas versões, centrando-nos apenas nas novidades introduzidas por cada uma delas.