Essa a parte 5 de passo a passo de como desenvolver uma aplicação web do zero usando o framework Spring.
Na Parte 1, configuramos o ambiente e uma aplicação básica. Na Parte 2, refinamos a aplicação que construímos. A Parte 3 adicionou a lógica de negócios e as unidades de teste e na Parte 4 construímos a interface web da aplicação. Agora é a vez de introduzir a persistência do baco de dados.
Vimos nas partes anteriores como carregar alguns objetos de negócio usando as definições do bean em um arquivo de configuração. É óbvio que isso funcionaria na vida real – mais quando reiniciássemos os servidor voltaríamos aos preços originais. Precisamos adicionar o código para persistir essas mudanças em um banco de dados.
5.1. Criar o script de inicialização do banco de dados
Antes que possamos começar a desenvolver o código de persistência, precisamos de um banco de dados. Nesse tutorial, planejamos usar HSQL, que é um bom banco de dados open-source escrito em Java. Esse banco de dados é distribuido junto o Spring, assim podemos apenas copiar o arquivo jar para o diretório lib da aplicação web. Copie hsqldv.jar do diretório 'spring-framework-2.5/lib/hsqldb' para o diretório 'springapp/war/WEB-INF/lib'. Usaremos o HSQL no modo servidor standalone. Isso significa que teremos que iniciar um banco de dados separado ao invés de carregar um banco de dados embutido, mas nos dá fácil acesso a alterações no banco enquanto estivermos rodando a aplicação web.
Precisamos de um arquivo de script ou batch para iniciar o banco de dados. Crie um diretório ‘db’ dentro do diretório ‘springapp’. Esse novo diretório irá conter todos os arquivos do banco de dados. Agora, iremos criar um script de inicialização:
Para Linux/MacOSX crie:
'springapp/db/server.sh':
java -classpath ../war/WEB-INF/lib/hsqldb.jar org.hsqldb.Server -database test
Não esqueça de alterar a permissão para execução executando o comando ‘chmod +x server.sh’.
Para Windows, crie>:
'springapp/db/server.bat':
java -classpath ..\war\WEB-INF\lib\hsqldb.jar org.hsqldb.Server -database test
Agora você pode abrir uma janela de terminal, mudar para o diretório springapp/db e iniciar o banco de dados executando um desses scripts.
5.2. Criar tabelas e scripts de teste de dados
Em primeiro lugar, vamos revisar as sentenças SQL necessárias para criar as tabelas. Criaremos o arquivo ‘create_products.sql’ no diretório db.
'springapp/db/create_products.sql':
CREATE TABLE products ( id INTEGER NOT NULL PRIMARY KEY, description varchar(255), price decimal(15,2) ); CREATE INDEX products_description ON products(description);
Agora precisamos adicionar nossos dados de teste. Crie o arquivo ‘load_data.sql’ no diretório db.
'springapp/db/load_data.sql':
INSERT INTO products (id, description, price) values(1, 'Lamp', 5.78); INSERT INTO products (id, description, price) values(2, 'Table', 75.29); INSERT INTO products (id, description, price) values(3, 'Chair', 22.81);
Na próxima seção iremos adicionar alguns alvos Ant para preparar esses scripts para que possamos executa-los.
5.3. Adicionar as tarefas do Ant pars executar os scripts e carregar os dados de teste
Criaremos as tabelas e preencheremos elas com os dados de teste usando a tarefa embutida do Ant ‘sql’. Para usar esta tarefa, precisamos adicionar uma propriedade de conexão com banco de dados ao arquivo de propriedades da compilação.
'springapp/build.properties':
# Ant properties for building the springapp appserver.home=${user.home}/apache-tomcat-6.0.14 # for Tomcat 5 use $appserver.home}/server/lib # for Tomcat 6 use $appserver.home}/lib appserver.lib=${appserver.home}/lib deploy.path=${appserver.home}/webapps tomcat.manager.url=http://localhost:8080/manager tomcat.manager.username=tomcat tomcat.manager.password=s3cret db.driver=org.hsqldb.jdbcDriver db.url=jdbc:hsqldb:hsql://localhost db.user=sa db.pw=
Agora iremos adicionar os alvos que precisamos ao script de compilação. Existem alvos para criar e deletar tabelas e carregar e apagar dados de teste.
Adicione as seguintes linhas a ‘springapp/build.xml’:
<target name="createTables"> <echo message="CREATE TABLES USING: ${db.driver} ${db.url}"/> <sql driver="${db.driver}" url="${db.url}" userid="${db.user}" password="${db.pw}" onerror="continue" src="db/create_products.sql"> <classpath refid="master-classpath"/> </sql> </target> <target name="dropTables"> <echo message="DROP TABLES USING: ${db.driver} ${db.url}"/> <sql driver="${db.driver}" url="${db.url}" userid="${db.user}" password="${db.pw}" onerror="continue"> <classpath refid="master-classpath"/> DROP TABLE products; </sql> </target> <target name="loadData"> <echo message="LOAD DATA USING: ${db.driver} ${db.url}"/> <sql driver="${db.driver}" url="${db.url}" userid="${db.user}" password="${db.pw}" onerror="continue" src="db/load_data.sql"> <classpath refid="master-classpath"/> </sql> </target> <target name="printData"> <echo message="PRINT DATA USING: ${db.driver} ${db.url}"/> <sql driver="${db.driver}" url="${db.url}" userid="${db.user}" password="${db.pw}" onerror="continue" print="true"> <classpath refid="master-classpath"/> SELECT * FROM products; </sql> </target> <target name="clearData"> <echo message="CLEAR DATA USING: ${db.driver} ${db.url}"/> <sql driver="${db.driver}" url="${db.url}" userid="${db.user}" password="${db.pw}" onerror="continue"> <classpath refid="master-classpath"/> DELETE FROM products; </sql> </target> <target name="shutdownDb"> <echo message="SHUT DOWN DATABASE USING: ${db.driver} ${db.url}"/> <sql driver="${db.driver}" url="${db.url}" userid="${db.user}" password="${db.pw}" onerror="continue"> <classpath refid="master-classpath"/> SHUTDOWN; </sql> </target>
Agora você pode executar ‘ant createTabels loadData printData’ para preparar os dados de teste que usaremos mais tarde.
5.4. Criar uma implementação de um DAO (Data Access Object) para o JDBC
Começamos com a criação de um novo diretório ‘springapp/src/repository’ que irá conter todas as classes usadas para acesso ao banco de dados. Nesse diretório, criamos uma nova interface chamada ProductDao. Essa será a interface que definirá a funcionalidade que a implementação do DAO irá fornecer – podemos escolher ter mais de uma implementação algum dia.
'springapp/src/springapp/repository/ProductDao.java':
package springapp.repository; import java.util.List; import springapp.domain.Product; public interface ProductDao { public List<Product> getProductList(); public void saveProduct(Product prod); }
Seguiremos agora com uma classe chamada JdbcProductDao, que será a implementação JDBC de nossa interface. O Spring fornece um framework de abstração do JDBC que faremos uso. A grande diferença entre usar o JDBC diretamente e usar o framework JDBC do Spring é que não teremos que nos preocupar em abrir e fechar a conexão ou uma sentença. Tudo isso é feito internamente. Outra vantagem é que não precisaremos manipular nenhuma exceção, a menos que você queira. O Spring envolve todas as SQLException em sua própria hierarquia de exceções herdade de DataAccessException. Se você quiser, pode manipular essa esceção, mas já que muitas exceções de banco de dados são impossíveis de recuperar de alguma forma, você pode muito bem deixar a exceção ser propagada para um nível mais alto. A classe SimpleJdbcDaoSupport fornece um acesso conveniente para um já configurado SimpleJdbcTemplate, assim estenderemos essa classe. Tudo que temos que fornecer na aplicação é um DataSource configurado.
'springapp/src/springapp/repository/JdbcProductDao.java':
package springapp.repository; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.simple.ParameterizedRowMapper; import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport; import springapp.domain.Product; public class JdbcProductDao extends SimpleJdbcDaoSupport implements ProductDao { /** Logger for this class and subclasses */ protected final Log logger = LogFactory.getLog(getClass()); public List<Product> getProductList() { logger.info("Getting products!"); List<Product> products = getSimpleJdbcTemplate().query( "select id, description, price from products", new ProductMapper()); return products; } public void saveProduct(Product prod) { logger.info("Saving product: " + prod.getDescription()); int count = getSimpleJdbcTemplate().update( "update products set description = :description, price = :price where id = :id", new MapSqlParameterSource().addValue("description", prod.getDescription()) .addValue("price", prod.getPrice()) .addValue("id", prod.getId())); logger.info("Rows affected: " + count); } private static class ProductMapper implements ParameterizedRowMapper<Product> { public Product mapRow(ResultSet rs, int rowNum) throws SQLException { Product prod = new Product(); prod.setId(rs.getInt("id")); prod.setDescription(rs.getString("description")); prod.setPrice(new Double(rs.getDouble("price"))); return prod; } } }
Vamos dar uma olhada nos dois métodos DAO dessa classe. Já que estamos estendendo SimpleJdbcSupport nós obtemos um SImpleJdbcTemplate preparado e pronto para uso. Esse acesso é feito chamando o método getSimpleJdbcTemplate().
O primeiro método, getProductList(), executa uma consulta usando SimpleJdbcTemplate. Nós simplesmente fornecemos uma senteça SQL e uma classe que pode manipular o mapeamento entre o ResultSet e a classe Product. Em nosso caso o mapeamento da classe é feita por ProductMapper, que definimos como uma classe interna do DAO.
O ProductMapper implementa a interface ParameterizedRowMapper que define um único método chamado mapRow que precisa ser implementado. Esse método mapeará os dados de cada linha na classe que representa a entidade que estamos recuperando da consulta. Já que RowMapper é parametrizado, o método mapRow retorna o tipo que é criado.
O segundo método é saveProduct e também usa SimpleJdbcTemplate. Dessa vez chamamos o método update passando uma senteça SQL junto com os valores dos parâmetros na forma de um MapSqlParameterSource. Usando um MapSqlParameterSource, nos é permitido usar os nomes dos parâmetros ao invés dos caracteres “?” típicos que você usaria se escrevesse código usanado JDBC. Esses parâmetros tornam nosso código mais explicito e evita problemas causados por parâmetros sendo passados fora de orde, etc. O método update retorna o número de linhas afetadas.
Precisamos a armazenar o valor da chave primária de cada produto na classe Product. Essa chave será usada quando nós formos salvar cada alteração no objeto no banco de dados. Para armazenar essa chave, nós adicionamos um campo privado chamado ‘id’ com seus respectivos setter e getter em Product.java.
'springapp/src/springapp/domain/Product.java':
package springapp.domain; import java.io.Serializable; public class Product implements Serializable { private int id; private String description; private Double price; public void setId(int i) { id = i; } public int getId() { return id; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("Description: " + description + ";"); buffer.append("Price: " + price); return buffer.toString(); } }
Isso completa a implementação JDBC de nossa camada de persistência.
5.5. Implementando os testes da implementação DAO do JDBC
Chegou o momento de adicionar os testes para a nossa implementação DAO. O Spring fornece um framework de teste extensivo que suporta JUnit 3.8 e 4 assim como TestNG. Não temos cobrir tudo isso nesse guia mas iremos mostrar uma implementação simples do suporte a JUnit 3.8. Precisamos adicionar o arquivo jar contendo o framework de testes do Spring ao nosso projeto. Copie spring-test.jar do diretório 'spring-framework-2.5/dist/modules' para o diretório 'springapp/war/WEB-INF/lib'
Agora podemos criar nosso teste. Extendendo AbstractTransactionalDataSourceSpringContextTests nós obtemos vários recursos de graça. Temos injeção de dependência em qualquer setter público do contexto da aplicação. Esse contexto de aplicação é carregado pelo framework de teste. Tudo que precisamos fazer é especificar o nome ao método getConfigLocations. Também temos a oportunidade de preparar nosso banco de dados com os dados de teste adequados no método onSetUpInTransaction. Isso é importante, já que não saberemos o estado do banco de dados quando formos executar nosso testes. No momento que a tabela for criada, limparemos ela e carregaremos os dados necessários para o nosso teste. Já que estendemos um teste “Transacional”, qualquer alteração que fizermos será automaticamente desfeita quando o teste for finalizado. Os métodos deleteFromTables e executeSqlScripts são definidos na super-classe, de modo que não precisamos implementa-las para cada teste. Somente passe os nomes das tabelas a serem limpas e o nome do script que contém os dados de teste.
'springapp/test/springapp/domain/JdbcProductDaoTests.java':
package springapp.repository; import java.util.List; public class JdbcProductDaoTests extends AbstractTransactionalDataSourceSpringContextTests { private ProductDao productDao; public void setProductDao(ProductDao productDao) { this.productDao = productDao; } @Override protected String[] getConfigLocations() { return new String[] {"classpath:test-context.xml"}; } @Override protected void onSetUpInTransaction() throws Exception { super.deleteFromTables(new String[] {"products"}); super.executeSqlScript("file:db/load_data.sql", true); } public void testGetProductList() { List<Product> products = productDao.getProductList(); assertEquals("wrong number of products?", 3, products.size()); } public void testSaveProduct() { List<Product> products = productDao.getProductList(); for (Product p : products) { p.setPrice(200.12); productDao.saveProduct(p); } List<Product> updatedProducts = productDao.getProductList(); for (Product p : updatedProducts) { assertEquals("wrong price of product?", 200.12, p.getPrice()); } } }
Não temos o arquivo de contexto da aplicação para esse teste ainda, então vamos criar esse arquivo no diretório ‘springapp/test’:
'springapp/test/test-context.xml':
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <!-- the test application context definition for the jdbc based tests --> <bean id="productDao"> <property name="dataSource" ref="dataSource" /> </bean> <bean id="dataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="propertyConfigurer" > <property name="locations"> <list> <value>classpath:jdbc.properties</value> </list> </property> </bean> <bean id="transactionManager" > <property name="dataSource" ref="dataSource" /> </bean> </beans>
Definimos um productDao que é a classe que estamos testando. Temos também que definir um DatSource com marcadores para os valores de configurações. Esses valores são fornecidos por um arquivo de propriedades separado e durante a execução, o PropertyPlaceholderConfigurer que definimos lerá esse arquivo de propriedades e substituirá os marcadores pelos valores. Isso é conveniente já que isola os valores da conexão em seu próprio arquivo. Esses valores precisam frequentemente ser alterados durante o carregamento da aplicação. Nós colocamos esse novo arquivo em ‘war/WEB-INF/classes’ de forma que esteja disponível quando executarmos a aplicação e também quando carregarmos a aplicação web. O conteúdo desse arquivo será:
'springapp/war/WEB-INF/classes/jdbc.properties':
jdbc.driverClassName=org.hsqldb.jdbcDriver jdbc.url=jdbc:hsqldb:hsql://localhost jdbc.username=sa jdbc.password=
Já que adicionamos um arquivo de configuração ao diretório ‘tests’ e um arquivo jdbc.properties ao diretório ‘WEB-INF/classes’, vamos adicionar uma nova entrada no classpath para nosos testes. Devemos ter a seguinte definição da propriedade ‘test.dir’:
'springapp/build.xml':
... <property name="test.dir" value="test"/> <path id="test-classpath"> <fileset dir="${web.dir}/WEB-INF/lib"> <include name="*.jar"/> </fileset> <pathelement path="${build.dir}"/> <pathelement path="${test.dir}"/> <pathelement path="${web.dir}/WEB-INF/classes"/> </path> ...
Devemos agora ter o suficiente para que nossos testes possam ser executados mas queremos fazer uma mudança adicional no script de compilação. É uma boa prática separar qualquer teste integrado que dependa do banco de dados dos outros testes. Assim adicionamos um alvo ‘dbTarget’ separado e excluímos os testes do banco de dados do alvo ‘tests’.
'springapp/build.xml':
... <target name="tests" depends="build, buildtests" description="Run tests"> <junit printsummary="on" fork="false" haltonfailure="false" failureproperty="tests.failed" showoutput="true"> <classpath refid="test-classpath"/> <formatter type="brief" usefile="false"/> <batchtest> <fileset dir="${build.dir}"> <include name="**/*Tests.*"/> <exclude name="**/Jdbc*Tests.*"/> </fileset> </batchtest> </junit> <fail if="tests.failed"> tests.failed=${tests.failed} *********************************************************** *********************************************************** **** One or more tests failed! Check the output ... **** *********************************************************** *********************************************************** </fail> </target> <target name="dbTests" depends="build, buildtests,dropTables,createTables,loadData" description="Run db tests"> <junit printsummary="on" fork="false" haltonfailure="false" failureproperty="tests.failed" showoutput="true"> <classpath refid="test-classpath"/> <formatter type="brief" usefile="false"/> <batchtest> <fileset dir="${build.dir}"> <include name="**/Jdbc*Tests.*"/> </fileset> </batchtest> </junit> <fail if="tests.failed"> tests.failed=${tests.failed} *********************************************************** *********************************************************** **** One or more tests failed! Check the output ... **** *********************************************************** *********************************************************** </fail> </target> ...
Agora basta executar os testes para verificar se tudo está ok.
5.6. Sumário
Nós completamos a camada de persistência e na próxima parte integraremos ela a nossa aplicação web. Mas primeiro, vamos resumir o que conseguimos nessa parte.
- Primeiro configuramos nosso banco de dados e criamos os scripts de inicialização.
- Criamos scripts para serem usados para criar as tabelas e também carregar dados de teste.
- Em seguida, adicionamos algumas tarefas ao nosso script Ant para serem executados quando precisarmos criar o deletar a tabela e também para adicionar ou remover dados de teste.
- Criamos a classe DAO que fará o trabalho de persistência usando o SImpleJdbcTemplate do Spring.
- Finalmente criamos unidades de testes e os alvos ant para executar esses testes.
Abaixo a tela de como a estrutura de diretório de nosso projeto deve ficar depois das intruções acima.