Esta es la Parte 5 del tutorial paso a paso sobre sobre cómo desarrollar una aplicación web desde cero usando Spring Framework. En la Parte 1 hemos configurado el entorno y puesto en marcha una aplicación básica. En la Parte 2 hemos mejorado la aplicación que habíamos construido hasta entonces. En la Parte 3 hemos añadido toda la lógica de negocio y los tests unitarios, y en la Parte 4 hemos desarrollado la interface web. Ahora es el momento de introducir persistencia en base de datos. En las partes anteriores hemos visto cómo cargar algunos objetos de negocio definiendo beans en un archivo de configuración. Es obvio que esta solución nunca va a funcionar en el mundo real – cada vez que reiniciemos el servidor obtendremos de nuevo los precios originales. Necesitamos añadir código para persistir esos cambios en una base de datos.
Antes de que podamos comenzar a desarrollar el código de persistencia, necesitamos una base de datos. En lugar de confiar en una base de datos integrada con la propia aplicación, vamos a utilizar una base de datos separada. Hemos planeado usar MySQL, una buena base de datos de código libre. Sin embargo, los pasos que se muestran a continuación serán similares para otras bases de datos (p. ej. PostgreSQL, HSQL...). Desde el enlace anterior, se puede descargar MySQL para diferentes sistemas operativos. Una vez instalada, cada distribución proporciona sus correspondientes scripts de inicio para arrancar la base de datos.
Primero, creamos el fichero 'springapp.sql' (en el directorio 'db') con las sentencias SQL necesarias para la creación de la base de datos springapp. Este fichero creará también la tabla products, que alojará los productos de nuestra aplicación. Además, establecerá los permisos para el usuario springappuser.
'springapp/db/springapp.sql':
CREATE DATABASE springapp; GRANT ALL ON springapp.* TO springappuser@'%' IDENTIFIED BY 'pspringappuser'; GRANT ALL ON springapp.* TO springappuser@localhost IDENTIFIED BY 'pspringappuser'; USE springapp; CREATE TABLE products ( id INTEGER PRIMARY KEY, description varchar(255), price decimal(15,2) ); CREATE INDEX products_description ON products(description);
A continuación, necesitamos añadir nuestros datos de prueba. Para ello, creamos el archivo 'load_data.sql' en el directorio 'db' con el siguiente contenido:
'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);
Por último, ejecutamos las siguientes instrucciones sobre la línea de comandos para crear y rellenar la base de datos:
linux:springapp/db# mysql -u root -p Enter password: ... mysql> source springapp.sql mysql> source load_data.sql
Para poder acceder desde nuestra aplicación a la base de datos MySQL mediante JPA, debemos incluir las siguientes dependencias en el fichero 'pom.xml' (también hemos creado la propiedad org.springframework.data.version con el valor 2.0.7.RELEASE):
Group Id | Artifact Id | Version |
---|---|---|
mysql | mysql-connector-java | 5.1.46 |
org.hibernate | hibernate-core | 5.2.17.Final |
org.springframework.data | spring-data-jpa | ${org.springframework.data.version} |
Spring Data acelera el desarrollo de repositorios de acceso a datos mediante la extensió de un conjunto de clases donde los métodos básicos de acceso a datos (CRUD) ya están implementados y solamente es necesario añadir aquellos que no sigan una notación estándar. En nuestro caso, sencillamente implementaremos la siguiente classe llamada ProductRepository que extiende la clase CrudRepository de Spring Data.
'springapp/src/main/java/com/companyname/springapp/business/repositories/ProductRepository.java':
package com.companyname.springapp.business.repositories; import org.springframework.data.repository.CrudRepository; import com.companyname.springapp.business.entities.Product; public interface ProductRepository extends CrudRepository<Product, Integer> { }
Llegados a este punto, debemos modificar la clase Product para que se persista correctamente. Para ello modificamos el fichero 'Product.java' y añadimos las anotaciones de JPA que realizan el mapeo entre los campos del objeto y aquellos de la base de datos. Asimismo, hemos añadido el campo id para mapear la clave primaria de la tabla products.
'springapp/src/main/java/com/companyname/springapp/business/entities/Product.java':
package com.companyname.springapp.business.entities; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="products") public class Product { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String description; private Double price; public Integer getId() { return id; } public void setId(Integer id) { this.id = 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(); } }
Los pasos anteriores completan la implementación JPA en nuestra capa de persistencia.
Es el momento de añadir tests a nuestro repositorio sobre JPA. Para ello, crearemos la clase 'ProductRepositoryTests' dentro de paquete 'com.companyname.springapp.business.repositories' del la carpeta 'src/test/java'.
'springapp/src/test/java/com/companyname/springapp/business/repositories/ProductRepositoryTests.java':
package com.companyname.springapp.business.repositories; import java.util.List; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import com.companyname.springapp.business.SpringappBusinessConfig; import com.companyname.springapp.business.entities.Product; @RunWith(SpringRunner.class) @ContextConfiguration(classes = {SpringappBusinessConfig.class}) public class ProductRepositoryTests { @Autowired private ProductRepository productRepository; @Test public void testGetProductList() { List<Product> products = (List<Product>) productRepository.findAll(); assertEquals(products.size(), 3, 0); } @Test public void testSaveProduct() { List<Product> products = (List<Product>) productRepository.findAll(); Product p = products.get(0); Double price = p.getPrice(); p.setPrice(200.12); productRepository.save(p); List<Product> updatedProducts = (List<Product>) productRepository.findAll(); Product p2 = updatedProducts.get(0); assertEquals(p2.getPrice(), 200.12, 0); p2.setPrice(price); productRepository.save(p2); } }
Aún modificamos el archivo que contiene la configuración del contexto de la aplicación para que contenga todos los beans relacionados con el acceso a la base de datos:
'springapp/src/java/com/companyname/springapp/business/SpringappBusinessConfig.java':
package com.companyname.springapp.business; import java.util.Properties; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import com.companyname.springapp.business.entities.Product; @Configuration @ComponentScan @PropertySource("classpath:application.properties") @EnableJpaRepositories("com.companyname.springapp.business.repositories") public class SpringappBusinessConfig { @Autowired private Environment env; @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(env.getProperty("db.driver")); dataSource.setUrl(env.getProperty("db.url")); dataSource.setUsername(env.getProperty("db.username")); dataSource.setPassword(env.getProperty("db.password")); return dataSource; } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); emf.setDataSource(dataSource()); emf.setPackagesToScan(Product.class.getPackage().getName()); HibernateJpaVendorAdapter hibernateJpa = new HibernateJpaVendorAdapter(); hibernateJpa.setDatabase(Database.MYSQL); hibernateJpa.setDatabasePlatform(env.getProperty("hibernate.dialect")); hibernateJpa.setGenerateDdl(env.getProperty("hibernate.generateDdl", Boolean.class)); hibernateJpa.setShowSql(env.getProperty("hibernate.show_sql", Boolean.class)); emf.setJpaVendorAdapter(hibernateJpa); Properties jpaProperties = new Properties(); jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto")); jpaProperties.put("hibernate.show_sql", env.getProperty("hibernate.show_sql")); jpaProperties.put("hibernate.format_sql", env.getProperty("hibernate.format_sql")); emf.setJpaProperties(jpaProperties); return emf; } @Bean public JpaTransactionManager transactionManager() { JpaTransactionManager txnMgr = new JpaTransactionManager(); txnMgr.setEntityManagerFactory(entityManagerFactory().getObject()); return txnMgr; } }
Hemos definido un DataSource cuyos valores de configuración serán tomados de un archivo de propiedades en tiempo de ejecución. Esto es conveniente puesto que separa los valores de conexión en su propio archivo, y estos valores a menudo suelen ser cambiados durante el despliegue de la aplicación. Vamos a poner este nuevo archivo tanto en el directorio 'src/main/java/resources' por lo que estará disponible tanto cuando ejecutemos los tests como cuando despleguemos la aplicación web. El contenido de este archivo de propiedades es:
'springapp/src/main/java/resources/application.properties':
#Database Configuration db.driver=com.mysql.jdbc.Driver db.url=jdbc:mysql://localhost:3306/springapp db.username=springappuser db.password=pspringappuser #Hibernate Configuration hibernate.dialect=org.hibernate.dialect.MySQLDialect hibernate.generateDdl = false hibernate.hbm2ddl.auto=none hibernate.show_sql=false hibernate.format_sql=true
Ahora disponemos del codigo suficiente para ejecutar los tests de 'ProductRepositoryTests' y hacerlos pasar.
Ya hemos completado la capa de persistencia y en la próxima parte vamos a integrarla con nuestra aplicación web. Pero primero, resumamos rápidamente todo lo que hemos hecho en esta parte.
Primero hemos configurado nuestra base de datos y ejecutado las sentencias SQL para crear una tabla en la base de datos y cargar algunos datos de prueba.
Hemos creado una clase DAO que manejará el trabajo de persistencia mediante JPA usando la clase Product.
Finalmente hemos creado tests de integración para comprobar su funcionamiento.
A continuación puedes ver una captura de pantalla que muestra el aspecto que debería tener la estructura de directorios del proyecto después de seguir todas las instrucciones anteriores.
La estructura de directorios del proyecto al final de la parte 5