Chapter Introduction

Welcome to Chapter 9! In this chapter, we’re taking a significant leap in building our “Basic To-Do List Application” by introducing data persistence. Up until now, any data we’ve worked with would vanish as soon as our application stopped. That’s not very useful for a To-Do list! Here, we will design the data model for our To-Do items and implement the persistence layer using Java Persistence API (JPA) with Hibernate, backed by Spring Data JPA.

This step is crucial because it transforms our application from a transient utility into a stateful, practical tool where user data can be stored, retrieved, and managed over time. We’ll leverage the power of Object-Relational Mapping (ORM) to map our Java objects directly to a relational database, abstracting away much of the boilerplate SQL.

Before proceeding, ensure you have completed the initial Spring Boot project setup from previous chapters, including a basic project structure and a working pom.xml (or build.gradle). You should have a foundational Spring Boot application running, perhaps with a simple “Hello World” endpoint. By the end of this chapter, you will have a fully functional data layer capable of storing and retrieving To-Do items from a database.

Planning & Design

For our Basic To-Do List application, the core data we need to manage is a “To-Do Item.” Each item will need a unique identifier, a description, and a status indicating whether it’s completed or not. We’ll also add timestamps for creation and last update, which are essential for auditing and ordering.

Component Architecture for this Feature

We’ll be adding two primary components to our existing Spring Boot application:

  1. Entity Class (TodoItem): A plain old Java object (POJO) annotated with JPA metadata to define how it maps to a database table. This is our data model.
  2. Repository Interface (TodoItemRepository): An interface that extends Spring Data JPA’s JpaRepository. This provides out-of-the-box methods for common database operations (CRUD - Create, Read, Update, Delete) without writing any implementation code.

Database Schema

We’ll design a simple table to store our TodoItem entities. We’ll use an in-memory H2 database for development convenience, which will be automatically set up by Spring Boot. Later, for production, this can be easily swapped for a persistent database like PostgreSQL or MySQL.

The todo_item table will look something like this:

Column NameData TypeConstraintsDescription
idBIGINTPRIMARY KEY, AUTO_INCREMENTUnique identifier for the item
descriptionVARCHAR(255)NOT NULLThe task description
completedBOOLEANNOT NULL, DEFAULT FALSEStatus of the task
created_atTIMESTAMPNOT NULLTimestamp when created
updated_atTIMESTAMPNOT NULLTimestamp when last updated

File Structure

We’ll organize our new components within the standard Spring Boot project structure:

src/main/java/com/example/todolist/
├── ToDoListApplication.java
├── model/
│   └── TodoItem.java         <-- Our Entity
└── repository/
    └── TodoItemRepository.java <-- Our Repository Interface

Step-by-Step Implementation

a) Setup/Configuration

First, we need to add the necessary dependencies to our pom.xml (if you’re using Maven) or build.gradle (if you’re using Gradle). We’ll add spring-boot-starter-data-jpa for JPA/Hibernate integration and h2 database for an in-memory development database.

1. Add Dependencies to pom.xml

Open your pom.xml file located in the root of your project and add the following <dependency> blocks within the <dependencies> section.

<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version> <!-- Use the latest stable Spring Boot version for Java 25 -->
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>todo-list</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>todo-list</name>
    <description>Basic To-Do List Application</description>

    <properties>
        <java.version>25</java.version> <!-- Targeting Java 25 as per 2025-12-04 -->
    </properties>

    <dependencies>
        <!-- Spring Boot Web Starter (assuming you have this from previous chapters) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Data JPA for database persistence -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- H2 Database for in-memory development database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Spring Boot Test Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Explanation:

  • spring-boot-starter-data-jpa: This starter brings in Hibernate as the default JPA provider, along with Spring Data JPA, which simplifies repository creation.
  • h2: This is an in-memory relational database. It’s excellent for development and testing because it’s fast, lightweight, and doesn’t require separate installation. The database is created when the application starts and destroyed when it stops. We specify <scope>runtime</scope> because it’s only needed at runtime, not for compilation.
  • java.version: Set to 25 to target the latest stable Java version as of December 2025.

2. Configure application.properties

Create or open the src/main/resources/application.properties file and add the following configuration. This tells Spring Boot how to connect to our H2 database and configures Hibernate’s behavior.

