17. Aplicativo web MVC em uma arquitetura de três camadas – Exemplo 3 – SGBD Firebird
17.1. O banco de dados Firebird
Nesta nova versão, vamos instalar a lista de pessoas em uma tabela do banco de dados Firebird. No documento [http://tahe.developpez.com/divers/sql-firebird/], você encontrará informações para instalar e gerenciar este SGBD. A seguir, as capturas de tela são do IBExpert, um cliente de administração dos SGBD Interbase e Firebird.
O banco de dados se chama [dbpersonnes.gdb]. Ele contém uma tabela [PERSONNES]:

A tabela [PERSONNES] conterá a lista de pessoas gerenciadas pelo aplicativo web. Ela foi criada com os seguintes comandos SQL:
- linhas 2-10: a estrutura da tabela [PERSONNES], destinada a armazenar 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. 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:

O banco de dados [dbpersonnes.gdb] possui, além da tabela [PERSONNES], um objeto chamado gerador, denominado [GEN_PERSONNES_ID]. Esse gerador fornece números inteiros sucessivos que utilizaremos para atribuir um valor à chave primária [ID] da classe [PERSONNES]. Vejamos um exemplo para ilustrar seu funcionamento:
![]() |
![]() |
É possível observar que o valor do gerador [GEN_PERSONNES_ID] mudou (clique duas vezes nele + F5 para atualizar):
A ordem e SQL
permite, portanto, obter o seguinte valor do gerador [GEN_PERSONNES_ID]. GEN_ID é uma função interna do Firebird e [RDB$DATABASE], uma tabela de sistema desse SGBD.
17.2. O projeto Eclipse das camadas [dao] e [service]
Para desenvolver as camadas [dao] e [service] de nosso aplicativo com banco de dados, utilizaremos o seguinte projeto Eclipse [mvc-personnes-03]:

O projeto é um projeto Java simples, não um projeto web Tomcat. Vale lembrar que a versão 2 do nosso aplicativo utilizará a camada [web] da versão 1. Portanto, essa camada não precisa ser reescrita.
Pasta [src]
Esta pasta contém os códigos-fonte das camadas [dao] e [service]:

Nele encontram-se diversos 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 arquivos de configuração que devem estar no ClassPath do aplicativo.
Pasta [database]
Esta pasta contém o banco de dados Firebird de pessoas:
![]()
- [dbpersonnes.gdb] é o banco de dados.
- [dbpersonnes.sql] é o script SQL para geração do banco de dados:
Pasta [lib]
Esta pasta contém os arquivos necessários para o funcionamento do aplicativo:
![]() |
Destaca-se a presença do driver JDBC [firebirdsql-full.jar] do Firebird SGBD, bem como de vários arquivos [spring-*.jar]. Poderíamos ter usado o único arquivo [spring.jar], encontrado na pasta [dist] da distribuição, que contém todas as classes do Spring. Também é possível utilizar apenas os arquivos necessários para o projeto. Foi o que fizemos aqui, guiando-nos pelos erros de classes ausentes sinalizados pelo Eclipse e pelos nomes dos arquivos parciais do Spring. Todos esses arquivos da pasta [lib] foram colocados na pasta Classpath do projeto.
Pasta [dist]
Esta pasta conterá os arquivos resultantes da compilação das classes do aplicativo:
![]()
- [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] é composta pelas seguintes classes e interfaces:

- [IDao] é a interface apresentada pela camada [dao]
- [DaoImplCommon] é uma implementação dessa interface, na qual o grupo de pessoas está armazenado em uma tabela de banco de dados. [DaoImplCommon] reúne funcionalidades independentes da SGBD.
- [DaoImplFirebird] é uma classe derivada de [DaoImplCommon] para gerenciar especificamente um banco de dados Firebird.
- [DaoException] é o tipo das exceções não controladas, lançadas pela camada [dao]. Essa classe corresponde à versão 1.
A interface [IDao] é a seguinte:
- A interface possui os mesmos quatro métodos da versão anterior.
A classe [DaoImplCommon], que implementa essa 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], dependendo se é necessário adicionar ou modificar uma pessoa.
- 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/]:

[iBATIS] é um projeto Apache que facilita a construção de camadas [dao] baseadas em bancos de dados. Com o [iBATIS], a arquitetura da camada de acesso aos dados é a seguinte:
![]() |
O [iBATIS] se insere entre a camada [dao] da aplicação e o driver JDBC do banco de dados. Existem alternativas ao [iBATIS], como, por exemplo, a alternativa [Hibernate]:

![]() |
O uso do framework [iBATIS] requer dois arquivos [ibatis-common, ibatis-sqlmap], ambos localizados na pasta [lib] do projeto:
![]() |
A classe [SqlMapClientDaoSupport] encapsula a parte genérica da utilização do framework [iBATIS] e c.a.d. das partes de código encontradas em todas as camadas [dao] que utilizam a ferramenta [iBATIS]. Para escrever a parte não genérica do código, ou seja, o que é específico da camada [dao] que estamos escrevendo, basta derivar a classe [SqlMapClientDaoSupport]. É isso que fazemos aqui.
A classe [SqlMapClientDaoSupport] é definida da seguinte forma:

Entre os métodos dessa classe, um deles permite configurar o cliente [iBATIS] com o qual iremos utilizar o banco de dados:
![]()
O objeto [SqlMapClient sqlMapClient] é o objeto [IBATIS] utilizado para acessar um banco de dados. Por si só, ele implementa a camada [iBATIS] da nossa arquitetura:
![]() |
Uma sequência típica de ações com esse objeto é a seguinte:
- solicitar uma conexão a um pool de conexões
- abrir uma transação
- executar uma série de ordens SQL armazenadas em um arquivo de configuração
- fechar a transação
- devolver a conexão ao pool
Se nossa implementação [DaoImplCommon] trabalhasse diretamente com [iBATIS], ela teria que repetir essa sequência constantemente. Apenas a operação 3 é específica da camada [dao]; as demais operações são genéricas. A classe Spring [SqlMapClientDaoSupport] se encarregará das operações 1, 2, 4 e 5, delegando a operação 3 à sua classe derivada, neste caso, a classe [DaoImplCommon].
Para funcionar, a classe [SqlMapClientDaoSupport] precisa de uma referência ao objeto iBATIS [SqlMapClient sqlMapClient], que fará a comunicação com o banco de dados. Esse objeto precisa de duas coisas para funcionar:
- um objeto [DataSource] conectado ao banco de dados, ao qual ele solicitará conexões
- um (ou mais) arquivo(s) de configuração onde estão externalizadas as ordens SQL a serem executadas. De fato, essas ordens não estão no código Java. Elas são identificadas por um código em um arquivo 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:
<!-- 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. Ela é inicializada pelo método [setSqlMapClient] da classe [DaoImpl]. Essa classe não possui esse método. É sua classe pai, [SqlMapClientDaoSupport], que o possui. Portanto, é ela que, na verdade, está sendo inicializada aqui.
Agora, na linha 4, faz-se referência a um objeto chamado “sqlMapClient”, que ainda precisa ser criado. Esse objeto, como já foi dito, é do tipo [SqlMapClient], um tipo [iBATIS]:

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

Vale lembrar que estamos tentando instanciar um objeto que implemente a interface [SqlMapClient]. Aparentemente, esse não é o caso da classe [SqlMapClientFactoryBean]. Ela implementa a interface [FactoryBean] (ver acima). Essa interface possui o seguinte método [getObject()]:
![]()
Quando se solicita ao Spring uma instância de um objeto que implemente a interface [FactoryBean], ele:
- cria uma instância [I] da classe — neste caso, ele cria uma instância do tipo [SqlMapClientFactoryBean].
- retorna ao método chamador o resultado do método [I].getObject() — o método [SqlMapClientFactoryBean].getObject() retornará aqui um objeto que implementa a interface [SqlMapClient].
Para poder retornar um objeto que implemente a interface [SqlMapClient], a classe [SqlMapClientFactoryBean] precisa de duas informações necessárias para esse objeto:
- um objeto [DataSource] conectado ao banco de dados, ao qual ele solicitará conexões
- um (ou mais) arquivo(s) de configuração onde estão externalizadas as ordens SQL a serem executadas
A classe [SqlMapClientFactoryBean] possui os métodos set para inicializar essas duas propriedades:

Estamos avançando... Nosso arquivo de configuração está ficando mais definido 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 acabamos de explicar, sabemos que, quando solicitamos ao Spring uma instância desse bean, obtemos um objeto que implementa a interface iBATIS [SqlMapClient]. É esse último objeto que será obtido na linha 14.
- linhas 7-9: indicamos que o arquivo de configuração necessário para o objeto iBATIS [SqlMapClient] se chama “sql-map-config-firebird.xml” e que ele deve ser procurado no ClassPath do aplicativo. O método [SqlMapClientFactoryBean].setConfigLocation é utilizado aqui.
- linhas 4-6: inicializamos a propriedade [dataSource] de [SqlMapClientFactoryBean] com seu método [setDataSource].
Na linha 5, fazemos referência a um bean chamado “dataSource”, que ainda precisa ser criado. Se observarmos o parâmetro esperado pelo método [setDataSource] de [SqlMapClientFactoryBean], vemos que ele é do tipo [DataSource]:

Mais uma vez, estamos diante de uma interface para a qual precisamos encontrar uma classe de implementação. A função dessa classe é fornecer a um aplicativo, de maneira eficiente, conexões com um banco de dados específico. Um SGBD não pode manter abertas simultaneamente um grande número de conexões. Para reduzir o número de conexões abertas em um determinado momento, somos levados, a cada interação com o banco de dados, a:
- abrir uma conexão
- iniciar uma transação
- emitir comandos SQL
- fechar a transação
- fechar a conexão
Abrir e fechar conexões repetidamente consome muito tempo. Para resolver esses dois problemas (limitar tanto o número de conexões abertas em um determinado momento quanto o custo de abri-las e fechá-las), as classes que implementam a interface [DataSource] costumam proceder da seguinte maneira:
- elas abrem, logo após sua instanciação, N conexões com o banco de dados em questão. N geralmente tem um valor padrão e, na maioria das vezes, pode ser definido em um arquivo de configuração. Essas N conexões permanecerão abertas o tempo todo e formam um pool de conexões disponíveis para os threads do aplicativo.
- Quando um thread da aplicação solicita a abertura de uma conexão, o objeto [DataSource] fornece a ele uma das N conexões abertas no início, caso ainda haja alguma disponível. Quando a aplicação fecha a conexão, esta, na verdade, não é fechada, mas simplesmente devolvida ao pool de conexões disponíveis.
Existem diversas implementações da interface [DataSource] disponíveis gratuitamente. Aqui, utilizaremos a implementação [commons DBCP], disponível no URL [http://jakarta.apache.org/commons/dbcp/]:

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

Essa classe nos fornecerá um pool de conexões para acessar o banco de dados Firebird [dbpersonnes.gdb] do nosso aplicativo. Para isso, é preciso fornecer a ela as informações necessárias para criar as conexões do pool:
- o nome do driver JDBC a ser utilizado – inicializado com [setDriverClassName]
- o nome da URL do banco de dados a ser utilizado – inicializado com [setUrl]
- o identificador do usuário proprietário da conexão – inicializado com [setUsername] (e não setUserName, como seria de se esperar)
- sua senha — inicializada com [setPassword]
O arquivo 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 tags <value> da 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 driver JDBC do Firebird SGBD
- linhas 11-13: a URL do banco de dados Firebird [dbpersonnes.gdb]. É preciso prestar atenção especial à forma como ela é escrita. Não deve haver nenhum espaço entre as tags <value> e a URL.
- linhas 14-16: o proprietário da conexão – neste caso, [sysdba], que é o administrador padrão das distribuições do Firebird
- linhas 17-19: sua senha, [masterkey] – também o valor padrão
Avançamos bastante, mas ainda há pontos de configuração a serem esclarecidos: a linha 28 faz referência ao arquivo [sql-map-config-firebird.xml], que deve configurar o cliente [SqlMapClient] de iBATIS. Antes de analisarmos seu conteúdo, vamos mostrar a localização desses arquivos de configuração em nosso projeto do Eclipse:

- [spring-config-test-dao-firebird.xml] é o arquivo de configuração da camada [dao] que acabamos de analisar
- [sql-map-config-firebird.xml] é referenciado por [spring-config-test-dao-firebird.xml]. Vamos analisá-lo.
- [personnes-firebird.xml] é referenciado por [sql-map-config-firebird.xml]. Vamos analisá-lo.
Os três arquivos anteriores estão na pasta [src]. No Eclipse, isso significa que, durante a execução, eles estarão presentes na pasta [bin] do projeto (não representada acima). Essa pasta faz parte do ClassPath do aplicativo. No final, os três arquivos anteriores estarão, portanto, presentes no ClassPath do aplicativo. Isso é necessário.
O arquivo [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>
- esse arquivo deve ter <sqlMapConfig> como tag raiz (linhas 6 e 8)
- linha 7: a tag <sqlMap> serve para indicar os arquivos que contêm os comandos SQL a serem executados. Geralmente, embora não seja obrigatório, há um arquivo por tabela. Isso permite reunir os comandos SQL relativos a uma determinada tabela em um único arquivo. No entanto, é comum encontrar comandos SQL que envolvem várias tabelas. Nesse caso, a divisão anterior não se aplica. Basta lembrar que todos os arquivos designados pelas tags <sqlMap> serão mesclados. Esses arquivos são buscados no ClassPath do aplicativo.
O arquivo [personnes-firebird.xml] descreve os comandos SQL que serão emitidos na tabela [PERSONNES] do banco de dados Firebird [dbpersonnes.gdb]. 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#, #sobrenome#, #nome#, #dateNaissance#, #casada#,
#nbEnfants#) </insert>
<!-- atualizar um usuário -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#versão#+1, NOM=#sobrenome#, PRENOM=#nome#, DATENAISSANCE=#dateNaissance#,
MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# e
VERSION=#versão#</update>
<!-- excluir uma pessoa -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#valor# </delete>
</sqlMap>
- o arquivo deve ter <sqlMap> como tag raiz (linhas 7 e 45)
- linhas 9-10: para facilitar a criação do arquivo, 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: o comando SQL [insert], que insere um indivíduo 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 exclui uma pessoa da tabela [PERSONNES]
A função e o significado do conteúdo do arquivo [personnes-firebird.xml] serão explicados por meio do estudo 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 por um.
getAll
Este método permite obter todas as pessoas da lista. Seu código é o seguinte:
Lembremo-nos, antes de mais nada, de que a classe [DaoImplCommon] deriva da classe Spring [SqlMapClientDaoSupport]. É essa classe que possui o método [getSqlMapClientTemplate()] utilizado na linha 3 acima. Esse método tem a seguinte assinatura:
![]()
O tipo [SqlMapClientTemplate] encapsula o objeto [SqlMapClient] da camada [iBATIS]. É por meio dele que teremos acesso ao banco 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 contêm exceções em suas assinaturas. Os métodos das classes de implementação da interface [IDao] também não podem, portanto, conter exceções em suas assinaturas. Portanto, precisamos interceptar cada exceção [SQLException] lançada pela camada [iBATIS] e encapsulá-la em uma exceção não controlada. O tipo [DaoException] do nosso projeto seria adequado para esse encapsulamento.
Em vez de gerenciarmos essas exceções por conta própria, vamos confiá-las ao tipo Spring [SqlMapClientTemplate], que encapsula o objeto [SqlMapClient] da camada [iBATIS]. De fato, o [SqlMapClientTemplate] foi criado para interceptar as exceções [SQLException] lançadas pela camada [SqlMapClient] e encapsulá-las em um tipo [DataAccessException] não controlado. Esse comportamento nos convém. Basta lembrar que a camada [dao] agora pode lançar dois tipos de exceções não controladas:
- nosso tipo proprietário [DaoException]
- o tipo Spring [DataAccessException]
O tipo [SqlMapClientTemplate] é definido da seguinte forma:

Ele implementa a seguinte interface [SqlMapClientOperations]:

Essa interface define métodos capazes de processar o conteúdo do arquivo [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 arquivo de configuração
- [parameterObject]: o objeto “parâmetro” para um [select] configurado. O objeto “parâmetro” pode assumir duas formas:
- um objeto que segue o padrão JavaBean: os parâmetros da ordem [select] são, nesse caso, os nomes dos campos do JavaBean. Na execução da ordem [select], eles 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], essas chaves são substituídas por seus valores associados no dicionário.
- Se o [SELECT] não retornar nenhuma linha, o resultado [List] é um objeto vazio de elementos, mas não o null (a ser verificado).
[queryForObject]
![]()
Este método é idêntico em princípio ao anterior, mas retorna apenas um único objeto. Se o [SELECT] não retornar nenhuma linha, o resultado é o ponteiro null.
[insert]
![]()
Este método permite executar uma ordem SQL [insert] configurada pelo segundo parâmetro. O objeto retornado é a chave primária da linha que foi inserida. Não há obrigação de utilizar esse resultado.
[update]
![]()
Este método permite executar uma ordem SQL [update] configurada pelo segundo parâmetro. O resultado é o número de linhas modificadas 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 excluídas pela ordem SQL [delete].
Voltemos ao método [getAll] da classe [DaoImplCommon]:
- linha 4: a ordem [select], denominada “Personne.getAll”, é executada. Ela não está configurada e, portanto, 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: o comando SQL “Personne.getAll” não possui parâmetros (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. [iBATIS] baseia-se em JDBC. Sabe-se, portanto, que o resultado da consulta será obtido na forma de um objeto [ResultSet]. Na linha 23, o atributo [resultMap] da tag <select> indica ao [iBATIS] qual “resultMap " deve ser utilizado para transformar cada linha do [ResultSet] obtido em objeto. É o “resultMap” [Personne.map], definido nas linhas 12 a 21, que indica como passar de uma linha da tabela [PERSONNES] para um objeto do tipo [Personne]. O [iBATIS] utilizará essas correspondências para fornecer uma lista de objetos [Personne] a partir das linhas do objeto [ResultSet].
- A linha 3 do método [getAll] retorna, então, uma coleção de objetos [Personne]
- o método [queryForList] pode lançar uma exceção Spring [DataAccessException]. Deixamos que ela seja propagada.
Explicaremos os demais métodos da classe [AbstractDaoImpl] mais rapidamente, já que o essencial sobre o uso de [iBATIS] já foi abordado na análise do método [getAll].
getOne
Este método permite identificar uma pessoa por meio de seu [id]. Seu código é o seguinte:
- linha 4: solicita a execução do comando [select] denominado “Personne.getOne”. Este é o seguinte no arquivo [personnes-firebird.xml]:
<!-- obter uma pessoa específica -->
<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# indica o valor do parâmetro passado para a ordem SQL, quando esse parâmetro é do tipo simples: Integer, Double, String, ... Nos atributos da tag <select>, o atributo [parameterClass] indica que o parâmetro é do tipo inteiro (linha 2). Na linha 5 de [getOne], observa-se que esse parâmetro é o identificador da pessoa procurada na forma de um objeto Integer. Essa mudança de tipo é obrigatória, pois o segundo parâmetro de [queryForList] deve ser do tipo [Object].
O resultado da consulta [select] deverá ser transformado em objeto por meio do atributo [resultMap="Personne.map"] (linha 2). Assim, obter-se-á um tipo [Personne].
- linhas 7-11: se a consulta [select] não retornou nenhuma linha, recupera-se então o ponteiro null na linha 4. Isso significa que a pessoa procurada não foi encontrada. Nesse caso, lança-se uma consulta [DaoException] com código 2 (linhas 9-10).
- linha 13: se não houve exceção, retorna-se o objeto [Personne] solicitado.
deleteOne
Este método permite excluir uma pessoa identificada por seu [id]. Seu código é o seguinte:
- linhas 4-5: solicita a execução do comando [delete] denominado “Personne.deleteOne”. Este é o seguinte no arquivo [personnes-firebird.xml]:
<!-- excluir 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). Esse será o identificador da pessoa procurada (linha 5 de deleteOne)
- linha 4: o resultado do método [SqlMapClientTemplate].delete é o número de linhas excluídas.
- linhas 7-8: se a consulta [delete] não excluiu nenhuma linha, isso significa que a pessoa não existe. Executa-se uma consulta [DaoException] com código 2 (linha 8).
saveOne
Este método permite adicionar uma nova pessoa ou modificar uma pessoa existente. Seu código é o seguinte:
- linha 4: verifica-se a validade da pessoa com o método [check]. Esse método já existia na versão anterior e havia sido comentado na época. Ele executa um [DaoException] se a pessoa for inválida. Deixa-se que esse código seja exibido.
- linha 6: se chegamos até aqui, é porque 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). Nos dois 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. Seu código é o seguinte:
- linha 4: define-se como 1 o número da versão da pessoa que está sendo criada
- linha 9: realiza-se a inserção por meio 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#, #sobrenome#, #nome#, #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 será inserida na tabela [PERSONNES] (linhas 5-8). Há um problema a ser resolvido. Durante uma inserção, o objeto [Personne] a ser inserido tem seu ID igual a -1. É necessário substituir esse valor por uma chave primária válida. Para isso, utilizam-se as linhas 2 a 4 da tag <selectKey> acima. Elas indicam:
- (continuação)
- a consulta SQL a ser executada para obter um valor de chave primária. A consulta indicada aqui é a que apresentamos no parágrafo 17.1. Há dois pontos a serem observados:
- “as 'value'” é obrigatório. Também é possível escrever “as value”, mas “value” é uma palavra-chave do Firebird que precisou ser protegida por aspas.
- A tabela do Firebird se chama, na verdade, [RDB$DATABASE]. Mas o caractere $ é interpretado como [iBATIS]. Ele foi protegido por meio da duplicação do nome.
- 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 esse campo.
- a consulta SQL a ser executada para obter um valor de chave primária. A consulta indicada aqui é a que apresentamos no parágrafo 17.1. Há dois pontos a serem observados:
- linhas 6-7: para fins de teste, teremos que aguardar 10 ms antes de realizar a inserção, a fim de verificar se há conflitos entre threads que tentariam fazer adições simultaneamente.
updatePersonne
Este método permite modificar um registro já existente na tabela [PERSONNES]. Seu código é o seguinte:
- Uma atualização pode falhar por pelo menos dois motivos:
- a pessoa a ser atualizada não existe
- a pessoa a ser atualizada existe, mas o thread que deseja alterá-la não possui a versão correta
- linhas 7-8: a consulta SQL [update], denominada “Personne.updateOne”, é executada. Ela é a seguinte:
<!-- atualizar um usuário -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#versão#+1, NOM=#sobrenome#, PRENOM=#nome#, 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 registro a ser modificado (linha 8 – updatePersonne).
- Deseja-se alterar apenas a pessoa da tabela [PERSONNES] que tenha o mesmo número [id] e a mesma versão [version] 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 os dados da pessoa do parâmetro e 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 zero, é executado um [DaoException] com código 2, indicando que, ou a pessoa a ser atualizada não existe, ou ela mudou de versão nesse intervalo.
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 realizar testes intensivos, podemos começar com um programa simples do tipo [main] que exibirá o conteúdo da tabela [PERSONNES]. Trata-se da classe [MainTestDaoFirebird]:
O arquivo de configuração [spring-config-test-dao-firebird.xml] da camada [dao], utilizado nas linhas 13 e 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 tags <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 arquivo é o mesmo analisado no parágrafo 17.3.2.
Para o teste, o Firebird SGBD é iniciado. O conteúdo da tabela [PERSONNES] é o seguinte:

A execução do programa [MainTestDaoFirebird] apresenta os seguintes resultados na tela:

Conseguimos obter a lista de pessoas. Podemos passar para o teste JUnit.
O teste JUnit [TestDaoFirebird] é o seguinte:
- Os testes de [test1] a [test5] são os mesmos da versão 1, exceto o [test4], que sofreu uma pequena alteração. O teste [test6], por sua vez, é novo. Comentaremos apenas esses dois testes.
[test4]
O [test4] tem como objetivo testar o método [updatePersonne - DaoImplCommon]. Lembramos o código deste último:
- linhas 4-5: aguarda-se 10 ms. Assim, forçamos o thread que executa o [updatePersonne] a perder o processador, o que pode aumentar nossas chances de observar conflitos de acesso entre threads concorrentes.
[test4] inicia N=100 threads encarregadas de incrementar, simultaneamente, em 1 o número de filhos da mesma pessoa. Queremos observar como os conflitos de versão e os conflitos de acesso são gerenciados.
As threads são criadas nas linhas 8 a 13. Cada uma aumentará em 1 o número de filhos da pessoa criada nas linhas 3 a 5. As threads de atualização [ThreadDaoMajEnfants ] são as seguintes:
Uma atualização de pessoa pode falhar porque a pessoa que se deseja modificar não existe ou porque ela já foi atualizada anteriormente por outro thread. Esses dois casos são tratados aqui nas linhas 67 a 69. De fato, nesses dois casos, o método [updatePersonne] lança um [DaoException] com código 2. A thread será então levada 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 a thread que executa o [insertPersonne] a perder o processador e, assim, aumentar nossas chances de observar conflitos causados por threads que realizam inserções simultaneamente.
O código de [test6] é o seguinte:
Criamos 100 threads que irão inserir, ao mesmo tempo, 100 pessoas diferentes. Todas essas 100 threads receberão uma chave primária para a pessoa que devem inserir e, em seguida, serão interrompidas por 10 ms (linha 10 – insertPersonne) antes de poderem realizar a inserção. Queremos verificar se tudo está ocorrendo corretamente e, principalmente, se elas realmente obtêm valores diferentes de chave primária.
- linhas 7-11: é criada uma matriz com 100 pessoas. Todas essas pessoas são cópias da pessoa p criada nas linhas 4-5.
- linhas 14-17: os 100 threads de inserção são iniciados. Cada um deles é responsável por inserir uma das 100 pessoas criadas anteriormente.
- linhas 19-23: [test6] aguarda o término de cada uma das 100 threads que iniciou. Ao detectar o término da thread nº i, ele exclui a pessoa que essa 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 realizar essa inserção.
- linha 30: a pessoa é inserida. Se ocorrer uma exceção, ela é 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 examinar os logs de tela. Eles 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 (falha na atualização).
--- 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 driver JDBC do Firebird, descrita na linha 6.
- linha 6 – o driver 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 tentavam atualizar simultaneamente a mesma linha da tabela [PERSONNES].
Não se trata de um erro irrecuperável. O thread que intercepta essa exceção pode tentar novamente a atualização. Para isso, é necessário modificar o código de [ThreadDaoMajEnfants]:
- linha 8: estamos tratando uma exceção do tipo [DaoException]. De acordo com o que foi dito, deveríamos tratar a exceção que surgiu nos testes, do tipo [org.springframework.jdbc.UncategorizedSQLException]. No entanto, não podemos nos limitar a tratar apenas desse tipo, que é um tipo genérico do Spring destinado a encapsular exceções que ele não reconhece. O Spring reconhece as exceções geradas pelos drivers JDBC de uma série de SGBD, como Oracle, MySQL, Postgres, DB2, SQL Server, ... mas não o Firebird. Além disso, qualquer exceção lançada pelo driver JDBC do Firebird é encapsulada no tipo Spring [org.springframework.jdbc.UncategorizedSQLException]:

Vemos acima que a classe [UncategorizedSQLException] deriva da classe [DataAccessException], que mencionamos no parágrafo 17.3.3. É possível identificar a exceção que foi encapsulada em [UncategorizedSQLException] por meio de seu método [getSQLException]:
![]()
Essa exceção do tipo [SQLException] é a lançada pela camada [iBATIS], que, por sua vez, encapsula a exceção lançada pelo driver JDBC do banco de dados. A causa exata da exceção do tipo [SQLException] pode ser obtida pelo método:
![]()
Obtém-se o objeto do tipo [Throwable], que foi lançado pelo driver JDBC:

O tipo [Throwable] é a classe pai de [Exception].
Aqui, precisaremos verificar se o objeto do tipo [Throwable], lançado pelo driver JDBC do Firebird e causa daexceção [SQLException] lançada pela camada [iBATIS] seja, de fato, 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 usar 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 essa thread só poderá funcionar com o Firebird SGBD. O mesmo se aplica ao teste [test4], que utiliza essa thread. Queremos evitar isso. De fato, desejamos que nossos testes JUnit permaneçam válidos independentemente do SGBD utilizado. Para alcançar esse resultado, decidimos que a camada [dao] iniciará um [DaoException] de código 4 quando for detectada 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)
Nossa camada [dao] deve, portanto, ser capaz de reconhecer uma exceção do tipo “conflito de atualização”. Essa exceção é emitida por um driver JDBC e é específica a ele. Essa exceção deve ser tratada no método [updatePersonne] da classe [DaoImplCommon]:
As linhas 7 a 11 devem estar entre try e catch. Para o SGBD do Firebird, precisamos 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 esse tipo de teste no [DaoImplCommon], vamos vincular essa classe ao Firebird SGBD, o que obviamente não é desejável. Se quisermos manter o caráter genérico da classe [DaoImplCommon], precisamos derivá-la e tratar a exceção em uma classe específica para o Firebird. É isso que faremos agora.
17.4.2. A classe [DaoImplFirebird]
Seu código é o seguinte:
- linha 5: a classe [DaoImplFirebird] deriva de [DaoImplCommon], a classe que acabamos de analisar. Ela redefine, nas linhas 8 a 33, o método [updatePersonne] que está nos causando problemas.
- linha 20: interceptamos a exceção do Spring do tipo [UncategorizedSQLException]
- linhas 21-22: verificamos se a exceção subjacente do tipo [SQLException], lançada pela camada [iBATIS], foi causada por uma exceção do tipo [org.firebirdsql.jdbc.FBSQLException]
- linha 25: verifica-se ainda que o código de erro dessa exceção do Firebird é 335544336, o código de erro do “deadlock”.
- linhas 26-27: se todas essas condições forem atendidas, é lançada uma [DaoException] com código 4.
- linhas 36-44: o método [wait] permite interromper o thread atual por N milissegundos. Ele serve apenas para fins de teste.
Estamos prontos para os testes da nova camada [dao].
17.4.3. Testes da implementação [DaoImplFirebird]
O arquivo de configuração dos testes [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>
<!-- 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 tags <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 havia falhado anteriormente, são os seguintes:

O [test4] foi bem-sucedido. As últimas linhas dos logs de tela são as seguintes:
A última linha indica que foi o thread nº 36 que terminou por último. A linha 3 mostra um conflito de versão que forçou o thread nº 36 a reiniciar seu procedimento de atualização do perfil da pessoa (linha 4). Outros registros mostram conflitos de acesso durante as atualizações:
A linha 2 mostra que o thread nº 75 falhou durante sua atualização devido a um conflito de atualização: quando o comando SQL [update] foi emitido na tabela [PERSONNES], a linha que precisava ser atualizada estava bloqueada por outro thread. Esse conflito de acesso obrigará o thread nº 75 a tentar novamente a atualização.
Para concluir com o [test4], observa-se uma diferença notável em relação aos resultados do mesmo teste na versão 1, onde ele 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 precisamos sincronizar a camada [dao]. Simplesmente gerenciamos os conflitos de acesso sinalizados pelo Firebird.
Vamos agora executar o teste JUnit na íntegra a partir da camada [dao]:

Portanto, parece que temos uma camada [dao] válida. Para declará-la válida com alta probabilidade, precisaríamos 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 [service]
- [ServiceImpl] é uma implementação dessa interface
A interface [IService] é a seguinte:
- A interface possui os mesmos quatro métodos da versão 1, mas conta com mais dois:
- saveMany: permite salvar várias pessoas ao mesmo tempo de forma atômica. Ou todas são salvas, ou nenhuma é salva.
- deleteMany: permite excluir várias pessoas ao mesmo tempo de forma atômica. Ou todas são excluídas, ou nenhuma é.
Esses dois métodos não serão utilizados pelo aplicativo web. Nós os adicionamos para ilustrar o conceito de transação em um banco de dados. De fato, os dois métodos deverão ser executados dentro de uma transação para obter a atomicidade desejada.
A classe [ServiceImpl] que implementa essa interface será a seguinte:
- Os métodos [getAll, getOne, insertOne, saveOne] utilizam os métodos da camada [dao] com o mesmo nome.
- linhas 42-47: o método [saveMany] salva, uma a uma, as pessoas da tabela passada como parâmetro.
- linhas 50-55: o método [deleteMany] exclui, uma a uma, as pessoas cuja tabela foi passada como parâmetro pelo método id
Já mencionamos que os métodos [saveMany] e [deleteMany] devem ser executados dentro de uma transação para garantir o princípio “tudo ou nada” desses métodos. Podemos constatar que o código acima ignora totalmente esse conceito de transação. Ela só aparecerá no arquivo de configuração da camada [service].
17.5.2. Configuração da camada [service]
Acima, na linha 11, vemos que a implementação [ServiceImpl] possui uma referência à camada [dao]. Esta, assim como na versão 1, será inicializada pelo Spring no momento da instanciação da camada [service - ServiceImpl]. O arquivo 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 deixe espaços entre as duas tags <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>
<!-- gerenciador 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>
- linhas 1-36: configuração da camada [dao]. Essa 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 observar que a implementação da camada [service] é feita pelo tipo [TransactionProxyFactoryBean]. Esperávamos encontrar o tipo [ServiceImpl]. [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 primeiro examinar a classe [TransactionProxyFactoryBean]:

Vemos que ela implementa a interface [FactoryBean]. Já nos deparamos com essa interface. Sabemos que, quando uma aplicação solicita ao Spring uma instância de um tipo que implemente [FactoryBean], o Spring não retorna uma instância [I] desse tipo, mas sim o objeto retornado pelo método [I].getObject():
![]()
No nosso caso, a camada [service] será implementada pelo objeto retornado por [TransactionProxyFactoryBean].getObject(). Qual é a natureza desse objeto? Não vamos entrar em detalhes, pois eles são complexos. Eles se enquadram no que chamamos de 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 interceptador 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, AOP insere a classe [intercepteur] entre [C1] e [C2]. Para que isso seja possível, é claro que a classe [intercepteur] deve apresentar à [C1] a mesma interface [I2] que a [C2].
Para que isso pode servir? A documentação do Spring fornece alguns exemplos. Pode-se querer, por exemplo, gerar logs sempre que houver chamadas a um método M específico de [C2], para realizar uma auditoria desse método. No [intercepteur], escreveremos então um método [M] que gera esses registros. A chamada de [C1] para [C2].M ocorrerá da seguinte forma (ver esquema acima):
- [C1] chama o método M de [C2]. Na verdade, será chamado o método M de [intercepteur]. Isso é 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 logs e chama o método M de [C2], inicialmente visado por [C1].
- O método M de [C2] é executado e retorna seu resultado ao método M de [intercepteur], que pode, eventualmente, acrescentar algo ao que foi feito no passo 2.
- O método M de [intercepteur] retorna um resultado ao método chamador de [C1]
Vemos 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, portanto, amplia o método M do [C2]. Assim, podemos ver a tecnologia AOP como uma forma de ampliar a interface apresentada por uma classe.
Como esse conceito se aplica à nossa camada [service]? Se implementarmos a camada [service] diretamente com uma instância [ServiceImpl], nossa aplicação web terá a seguinte arquitetura:
![]() |
Se implementarmos a camada [service] com uma instância [TransactionProxyFactoryBean], teremos a seguinte arquitetura:
![]() |
Pode-se dizer que a camada [service] é instanciada com dois objetos:
- o objeto que chamamos acima de [proxy transactionnel] e que, na verdade, é o objeto retornado pelo método [getObject] de [TransactionProxyFactoryBean]. É esse objeto que fará a interface da camada [service] com a camada [web]. Ele implementa, por definição, a interface [IService].
- uma instância [ServiceImpl], que também implementa a interface [IService]. Somente ela sabe como trabalhar com a camada [dao], por isso é necessária.
Imaginemos que a camada [web] chame o método [saveMany] da interface [IService]. Sabemos que, do ponto de vista funcional, as adições/atualizações realizadas por esse método devem ocorrer dentro de uma transação. Ou todas são bem-sucedidas, ou nenhuma é executada. Apresentamos o método [saveMany] da classe [ServiceImpl] e destacamos que ele não possui o conceito de transação. O método [saveMany] da classe [proxy transactionnel] irá incorporar esse conceito de transação ao método [saveMany] da classe [ServiceImpl]. Vamos seguir o esquema acima:
- a camada [web] chama o método [saveMany] da interface [IService].
- o método [saveMany] de [proxy transactionnel] é executado. Ele inicia uma transação. É necessário que ele disponha de informações suficientes para isso, notadamente um objeto [DataSource] para obter uma conexão com o SGBD. Em seguida, ele chama o método [saveMany] de [ServiceImpl].
- Esta é executada. Ela chama repetidamente a camada [dao] para executar as inserções ou atualizações. As ordens SQL executadas nessa ocasião são realizadas na transação iniciada no passo 2.
- Suponhamos que uma dessas 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 realiza nenhuma ação e permite que a exceção seja repassada até o 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 ela para cancelar todas as atualizações, em seguida, permite que a exceção seja propagada até a camada [web], que será responsável por gerenciá-la.
Na etapa 4, supusemos que uma das inserções ou atualizações falhasse. Se não for esse o caso, em [5] nenhuma exceção é propagada. O mesmo ocorre em [6]. Nesse caso, o método [saveMany] de [proxy transactionnel] executa um [commit] da transação para validar todas as atualizações.
Agora temos uma ideia mais precisa da arquitetura implementada pelo bean [TransactionProxyFactoryBean]. Voltemos à configuração deste:
<!-- gerenciador 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 acompanhar essa configuração à luz da arquitetura que está configurada:
![]() |
- O [proxy transactionnel] será responsável pelo gerenciamento das transações. O Spring oferece várias estratégias para o gerenciamento delas. O [proxy transactionnel] precisa de uma referência ao gerenciador de transações escolhido.
- linhas 11–13: definem o atributo [transactionManager] do bean [TransactionProxyFactoryBean] com uma referência a um gerenciador de transações. Este é definido nas linhas 2–7.
- linhas 2–7: o gerenciador de transações é do tipo [DataSourceTransactionManager]:

[DataSourceTransactionManager] é um gerenciador de transações adaptado aos SGBD acessados por meio de um objeto [DataSource]. Ele só consegue gerenciar transações em um único SGBD. Ele não consegue gerenciar transações distribuídas por vários SGBD. Neste caso, temos apenas um único SGBD. Portanto, esse gerenciador de transações é adequado. Quando o [proxy transactionnel] iniciar uma transação, ele o fará por meio de uma conexão vinculada à thread. É essa conexão que será utilizada em todas as camadas que levam ao banco de dados: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].
A classe [DataSourceTransactionManager] precisa saber qual é a fonte de dados à qual deve solicitar uma conexão para vinculá-la ao thread. Isso é definido nas linhas 4 a 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]. Essa informação é necessária por dois motivos:
- a classe [ServiceImpl] deve ser instanciada, pois é ela que garante a comunicação com a camada [dao]
- [TransactionProxyFactoryBean] deve gerar um proxy que apresente à camada [web] a mesma interface que [ServiceImpl].
- linhas 21-27: indicam quais métodos de [ServiceImpl] o proxy deve interceptar. O atributo [transactionAttributes], na linha 21, indica quais métodos de [ServiceImpl] exigem uma transação e quais são os atributos dessa transação:
- linha 23: os métodos cujos nomes começam com get [getOne, getAll] são executados em uma transação com o atributo [PROPAGATION_REQUIRED,readOnly]:
- PROPAGATION_REQUIRED: o método é executado em uma transação se já houver uma associada à thread; caso contrário, uma nova é criada e o método é executado nela.
- readOnly: transação somente leitura
Aqui, os métodos [getOne] e [getAll] de [ServiceImpl] serão executados em uma transação, embora, na verdade, isso não seja necessário. Trata-se, em todos os casos, de uma operação constituída por uma única ordem SELECT. Não vemos utilidade em colocar esse SELECT em uma transação.
- linha 24: os métodos cujos nomes começam com “save”, como [saveOne, saveMany], são executados em uma transação de 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].
Em nossa camada [service], apenas os métodos [saveMany] e [deleteMany] precisam ser executados em 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. Testes da camada [service]
Agora que escrevemos e configuramos a camada [service], propomos testá-la com os testes JUnit:

O arquivo de configuração [spring-config-test-service-firebird.xml] da camada [service] é aquele 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 arquivo [spring-config-test-service-firebird.xml], o mesmo analisado na seção anterior.
- Os testes de [test1] a [test6] são idênticos, em essência, aos seus equivalentes 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 em uma transação.
- O método [test7] tem como objetivo testar os métodos [saveMany] e [deleteMany]. Queremos verificar se eles são executados corretamente em uma transação. Vamos comentar o código desse método:
- linhas 62-63: contamos o número de pessoas [nbPersonnes1] atualmente na lista
- linhas 67-72: são criadas três pessoas
- linhas 73-83: essas três pessoas são gravadas 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, por sua vez, tem um id igual a -2. Portanto, não se trata de uma inserção, mas de uma atualização. Essa atualização irá falhar, pois não há nenhuma pessoa com um id igual a -2 na tabela [PERSONNES]. A camada [dao], portanto, lançará uma exceção que será propagada até a camada [service]. A existência dessa exceção é verificada na linha 83.
- Devido à exceção anterior, a camada [service] deve gerar um [rollback] a partir do conjunto de ordens SQL emitidas durante a execução do método [saveMany], isso porque esse método é executado em uma transação. Linhas 86-87: verifica-se se o número de pessoas na lista não mudou 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: exclui-se um grupo de pessoas composto pelas pessoas p1 e p2, que acabamos de adicionar, e por uma pessoa inexistente (id = -1). O método [deleteMany] é utilizado para isso, na linha 108. Esse método irá falhar, pois não há nenhuma pessoa com um id igual a –1 na tabela [PERSONNES]. A camada [dao], portanto, lançará uma exceção que será propagada até a camada [service]. A existência dessa exceção é verificada na linha 114.
- Devido à exceção anterior, a camada [service] deveria gerar um [rollback] a partir do conjunto de ordens SQL emitidas durante a execução do método [deleteMany], isso porque esse método é executado em uma transação. Linhas 116-117: verifica-se se o número de pessoas na lista não sofreu alteração e, portanto, se as exclusões de p1 e p2 não ocorreram.
- linha 122: exclui-se um grupo composto apenas pelas pessoas p1 e p2. Isso deveria ser bem-sucedido. O restante do método verifica se esse é realmente o caso.
A execução dos testes produz os seguintes resultados:

Os sete testes foram bem-sucedidos. Consideraremos nossa camada [service] como operacional.
17.7. A camada [web]
Vamos relembrar a arquitetura geral do aplicativo web a ser construído:
![]() |
Acabamos de construir as camadas [dao] e [service], que permitem trabalhar com um banco de dados Firebird. Escrevemos uma versão 1 dessa 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. De fato, ela se dirigia a uma camada [service] que implementava a interface [IService]. Como a nova camada [service] implementa essa mesma interface, a camada [web] não precisa ser modificada.
No artigo anterior, a versão 1 do aplicativo havia sido testada com o projeto Eclipse [mvc-personnes-02B], no qual as camadas [web, service, dao, entites] haviam sido colocadas em arquivos .jar:
![]() |
A pasta [src] estava vazia. As classes das camadas estavam nos arquivos [personnes-*.jar ]:
![]() |
Para testar a versão 2, no Eclipse, duplicamos a pasta Eclipse [mvc-personnes-02B] como [mvc-personnes-03B] (copiar/colar):

No projeto [mvc-personnes-03], exportamos as camadas [dao] e [service], respectivamente, para os arquivos [personnes-dao.jar] e [personnes-service.jar] da pasta [dist] do projeto:

Copiamos esses dois arquivos e, em seguida, no Eclipse, os colamos na pasta [WEB-INF/lib] do projeto [mvc-personnes-03B], onde eles substituirão os arquivos com o mesmo nome da versão anterior.
![]() |
Também copiamos e colamos 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] na pasta [WEB-INF/lib] do projeto [mvc-personnes-03B]. Esses arquivos são necessários para as novas camadas [dao] e [service].
Feito isso, 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 arquivos de configuração das camadas [dao] e [service]:

