Skip to content

Commit 80b2477

Browse files
authored
Merge pull request #235 from nanotaboada/feature/migrate-from-h2-to-sqlite
feat: migrate database from H2 to SQLite
2 parents 3d131eb + b78db1d commit 80b2477

File tree

15 files changed

+220
-352
lines changed

15 files changed

+220
-352
lines changed

.github/copilot-instructions.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Project Overview
44

5-
This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targeting **JDK 25 (LTS)**. The application demonstrates a clean, layered architecture implementing a CRUD API for managing books. It uses an in-memory H2 database for data persistence and includes comprehensive test coverage.
5+
This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targeting **JDK 25 (LTS)**. The application demonstrates a clean, layered architecture implementing a CRUD API for managing books. It uses a **SQLite database** for runtime persistence (with a pre-seeded database in Docker) and **H2 in-memory** for fast test execution.
66

77
**Key URLs:**
88

@@ -17,7 +17,7 @@ This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targ
1717
- **Java**: JDK 25 (LTS) - use modern Java features where appropriate
1818
- **Spring Boot**: 4.0.0 with modular starter dependencies (WebMVC, Data JPA, Validation, Cache, Actuator)
1919
- **Build Tool**: Maven 3.9+ (use `./mvnw` wrapper, NOT system Maven)
20-
- **Database**: H2 in-memory database (runtime scope)
20+
- **Database**: SQLite (runtime) with Xerial JDBC driver; H2 in-memory (test scope only)
2121

2222
### Key Dependencies
2323

@@ -26,6 +26,8 @@ This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targ
2626
- **SpringDoc OpenAPI** 2.8.14: API documentation (Swagger UI)
2727
- **JaCoCo** 0.8.14: Code coverage reporting
2828
- **AssertJ** 3.27.6: Fluent test assertions
29+
- **SQLite JDBC** 3.47.1.0: SQLite database driver (Xerial)
30+
- **Hibernate Community Dialects**: Provides `SQLiteDialect` for JPA/Hibernate
2931

3032
### Testing
3133

@@ -36,8 +38,8 @@ This is a RESTful Web Service proof-of-concept built with **Spring Boot 4** targ
3638

3739
### DevOps & CI/CD
3840

39-
- **Docker**: Multi-stage build with Eclipse Temurin Alpine images
40-
- **Docker Compose**: Local containerized deployment
41+
- **Docker**: Multi-stage build with Eclipse Temurin Alpine images and pre-seeded SQLite database
42+
- **Docker Compose**: Local containerized deployment with persistent storage volume
4143
- **GitHub Actions**: CI pipeline with coverage reporting (Codecov, Codacy)
4244

4345
## Project Structure
@@ -54,7 +56,7 @@ src/main/java/ar/com/nanotaboada/java/samples/spring/boot/
5456
└── models/ # Domain entities & DTOs
5557
├── Book.java # JPA entity
5658
├── BookDTO.java # Data Transfer Object with validation
57-
└── BooksDataInitializer.java # Seed data
59+
└── UnixTimestampConverter.java # JPA converter for LocalDate ↔ Unix timestamp
5860
5961
src/test/java/.../test/
6062
├── controllers/ # Controller tests (@WebMvcTest)
@@ -64,12 +66,18 @@ src/test/java/.../test/
6466
└── BookDTOFakes.java # Test data factory for BookDTO
6567
6668
src/main/resources/
67-
├── application.properties # Application configuration
69+
├── application.properties # Application configuration (SQLite)
6870
└── logback-spring.xml # Logging configuration
6971
72+
src/test/resources/
73+
└── application.properties # Test configuration (H2 in-memory)
74+
7075
scripts/
71-
├── entrypoint.sh # Docker container entrypoint
76+
├── entrypoint.sh # Docker entrypoint (copies seed DB on first run)
7277
└── healthcheck.sh # Docker health check using Actuator
78+
79+
storage/
80+
└── books-sqlite3.db # Pre-seeded SQLite database with sample books
7381
```
7482

7583
**Package Naming Convention**: `ar.com.nanotaboada.java.samples.spring.boot.<layer>`
@@ -164,6 +172,15 @@ docker compose logs -f
164172
- `9000`: Main API server
165173
- `9001`: Actuator management endpoints
166174

175+
**Persistent Storage**:
176+
177+
The Docker container uses a "hold" pattern for the pre-seeded SQLite database:
178+
179+
1. Build stage copies `storage/books-sqlite3.db` to `/app/hold/` in the image
180+
2. On first container run, `entrypoint.sh` copies the database to `/storage/` volume
181+
3. Subsequent runs use the existing database from the volume
182+
4. To reset: `docker compose down -v` removes volumes, next `up` restores seed data
183+
167184
## Common Tasks & Patterns
168185

169186
### Adding a New REST Endpoint
@@ -203,15 +220,18 @@ docker compose logs -f
203220
### Build Failures
204221

205222
- **Lombok not working**: Ensure annotation processor is enabled in IDE and `maven-compiler-plugin` includes Lombok path
206-
- **Tests failing**: Check if H2 database is properly initialized; review `BooksDataInitializer.seed()`
223+
- **Tests failing**: Tests use H2 in-memory database via `src/test/resources/application.properties`
207224
- **Port already in use**: Change `server.port` in `application.properties` or kill process using ports 9000/9001
208225
- **JAVA_HOME not set**: Run `export JAVA_HOME=$(/usr/libexec/java_home -v 25)` on macOS or set to JDK 25 path on other systems
209226
- **CacheManager errors in tests**: Add `@AutoConfigureCache` annotation to slice tests (`@WebMvcTest`, `@DataJpaTest`)
227+
- **SQLite file not found**: Ensure `storage/books-sqlite3.db` exists for local development
210228

211229
### Docker Issues
212230

213231
- **Container health check failing**: Verify Actuator is accessible at `http://localhost:9001/actuator/health`
214232
- **Build context too large**: Ensure `.dockerignore` excludes `target/` and `.git/`
233+
- **Database not persisting**: Check that `java-samples-spring-boot_storage` volume exists (`docker volume ls`)
234+
- **Stale seed data**: Run `docker compose down -v` to remove volumes and restore fresh seed data on next `up`
215235