# src/main/resources/application.properties
# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:todolistdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA/Hibernate Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# H2 Console (for viewing database content during development)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Explanation:

  • spring.datasource.url: Specifies the H2 in-memory database named todolistdb. DB_CLOSE_DELAY=-1 prevents the database from closing as long as connections are open, and DB_CLOSE_ON_EXIT=FALSE keeps it alive until the JVM exits, which is good for development.
  • spring.datasource.username and spring.datasource.password: Default credentials for H2.
  • spring.jpa.database-platform: Tells Hibernate to use the H2 dialect for generating SQL.
  • spring.jpa.hibernate.ddl-auto=update: CRITICAL FOR DEVELOPMENT. This setting tells Hibernate to automatically update the database schema based on our JPA entities. For production, you would typically set this to validate or none and use a dedicated migration tool like Flyway or Liquibase for schema management.
  • spring.jpa.show-sql=true and spring.jpa.properties.hibernate.format_sql=true: These are excellent for development and debugging. They instruct Hibernate to log all generated SQL queries to the console, formatted for readability. Disable this in production to avoid sensitive data in logs and reduce log verbosity.
  • spring.h2.console.enabled=true and spring.h2.console.path=/h2-console: Enables the H2 web console, allowing you to browse the database schema and data via your web browser at http://localhost:8080/h2-console.

b) Core Implementation

Now, let’s create our TodoItem entity and TodoItemRepository interface.

1. Create the TodoItem Entity

Create a new package model inside your main application package (e.g., com.example.todolist.model) and create the TodoItem.java file.

// src/main/java/com/example/todolist/model/TodoItem.java
package com.example.todolist.model;

import jakarta.persistence.*; // Use jakarta.persistence for JPA 3.x+ (Spring Boot 3.x+)
import java.time.LocalDateTime;
import java.util.Objects;

/**
 * Represents a single To-Do item in the application.
 * This entity will be mapped to a 'todo_item' table in the database.
 */
@Entity // Marks this class as a JPA entity
@Table(name = "todo_items") // Specifies the table name in the database
public class TodoItem {

    @Id // Marks 'id' as the primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Configures ID generation strategy
    private Long id;

    @Column(nullable = false) // Ensures the 'description' column cannot be null
    private String description;

    @Column(nullable = false) // Ensures the 'completed' column cannot be null
    private boolean completed;

    @Column(name = "created_at", nullable = false, updatable = false) // Store creation timestamp, not updatable
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false) // Store last update timestamp
    private LocalDateTime updatedAt;

    // --- Constructors ---
    public TodoItem() {
        // Default constructor required by JPA
    }

    public TodoItem(String description) {
        this.description = description;
        this.completed = false; // Default to not completed
    }

    // --- Lifecycle Callbacks for Auditing ---
    @PrePersist // Method to be called before an entity is first persisted
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate // Method to be called before an entity is updated
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    // --- Getters and Setters ---
    public Long getId() {
        return id;
    }

    // Setter for ID is often omitted or made package-private as ID is usually managed by JPA
    // public void setId(Long id) { this.id = id; }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        // Basic validation: description should not be null or empty
        if (description == null || description.trim().isEmpty()) {
            throw new IllegalArgumentException("Description cannot be null or empty.");
        }
        this.description = description;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    // createdAt should not be set manually after creation, managed by @PrePersist
    // private void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    // updatedAt should not be set manually, managed by @PreUpdate
    // private void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }

    // --- hashCode and equals for proper collection behavior ---
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TodoItem todoItem = (TodoItem) o;
        return Objects.equals(id, todoItem.id); // Equality based on ID for entities
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // Hash code based on ID
    }

    // --- toString for logging and debugging ---
    @Override
    public String toString() {
        return "TodoItem{" +
               "id=" + id +
               ", description='" + description + '\'' +
               ", completed=" + completed +
               ", createdAt=" + createdAt +
               ", updatedAt=" + updatedAt +
               '}';
    }
}

