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]:

A tabela [PERSONNES] conterá a lista de pessoas geridas pela aplicação web. Foi criada com os seguintes comandos SQL:
- 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:

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
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]:

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]:

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:
![]()
- [dbpersonnes.gdb] é a base de dados.
- [dbpersonnes.sql] é o script SQL para a geração da base de dados:
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:
![]()
- [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:

- [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:
- A interface possui os mesmos quatro métodos que na versão anterior.
A classe [DaoImplCommon] que implementa esta interface será a seguinte:
- 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/]:

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]:

![]() |
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:

Entre os métodos desta classe, um deles permite configurar o cliente [iBATIS] com o qual iremos explorar a base de dados:
![]()
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:
- solicitar uma ligação a um conjunto de ligações
- abrir uma transação
- executar uma série de ordens SQL armazenadas num ficheiro de configuração
- encerrar a transação
- 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]:

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

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()]:
![]()
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:

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]:

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/]:

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:

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:
- o nome do controlador JDBC a utilizar – inicializado com [setDriverClassName]
- o nome da URL da base de dados a utilizar – inicializado com [setUrl]
- o identificador do utilizador proprietário da ligação – inicializado com [setUsername] (e não setUserName, como seria de esperar)
- 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:

- 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:
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:
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:
![]()
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:
![]()
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:

Implementa a seguinte interface [SqlMapClientOperations]:

Esta interface define métodos capazes de explorar o conteúdo do ficheiro [personnes-firebird.xml]:
[queryForList]
![]()
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]
![]()
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]
![]()
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]
![]()
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]
![]()
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]:
- 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:
- 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:
- 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:
- 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:
- 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.
- 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:
- 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:
- Uma atualização pode falhar por, pelo menos, duas razões:
- a pessoa a atualizar não existe
- 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:

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]:
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:

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

Conseguimos, de facto, obter a lista de pessoas. Podemos passar ao teste JUnit.
O teste JUnit [TestDaoFirebird] é o seguinte:
- 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:
- 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.
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:
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:
- 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:
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:
- 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]:
- 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]:

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]:
![]()
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:
![]()
Obtém-se o objeto do tipo [Throwable], que foi lançado pelo controlador JDBC:

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:
- 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]:
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:
- 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:

O [test4] foi bem-sucedido. As últimas linhas dos registos de ecrã são as seguintes:
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:
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]:

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:
![]()
- [IService] é a interface apresentada pela camada [service]
- [ServiceImpl] é uma implementação desta
A interface [IService] é a seguinte:
- 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:
- 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]:

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():
![]()
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):
- 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].
- O método M de [intercepteur] gera os registos e chama o método M de [C2], inicialmente visado por [C1].
- 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.
- 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:
- a camada [web] chama o método [saveMany] da interface [IService].
- 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].
- 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.
- 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].
- Esta instância não faz nada e permite que a exceção seja propagada até ao método [saveMany] de [proxy transactionnel].
- 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]:

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:

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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
- 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:

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

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:

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]:

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:

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

Adicionamos uma nova pessoa através do link [Ajout]:
![]() | ![]() |
Verificamos a adição na base de dados:

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]:

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

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

O utilizador U2 faz o mesmo:

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:

Encontra a pessoa [Perrichon] tal como U1 a alterou (nome em maiúsculas).
E a base de dados nisto tudo? Vejamos:

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.

