216236
### Common Pitfalls
217237

@@ -221,6 +241,13 @@ docker compose logs -f
221241
- **Repository interfaces**: Custom query methods may not show in coverage (JaCoCo limitation)
222242
- **Spring Boot 4.0 modular packages**: Test annotations like `@WebMvcTest`, `@DataJpaTest`, and `@AutoConfigureCache` are now in modular packages (e.g., `org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest`)
223243

244+
### SQLite Configuration Notes
245+
246+
- **Date storage**: LocalDate fields are stored as Unix timestamps (INTEGER) for robustness - no parsing issues
247+
- **Converter**: `UnixTimestampConverter` handles LocalDate ↔ epoch seconds conversion via JPA `@Convert`
248+
- **DDL auto**: Use `ddl-auto=none` since the database is pre-seeded (SQLite has limited ALTER TABLE support)
249+
- **Tests use H2**: The converter works seamlessly with both H2 and SQLite databases
250+
224251
## CI/CD Pipeline
225252

226253
### GitHub Actions Workflow (`.github/workflows/maven.yml`)

Dockerfile

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,17 @@ COPY --chmod=555 assets/ ./assets/
4545
COPY --chmod=555 scripts/entrypoint.sh ./entrypoint.sh
4646
COPY --chmod=555 scripts/healthcheck.sh ./healthcheck.sh
4747

48-
# Add system user
49-
RUN addgroup -S spring && \
50-
adduser -S -G spring spring
48+
# The 'hold' is our storage compartment within the image. Here, we copy a
49+
# pre-seeded SQLite database file, which Compose will mount as a persistent
50+
# 'storage' volume when the container starts up.
51+
COPY --chmod=555 storage/ ./hold/
52+
53+
# Install SQLite runtime libs, add non-root user and prepare volume mount point
54+
RUN apk add --no-cache sqlite-libs && \
55+
addgroup -S spring && \
56+
adduser -S -G spring spring && \
57+
mkdir -p /storage && \
58+
chown -R spring:spring /storage
5159

5260
USER spring
5361

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ A proof-of-concept RESTful Web Service built with **Spring Boot 4** targeting **
3131
The service showcases:
3232

3333
- Multi-layer architecture (Controllers → Services → Repositories)
34-
- In-memory H2 database with JPA/Hibernate
34+
- SQLite database with JPA/Hibernate (H2 for tests)
3535
- Spring Cache abstraction for performance optimization
3636
- Comprehensive test coverage with JUnit 5, Mockito, and AssertJ
3737
- OpenAPI 3.0 documentation with Swagger UI
3838
- Production-ready monitoring with Spring Boot Actuator
39-
- Containerized deployment with Docker
39+
- Containerized deployment with Docker and persistent storage
4040

4141
## Features
4242

@@ -47,7 +47,7 @@ The service showcases:
4747
-**API Documentation** - Interactive Swagger UI powered by SpringDoc OpenAPI
4848
-**Health Monitoring** - Spring Boot Actuator endpoints
4949
-**Test Coverage** - JaCoCo reports with Codecov/Codacy integration
50-
-**Docker Support** - Multi-stage builds with Eclipse Temurin Alpine images
50+
-**Docker Support** - Multi-stage builds with pre-seeded SQLite database
5151
-**CI/CD Ready** - GitHub Actions with automated testing and container builds
5252