Explanation of TodoItem.java:

  • @Entity: Marks TodoItem as a JPA entity. Hibernate will manage instances of this class and map them to database records.
  • @Table(name = "todo_items"): Explicitly names the database table. It’s good practice to specify this, especially if your class name doesn’t match the desired table name.
  • @Id: Designates the id field as the primary key.
  • @GeneratedValue(strategy = GenerationType.IDENTITY): Configures the primary key to be generated by the database using an auto-incrementing column. This is a common and efficient strategy for many databases.
  • @Column(nullable = false): Ensures the description and completed fields cannot be null in the database.
  • @Column(name = "created_at", nullable = false, updatable = false): Defines the column name for createdAt and makes it non-nullable. updatable = false ensures that once set, this timestamp cannot be changed by subsequent updates.
  • @PrePersist and @PreUpdate: These are JPA lifecycle callback annotations. onCreate() is called before a new TodoItem is saved, setting both createdAt and updatedAt. onUpdate() is called before an existing TodoItem is updated, refreshing only updatedAt. This is a robust way to manage auditing timestamps.
  • java.time.LocalDateTime: Modern Java date/time API for handling timestamps.
  • equals() and hashCode(): Crucial for entities. For JPA entities, it’s generally best to base equals and hashCode on the primary key (id) once it’s assigned. This ensures correct behavior in collections (like Set) and when comparing objects.
  • toString(): Useful for debugging and logging.
  • Production-Ready Considerations:
    • Basic validation in setDescription to prevent empty descriptions. More complex validation (e.g., using Spring’s @Valid and JSR 380 Bean Validation annotations) would typically reside in a DTO or service layer, but a basic check here is a good start for entity integrity.
    • Using jakarta.persistence namespace, which is part of Jakarta EE 9+ and used by Spring Boot 3.x+.

2. Create the TodoItemRepository Interface

Create a new package repository inside your main application package (e.g., com.example.todolist.repository) and create the TodoItemRepository.java file.

// src/main/java/com/example/todolist/repository/TodoItemRepository.java
package com.example.todolist.repository;

import com.example.todolist.model.TodoItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Spring Data JPA repository for TodoItem entities.
 * Provides standard CRUD operations and custom query methods.
 */
@Repository // Marks this interface as a Spring Data repository component
public interface TodoItemRepository extends JpaRepository<TodoItem, Long> {

    /**
     * Finds all TodoItem entities that match the given completion status.
     * Spring Data JPA automatically generates the query based on the method name.
     *
     * @param completed The completion status to search for (true for completed, false for not completed).
     * @return A list of TodoItem entities matching the status.
     */
    List<TodoItem> findByCompleted(boolean completed);

    /**
     * Finds all TodoItem entities whose description contains the given keyword (case-insensitive).
     *
     * @param keyword The keyword to search for in the description.
     * @return A list of TodoItem entities matching the keyword.
     */
    List<TodoItem> findByDescriptionContainingIgnoreCase(String keyword);

    // Spring Data JPA automatically provides:
    // - save(TodoItem entity): Saves a new entity or updates an existing one.
    // - findById(ID id): Retrieves an entity by its ID.
    // - findAll(): Retrieves all entities.
    // - deleteById(ID id): Deletes an entity by its ID.
    // - etc.
}

Explanation of TodoItemRepository.java:

  • @Repository: A specialization of @Component that indicates that an annotated class is a “Repository” (or Data Access Object) and provides a hint to the Spring container for autodetecting them.
  • extends JpaRepository<TodoItem, Long>: This is the magic of Spring Data JPA. By extending JpaRepository, our TodoItemRepository automatically inherits a rich set of CRUD operations (like save, findById, findAll, deleteById, etc.) for TodoItem entities, where Long is the type of the primary key. We don’t need to write any implementation code!
  • findByCompleted(boolean completed): This is an example of a derived query method. Spring Data JPA parses the method name and automatically generates the corresponding SQL query. findByCompleted translates to SELECT * FROM todo_items WHERE completed = ?.
  • findByDescriptionContainingIgnoreCase(String keyword): Another derived query method, translating to SELECT * FROM todo_items WHERE LOWER(description) LIKE LOWER('%?%').
  • Production-Ready Considerations: Spring Data JPA significantly reduces boilerplate code, leading to cleaner, more maintainable data access layers. Using derived queries is efficient for simple lookups. For complex queries, you would use @Query annotations or Specification API.

c) Testing This Component

To verify our data model and persistence layer, we’ll perform a quick integration test using CommandLineRunner. This allows us to execute some code immediately after the Spring Boot application starts up, demonstrating basic CRUD operations.

1. Update ToDoListApplication.java

Modify your main application class (e.g., src/main/java/com/example/todolist/ToDoListApplication.java) to implement CommandLineRunner and inject TodoItemRepository.

// src/main/java/com/example/todolist/ToDoListApplication.java
package com.example.todolist;

