<div class="page"> <div class="cover text-center"> <img class="mx-auto" src=/itb/images/logo_mislata.png alt="logo"> # Bases de dades incrustades <div class="text-end fit-content ms-auto my-3 mt-auto pt-3"> <p><strong>Autor:</strong> Joan Puigcerver Ibáñez</p> <p><strong>Correu electrònic:</strong> j.puigcerveribanez@edu.gva.es</p> <p><strong>Curs:</strong> 2024/2025</p> </div> <div> <p class="fw-bold mb-0">Llicència: BY-NC-SA</p> <p class="d-none d-md-block">(Reconeixement - No Comercial - Compartir Igual)</p> <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ca" target="_blank"> <img class="mx-auto" src="/itb/images/license.png" alt="Licence"/> </a> </div><!--license--> </div><!--cover--> </div><!--page--> {:toc} ## Introducció Quan es realitzen proves sobre una aplicació que utilitza una base de dades, generalment no es bona idea realitzar-les sobre la base de dades real, ja que es poden modificar les dades i afectaria al funcionament de l'aplicació. Una bona pràctica és executar les proves en un __entorn de proves aïllat__, on les operacions sobre la base de dades no afecten a la base de dades real. Una de les opcions és crear una __base de dades de prova__, que es crea i es destrueix cada vegada que s'executen les proves. Aquesta base de dades es crea amb les dades necessàries per realitzar les proves. Aquesta opció és la que més s'acosta a la realitat, ja que les proves es realitzen sobre una base de dades equivalent a la real. No obstant això, aquesta opció és més lenta i costosa, ja que s'ha de crear i destruir la base de dades en un servidor cada vegada que s'executen les proves. Una alternativa és utilitzar una __base de dades incrustada__, que es crea en memòria i es destrueix quan s'acaben les proves. Aquesta opció és més ràpida i menys costosa, ja que no tens la necessitat d'instal·lar cap sistema gestor de bases de dades. No obstant això, aquesta opció no és tan realista com la primera, ja que la base de dades incrustades no tenen exactament la mateixa estructura que la base de dades real i la sintaxi SQL pot variar. En qualsevol dels dos casos, l'objectiu és crear una base de dades amb la estructura idèntica a la que s'utilitzarà en producció, sobre la qual es poden realitzar les proves sense modificar la base de dades real. En aquest material es mostra com realitzar proves sobre una base de dades incrustada en un projecte de Spring Boot. ::: warning Aquest material no mostra la millor manera de realitzar aquests tests, ja que no s'utilitzen les característiques que proporciona Spring Boot, sino una manera més "artesanal" per entendre el concepte d'aquest tipus de proves. ::: ## Dependències ::: warning Les dependències següents han segut utilitzades per incorporar bases de dades incrustades al projecte de Spring Boot. ::: ```xml <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> ``` ## Fitxers de configuració Els fitxers de configuració en un projecte són essencials per definir les propietats de l'aplicació en funció de l'entorn en el que s'execute l'aplicació. ### application.properties En aquest fitxer es defineixen les propietats de l'aplicació, com ara la configuració de la base de dades. ```properties app.profiles.active=dev ``` ### application-dev.properties En aquest fitxer es defineixen les propietats de l'aplicació per l'entorn de desenvolupament. ```properties app.datasource.url = jdbc:mysql://localhost:3306/llibreview app.datasource.username = root app.datasource.password = 1234 dao=jdbc ``` ### application.properties (test) En aquest fitxer es defineixen les propietats de l'aplicació per l'entorn de proves. En aquest cas, el fitxer ha d'estar situat a la carpeta __test/resources__. ::: note Pareu atenció a l'opció `MODE=MYSQL`. Aquesta opció permet utilitzar la sintaxi SQL de MySQL en la base de dades H2. ::: ```properties app.datasource.url=jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1 app.datasource.username=sa app.datasource.password=sa ``` ### AppPropertiesReader Classe que llegeix les propietats de l'aplicació. ::: warning Aquesta classe no és la millor manera de llegir les propietats de l'aplicació, ja que Spring Boot proporciona una manera més fàcil i eficient de llegir les propietats. ::: ```java package com.fpmislata.daw1.projectedaw1.common; import lombok.extern.log4j.Log4j2; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @Log4j2 public class AppPropertiesReader { private static final Properties properties = new Properties(); static { loadProperties("application.properties"); // Carga las propiedades por defecto // Detectar el perfil y cargar las propiedades correspondientes String activeProfile = getProperty("app.profiles.active"); if (activeProfile != null) { log.info("Perfil actiu: " + activeProfile); loadProperties("application-" + activeProfile + ".properties"); } else { log.warn("No s'ha especificat cap perfil actiu"); } } private static void loadProperties(String filename) { try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename)) { if (input == null) { System.out.println("No s'ha pogut trobar el fitxer de configuració: " + filename); return; } // Cargar las propiedades desde el archivo de configuración properties.load(input); } catch (IOException e) { e.printStackTrace(); } } public static String getProperty(String key) { return properties.getProperty(key); } } ``` ## Connexió a la base de dades La connexió a la base de dades es realitza mitjançant la classe `DatabaseConnection`, que s'encarrega de llegir les propietats del fitxer de configuració i establir la connexió amb la base de dades. Aquesta classe depèn de la llibreria `ScriptRunner`, que permet executar scripts SQL. - Disponible a https://gist.github.com/alphaville/5044538 ::: note La implementació de la connexió pot variar respecte al codi proporcionat en el projecte. ::: ```java package com.fpmislata.daw1.projectedaw1.persistance.dao.impl.jdbc.database; import com.fpmislata.daw1.projectedaw1.common.AppPropertiesReader; import lombok.Getter; import lombok.extern.log4j.Log4j2; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; @Log4j2 public class DatabaseConnection { private static DatabaseConnection instance = null; public static DatabaseConnection getInstance() { if (instance == null) { instance = new DatabaseConnection(); } return instance; } private final String dbUrl; private final String dbUser; private final String dbPassword; @Getter private final Connection connection; private DatabaseConnection () { dbUrl = AppPropertiesReader.getProperty("app.datasource.url"); dbUser = AppPropertiesReader.getProperty("app.datasource.username"); dbPassword = AppPropertiesReader.getProperty("app.datasource.password"); log.info("Establint la connexió amb la base de dades..."); try { connection = DriverManager.getConnection( dbUrl, dbUser, dbPassword ); log.info("Connexió establerta amb èxit amb els paràmetres:"); log.info(this.getParameters()); } catch (SQLException e) { log.error(e.getMessage()); throw new RuntimeException("Connection paramaters :\n\n" + getParameters() + "\nOriginal exception message: " + e.getMessage()); } } private String getParameters (){ return String.format("url: %s\nUser: %s\nPassword: %s\n", dbUrl, dbUser, dbPassword ); } @SuppressWarnings("SqlSourceToSinkFlow") public PreparedStatement prepareStatement(String sql) throws SQLException { return connection.prepareStatement(sql); } public void executeScript(String scriptPath) { try { ScriptRunner scriptRunner = new ScriptRunner(connection, false, false); InputStream scriptStream = getClass().getClassLoader().getResourceAsStream(scriptPath); if (scriptStream == null) throw new RuntimeException("Script not found: " + scriptPath); scriptRunner.runScript(new InputStreamReader(scriptStream)); } catch (IOException | SQLException e) { log.error(String.format("Error executing script %s:\n %s\n", scriptPath, e.getMessage())); throw new RuntimeException(e); } } } ``` ## Codi font ### LlibreDaoJdbc Aquesta classe conté els mètodes que realitzen consultes sobre la bases de dades que volem provar. ```java package com.fpmislata.daw1.projectedaw1.persistance.dao.impl.jdbc; import com.fpmislata.daw1.projectedaw1.domain.entity.Llibre; import com.fpmislata.daw1.projectedaw1.persistance.dao.LlibreDao; import com.fpmislata.daw1.projectedaw1.persistance.dao.impl.jdbc.database.DatabaseConnection; import com.fpmislata.daw1.projectedaw1.persistance.dao.impl.jdbc.rowmapper.LlibreRowMapper; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; public class LlibreDaoJdbc implements LlibreDao { private final DatabaseConnection databaseConnection; private final LlibreRowMapper llibreRowMapper; public LlibreDaoJdbc() { this.databaseConnection = DatabaseConnection.getInstance(); this.llibreRowMapper = new LlibreRowMapper(); } @Override public List<Llibre> findAll() { String sql = "SELECT * FROM llibre"; try (PreparedStatement preparedStatement = databaseConnection.prepareStatement(sql)) { ResultSet rs = preparedStatement.executeQuery(); return llibreRowMapper.map(rs); } catch (SQLException e) { throw new RuntimeException(e.getMessage()); } } @Override public Llibre findByIsbn(String isbn) { String sql = "SELECT * FROM llibre where isbn = ?"; try (PreparedStatement preparedStatement = databaseConnection.prepareStatement(sql)) { preparedStatement.setString(1, isbn); ResultSet rs = preparedStatement.executeQuery(); List<Llibre> llibreList = llibreRowMapper.map(rs); return llibreList.isEmpty() ? null : llibreList.get(0); } catch (SQLException e) { throw new RuntimeException(e.getMessage()); } } } ``` ## Proves ### LlibreDaoJdbcTest Per realitzar les proves de la classe `LlibreDaoJdbc` ens hem crear una base de dades incrustada amb unes dades de prova. Abans de començar les proves, cal inicialitzar la base de dades i crear les taules i les dades necessàries per realitzar les proves. Això ho realitzem amb el mètode `@BeforeAll void setup()`, on s'executen els scripts `schema.sql` i `data.sql`. - `schema.sql`: Crea les tables de la base de dades. - `data.sql`: Inserta les dades de prova. A més, perquè cada prova s'execute de manera independent a la resta, podem utilitzar transaccions. Establim el mode `autoCommit` a `false` per evitar que les modificacions a la base de dades s'efectuen. Després de cada prova (`@AfterEach`), es realitza un `rollback` per desfer els canvis. ```java package com.fpmislata.daw1.projectedaw1.unit.persistance.dao.jdbc; import com.fpmislata.daw1.projectedaw1.domain.entity.Llibre; import com.fpmislata.daw1.projectedaw1.persistance.dao.LlibreDao; import com.fpmislata.daw1.projectedaw1.persistance.dao.impl.jdbc.LlibreDaoJdbc; import com.fpmislata.daw1.projectedaw1.persistance.dao.impl.jdbc.database.DatabaseConnection; import org.junit.jupiter.api.*; import java.sql.SQLException; import java.time.LocalDate; import java.util.List; import static org.junit.jupiter.api.Assertions.*; public class LlibreDaoJdbcTest { private final LlibreDao llibreDao = new LlibreDaoJdbc(); private static final DatabaseConnection connection = DatabaseConnection.getInstance(); public final List<Llibre> expectedLlibreList = List.of( new Llibre( "1", "Llibre 1", "Resum 1", LocalDate.parse("2024-01-01"), 100, "imatge1.png" ), new Llibre( "2", "Llibre 2", "Resum 2", LocalDate.parse("2024-01-02"), 200, "imatge2.png" ), new Llibre( "3", "Llibre 3", "Resum 3", LocalDate.parse("2024-01-03"), 300, "imatge3.png" ), new Llibre( "4", "Llibre 4", "Resum 4", LocalDate.parse("2024-01-04"), 400, "imatge4.png" ), new Llibre( "5", "Llibre 5", "Resum 5", LocalDate.parse("2024-01-05"), 500, "imatge5.png" ), new Llibre( "6", "Llibre 6", "Resum 6", LocalDate.parse("2024-01-06"), 600, "imatge6.png" ) ); @BeforeAll static void setup() throws SQLException { connection.executeScript("schema.sql"); connection.executeScript("data.sql"); connection.getConnection().setAutoCommit(false); } @AfterEach void tearDown() throws SQLException { connection.getConnection().rollback(); } @Test void findAll_shouldReturnAllLlibres() { List<Llibre> result = llibreDao.findAll(); assertEquals(expectedLlibreList, result); } @Test void findByIsbn_shouldReturnLlibre() { Llibre result = llibreDao.findByIsbn("1"); assertEquals(expectedLlibreList.get(0), result); } @Test void findByDifferentIsbn_shouldReturnDifferentLlibre() { Llibre result = llibreDao.findByIsbn("2"); assertEquals(expectedLlibreList.get(1), result); } @Test void findByNonExistingIsbn_shouldReturnNull() { Llibre result = llibreDao.findByIsbn("7"); assertNull(result); } } ``` ### schema.sql Els scripts han d'estar dins de la carpeta __test/resources__ del projecte. ```sql create table llibre ( isbn varchar(50) primary key, titol varchar(100) not null, resum text, nombre_pagines int unsigned, data_publicacio date not null, ruta_imatge varchar(200) not null ); ``` ### data.sql ```sql insert into llibre (isbn, titol, resum, nombre_pagines, data_publicacio, ruta_imatge) values ('1', 'Llibre 1', 'Resum 1', 100, '2024-01-01', 'imatge1.png'), ('2', 'Llibre 2', 'Resum 2', 200, '2024-01-02', 'imatge2.png'), ('3', 'Llibre 3', 'Resum 3', 300, '2024-01-03', 'imatge3.png'), ('4', 'Llibre 4', 'Resum 4', 400, '2024-01-04', 'imatge4.png'), ('5', 'Llibre 5', 'Resum 5', 500, '2024-01-05', 'imatge5.png'), ('6', 'Llibre 6', 'Resum 6', 600, '2024-01-06', 'imatge6.png'); ```