5353
## Architecture
@@ -130,6 +130,17 @@ docker compose down
130130
- `9000` - Main API server
131131
- `9001` - Actuator management endpoints
132132

133+
**Persistent Storage:**
134+
135+
The Docker container uses a pre-seeded SQLite database with sample book data. On first run, the database is copied from the image to a named volume (`java-samples-spring-boot_storage`) ensuring data persistence across container restarts.
136+
137+
To reset the database to its initial state:
138+
139+
```bash
140+
docker compose down -v # Remove volumes
141+
docker compose up # Fresh start with seed data
142+
```
143+
133144
## API Reference
134145

135146
The Books API provides standard CRUD operations:
@@ -180,6 +191,7 @@ open target/site/jacoco/index.html
180191
**Test Structure:**
181192

182193
- **Unit Tests** - `@WebMvcTest`, `@DataJpaTest` for isolated layer testing (with `@AutoConfigureCache` for caching support)
194+
- **Test Database** - H2 in-memory database for fast, isolated test execution
183195
- **Mocking** - Mockito with `@MockitoBean` for dependency mocking
184196
- **Assertions** - AssertJ fluent assertions
185197
- **Naming Convention** - BDD style: `given<Condition>_when<Action>_then<Expected>`
@@ -190,6 +202,13 @@ open target/site/jacoco/index.html
190202
- Services: 100%
191203
- Repositories: Custom query methods (interfaces excluded by JaCoCo design)
192204

205+
**SQLite Configuration Notes:**
206+
207+
- Dates are stored as Unix timestamps (INTEGER) for robustness - no date format parsing issues
208+
- A JPA `AttributeConverter` handles LocalDate ↔ epoch seconds conversion transparently (UTC-based)
209+
- Use `ddl-auto=none` since the database is pre-seeded (SQLite has limited ALTER TABLE support)
210+
- Tests use H2 in-memory database - the converter works seamlessly with both databases
211+
193212
## Documentation
194213

195214
- **API Documentation**: Swagger UI at `http://localhost:9000/swagger/index.html`

compose.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ services:
1010
ports:
1111
- "9000:9000"
1212
- "9001:9001"
13+
volumes:
14+
- storage:/storage/
1315
environment:
1416
- SPRING_PROFILES_ACTIVE=production
17+
- STORAGE_PATH=/storage/books-sqlite3.db
1518
restart: unless-stopped
19+
20+
volumes:
21+
storage:
22+
name: java-samples-spring-boot_storage

pom.xml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,40 @@
146146
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
147147
<scope>test</scope>
148148
</dependency>
149+
<!-- SQLite JDBC Driver ============================================ -->
150+
<!--
151+
SQLite JDBC is a library for accessing SQLite databases through
152+
the JDBC API. It provides a lightweight, file-based database that
153+
persists data across application restarts.
154+
https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc
155+
-->
156+
<dependency>
157+
<groupId>org.xerial</groupId>
158+
<artifactId>sqlite-jdbc</artifactId>
159+
<version>3.51.0.0</version>
160+
<scope>runtime</scope>
161+
</dependency>
162+
<!-- Hibernate Community Dialects ==================================== -->
163+
<!--
164+
Provides SQLite dialect for Hibernate ORM. Required for JPA/Hibernate
165+
to correctly generate SQL statements for SQLite databases.
166+
https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-community-dialects
167+
-->
168+
<dependency>
169+
<groupId>org.hibernate.orm</groupId>
170+
<artifactId>hibernate-community-dialects</artifactId>
171+
</dependency>
149172
<!-- H2 Database Engine ============================================ -->
150173
<!--
151-
Provides a fast in-memory database that supports JDBC API and R2DBC
152-
access, with a small (2mb) footprint. Supports embedded and server
153-
modes as well as a browser based console application.
174+
Provides a fast in-memory database for testing. Used only in test
175+
scope for fast unit and integration test execution.
154176
https://mvnrepository.com/artifact/com.h2database/h2
155177
-->
156178
<dependency>
157179
<groupId>com.h2database</groupId>
158180
<artifactId>h2</artifactId>
159181
<version>2.4.240</version>
160-
<scope>runtime</scope>
182+
<scope>test</scope>
161183
</dependency>
162184
<!-- =============================================================== -->
163185
<!-- Operations -->

scripts/entrypoint.sh

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
#!/bin/sh
22
set -e
33

4-
echo "Starting Spring Boot container..."
4+
echo "Executing entrypoint script..."
55