import com.example.todolist.model.TodoItem;
import com.example.todolist.repository.TodoItemRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.List;
import java.util.Optional;

@SpringBootApplication
public class ToDoListApplication {

    private static final Logger logger = LoggerFactory.getLogger(ToDoListApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(ToDoListApplication.class, args);
    }

    /**
     * CommandLineRunner bean to execute code after application startup.
     * Used here for initial data population and testing the repository.
     *
     * @param todoItemRepository The repository for TodoItem entities.
     * @return A CommandLineRunner instance.
     */
    @Bean
    public CommandLineRunner run(TodoItemRepository todoItemRepository) {
        return args -> {
            logger.info("Application started. Performing basic repository tests...");

            // 1. Create and Save new TodoItems
            TodoItem item1 = new TodoItem("Learn Java 25 features");
            TodoItem item2 = new TodoItem("Build To-Do List application");
            TodoItem item3 = new TodoItem("Write unit tests for persistence layer");
            item3.setCompleted(true); // Mark as completed

            logger.info("Saving TodoItem: {}", item1.getDescription());
            todoItemRepository.save(item1);
            logger.info("Saved TodoItem: {}", item1);

            logger.info("Saving TodoItem: {}", item2.getDescription());
            todoItemRepository.save(item2);
            logger.info("Saved TodoItem: {}", item2);

            logger.info("Saving TodoItem: {}", item3.getDescription());
            todoItemRepository.save(item3);
            logger.info("Saved TodoItem: {}", item3);

            // 2. Retrieve all TodoItems
            logger.info("Retrieving all TodoItems...");
            List<TodoItem> allItems = todoItemRepository.findAll();
            allItems.forEach(item -> logger.info("Found: {}", item));

            // 3. Find TodoItems by completion status
            logger.info("Retrieving completed TodoItems...");
            List<TodoItem> completedItems = todoItemRepository.findByCompleted(true);
            completedItems.forEach(item -> logger.info("Completed: {}", item));

            logger.info("Retrieving incomplete TodoItems...");
            List<TodoItem> incompleteItems = todoItemRepository.findByCompleted(false);
            incompleteItems.forEach(item -> logger.info("Incomplete: {}", item));

            // 4. Update a TodoItem
            if (item1.getId() != null) {
                Optional<TodoItem> foundItem = todoItemRepository.findById(item1.getId());
                foundItem.ifPresent(item -> {
                    logger.info("Updating TodoItem: {}", item.getDescription());
                    item.setDescription("Master Java 25 features and concurrency");
                    item.setCompleted(true);
                    todoItemRepository.save(item); // Save updates
                    logger.info("Updated TodoItem: {}", item);
                });
            }

            // 5. Find by description containing keyword
            logger.info("Searching for 'Java' in descriptions...");
            List<TodoItem> javaTasks = todoItemRepository.findByDescriptionContainingIgnoreCase("Java");
            javaTasks.forEach(item -> logger.info("Found 'Java' task: {}", item));

            // 6. Delete a TodoItem (optional, for demonstration)
            // logger.info("Deleting TodoItem: {}", item2.getDescription());
            // todoItemRepository.deleteById(item2.getId());
            // logger.info("Remaining items after deletion:");
            // todoItemRepository.findAll().forEach(item -> logger.info("Found: {}", item));

            logger.info("Repository tests finished.");
        };
    }
}

Explanation of ToDoListApplication.java changes:

  • @SpringBootApplication: This annotation is a convenience annotation that adds @Configuration, @EnableAutoConfiguration, and @ComponentScan.
  • CommandLineRunner: This functional interface is executed once the application context is loaded. It’s perfect for running initialization code or simple tests like these.
  • @Bean: Marks the run method as a Spring bean producer. Spring will automatically inject the TodoItemRepository instance.
  • logger: We use slf4j and logback (default with Spring Boot) for robust logging. logger.info() messages provide clear output on what’s happening.
  • Incremental Testing: This CommandLineRunner demonstrates:
    • Saving new entities.
    • Retrieving all entities.
    • Using a custom derived query (findByCompleted).
    • Updating an existing entity.
    • Using another custom derived query (findByDescriptionContainingIgnoreCase).
  • Debugging Tips: Pay close attention to the console output. You should see Hibernate generating SQL statements (INSERT, SELECT, UPDATE) and our logger.info messages showing the state of the TodoItem objects. If you see errors, check your application.properties and entity annotations carefully.

