17. Aplicação Web MVC numa arquitetura de 3 camadas – Exemplo 3 – SGBD Firebird
17.1. A base de dados Firebird
Nesta nova versão, iremos armazenar a lista de pessoas numa tabela da base de dados Firebird. Informações sobre a instalação e gestão deste SGBD podem ser encontradas no documento [http://tahe.developpez.com/divers/sql-firebird/]. As capturas de ecrã abaixo são do IBExpert, um cliente de administração para os SGBDs Interbase e Firebird.
A base de dados tem o nome [dbpersonnes.gdb]. Contém uma tabela chamada [PERSONNES]:

A tabela [PERSONNES] conterá a lista de pessoas geridas pela aplicação web. Foi criada utilizando as seguintes instruções SQL:
- Linhas 2–10: A estrutura da tabela [PERSONNES], concebida para armazenar objetos do tipo [Person], reflete a estrutura desse objeto. Uma vez que o tipo booleano não existe no Firebird, o campo [MARRIED] (linha 8) foi declarado como sendo do tipo [SMALLINT], um inteiro. O seu valor será 0 (solteiro) ou 1 (casado).
- Linhas 13–16: restrições de integridade que espelham as do validador de dados [ValidatePerson].
- Linha 19: O campo ID é a chave primária da tabela [PERSONNES]
A tabela [PERSONNES] poderia ter o seguinte conteúdo:

A base de dados [dbpersonnes.gdb] contém, além da tabela [PERSONNES], um objeto chamado gerador denominado [GEN_PERSONNES_ID]. Este gerador produz números inteiros sequenciais que utilizaremos para atribuir um valor à chave primária [ID] da tabela [PERSONNES]. Vejamos um exemplo para ilustrar como funciona:
![]() |
![]() |
Podemos ver que o valor do gerador [GEN_PERSONNES_ID] mudou (clique duas vezes nele + F5 para atualizar):
retorna, portanto, o seguinte valor para o gerador [GEN_PERSONNES_ID]. GEN_ID é uma função interna do Firebird e [RDB$DATABASE] é uma tabela de sistema neste SGBD.
17.2. O projeto Eclipse para as camadas [dao] e [service]
Para desenvolver as camadas [dao] e [service] da nossa aplicação de base de dados, utilizaremos o seguinte projeto Eclipse [mvc-personnes-03]:

O projeto é um projeto Java simples, não um projeto web Tomcat. Lembre-se de que a versão 2 da nossa aplicação utilizará a camada [web] da versão 1. Esta camada, portanto, não precisa de ser escrita.
Pasta [src]
Esta pasta contém o código-fonte das camadas [dao] e [service]:

Contém vários pacotes:
- [istia.st.mvc.personnes.dao]: contém a camada [dao]
- [istia.st.mvc.personnes.entites]: contém a classe [Person]
- [istia.st.mvc.people.service]: contém a classe [service]
- [istia.st.mvc.personnes.tests]: contém os testes JUnit para as camadas [dao] e [service]
bem como ficheiros de configuração que devem estar no ClassPath da aplicação.
pasta [database]
Esta pasta contém a base de dados Firebird para pessoas:
![]()
- [dbpersonnes.gdb] é a base de dados.
- [dbpersonnes.sql] é o script SQL para gerar a base de dados:
Pasta [lib]
Esta pasta contém os ficheiros necessários para a aplicação:
![]() |
Repare na presença do controlador JDBC [firebirdsql-full.jar] para o SGBD Firebird, bem como de vários ficheiros [spring-*.jar]. Poderíamos ter utilizado o único ficheiro [spring.jar] encontrado na pasta [dist] da distribuição, que contém todas as classes do Spring. Também podemos utilizar apenas os arquivos necessários para o projeto. Foi isso que fizemos aqui, guiados pelos erros de classes em falta relatados pelo Eclipse e pelos nomes dos arquivos parciais do Spring. Todos estes arquivos da pasta [lib] foram colocados no Classpath do projeto.
Pasta [dist]
Esta pasta conterá os arquivos resultantes da compilação das classes da aplicação:
![]()
- [personnes-dao.jar]: arquivo da camada [dao]
- [personnes-service.jar]: arquivo da camada [service]
17.3. A camada [dao]
17.3.1. Componentes da camada [dao]
A camada [dao] é composta pelas seguintes classes e interfaces:

- [IDao] é a interface fornecida pela camada [dao]
- [DaoImplCommon] é uma implementação desta interface, na qual o grupo de pessoas é armazenado numa tabela de base de dados. [DaoImplCommon] agrupa funcionalidades independentes do SGBD.
- [DaoImplFirebird] é uma classe derivada de [DaoImplCommon] para gerir especificamente uma base de dados Firebird.
- [DaoException] é o tipo de exceções não tratadas lançadas pela camada [dao]. Esta classe é da versão 1.
A interface [IDao] é a seguinte:
- A interface tem os mesmos quatro métodos da 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, [insertPerson] e [updatePerson], dependendo se é necessário adicionar ou modificar uma pessoa.
- linha 50: o método privado [check] é o mesmo da versão anterior. Não o abordaremos novamente aqui.
- Linha 8: Para implementar a interface [IDao], a classe [DaoImpl] estende a classe Spring [SqlMapClientDaoSupport].
17.3.2. A camada de acesso a dados [iBATIS]
A classe Spring [SqlMapClientDaoSupport] utiliza uma estrutura de terceiros [Ibatis SqlMap] disponível no URL [http://ibatis.apache.org/]:

[iBATIS] é um projeto Apache que facilita a construção de camadas [DAO] orientadas por bases de dados. Com o [iBATIS], a arquitetura da camada de acesso aos dados é a seguinte:
![]() |
O [iBATIS] situa-se entre a camada [DAO] da aplicação e o controlador JDBC da base de dados. Existem alternativas ao [iBATIS], tais como o [Hibernate]:

![]() |
A utilização da estrutura [iBATIS] requer dois arquivos [ibatis-common, ibatis-sqlmap], ambos colocados na pasta [lib] do projeto:
![]() |
A classe [SqlMapClientDaoSupport] encapsula a parte genérica da utilização da estrutura [iBATIS], ou seja, os segmentos de código presentes em todas as camadas [DAO] que utilizam a ferramenta [iBATIS]. Para escrever a parte não genérica do código — ou seja, o código específico da camada [DAO] que estamos a escrever — basta derivar a classe [SqlMapClientDaoSupport]. É isso que estamos a fazer aqui.
A classe [SqlMapClientDaoSupport] é definida da seguinte forma:

Entre os métodos desta classe, um deles permite-nos configurar o cliente [iBATIS] com o qual iremos operar 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 partir de um conjunto de ligações
- abrir uma transação
- executar uma série de instruções SQL armazenadas num ficheiro de configuração
- fechar a transação
- devolver a ligação ao conjunto
Se a nossa implementação [DaoImplCommon] funcionasse diretamente com o [iBATIS], teria de executar esta sequência repetidamente. Apenas a operação 3 é específica da camada [dao]; as outras operações são genéricas. A classe Spring [SqlMapClientDaoSupport] irá tratar das operações 1, 2, 4 e 5 por si própria, delegando a operação 3 à sua classe derivada, neste caso a classe [DaoImplCommon].
Para funcionar, a classe [SqlMapClientDaoSupport] requer uma referência ao objeto iBATIS [SqlMapClient sqlMapClient], que irá tratar da comunicação com a base de dados. Este objeto requer duas coisas para funcionar:
- um objeto [DataSource] ligado à base de dados a partir da qual solicitará ligações
- um (ou mais) ficheiros de configuração onde as instruções SQL a executar são externalizadas. De facto, estas não se encontram no código Java. São identificadas por um código num ficheiro de configuração, e o objeto [SqlMapClient sqlMapClient] utiliza este código para executar uma instrução SQL específica.
Uma configuração preliminar da nossa camada [dao] que refletisse a arquitetura acima seria a seguinte:
<!-- the [dao] layer access classes -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
Aqui, a propriedade [sqlMapClient] (linha 3) da classe [DaoImplCommon] (linha 2) é inicializada. É inicializada pelo método [setSqlMapClient] da classe [DaoImpl]. Esta classe não possui este método. É a sua classe pai [SqlMapClientDaoSupport] que o possui. Portanto, é, na verdade, esta classe que é inicializada aqui.
Agora, na linha 4, fazemos referência a um objeto chamado "sqlMapClient" que ainda não foi criado. Como mencionado, este objeto é do tipo [SqlMapClient], um tipo [iBATIS]:

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

Lembre-se de que pretendemos instanciar um objeto que implemente a interface [SqlMapClient]. Este não parece ser o caso da classe [SqlMapClientFactoryBean]. Esta classe implementa a interface [FactoryBean] (ver acima). Possui o seguinte método [getObject()]:
![]()
Quando é solicitado ao Spring uma instância de um objeto que implemente a interface [FactoryBean], ele:
- cria uma instância [I] da classe — neste caso, cria uma instância do tipo [SqlMapClientFactoryBean].
- retorna ao método de chamada o resultado do método [I].getObject() — o método [SqlMapClientFactoryBean].getObject() irá retornar um objeto que implementa a interface [SqlMapClient].
Para devolver um objeto que implementa a interface [SqlMapClient], a classe [SqlMapClientFactoryBean] necessita de duas informações necessárias para esse objeto:
- um objeto [DataSource] conectado à base de dados da qual solicitará conexões
- um (ou mais) ficheiros de configuração onde estão armazenadas as instruções SQL a executar
A classe [SqlMapClientFactoryBean] possui métodos set para inicializar estas duas propriedades:

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

Mais uma vez, estamos a lidar com uma interface para a qual precisamos de encontrar uma classe de implementação. O papel dessa classe é fornecer de forma eficiente a uma aplicação ligações a uma base de dados específica. Um SGBD não pode manter um grande número de ligações abertas simultaneamente. Para reduzir o número de ligações abertas em qualquer momento, para cada interação com a base de dados, devemos:
- abrir uma ligação
- iniciar uma transação
- executar instruções SQL
- fechar a transação
- fechar a ligação
Abrir e fechar ligações repetidamente é demorado. Para resolver estas duas questões — limitar o número de ligações abertas em qualquer momento e reduzir a sobrecarga de as abrir e fechar — as classes que implementam a interface [DataSource] procedem frequentemente da seguinte forma:
- Após a instanciação, elas abrem N ligações à base de dados de destino. N tem geralmente um valor predefinido e pode normalmente ser definido num ficheiro de configuração. Estas N ligações permanecem abertas em todos os momentos e formam um conjunto de ligações disponíveis para os threads da aplicação.
- Quando uma thread da aplicação solicita uma ligação, o objeto [DataSource] fornece-lhe uma das N ligações abertas no arranque, caso ainda haja alguma disponível. Quando a aplicação fecha a ligação, esta não é realmente fechada, mas simplesmente devolvida ao conjunto de ligações disponíveis.
Existem várias implementações disponíveis gratuitamente da interface [DataSource]. Aqui, utilizaremos a implementação [commons DBCP] disponível no URL [http://jakarta.apache.org/commons/dbcp/]:

A utilização da ferramenta [commons DBCP] requer dois arquivos [commons-dbcp, commons-pool], ambos colocados na pasta [lib] do projeto:
![]() |
A classe [BasicDataSource] do [commons DBCP] fornece a implementação [DataSource] de que precisamos:

Esta classe irá fornecer-nos um conjunto de ligações para aceder à base de dados Firebird da nossa aplicação [dbpersonnes.gdb]. Para tal, temos de lhe fornecer as informações necessárias para criar as ligações nesse conjunto:
- o nome do controlador JDBC a utilizar – inicializado com [setDriverClassName]
- o URL da base de dados a utilizar – inicializado com [setUrl]
- o nome de utilizador 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 para a nossa camada [dao] poderia ter o seguinte aspeto:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data source DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- warning: do not leave spaces between the two <value> tags in the url -->
<property name="url">
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- the [dao] layer access classes -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
- linhas 7–9: o nome do controlador JDBC para o SGBD Firebird
- linhas 11-13: o URL da base de dados Firebird [dbpersonnes.gdb]. Preste muita atenção à forma como isto está escrito. Não deve haver espaços entre as tags <value> e o URL.
- linhas 14-16: o proprietário da ligação – neste caso, [sysdba], que é o administrador predefinido para as distribuições Firebird
- Linhas 17–19: a sua palavra-passe [masterkey] — também o valor predefinido
Fizemos progressos significativos, mas ainda há alguns pontos de configuração a esclarecer: a linha 28 faz referência ao ficheiro [sql-map-config-firebird.xml], que deve configurar o iBATIS [SqlMapClient]. Antes de examinar o seu conteúdo, vamos mostrar a localização destes ficheiros de configuração no nosso projeto Eclipse:

- [spring-config-test-dao-firebird.xml] é o ficheiro de configuração para a camada [dao] que acabámos de examinar
- [sql-map-config-firebird.xml] é referenciado por [spring-config-test-dao-firebird.xml]. Vamos examiná-lo.
- [personnes-firebird.xml] é referenciado por [sql-map-config-firebird.xml]. Vamos examiná-lo.
Os três ficheiros acima mencionados estão localizados na pasta [src]. No Eclipse, isto significa que, em tempo de execução, estarão presentes na pasta [bin] do projeto (não mostrada acima). Esta pasta faz parte do ClassPath da aplicação. Por conseguinte, os três ficheiros acima mencionados estarão presentes no ClassPath da aplicação. Isto é necessário.
O ficheiro [sql-map-config-firebird.xml] é o seguinte:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
- Este ficheiro deve ter <sqlMapConfig> como sua tag raiz (linhas 6 e 8)
- Linha 7: A tag <sqlMap> é utilizada para especificar os ficheiros que contêm as instruções SQL a executar. Frequentemente — embora não necessariamente — existe um ficheiro por tabela. Isto permite que as instruções SQL para uma determinada tabela sejam agrupadas num único ficheiro. No entanto, são comuns instruções SQL que envolvem várias tabelas. Nesses casos, a estrutura anterior não se aplica. É simplesmente importante lembrar que todos os ficheiros designados pelas tags <sqlMap> serão fundidos. Estes ficheiros são procurados no ClassPath da aplicação.
O ficheiro [personnes-firebird.xml] descreve as instruções SQL que serão executadas na tabela [PERSONNES] na base de dados Firebird [dbpersonnes.gdb]. O seu conteúdo é o seguinte:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap>
<!-- alias class [Person] -->
<typeAlias alias="Personne.classe"
type="istia.st.mvc.personnes.entites.Personne"/>
<!-- mapping table [PERSONNES] - object [Person] -->
<resultMap id="Personne.map"
class="Personne.classe">
<result property="id" column="ID" />
<result property="version" column="VERSION" />
<result property="nom" column="NOM"/>
<result property="prenom" column="PRENOM"/>
<result property="dateNaissance" column="DATENAISSANCE"/>
<result property="marie" column="MARIE"/>
<result property="nbEnfants" column="NBENFANTS"/>
</resultMap>
<!-- list of all persons -->
<select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
<!-- get a specific person -->
<select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#value#</select>
<!-- add a person -->
<insert id="Personne.insertOne" parameterClass="Personne.classe">
<selectKey keyProperty="id">
SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
</selectKey>
insert into
PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS)
VALUES(#id#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#,
#nbEnfants#) </insert>
<!-- update a person -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#,
MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and
VERSION=#version#</update>
<!-- delete a person -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#value# </delete>
</sqlMap>
- O ficheiro deve ter <sqlMap> como tag raiz (linhas 7 e 45)
- Linhas 9–10: Para facilitar a escrita do ficheiro, atribuímos o alias [Person.class] à classe [istia.st.springmvc.personnes.entites.Person].
- Linhas 12–21: definem os mapeamentos entre as colunas da tabela [PERSONNES] e os campos do objeto [Personne].
- Linhas 23–24: A instrução SQL [SELECT] para recuperar todas as pessoas da tabela [PERSONNES]
- linhas 26–27: a instrução SQL [SELECT] para recuperar uma pessoa específica da tabela [PERSONNES]
- Linhas 29–36: a instrução SQL [insert] que insere uma pessoa na tabela [PERSONS]
- linhas 38-41: a instrução SQL [update] que atualiza uma pessoa na tabela [PERSONS]
- linhas 42–44: o comando SQL [delete] que elimina uma pessoa da tabela [PERSONS]
O papel e o significado do conteúdo do ficheiro [people-firebird.xml] serão explicados através de uma análise da classe [DaoImplCommon], que implementa a camada [dao].
17.3.3. A classe [DaoImplCommon]
Vamos rever a arquitetura de acesso aos dados:
![]() |
A classe [DaoImplCommon] é a seguinte:
Vamos analisar os métodos um por um.
getAll
Este método recupera todas as pessoas da lista. O seu código é o seguinte:
Primeiro, recordemos que a classe [DaoImplCommon] deriva da classe [SqlMapClientDaoSupport] do Spring. É esta classe que fornece o método [getSqlMapClientTemplate()] utilizado na linha 3 acima. Este método tem a seguinte assinatura:
![]()
O tipo [SqlMapClientTemplate] encapsula o objeto [SqlMapClient] da camada [iBATIS]. É através deste que acederemos à base de dados. O tipo SqlMapClient do [iBATIS] poderia ser utilizado diretamente, uma vez que a classe [SqlMapClientDaoSupport] tem acesso ao mesmo:
![]()
A desvantagem da classe [iBATIS] SqlMapClient é que ela lança exceções [SQLException], um tipo de exceção controlada, ou seja, uma que deve ser tratada por um bloco try/catch ou declarada na assinatura dos métodos que a lançam. No entanto, lembremo-nos de que a camada [dao] implementa uma interface [IDao] cujos métodos não incluem exceções nas suas assinaturas. Os métodos das classes que implementam a interface [IDao] também não podem, portanto, ter exceções nas suas assinaturas. Temos, portanto, de interceptar todas as [SQLException] lançadas pela camada [iBATIS] e encapsulá-las numa exceção não verificada. O tipo [DaoException] do nosso projeto seria adequado para esta encapsulação.
Em vez de tratarmos estas exceções nós próprios, iremos confiá-las ao tipo Spring [SqlMapClientTemplate], que encapsula o objeto [SqlMapClient] da camada [iBATIS]. De facto, o [SqlMapClientTemplate] foi concebido para interceptar exceções [SQLException] lançadas pela camada [SqlMapClient] e encapsulá-las num tipo de exceção não tratada [ DataAccessException]. Este comportamento convém-nos. Basta lembrarmo-nos de que a camada [dao] é agora capaz de lançar dois tipos de exceções não tratadas:
- o nosso tipo personalizado [DaoException]
- o tipo [DataAccessException] do Spring
O tipo [SqlMapClientTemplate] é definido da seguinte forma:

Ele implementa a seguinte interface [SqlMapClientOperations]:

Esta interface define métodos capazes de utilizar o conteúdo do ficheiro [people-firebird.xml]:
[queryForList]
![]()
Este método permite emitir uma instrução [SELECT] e recuperar o resultado como uma lista de objetos:
- [statementName]: o identificador (id) da instrução [select] no ficheiro de configuração
- [parameterObject]: o objeto «parameter» para um [SELECT] parametrizado. O objeto «parameter» pode assumir duas formas:
- um objeto em conformidade com o padrão JavaBean: os parâmetros da instrução [SELECT] são, então, os nomes dos campos do JavaBean. Quando a instrução [SELECT] é executada, estes são substituídos pelos valores desses campos.
- um dicionário: os parâmetros da instrução [select] são, então, as chaves do dicionário. Quando a instrução [select] é executada, estas são substituídas pelos seus valores associados no dicionário.
- Se a instrução [SELECT] não devolver nenhuma linha, o resultado [List] é um objeto vazio, mas não nulo (a verificar).
[queryForObject]
![]()
Este método é conceptualmente idêntico ao anterior, mas devolve apenas um único objeto. Se o [SELECT] não devolver nenhuma linha, o resultado é o ponteiro nulo.
[insert]
![]()
Este método executa uma instrução SQL [insert] configurada pelo segundo parâmetro. O objeto devolvido é a chave primária da linha que foi inserida. Não é necessário utilizar este resultado.
[update]
![]()
Este método executa uma instrução SQL [update] configurada pelo segundo parâmetro. O resultado é o número de linhas modificadas pela instrução SQL [update].
[delete]
![]()
Este método executa uma instrução SQL [delete] configurada pelo segundo parâmetro. O resultado é o número de linhas eliminadas pela instrução SQL [delete].
Voltemos ao método [getAll] da classe [DaoImplCommon]:
- Linha 4: A instrução [select] denominada "Person.getAll" é executada. Não tem parâmetros, pelo que o objeto "parameter" é nulo.
Em [people-firebird.xml], a instrução [select] denominada "Person.getAll" é a seguinte:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap>
<!-- alias class [Person] -->
<typeAlias alias="Personne.classe"
type="istia.st.mvc.personnes.entites.Personne"/>
<!-- mapping table [PERSONNES] - object [Person] -->
<resultMap id="Personne.map"
class="Personne.classe">
<result property="id" column="ID" />
<result property="version" column="VERSION" />
<result property="nom" column="NOM"/>
<result property="prenom" column="PRENOM"/>
<result property="dateNaissance" column="DATENAISSANCE"/>
<result property="marie" column="MARIE"/>
<result property="nbEnfants" column="NBENFANTS"/>
</resultMap>
<!-- list of all persons -->
<select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
- Linha 23: A instrução SQL "Person.getAll" não é parametrizada (não há parâmetros no texto da consulta).
- A linha 3 do método [getAll] solicita a execução da consulta [select] denominada «Personne.getAll». Esta consulta será executada. O [iBATIS] baseia-se no JDBC. Sabemos, portanto, que o resultado da consulta será devolvido como um objeto [ResultSet]. Linha 23: o atributo [resultMap] da tag <select> indica ao [iBATIS] qual o «resultMap» a utilizar para converter cada linha do [ResultSet] obtido num objeto. É o "resultMap" [Person.map] definido nas linhas 12–21 que especifica como mapear uma linha da tabela [PERSONNES] para um objeto do tipo [Person]. O [iBATIS] utilizará estes mapeamentos para devolver uma lista de objetos [Person] com base nas linhas do [ResultSet].
- A linha 3 do método [getAll] devolve então uma coleção de objetos [Person]
- O método [queryForList] pode lançar uma [DataAccessException] do Spring. Deixamos que ela se propague.
Explicaremos os outros métodos da classe [AbstractDaoImpl] de forma mais sucinta, uma vez que os fundamentos da utilização do [iBATIS] já foram abordados na discussão do método [getAll].
getOne
Este método recupera uma pessoa identificada pelo seu [id]. O seu código é o seguinte:
- linha 4: solicita a execução da instrução [select] denominada "Person.getOne". Isto corresponde ao seguinte no ficheiro [people-firebird.xml]:
<!-- get a specific person -->
<select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM
PERSONNES WHERE ID=#value#</select>
A consulta SQL é configurada pelo parâmetro #value# (linha 4). O atributo #value# especifica o valor do parâmetro passado para a consulta SQL quando esse parâmetro é de um tipo simples: Integer, Double, String, etc. Nos atributos da tag <select>, o atributo [parameterClass] indica que o parâmetro é do tipo Integer (linha 2). Na linha 5 de [getOne], vemos que este parâmetro é o ID da pessoa que está a ser pesquisada, na forma de um objeto Integer. Esta conversão de tipo é obrigatória, uma vez que o segundo parâmetro de [queryForList] deve ser do tipo [Object].
O resultado da consulta [select] será convertido num objeto através do atributo [resultMap="Personne.map"] (linha 2). Obteremos, portanto, um tipo [Personne].
- Linhas 7–11: Se a consulta [select] não devolveu nenhuma linha, recuperamos o ponteiro nulo da linha 4. Isto significa que a pessoa procurada não foi encontrada. Neste caso, lançamos uma [DaoException] com o código 2 (linhas 9–10).
- linha 13: se não ocorreu nenhuma exceção, então o objeto [Person] solicitado é devolvido.
deleteOne
Este método permite eliminar uma pessoa identificada pelo seu [id]. O seu código é o seguinte:
- linhas 4-5: solicita a execução do comando [delete] denominado "Person.deleteOne". Trata-se do seguinte no ficheiro [people-firebird.xml]:
<!-- delete a person -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#value# </delete>
O comando SQL é configurado pelo parâmetro #value# (linha 3) do tipo [parameterClass="int"] (linha 2). Este será o ID da pessoa que está a ser procurada (linha 5 de deleteOne)
- linha 4: o resultado do método [SqlMapClientTemplate].delete é o número de linhas eliminadas.
- Linhas 7–8: Se a consulta [delete] não eliminou nenhuma linha, isso significa que a pessoa não existe. É lançada uma [DaoException] com o código 2 (linha 8).
saveOne
Este método permite adicionar uma nova pessoa ou modificar uma já existente. O seu código é o seguinte:
- linha 4: verificamos a validade da pessoa utilizando o método [check]. Este método já existia na versão anterior e tinha sido comentado na altura. Lança uma [DaoException] se a pessoa for inválida. Deixamos esta exceção propagar-se.
- linha 6: se chegarmos a este ponto, significa que não ocorreu nenhuma exceção. A pessoa é, portanto, válida.
- Linhas 6–11: Dependendo do ID da pessoa, trata-se de uma adição (ID = -1) ou de uma atualização (ID ≠ -1). Em ambos os casos, são chamados dois métodos internos da classe:
- insertPersonne: para adicionar
- updatePersonne: para a atualização
insertPerson
Este método permite adicionar uma nova pessoa. O seu código é o seguinte:
- Linha 4: Defina o número da versão da pessoa que está a ser criada como 1
- linha 9: insira o registo utilizando a consulta denominada "Person.insertOne", que é a seguinte:
<insert id="Personne.insertOne" parameterClass="Personne.classe">
<selectKey keyProperty="id">
SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
</selectKey>
insert into
PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS)
VALUES(#id#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#,
#nbEnfants#) </insert>
Esta é uma consulta parametrizada, e o parâmetro é do tipo [Person] (parameterClass="Person.class", linha 1). Os campos do objeto [Person] passado como parâmetro (linha 9 de insertPersonne) são utilizados para preencher as colunas da linha a ser inserida na tabela [PERSONS] (linhas 5–8). Temos um problema para resolver. Durante uma inserção, o objeto [Person] a ser inserido tem um ID igual a -1. Este valor deve ser substituído por uma chave primária válida. Para tal, utilizamos as linhas 2–4 da tag <selectKey> acima. Estas especificam:
- (continuação)
- a consulta SQL a executar para obter um valor de chave primária. A que aqui se apresenta é a que apresentámos na Secção 17.1. Há dois pontos que merecem destaque:
- "as 'value'" é obrigatório. Também pode escrever "as value", mas "value" é uma palavra-chave do Firebird que deve ser colocada entre aspas.
- A tabela Firebird chama-se, na verdade, [RDB$DATABASE]. No entanto, o caractere $ é interpretado pelo [iBATIS]. Foi escapado duplicando-o.
- O campo do objeto [Person] que deve ser inicializado com o valor recuperado pela instrução [SELECT], neste caso o campo [id]. Este campo é especificado pelo atributo [keyProperty] na linha 2.
- a consulta SQL a executar para obter um valor de chave primária. A que aqui se apresenta é a que apresentámos na Secção 17.1. Há dois pontos que merecem destaque:
- Linhas 6-7: Para fins de teste, vamos esperar 10 ms antes de realizar a inserção para verificar se há conflitos entre threads que tentam fazer adições simultaneamente.
updatePerson
Este método permite modificar uma pessoa já existente na tabela [PERSONNES]. O seu código é o seguinte:
- Uma atualização pode falhar por pelo menos duas razões:
- a pessoa a ser atualizada não existe
- a pessoa a ser atualizada existe, mas o segmento que tenta modificá-la não possui a versão correta
- linhas 7-8: é executada a consulta SQL [update] denominada «Person.updateOne». Tem o seguinte formato:
<!-- update a person -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#,
MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and
VERSION=#version#</update>
- (continuação)
- Linha 2: A consulta é parametrizada e aceita um tipo [Person] como parâmetro (parameterClass="Person.class"). Esta é a pessoa a ser modificada (linha 8 – updatePerson).
- Queremos apenas modificar a pessoa na tabela [PERSONS] que tenha o mesmo ID e versão que o parâmetro. É por isso que temos a restrição [WHERE ID=#id# and VERSION=#version#]. Se essa pessoa for encontrada, ela é atualizada com a pessoa do parâmetro e a sua versão é incrementada em 1 (linha 3 acima).
- Linha 9: Recuperamos o número de linhas atualizadas.
- Linhas 10–11: Se este número for zero, é lançada uma [DaoException] com o código 2, indicando que ou a pessoa a ser atualizada não existe, ou a sua versão mudou entretanto.
17.4. Testes para a camada [dao]
17.4.1. Testar a implementação [DaoImplCommon]
Agora que escrevemos a camada [dao], propomos testá-la com testes JUnit:

Antes de realizar testes exaustivos, podemos começar com um programa [main] simples que irá apresentar o conteúdo da tabela [PERSONNES]. Esta é a classe [MainTestDaoFirebird]:
O ficheiro de configuração [spring-config-test-dao-firebird.xml] para a camada [dao], utilizado nas linhas 13–14, é o seguinte:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data source DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- warning: do not leave spaces between the two <value> tags -->
<property name="url">
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- the [dao] layer access classes -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
Este ficheiro é o que foi abordado na Secção 17.3.2.
Para efeitos de teste, o SGBD Firebird é iniciado. O conteúdo da tabela [PERSONNES] é o seguinte:

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

Conseguimos obter a lista de pessoas com sucesso. Podemos agora prosseguir para o 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] é novo. Iremos comentar apenas estes dois testes.
[test4]
O [test4] tem como objetivo testar o método [updatePersonne - DaoImplCommon]. Aqui está o código desse método:
- Linhas 4-5: Esperamos 10 ms. Isto força o thread que está a executar [updatePerson] a perder a CPU, o que pode aumentar as nossas hipóteses de observar conflitos de acesso entre threads simultâneas.
[test4] lança N=100 threads encarregadas de incrementar simultaneamente o número de filhos da mesma pessoa em 1. Queremos ver como os conflitos de versão e os conflitos de acesso são tratados.
Os threads são criados nas linhas 8–13. Cada um deles incrementará em 1 o número de filhos da pessoa criada nas linhas 3–5. Os threads de atualização [ThreadDaoMajEnfants] são os seguintes:
A atualização de uma pessoa pode falhar porque a pessoa que pretendemos modificar não existe ou porque foi previamente atualizada por outro segmento de execução. Estes dois casos são tratados aqui nas linhas 67–69. Em ambos os casos, o método [updatePersonne] lança uma [DaoException] com o código 2. O segmento de execução será então forçado a reiniciar o procedimento de atualização desde o início (loop while, linha 34).
[test6]
[test6] destina-se a testar o método [insertPersonne - DaoImplCommon]. Aqui está o código desse método:
- Linhas 6-7: Esperamos 10 ms para forçar o thread que executa [insertPerson] a perder a CPU, aumentando assim as nossas hipóteses de observar conflitos causados por threads a realizar inserções ao mesmo tempo.
O código para [test6] é o seguinte:
Criamos 100 threads que irão inserir simultaneamente 100 pessoas diferentes. Estas 100 threads irão todas obter uma chave primária para a pessoa que precisam de inserir, sendo depois pausadas durante 10 ms (linha 10 – insertPerson) antes de poderem realizar a inserção. Queremos verificar se tudo corre bem e, em particular, se obtêm efetivamente valores de chave primária diferentes.
- Linhas 7–11: É criada uma matriz com 100 pessoas. Todas estas pessoas são cópias da pessoa p criada nas linhas 4–5.
- Linhas 14–17: São lançadas as 100 threads de inserção. Cada uma é responsável por inserir uma das 100 pessoas criadas anteriormente.
- Linhas 19–23: [test6] aguarda que cada uma das 100 threads que lançou termine. Quando deteta que a thread número i terminou, elimina a pessoa que essa thread acabou de inserir.
A thread de inserção [ThreadDaoInsertPersonne] é a seguinte:
- Linhas 19–22: O construtor da thread armazena a pessoa a ser inserida e a camada [DAO] a ser utilizada para a inserção.
- linha 30: a pessoa é inserida. Se ocorrer uma exceção, esta é propagada para [test6].
Testes
Durante os testes, obtemos os seguintes resultados:
![]() |
O teste [test4] falha, portanto. O número de filhos baixou para 69, em vez dos 100 esperados. O que aconteceu? Vamos examinar os registos de ecrã. Estes mostram a existência de exceções lançadas pelo Firebird:
Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];
--- The error occurred in personnes-firebird.xml.
--- The error occurred while applying a parameter map.
--- Check the Personne.updateOne-InlineParameterMap.
--- Check the statement (update failed).
--- Cause: org.firebirdsql.jdbc.FBSQLException: GDS Exception. 335544336. deadlock
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:
--- The error occurred in personnes-firebird.xml.
--- The error occurred while applying a parameter map.
- Linha 1 – Ocorreu uma exceção Spring [org.springframework.jdbc.UncategorizedSQLException]. Esta é uma exceção não capturada que foi utilizada para envolver uma exceção lançada pelo controlador JDBC do Firebird, descrita na linha 6.
- Linha 6 – O controlador JDBC do Firebird lançou uma exceção do tipo [org.firebirdsql.jdbc.FBSQLException] com o código de erro 335544336.
- Linha 7: indica que houve um conflito de concorrência entre duas threads que estavam a tentar atualizar a mesma linha na tabela [PERSONNES] ao mesmo tempo.
Este não é um erro fatal. A thread que captura esta exceção pode tentar novamente a atualização. Para tal, modifique o código em [ThreadDaoMajEnfants]:
- Linha 8: Tratamos uma exceção do tipo [DaoException]. Com base no que foi dito, devemos tratar a exceção que surgiu durante os testes, do tipo [org.springframework.jdbc.UncategorizedSQLException]. No entanto, não podemos simplesmente tratar este tipo, que é um tipo genérico do Spring destinado a encapsular exceções que não reconhece. O Spring reconhece exceções lançadas pelos controladores JDBC de vários SGBDs, como Oracle, MySQL, Postgres, DB2, SQL Server, ... mas não o Firebird. Portanto, qualquer exceção lançada pelo controlador JDBC do Firebird é encapsulada no tipo Spring [org.springframework.jdbc.UncategorizedSQLException]:

Como mostrado acima, a classe [UncategorizedSQLException] deriva da classe [DataAccessException] mencionada na secção 17.3.3. É possível determinar qual a exceção que foi encapsulada na [UncategorizedSQLException] utilizando o seu método [getSQLException]:
![]()
Esta [SQLException] é a que é lançada pela camada [iBATIS], que por sua vez encapsula a exceção lançada pelo controlador JDBC da base de dados. A causa exata da [SQLException] pode ser obtida utilizando o método:
![]()
Obtemos o objeto do tipo [Throwable] que foi lançado pelo controlador JDBC:

O tipo [Throwable] é a classe pai de [Exception].
Aqui, precisamos de verificar se o objeto [Throwable] lançado pelo controlador JDBC do Firebird — que causou a [SQLException] lançada pela camada [iBATIS] — é, de facto, uma exceção do tipo [org.firebirdsql.gds.GDSException] com o código de erro 335544336. Para recuperar o código de erro, podemos utilizar o método [getErrorCode()] da classe [org.firebirdsql.gds.GDSException].
Se utilizarmos a exceção [org.firebirdsql.gds.GDSException] no código [ThreadDaoMajEnfants], então este thread só funcionará com o SGBD Firebird. O mesmo se aplica ao teste [test4] que utiliza este thread. Queremos evitar isso. Na verdade, queremos que os nossos testes JUnit permaneçam válidos independentemente do SGBD utilizado. Para conseguir isso, decidimos que a camada [dao] lançará uma [DaoException] com o código 4 sempre que for detetada uma exceção de «conflito de atualização», independentemente do SGBD subjacente. Assim, o thread [ThreadDaoMajEnfants] pode ser reescrito da seguinte forma:
- linhas 34-36: a exceção [DaoException] com o código 4 é capturada. O segmento [ThreadDaoMajEnfants] será forçado a reiniciar o procedimento de atualização desde o início (linha 10)
A nossa camada [dao] deve, portanto, ser capaz de reconhecer uma exceção de «conflito de atualização». Esta exceção é lançada por um controlador JDBC e é específica deste. Esta exceção deve ser tratada no método [updatePerson] da classe [DaoImplCommon]:
As linhas 7–11 devem estar incluídas num bloco try/catch. Para o SGBD Firebird, precisamos de verificar se a exceção que causou a falha na atualização é do tipo [org.firebirdsql.gds.GDSException] e tem o código de erro 335544336. Se colocarmos este tipo de teste em [DaoImplCommon], iremos vincular esta classe ao SGBD Firebird, o que é obviamente indesejável. Se quisermos manter a classe [DaoImplCommon] de uso geral, precisamos de a derivar e tratar a exceção numa classe específica do Firebird. É isso que estamos a fazer agora.
17.4.2. A classe [DaoImplFirebird]
O seu código é o seguinte:
- Linha 5: A classe [DaoImplFirebird] deriva de [DaoImplCommon], a classe que acabámos de estudar. Ela redefine, nas linhas 8–33, o método [updatePersonne] que nos está a causar problemas.
- linhas 20: capturamos a exceção Spring do tipo [UncategorizedSQLException]
- linhas 21–22: verificamos que a exceção subjacente do tipo [SQLException], lançada pela camada [iBATIS], é causada por uma exceção do tipo [org.firebirdsql.jdbc.FBSQLException]
- Linha 25: verificamos também que o código de erro para esta exceção do Firebird é 335544336, o código de erro «deadlock».
- linhas 26-27: se todas estas condições forem satisfeitas, é lançada uma [DaoException] com o código 4.
- linhas 36-44: o método [wait] pausa o thread atual por N milissegundos. É útil apenas para testes.
Estamos prontos para testar a nova camada [dao].
17.4.3. Testar a implementação [DaoImplFirebird]
O ficheiro de configuração de teste [spring-config-test-dao-firebird.xml] é modificado para utilizar a implementação [DaoImplFirebird]:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data source DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- warning: do not leave spaces between the two <value> tags -->
<property name="url">
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- the [dao] layer access classes -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
- Linha 32: a nova implementação [DaoImplFirebird] da camada [dao].
Os resultados do teste [test4], que anteriormente tinha falhado, são os seguintes:

[test4] aprovado. As últimas linhas dos registos de ecrã são as seguintes:
A última linha indica que o thread n.º 36 foi o último a terminar. A linha 3 mostra um conflito de versão que obrigou o thread n.º 36 a reiniciar o seu procedimento de atualização de pessoas (linha 4). Outros registos mostram conflitos de acesso durante as atualizações:
A linha 2 mostra que o thread n.º 75 falhou durante a sua atualização devido a um conflito de atualização: quando o comando SQL [update] foi emitido na tabela [PERSONNES], a linha que precisava de ser atualizada estava bloqueada por outro thread. Este conflito de acesso forçará o thread n.º 75 a tentar novamente a sua atualização.
Para concluir com o [test4], observamos uma diferença significativa em relação aos resultados do mesmo teste na versão 1, onde falhou devido a problemas de sincronização. Uma vez que os métodos na camada [dao] da versão 1 não estavam sincronizados, ocorreram conflitos de acesso. Aqui, não foi necessário sincronizar a camada [dao]. Simplesmente tratámos os conflitos de acesso reportados pelo Firebird.
Vamos agora executar todo o teste JUnit para a camada [dao]:

Parece, portanto, que temos uma camada [dao] válida. Para a declarar válida com um elevado grau de certeza, precisaríamos de realizar mais testes. No entanto, vamos considerá-la operacional.
17.5. A camada [service]
17.5.1. Os componentes da camada [service]
A camada [service] é composta pelas seguintes classes e interfaces:
![]()
- [IService] é a interface apresentada pela camada [serviço]
- [ServiceImpl] é uma implementação desta interface
A interface [IService] é a seguinte:
- A interface possui os mesmos quatro métodos da versão 1, mas tem dois adicionais:
- saveMany: permite guardar várias pessoas ao mesmo tempo de forma atómica. Ou são todas guardadas, ou nenhuma é.
- deleteMany: permite eliminar várias pessoas ao mesmo tempo de forma atómica. Ou são todas eliminadas, ou nenhuma é.
Estes dois métodos não serão utilizados pela aplicação web. Adicionámo-los para ilustrar o conceito de transação de base de dados. Ambos os métodos devem ser executados dentro de uma transação para alcançar a atomicidade desejada.
A classe [ServiceImpl] que implementa esta interface será a seguinte:
- Os métodos [getAll, getOne, insertOne, saveOne] chamam os métodos da camada [dao] com os mesmos nomes.
- Linhas 42–47: O método [saveMany] guarda, um a um, as pessoas na matriz passada como parâmetro.
- Linhas 50–55: O método [deleteMany] elimina, uma a uma, as pessoas cujos IDs são passados como um parâmetro de matriz.
Mencionámos que os métodos [saveMany] e [deleteMany] devem ser executados dentro de uma transação para garantir a natureza «tudo ou nada» destes métodos. Podemos ver que o código acima ignora completamente este conceito de transações. Isto só aparecerá no ficheiro de configuração da camada [service].
17.5.2. Configuração da camada [ service]
Acima, na linha 11, vemos que a implementação [ServiceImpl] mantém uma referência à camada [dao]. Esta, tal como na versão 1, será inicializada pelo Spring quando a camada [service - ServiceImpl] for instanciada. O ficheiro de configuração que permitirá a instanciação da camada [service] é o seguinte:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data source DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<property name="url">
<!-- warning: do not leave spaces between the two <value> tags -->
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- the [dao] layer access class -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
<!-- transaction manager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- access classes to the [service] layer -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- linhas 1–36: configuração da camada [dao]. Esta configuração foi explicada ao abordar a camada [dao] na secção 17.3.2.
- Linhas 38–64: configurar a camada [service]
Na linha 46, podemos ver que a camada [service] é implementada pelo tipo [TransactionProxyFactoryBean]. Esperávamos encontrar o tipo [ServiceImpl]. [TransactionProxyFactoryBean] é um tipo predefinido do Spring. Como é possível que um tipo predefinido implemente a interface [IService], que é específica da nossa aplicação?
Vamos primeiro dar uma olhada na classe [TransactionProxyFactoryBean]:

Vemos que ela implementa a interface [FactoryBean]. Já nos deparámos com esta interface. Sabemos que, quando uma aplicação solicita uma instância de um tipo que implementa [FactoryBean] ao Spring, o Spring não devolve uma instância [I] desse tipo, mas sim o objeto devolvido pelo método [I].getObject():
![]()
No nosso caso, a camada [service] será implementada pelo objeto devolvido por [TransactionProxyFactoryBean].getObject(). Qual é a natureza deste objeto? Não entraremos em detalhes, pois são complexos. Estes enquadram-se no que é conhecido como Spring AOP (Programação Orientada a Aspectos). Tentaremos esclarecer as coisas com alguns diagramas simples. A AOP permite o seguinte:
- Temos duas classes, C1 e C2, em que C1 utiliza a interface [I2] fornecida por C2:
![]() |
- Graças à AOP, podemos colocar um interceptor entre as classes C1 e C2 de uma forma transparente para ambas as classes:
![]() |
A classe [C1] foi compilada para funcionar com a interface [I2] que [C2] implementa. Em tempo de execução, o AOP coloca a classe [interceptor] entre [C1] e [C2]. Para que isso seja possível, a classe [interceptor] deve, naturalmente, apresentar a mesma interface [I2] à [C1] que [C2] apresenta.
Para que serve isto? A documentação do Spring fornece alguns exemplos. Por exemplo, poderá querer registar chamadas a um método específico M de [C2] para auditar esse método. Em [interceptor], escreveria então um método [M] que executa esses registos. A chamada de [C1] para [C2].M decorrerá da seguinte forma (ver diagrama acima):
- [C1] chama o método M de [C2]. Na verdade, é o método M de [interceptor] que será chamado. Isto é possível se [C1] se dirigir a uma interface [I2] em vez de a uma implementação específica de [I2]. Tudo o que é necessário é que [interceptor] implemente [I2].
- O método M de [interceptor] regista a informação e chama o método M de [C2] que foi inicialmente visado por [C1].
- O método M de [C2] é executado e devolve o seu resultado ao método M de [interceptor], que pode, opcionalmente, adicionar algo ao que foi feito no passo 2.
- O método M de [interceptor] devolve um resultado ao método de chamada de [C1]
Vemos que o método M de [interceptor] pode fazer algo antes e depois da chamada ao método M de [C2]. Do ponto de vista de [C1], isso enriquece, portanto, o método M de [C2]. Podemos, assim, ver a tecnologia AOP como uma forma de enriquecer a interface apresentada por uma classe.
Como é que este conceito se aplica à nossa camada [service]? Se implementarmos a camada [service] diretamente com uma instância [ServiceImpl], a nossa aplicação web terá a seguinte arquitetura:
![]() |
Se implementarmos a camada [service] com uma instância [TransactionProxyFactoryBean], teremos a seguinte arquitetura:
![]() |
Podemos dizer que a camada [service] é instanciada com dois objetos:
- o objeto a que nos referimos acima como [proxy transacional], que é, na verdade, o objeto devolvido pelo método [getObject] de [TransactionProxyFactoryBean]. Este objeto atua como interface entre a camada [serviço] e a camada [web]. Por definição, implementa a interface [IService].
- uma instância de [ServiceImpl], que também implementa a interface [IService]. Só ela sabe como interagir com a camada [dao], pelo que é necessária.
Imaginemos que a camada [web] chama o método [saveMany] da interface [IService]. Sabemos que, funcionalmente, as inserções/atualizações realizadas por este método devem ser feitas dentro de uma transação. Ou todas são bem-sucedidas, ou nenhuma é executada. Apresentámos o método [saveMany] da classe [ServiceImpl] e observámos que lhe faltava o conceito de transação. O método [saveMany] do [proxy transacional] irá melhorar o método [saveMany] da classe [ServiceImpl] com este conceito de transação. Vamos seguir o diagrama acima:
- A camada [web] chama o método [saveMany] da interface [IService].
- O método [saveMany] do [proxy transacional] é executado. Ele inicia uma transação. Para tal, deve dispor de informação suficiente, nomeadamente um objeto [DataSource] para estabelecer uma ligação ao SGBD. Em seguida, chama o método [saveMany] de [ServiceImpl].
- Este método é executado. Ele chama repetidamente a camada [dao] para realizar as inserções ou atualizações. As instruções SQL executadas neste momento são executadas dentro da transação iniciada no passo 2.
- Suponha que uma destas operações falhe. A camada [dao] propagará uma exceção até à camada [service], especificamente ao método [saveMany] da instância [ServiceImpl].
- Este método não faz nada e permite que a exceção se propague até ao método [saveMany] do [transactional proxy].
- Ao receber a exceção, o método [saveMany] do [transactional proxy], que é o proprietário da transação, executa um [rollback] para cancelar todas as atualizações e, em seguida, permite que a exceção se propague até à camada [web], que será responsável por tratá-la.
Na etapa 4, assumimos que uma das inserções ou atualizações falhou. Se não for esse o caso, em [5] nenhuma exceção é propagada. O mesmo se aplica em [6]. Neste caso, o método [saveMany] do [transactional proxy] confirma a transação para validar todas as atualizações.
Temos agora uma visão mais clara da arquitetura implementada pelo bean [TransactionProxyFactoryBean]. Vamos rever a sua configuração:
<!-- transaction manager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- access classes to the [service] layer -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
Vamos examinar esta configuração à luz da arquitetura que está definida:
![]() |
- O [proxy transacional] irá gerir as transações. O Spring oferece várias estratégias de gestão de transações. O [proxy transacional] requer uma referência ao gestor de transações escolhido.
- Linhas 11–13: definem o atributo [transactionManager] do bean [TransactionProxyFactoryBean] com uma referência a um gestor de transações. Este é definido nas linhas 2–7.
- Linhas 2–7: O gestor de transações é do tipo [DataSourceTransactionManager]:

[DataSourceTransactionManager] é um gestor de transações adequado para SGBDs acedidos através de um objeto [DataSource]. Só pode gerir transações num único SGBD. Não pode gerir transações distribuídas por vários SGBDs. Aqui, temos apenas um SGBD. Portanto, este gestor de transações é adequado. Quando o [proxy transacional] inicia uma transação, fá-lo numa ligação associada ao thread. Esta ligação será utilizada em todas as camadas que conduzem à base de dados: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].
A classe [DataSourceTransactionManager] precisa de saber a fonte de dados à qual deve solicitar uma ligação para associar à thread. Isto é definido nas linhas 4–6: é a mesma fonte de dados que a utilizada pela camada [dao] (ver secção 17.5.2).
- Linhas 14–19: O atributo «target» especifica a classe a ser interceptada, neste caso a classe [ServiceImpl]. Esta informação é necessária por duas razões:
- a classe [ServiceImpl] deve ser instanciada, uma vez que lida com a comunicação com a camada [dao]
- o [TransactionProxyFactoryBean] deve gerar um proxy que apresente a mesma interface que [ServiceImpl] à camada [web].
- Linhas 21–27: especificam quais métodos de [ServiceImpl] o proxy deve interceptar. O atributo [transactionAttributes] na linha 21 indica quais métodos de [ServiceImpl] requerem uma transação e quais são os atributos da transação:
- linha 23: os métodos cujos nomes começam por get [getOne, getAll] são executados dentro de uma transação com os atributos [PROPAGATION_REQUIRED, readOnly]:
- PROPAGATION_REQUIRED: o método é executado numa transação se já existir uma associada ao segmento; caso contrário, é criada uma nova e o método é executado dentro dela.
- readOnly: transação de leitura apenas
Aqui, os métodos [getOne] e [getAll] de [ServiceImpl] serão executados dentro de uma transação, mesmo que isso não seja realmente necessário. Cada operação consiste numa única instrução SELECT. Não vemos sentido em colocar esta instrução SELECT dentro de uma transação.
- Linha 24: Os métodos cujos nomes começam por «save» — [saveOne] e [saveMany] — são executados dentro de uma transação com o atributo [PROPAGATION_REQUIRED].
- Linha 25: Os métodos [deleteOne] e [deleteMany] de [ServiceImpl] estão configurados de forma idêntica aos métodos [saveOne] e [saveMany].
Na nossa camada [service], apenas os métodos [saveMany] e [deleteMany] precisam de ser executados dentro de uma transação. A configuração poderia ter sido reduzida às seguintes linhas:
<property name="transactionAttributes">
<props>
<prop key="saveMany">PROPAGATION_REQUIRED</prop>
<prop key="deleteMany">PROPAGATION_REQUIRED</prop>
</props>
</property>
17.6. Testar a camada [service]
Agora que escrevemos e configuramos a camada [service], vamos testá-la utilizando testes JUnit:

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

Todos os sete testes foram bem-sucedidos. Consideraremos a nossa camada [service] como operacional.
17.7. A camada [w eb]
Vamos rever a arquitetura geral da aplicação web a ser construída:
![]() |
Acabámos de criar as camadas [dao] e [service] para trabalhar com uma base de dados Firebird. Escrevemos uma versão 1 desta aplicação em que as camadas [dao] e [service] trabalhavam com uma lista de pessoas na memória. A camada [web] escrita nessa altura continua válida. Com efeito, interagia com uma camada [service] que implementava a interface [IService]. Uma vez que a nova camada [service] implementa esta mesma interface, a camada [web] não precisa de ser modificada.
No artigo anterior, a versão 1 da aplicação foi testada com o projeto Eclipse [mvc-personnes-02B], onde as camadas [web, service, dao, entities] foram empacotadas em ficheiros .jar:
![]() |
A pasta [src] estava vazia. As classes das camadas estavam nos arquivos [people-*.jar]:
![]() |
Para testar a versão 2, no Eclipse duplicamos a pasta [mvc-personnes-02B] para [mvc-personnes-03B] (copiar/colar):

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

Copiamos estes dois ficheiros e, em seguida, no Eclipse, colamo-los na pasta [WEB-INF/lib] do projeto [mvc-personnes-03B], onde substituirão os ficheiros com o mesmo nome da versão anterior.
![]() |
Copiamos e colamos também os arquivos [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] da pasta [lib] do projeto [mvc-personnes-03] para a pasta [WEB-INF/lib] do projeto [mvc-personnes-03B]. Estes ficheiros JAR são necessários para as novas camadas [dao] e [service].
Depois de fazer isso, incluímos os novos ficheiros JAR no Classpath do projeto: [clique com o botão direito do rato no projeto -> Propriedades -> Java Build Path -> Adicionar JARs].
A pasta [src] contém os ficheiros de configuração para as camadas [dao] e [service]:

O ficheiro [spring-config.xml] configura as camadas [dao] e [service] da aplicação web. Na nova versão, é idêntico ao ficheiro [spring-config-test-service-firebird.xml] utilizado para configurar o teste da camada de serviço no projeto [mvc-personnes-03]. Por isso, copiamos e colamos de um para o outro:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data source DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<property name="url">
<!-- warning: do not leave spaces between the two <value> tags -->
<value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
</property>
<property name="username">
<value>sysdba</value>
</property>
<property name="password">
<value>masterkey</value>
</property>
</bean>
<!-- SqlMapCllient -->
<bean id="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource">
<ref local="dataSource"/>
</property>
<property name="configLocation">
<value>classpath:sql-map-config-firebird.xml</value>
</property>
</bean>
<!-- the [dao] layer access class -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
<!-- transaction manager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- access classes to the [service] layer -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- Linha 12: o URL da base de dados Firebird. Continuamos a utilizar a base de dados utilizada para testar as camadas [dao] e [service]
Implementamos o projeto web [mvc-personnes-03B] no Tomcat:
![]() | ![]() |
Estamos prontos para o teste . O SGBD Firebird está em execução. O conteúdo da tabela [PERSONNES] é o seguinte:

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

Adicionamos uma nova pessoa utilizando o link [Add]:
![]() | ![]() |
Verificamos a adição na base de dados:

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

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

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

O utilizador U2 faz o mesmo:

O utilizador U1 faz alterações e submete:
![]() |
O utilizador U2 faz o mesmo:
![]() |
O utilizador U2 volta à lista de pessoas utilizando o link [Cancelar] no formulário:

Encontra a pessoa [Perrichon] tal como foi alterada pelo U1 (nome em maiúsculas).
E quanto à base de dados? Vamos dar uma olhadela:

O nome da Pessoa n.º 899 está, de facto, em maiúsculas, na sequência da modificação feita por U1.
17.8. Conclusão
Vamos recapitular o que pretendíamos fazer. Tínhamos uma aplicação web com a seguinte arquitetura de três camadas:
onde as camadas [dao] e [service] trabalhavam com uma lista de dados na memória que se perdia quando o servidor web era desligado. Essa era a versão 1. Na versão 2, as camadas [service] e [dao] foram reescritas para que a lista de pessoas fosse armazenada numa tabela de base de dados. Agora é persistente. Propomos agora examinar o impacto que a mudança do SGBD tem na nossa aplicação. Para tal, iremos construir três novas versões da nossa aplicação web:
![]() |
- Versão 3: o SGBD é o Postgres
- Versão 4: o SGBD é o MySQL
- Versão 5: o SGBD é o SQL Server Express 2005
As alterações são feitas nos seguintes locais:
- A classe [DaoImplFirebird] implementa a funcionalidade da camada [dao] relacionada com o SGBD Firebird. Se este requisito persistir, será substituída pelas classes [DaoImplPostgres], [DaoImplMySQL] e [DaoImplSqlExpress], respetivamente.
- O ficheiro de mapeamento iBATIS [personnes-firebird.xml] para o SGBD Firebird será substituído pelos ficheiros de mapeamento [personnes-postgres.xml], [personnes-mysql.xml] e [personnes-sqlexpress.xml], respetivamente.
- A configuração do objeto [DataSource] na camada [dao] é específica de um SGBD. Por conseguinte, irá mudar com cada versão.
- O controlador JDBC do SGBD também muda com cada versão
Para além destes pontos, tudo o resto permanece igual. Nas secções seguintes, descrevemos estas novas versões, focando-nos exclusivamente nas novas funcionalidades introduzidas por cada uma delas.

