6-
echo "✔ Server Port: ${SERVER_PORT:-9000}"
7-
echo "✔ Management Port: ${MANAGEMENT_PORT:-9001}"
8-
echo "✔ Active Profile(s): ${SPRING_PROFILES_ACTIVE:-default}"
6+
IMAGE_STORAGE_PATH="/app/hold/books-sqlite3.db"
7+
VOLUME_STORAGE_PATH="/storage/books-sqlite3.db"
98

10-
echo "🚀 Launching Spring Boot app..."
9+
echo "✔ Starting container..."
10+
11+
if [ ! -f "$VOLUME_STORAGE_PATH" ]; then
12+
echo "⚠️ No existing database file found in volume."
13+
if [ -f "$IMAGE_STORAGE_PATH" ]; then
14+
echo "Copying database file to writable volume..."
15+
if cp "$IMAGE_STORAGE_PATH" "$VOLUME_STORAGE_PATH"; then
16+
echo "✔ Database initialized at $VOLUME_STORAGE_PATH"
17+
else
18+
echo "❌ Failed to copy database from $IMAGE_STORAGE_PATH to $VOLUME_STORAGE_PATH"
19+
echo " Check file permissions and available disk space."
20+
exit 1
21+
fi
22+
else
23+
echo "⚠️ Database file missing at $IMAGE_STORAGE_PATH"
24+
exit 1
25+
fi
26+
else
27+
echo "✔ Existing database file found. Skipping seed copy."
28+
fi
29+
30+
echo "✔ Ready!"
31+
echo "🚀 Launching app..."
1132
exec "$@"

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/Application.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
package ar.com.nanotaboada.java.samples.spring.boot;
22

3-
import java.util.List;
4-
53
import org.modelmapper.ModelMapper;
6-
import org.springframework.boot.CommandLineRunner;
74
import org.springframework.boot.SpringApplication;
85
import org.springframework.boot.autoconfigure.SpringBootApplication;
96
import org.springframework.cache.annotation.EnableCaching;
107
import org.springframework.context.annotation.Bean;
118

12-
import ar.com.nanotaboada.java.samples.spring.boot.models.Book;
13-
import ar.com.nanotaboada.java.samples.spring.boot.models.BooksDataInitializer;
14-
import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository;
15-
169
/**
1710
* A configuration class that declares one or more Bean methods and also
1811
* triggers auto-configuration and component scanning.
@@ -21,27 +14,11 @@
2114
@EnableCaching
2215
public class Application {
2316

24-
private final BooksRepository repository;
25-
26-
public Application(BooksRepository repository) {
27-
this.repository = repository;
28-
}
29-
3017
@Bean
3118
ModelMapper modelMapper() {
3219
return new ModelMapper();
3320
}
3421

35-
@Bean
36-
CommandLineRunner seed() {
37-
return args -> {
38-
List<Book> books = BooksDataInitializer.seed();
39-
if (books != null) {
40-
repository.saveAll(books);
41-
}
42-
};
43-
}
44-
4522
public static void main(String[] args) {
4623
SpringApplication.run(Application.class, args);
4724
}

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/Book.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22

33
import java.time.LocalDate;
44

5-
import jakarta.persistence.Entity;
6-
import jakarta.persistence.Id;
7-
import jakarta.persistence.Lob;
8-
import jakarta.persistence.Table;
9-
105
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
116
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
127
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
138
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
149

10+
import jakarta.persistence.Column;
11+
import jakarta.persistence.Convert;
12+
import jakarta.persistence.Entity;
13+
import jakarta.persistence.Id;
14+
import jakarta.persistence.Table;
15+
import lombok.AllArgsConstructor;
1516
import lombok.Data;
1617
import lombok.NoArgsConstructor;
17-
import lombok.AllArgsConstructor;
1818

1919
@Entity
2020
@Table(name = "books")
@@ -28,11 +28,21 @@ public class Book {
2828
private String subtitle;
2929
private String author;
3030
private String publisher;
31+
/**
32+
* Stored as Unix timestamp (INTEGER) in SQLite for robustness.
33+
* The converter handles LocalDate ↔ epoch seconds conversion.
34+
*/
3135
@JsonDeserialize(using = LocalDateDeserializer.class)
3236
@JsonSerialize(using = LocalDateSerializer.class)
37+
@Convert(converter = UnixTimestampConverter.class)
3338
private LocalDate published;
3439
private int pages;
35-
@Lob
40+
/**
41+
* Maximum length set to 8192 (8^4 = 2^13) characters.
42+
* This power-of-two value provides ample space for book descriptions
43+
* while remaining compatible with both H2 (VARCHAR) and SQLite (TEXT).
44+
*/
45+
@Column(length = 8192)
3646
private String description;
3747
private String website;
3848
}

0 commit comments

Comments
 (0)