Production Considerations

When moving from development to production, several aspects of our data model and persistence layer need careful attention.

  1. Error Handling:

    • Database Connection Issues: In a production environment, the database might be unavailable. Spring Data JPA operations will throw DataAccessException (a runtime exception) or more specific subclasses if the database connection fails or queries encounter issues. These exceptions should be caught and handled gracefully in your service layer or REST controllers, perhaps returning appropriate HTTP status codes (e.g., 500 Internal Server Error) and user-friendly error messages, rather than exposing raw stack traces.
    • Transaction Management: Spring Boot automatically configures transaction management for JPA. Operations within a service method annotated with @Transactional are executed as a single atomic unit. If any part fails, the entire transaction is rolled back, ensuring data consistency. This is critical for production.
  2. Performance Optimization:

    • ddl-auto: As mentioned, spring.jpa.hibernate.ddl-auto=update is for development. In production, set it to validate (to check if schema matches entities) or none, and use schema migration tools like Flyway or Liquibase to manage database schema changes in a controlled, versioned manner.
    • N+1 Problem: Be aware of the N+1 select problem, which can occur with lazy-loaded relationships. When fetching a list of parent entities, then iterating through them and accessing a lazy-loaded child collection for each, it results in N additional queries. Strategies like JOIN FETCH in JPQL or using @EntityGraph can optimize this. (Not applicable yet for TodoItem as it has no relationships, but important for future entities).
    • Indexing: Ensure appropriate database indexes are created on frequently queried columns (e.g., completed, description if searched often) to speed up read operations. JPA annotations like @Index or explicit DDL scripts can be used.
    • Connection Pooling: Spring Boot defaults to HikariCP, a high-performance JDBC connection pool. Ensure its configuration (max pool size, connection timeout, etc.) is tuned for your production environment and expected load.
  3. Security Considerations:

    • SQL Injection: By using JPA and Hibernate, we inherently gain protection against SQL injection attacks because queries are constructed using prepared statements, where parameters are properly escaped. Avoid concatenating user input directly into JPQL or native SQL queries.
    • Data Validation: While we added basic validation in the entity, robust input validation should occur at the API entry points and service layer to prevent invalid or malicious data from reaching the persistence layer.
    • Sensitive Data: If your application handles sensitive data, ensure it’s encrypted both at rest (database encryption) and in transit (TLS/SSL for database connections).
  4. Logging and Monitoring:

    • Log Levels: In production, set spring.jpa.show-sql=false and adjust Hibernate logging to a less verbose level (e.g., WARN or ERROR) to prevent excessive logging from impacting performance and disk space. You might enable specific Hibernate categories (e.g., org.hibernate.SQL at DEBUG) only when troubleshooting.
    • Monitoring: Integrate with monitoring tools (e.g., Prometheus, Grafana, Micrometer) to track database connection pool metrics, query execution times, and overall application health. This helps identify performance bottlenecks and issues proactively.

Code Review Checkpoint

At this point, you’ve successfully laid the foundation for data persistence in our To-Do List application.

What we’ve built:

  • TodoItem Entity: A robust Java class representing our To-Do item, complete with JPA annotations for mapping to a database table, ID generation, non-nullable constraints, and automatic timestamp management for creation and updates.
  • TodoItemRepository Interface: A Spring Data JPA repository that provides powerful CRUD operations and custom query methods with minimal code, leveraging convention over configuration.
  • Database Configuration: Set up an in-memory H2 database for development and configured Hibernate to automatically manage the schema and log SQL queries.
  • Initial Data Population & Testing: Used CommandLineRunner to demonstrate saving, retrieving, and updating TodoItem instances, verifying the persistence layer works as expected.

Files created/modified:

  • pom.xml (or build.gradle): Added spring-boot-starter-data-jpa and h2 dependencies.
  • src/main/resources/application.properties: Added H2 and Hibernate configuration.
  • src/main/java/com/example/todolist/model/TodoItem.java: Created the JPA entity.
  • src/main/java/com/example/todolist/repository/TodoItemRepository.java: Created the Spring Data JPA repository.
  • src/main/java/com/example/todolist/ToDoListApplication.java: Modified to include CommandLineRunner for testing.

This persistence layer is now ready to be integrated with higher-level service and API layers, which will be the focus of upcoming chapters.

