<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');
```