Skip to content

19. Aplicação web MVC numa arquitetura de três camadas – Exemplo 5, MySQL

19.1. A base de dados MySQL

Nesta versão, iremos instalar a lista de pessoas numa tabela da base de dados MySQL 4.x. Utilizámos o pacote [Apache – MySQL – PHP] disponível no URL [http://www.easyphp.org]. A seguir, as capturas de ecrã provêm do cliente EMS MySQL Manager Lite [http://www.sqlmanager.net/fr/products/mysql/manager], um cliente de administração gratuito do SGBD MySQL.

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

Image

A tabela [PERSONNES] conterá a lista de pessoas geridas pela aplicação web. Foi criada com os seguintes comandos SQL:

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

MySQL 4.x parece mais simples do que os dois SGBD anteriores. Não consegui definir restrições (checks) para a tabela.

  • linha 10: a tabela deve ter o tipo [InnoDB] e não o tipo [MyISAM], que não suporta transações.
  • linha 2: a chave primária é do tipo auto_increment. Se inserirmos uma linha sem valor para a coluna ID da tabela, o MySQL irá gerar automaticamente um número inteiro para essa coluna. Isto irá evitar que tenhamos de gerar nós próprios as chaves primárias.

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

Image

Sabemos que, ao inserir um objeto [Personne] através da nossa camada [dao], o campo [id] desse objeto é igual a -1 antes da inserção e assume um valor diferente de -1 depois, sendo esse valor a chave primária atribuída à nova linha inserida na tabela [PERSONNES]. Vejamos, através de um exemplo, como podemos determinar esse valor.

A ordem SQL

SELECT LAST_INSERT_ID()

permite conhecer o último valor inserido no campo ID da tabela. Deve ser emitida após a inserção. Esta é uma diferença em relação às ordens SGBD, [Firebird] e [Postgres], nas quais se solicitava o valor da chave primária da pessoa adicionada antes da inserção. Iremos utilizá-lo no ficheiro [personnes-mysql.xml], que reúne as ordens SQL emitidas na base de dados.

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

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

Image

O projeto é um projeto Java simples, não um projeto web Tomcat.


Pasta [src]


Esta pasta contém os códigos-fonte das camadas [dao] e [service], bem como os ficheiros de configuração destas duas camadas:

Image

Todos os ficheiros cujo nome inclua «[mysql]» podem ter sido ou não alterados em relação às versões do Firebird e do Postgres. A seguir, descrevemos aqueles que foram alterados.


Pasta [database]


Este dossier contém o script de criação da base de dados MySQL relativa às pessoas:

Image

# EMS MySQL Manager Lite 3.2.0.1
# ---------------------------------------
# Host      : localhost
# Porta     : 3306
# Base de dados: dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

CREATE DATABASE `dbpersonnes`
    CHARACTER SET 'latin1'
    COLLATE 'latin1_swedish_ci';

USE `dbpersonnes`;

#
# Estrutura da tabela `personnes`: 
#

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

#
# Dados da tabela `personnes` (L IMIT 0,500)
#

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

COMMIT;

Pasta [lib]


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

De salientar a presença do controlador JDBC do SGBD MySQL. Todos estes ficheiros fazem parte do Classpath do projeto Eclipse.

19.3. A camada [dao]

A camada [dao] é a seguinte:

Image

Apresentamos apenas as alterações em relação à versão [Firebird].

O ficheiro de mapeamento [personne-mysql.xml] é 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="istia.st.mvc.personnes.entites.Personne">
        <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">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#versão#, #apelido#, #nome próprio#, #dateNaissance#, #casada#, 
         #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- atualizar um utilizador -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#versão#+1, NOM=#apelido#, PRENOM=#nome#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# e 
        VERSION=#versão#</update>
    <!-- eliminar uma pessoa -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#valor# </delete>
    <!-- obter o valor da chave primária [id] da última pessoa inserida -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

Tem o mesmo conteúdo que o [personnes-firebird.xml], com as seguintes diferenças:

  • a ordem SQL «Personne.insertOne» sofreu alterações nas linhas 29-37:
  • a ordem de inserção SQL é executada antes da ordem SELECT, o que permitirá recuperar o valor da chave primária da linha inserida
  • o comando de inserção SQL não tem valor para a coluna ID da tabela [PERSONNES]

Isto reflete o exemplo de inserção que comentámos no parágrafo 19.1.

Note-se que aqui pode existir uma possível fonte de problemas entre threads concorrentes. Imaginemos duas threads, Th1 e Th2, que realizam uma inserção ao mesmo tempo. Há, no total, quatro ordens SQL a emitir. Suponhamos que sejam executadas na seguinte ordem:

  1. inserção I1 de Th1
  2. inserção I2 da Th2
  3. seleção S1 do Th1
  4. select S2 de Th2

No passo 3, Th1 recupera a chave primária gerada na última inserção, ou seja, a de Th2 e não a sua. Não sei se o método [insert] de iBATIS está protegido para este caso específico. Vamos supor que o gere corretamente. Se não fosse esse o caso, teríamos de derivar a classe de implementação [DaoImplCommon] da camada [dao] para uma classe [DaoImplMySQL], na qual o método [insertPersonne] seria sincronizado. Isto resolveria o problema apenas para os threads da nossa aplicação. Se, no exemplo acima, Th1 e Th2 forem threads de duas aplicações diferentes, seria então necessário resolver o problema tanto com transações como com um nível de isolamento adequado (isolation level) entre transações. O nível [serializable], no qual as transações são executadas como se fossem executadas sequencialmente, seria adequado.

Note-se que este problema não existe com o Firebird e o Postgres, que executam o SELECT antes do INSERT. Se tivermos, por exemplo, a sequência:

  1. select S1 de Th1
  2. select S2 de Th2
  3. inserção I1 de Th1
  4. inserção de I2 de Th2

Nas etapas 1 e 2, Th1 e Th2 obtêm valores de chave primária do mesmo gerador. Esta operação é normalmente atómica, e Th1 e Th2 irão obter dois valores diferentes. Se a operação não fosse atómica e o Th1 e o Th2 recuperassem dois valores idênticos, a inserção efetuada na etapa 4 pelo Th2 falharia devido a uma duplicidade da chave primária. Trata-se de um erro totalmente recuperável e o Th2 pode tentar novamente a inserção.

Vamos deixar a operação «Personne.insertOne» tal como se encontra atualmente no ficheiro [personnes-mysql.xml], mas o leitor deve estar ciente de que existe aqui um potencial problema.

A classe de implementação [DaoImplCommon] da camada [dao] é a das duas versões anteriores.

A configuração da camada [dao] foi adaptada às camadas SGBD e [MySQL]. Assim, o ficheiro de configuração [spring-config-test-dao-mysql.xml] é 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>com.mysql.jdbc.Driver</value>
        </property>
        <property name="url">
            <value>jdbc:mysql://localhost/dbpersonnes</value>
        </property>
        <property name="username">
            <value>root</value>
        </property>
        <property name="password">
            <value></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-mysql.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 5-19: o bean [dataSource] aponta agora para a base de dados [MySQL] [dbpersonnes], cujo administrador é [root], sem palavra-passe. O leitor deverá alterar esta configuração de acordo com o seu próprio ambiente.
  • linha 31: a classe [DaoImplCommon] é a classe de implementação da camada [dao]

Feitas estas alterações, pode-se passar aos testes.

19.4. Testes das camadas [dao] e [service]

Os testes das camadas [dao] e [service] são os mesmos que para a versão [Firebird]. Os resultados obtidos são os seguintes:

Verifica-se que os testes foram concluídos com sucesso com a implementação [DaoImplCommon]. Não será necessário derivar esta classe, como tinha sido necessário fazer com as versões SGBD e [Firebird].

19.5. Testes da aplicação [web]

Para testar a aplicação web com o SGBD e o [MySQL], criamos um projeto Eclipse [mvc-personnes-05B] de forma semelhante à utilizada para criar o projeto [mvc-personnes-03B] com a base de dados Firebird (ver parágrafo 17.7). No entanto, tal como com o Postgres, não precisamos de recriar os arquivos [personnes-dao.jar] e [personnes-service.jar], uma vez que não alterámos nenhuma classe.

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

O SGBD e o MySQL são iniciados. O conteúdo da tabela [PERSONNES] é então o seguinte:

Image

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

Image

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

Verificamos a adição na base de dados:

Image

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