Common Issues & Solutions

  1. Issue: jakarta.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory or Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "TODO_ITEMS" not found

    • Reason: This usually means Hibernate couldn’t create the table or connect to the database. Common culprits are incorrect application.properties settings, missing H2 dependency, or ddl-auto not set to update (or create-drop).
    • Debugging:
      • Check pom.xml to ensure spring-boot-starter-data-jpa and h2 dependencies are present and correctly scoped.
      • Verify application.properties: Double-check spring.datasource.url, username, password, and especially spring.jpa.hibernate.ddl-auto=update.
      • Look at the application startup logs: Hibernate usually prints detailed messages about table creation. If you see No such file or directory or Connection refused, it’s a datasource URL issue.
    • Prevention: Always start with a minimal application.properties and add configurations incrementally. Ensure all dependencies are correctly declared.
  2. Issue: IllegalArgumentException: Description cannot be null or empty. from TodoItem.setDescription()

    • Reason: This is an error from our custom validation logic within the TodoItem entity’s setDescription method. It means you tried to create or update a TodoItem with a null or empty description.
    • Debugging:
      • Review the CommandLineRunner code or any other code that creates/updates TodoItem instances. Ensure the description field is always provided with a valid, non-empty string.
    • Prevention: Implement robust validation at the service or API layer to catch invalid input before it reaches the entity, providing cleaner error messages to the user.
  3. Issue: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'todoItemRepository': ... or No qualifying bean of type 'com.example.todolist.repository.TodoItemRepository' available

    • Reason: Spring couldn’t find or create an instance of your TodoItemRepository. This often happens if the repository interface isn’t in a package scanned by Spring Boot, or if the main application class doesn’t have @SpringBootApplication (which includes @ComponentScan).
    • Debugging:
      • Ensure TodoItemRepository.java is in a sub-package of your main application’s package (e.g., com.example.todolist.repository if ToDoListApplication is in com.example.todolist).
      • Verify that your ToDoListApplication.java has the @SpringBootApplication annotation.
      • Check for typos in package names or class names.
    • Prevention: Stick to the standard Spring Boot project structure. Keep entities and repositories in clearly defined sub-packages of your main application package.

Testing & Verification

To test and verify the work done in this chapter:

  1. Start the Application: Run your ToDoListApplication.java (e.g., from your IDE or using mvn spring-boot:run).
  2. Observe Console Output:
    • Look for Hibernate’s SQL statements (e.g., create table todo_items..., insert into todo_items...). This confirms schema generation and data insertion.
    • Check for the logger.info messages from your CommandLineRunner, which should detail the saving, retrieval, and updating of TodoItems.
  3. Access H2 Console:
    • Open your web browser and navigate to http://localhost:8080/h2-console.
    • The JDBC URL should pre-fill with jdbc:h2:mem:todolistdb. Use username sa and leave password blank. Click “Connect.”
    • You should see a TODO_ITEMS table in the schema browser on the left.
    • Execute a query like SELECT * FROM TODO_ITEMS; to view the data you inserted via CommandLineRunner. You should see the items, including the updated “Master Java 25 features…” and the createdAt/updatedAt timestamps.
  4. Verify Data Integrity: Check that:
    • All TodoItems inserted by CommandLineRunner are present.
    • The completed status is correct (e.g., item3 and the updated item1 should be TRUE).
    • createdAt and updatedAt timestamps are automatically populated and updated as expected.

If all these checks pass, congratulations! Your data model and persistence layer are fully functional.

Summary & Next Steps

In this chapter, we successfully designed and implemented the core data model and persistence layer for our Basic To-Do List Application. We introduced the TodoItem entity, configured JPA/Hibernate with Spring Data JPA, and set up an H2 in-memory database for development. We also performed initial testing using CommandLineRunner to confirm that our entities can be correctly stored, retrieved, and updated.

This is a fundamental step, as it provides the crucial ability for our application to remember state and manage user data. It also introduces you to industry-standard practices for data persistence in Java applications using Spring Boot.

In the next chapter, Chapter 10: Building the Service Layer & Business Logic, we will build upon this persistence layer by creating a service layer. This layer will encapsulate the business logic for managing To-Do items, acting as an intermediary between our data access layer and the future API layer. It’s where we’ll implement operations like adding, deleting, marking tasks as complete, and applying any specific business rules.