O arquivo [spring-config.xml] configura as camadas [dao] e [service] do aplicativo web. Na nova versão, ele é idêntico ao arquivo [spring-config-test-service-firebird.xml], que foi utilizado para configurar o teste da camada de serviço no projeto [mvc-personnes-03]. Portanto, fazemos 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 tags <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>
<!-- gerenciador 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: a URL do banco de dados Firebird. Continuamos a utilizar o banco 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 é iniciado em seguida. Com um navegador, acessamos a URL [http://localhost:8080/mvc-personnes-03B]:

Adicionamos uma nova pessoa pelo link [Ajout]:
![]() | ![]() |
Verificamos a adição no banco de dados:

O leitor é convidado a realizar outros testes com o [modification, suppression].
Vamos agora realizar o teste de conflitos de versão que havia sido feito na versão 1. [Firefox] será o navegador do usuário U1. Este solicita a URL [http://localhost:8080/mvc-personnes-03B]:

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

O usuário U1 acessa a edição do perfil da pessoa [Perrichon]:

O usuário U2 faz o mesmo:

O usuário U1 faz alterações e confirma:
![]() |
O usuário U2 faz o mesmo:
![]() |
O usuário U2 retorna à lista de pessoas por meio do link [Annuler] do formulário:

Ele encontra a pessoa [Perrichon] tal como U1 a modificou (nome em maiúsculas).
E o banco de dados nisso tudo? Vamos ver:

A pessoa nº 899 tem, de fato, o nome em maiúsculas após a alteração feita por U1.
17.8. Conclusion
Vamos relembrar o que queríamos fazer. Tínhamos um aplicativo 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, portanto, era perdida 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 em uma tabela de banco de dados. Assim, ela agora é persistente. Vamos agora analisar o impacto que a alteração em SGBD tem sobre nossa aplicação. Para isso, vamos criar três novas versões de nossa aplicação web:
![]() |
- versão 3: o SGBD usa Postgres
- versão 4: o SGBD é o MySQL
- versão 5: o SGBD é o SQL Server Express 2005
As alterações ocorrem nos seguintes locais:
- a classe [DaoImplFirebird] implementa funcionalidades da camada [dao] relacionadas ao SGBD Firebird. Caso essa necessidade persista, ela será substituída, respectivamente, pelas classes [DaoImplPostgres], [DaoImplMySQL] e [DaoImplSqlExpress].
- O arquivo de mapeamento [personnes-firebird.xml] de iBATIS para o Firebird SGBD será substituído, respectivamente, pelos arquivos de mapeamento [personnes-postgres.xml], [personnes-mysql.xml] e [personnes-sqlexpress.xml], respectivamente.
- A configuração do objeto [DataSource] da camada [dao] é específica para um SGBD. Portanto, ela mudará a cada versão.
- O driver JDBC do SGBD também muda a cada versão
Fora esses pontos, tudo permanece igual. A seguir, descrevemos essas novas versões, concentrando-nos apenas nas novidades trazidas por cada uma delas.

























