WL
Java Full Stack Developer
Wassim Lagnaoui

Lesson 13: Spring Boot Data Access with JPA and Hibernate part 1

Master JPA and Hibernate fundamentals: entity mapping, repositories, CRUD operations, and seamless database integration in Spring Boot applications.

Introduction

Real applications need to store and retrieve data permanently, not just keep it in memory where it disappears when the program stops. Spring Boot makes database integration incredibly simple through JPA (Java Persistence API) and Hibernate, which automatically translate your Java objects into database tables and vice versa. Instead of writing complex SQL statements and managing database connections manually, you can work with regular Java objects and let JPA handle all the database details behind the scenes. JPA provides a standard way to map Java classes to database tables, while Hibernate is the powerful implementation that makes it all work seamlessly. With Spring Data JPA, you can create repositories that provide ready-made methods for common database operations like saving, finding, and deleting records. This lesson introduces you to the fundamental concepts of entity mapping, repository creation, and basic CRUD operations that form the foundation of data persistence in Spring Boot applications.


Understanding JPA

Definition

JPA (Java Persistence API) is a specification that defines how Java objects should be mapped to relational databases. It provides a standard set of annotations and interfaces that allow you to work with databases using object-oriented programming instead of writing SQL queries. JPA handles the translation between your Java objects and database tables automatically, managing the conversion of data types, relationships between objects, and the generation of appropriate SQL statements for different database operations.

Analogy

Think of JPA like a universal translator at an international conference. You have speakers who only know Java (your objects) and an audience that only understands SQL (the database). The translator (JPA) sits between them, automatically converting everything the Java speaker says into perfect SQL that the database understands, and then translating the database's responses back into Java objects that your application can work with. Just like how the translator handles all the complexity of different languages and cultural nuances, JPA handles all the complexity of different database systems, SQL dialects, and data type conversions. You focus on your business logic in Java, and JPA takes care of making sure everything gets stored and retrieved correctly from the database.

Examples

Adding JPA dependency to Spring Boot:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Simple entity annotation:

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

JPA automatically creates table:

CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255)
);

Working with objects, not SQL:

User user = new User("Alice");
userRepository.save(user);  // JPA generates INSERT statement
User found = userRepository.findById(1L);  // JPA generates SELECT

Hibernate Basics

Definition

Hibernate is the most popular implementation of the JPA specification, providing the actual functionality that makes object-relational mapping work. It acts as the bridge between your Java application and the database, handling connection management, SQL generation, caching, and transaction management. Spring Boot includes Hibernate by default when you add the JPA starter, and it automatically configures Hibernate with sensible defaults while allowing you to customize settings as needed.

Analogy

If JPA is like the blueprint for building a universal translator, then Hibernate is the actual skilled translator who does the work. Imagine you have architectural plans (JPA specification) for building a bridge, but you need an experienced construction company (Hibernate) to actually build it. The construction company knows how to read the blueprints, has all the specialized equipment and expertise, handles all the permits and regulations, and deals with different types of terrain and weather conditions. Similarly, Hibernate knows how to implement the JPA specifications, has all the tools for database communication, handles different database vendors and their quirks, and optimizes performance through features like caching and lazy loading. You give Hibernate the JPA "blueprints" (your annotated entities), and it builds the entire data access infrastructure.

Examples

Hibernate auto-configuration in Spring Boot:

// No configuration needed - Hibernate is automatically configured
// when spring-boot-starter-data-jpa is added

Hibernate properties configuration:

# application.properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

Hibernate generates SQL automatically:

User user = userRepository.findByEmail("alice@example.com");
// Hibernate generates: SELECT * FROM user WHERE email = 'alice@example.com'

Transaction management:

@Transactional
public void updateUserProfile(Long id, String newName) {
    User user = userRepository.findById(id);
    user.setName(newName);  // Hibernate automatically saves changes
}

Entity Mapping

Definition

Entity mapping is the process of defining how Java classes correspond to database tables using JPA annotations. The @Entity annotation marks a class as a database entity, @Table specifies table details, @Column configures column properties, and various other annotations control how fields are mapped to database columns. Proper entity mapping ensures that your objects are stored correctly and efficiently in the database while maintaining data integrity and supporting the relationships between different entities.

Analogy

Entity mapping is like creating a detailed floor plan for converting an office building into apartments. You need to specify which rooms (Java fields) become which apartments (database columns), how big each apartment should be (column size), which features are required (not null constraints), and how the apartments connect to shared facilities like hallways and elevators (relationships). The @Entity annotation is like putting up a sign saying "This building contains apartments," while @Column annotations are like the detailed specifications for each apartment: "This room becomes a 2-bedroom unit with hardwood floors and cannot be left empty." Just as the floor plan ensures the conversion happens correctly and all apartments meet building codes, entity mapping ensures your objects are stored properly and meet database constraints.

Examples

Basic entity with column customization:

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "full_name", nullable = false, length = 100)
    private String name;
}

Entity with various field types:

@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;

    @Column(unique = true)
    private String sku;

    @Column(precision = 10, scale = 2)
    private BigDecimal price;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;
}

Enum mapping:

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;  // Stores as text: "PENDING", "COMPLETED"
}

Embedded objects:

@Embeddable
public class Address {
    private String street;
    private String city;
    private String zipCode;
}

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address address;  // Flattened into customer table
}

Primary Keys

Definition

Primary keys uniquely identify each record in a database table and are essential for JPA entities. The @Id annotation marks a field as the primary key, while @GeneratedValue specifies how the key should be automatically generated. Different generation strategies include AUTO (database decides), IDENTITY (auto-increment), SEQUENCE (database sequence), and TABLE (separate table for key generation). Choosing the right primary key strategy depends on your database type, performance requirements, and whether you need the key value immediately after saving.

Analogy

Primary keys are like unique identification numbers that every citizen gets from the government. Just as no two people can have the same social security number, no two database records can have the same primary key. The government (database) has different systems for assigning these numbers: some countries use sequential numbering (IDENTITY strategy), others use complex algorithms that include regional codes (SEQUENCE strategy), and some maintain central registries (TABLE strategy). The key characteristic is that once assigned, these numbers never change and always uniquely identify that specific person, just like how primary keys permanently and uniquely identify database records. When you're born (entity is created), you automatically get assigned a unique number that follows you throughout your entire life in the system.

Examples

Auto-increment primary key (most common):

@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // Database auto-increments: 1, 2, 3, 4...
}

UUID primary key:

@Entity
public class Session {
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    private String id;  // Generates: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Custom primary key:

@Entity
public class Product {
    @Id
    private String sku;  // Manually assigned: "LAPTOP-001", "MOUSE-002"

    // Constructor and setters needed for manual assignment
}

Sequence-based key:

@Entity
@SequenceGenerator(name = "user_seq", sequenceName = "user_sequence")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    private Long id;
}

Database Configuration

Definition

Spring Boot automatically configures database connections and JPA settings based on dependencies and properties you provide. You can use embedded databases like H2 for development and testing, or configure external databases like MySQL or PostgreSQL for production. Configuration involves setting up connection URLs, credentials, connection pooling, and JPA-specific settings like DDL generation and SQL logging. Spring Boot's auto-configuration means most settings work out of the box, but you can customize them through application properties.

Analogy

Database configuration is like setting up utilities for a new apartment. You need to specify which utility companies to use (database type), provide account information (connection credentials), set up service addresses (database URLs), and configure how much service you need (connection pool size). For a temporary setup, you might use portable generators and bottled water (H2 embedded database), but for permanent living, you connect to the city's power grid and water system (external databases like MySQL). Spring Boot is like a helpful building manager who already knows all the utility companies and can set up standard service automatically - you just need to tell them your apartment number (database URL) and account details (username/password), and they handle all the complex paperwork and connections.

Examples

H2 embedded database (development):

# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.h2.console.enabled=true

MySQL configuration:

spring.datasource.url=jdbc:mysql://localhost:3306/myapp
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

JPA configuration options:

spring.jpa.hibernate.ddl-auto=update  # create, update, validate, none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

Connection pool configuration:

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000

Spring Data Repositories

Definition

Spring Data repositories provide a high-level abstraction for data access, offering pre-built methods for common database operations without requiring you to write implementation code. By extending interfaces like JpaRepository or CrudRepository, you automatically get methods for saving, finding, updating, and deleting entities. Spring Data generates the actual implementation at runtime, handling all the complexity of SQL generation, transaction management, and result mapping. You can also define custom query methods using method naming conventions or custom queries.

Analogy

Spring Data repositories are like having a personal assistant who already knows how to handle all your common office tasks. When you hire this assistant (create a repository interface), they come with a predefined set of skills: filing documents (save), finding files (findById), organizing folders (findAll), updating records (save for updates), and disposing of old documents (delete). You don't need to train them on these basic tasks - they already know how to do them efficiently and correctly. If you need something special, you can give them specific instructions (custom query methods), and they'll figure out how to accomplish it. The assistant handles all the mundane details of where things are stored, how to organize them, and what procedures to follow, letting you focus on your actual work instead of data management logistics.

Examples

Basic repository interface:

public interface UserRepository extends JpaRepository<User, Long> {
    // Automatically provides: save, findById, findAll, delete, etc.
}

Using repository in service:

@Service
public class UserService {
    private final UserRepository userRepository;

    public User createUser(String name) {
        User user = new User(name);
        return userRepository.save(user);  // Automatically generates INSERT
    }
}

Repository methods available:

// All provided automatically by JpaRepository
userRepository.save(user);           // Insert or update
userRepository.findById(1L);         // Find by primary key
userRepository.findAll();            // Get all records
userRepository.deleteById(1L);       // Delete by primary key
userRepository.count();              // Count total records

Repository with custom methods:

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);       // Custom finder method
    boolean existsByEmail(String email);     // Check if exists
    void deleteByName(String name);          // Custom delete method
}

CRUD Operations

Definition

CRUD operations (Create, Read, Update, Delete) are the fundamental database operations that every data-driven application needs. Spring Data JPA makes these operations incredibly simple by providing ready-made methods that handle all the SQL generation and transaction management automatically. Create operations use the save() method, Read operations use findById() and findAll(), Update operations also use save() with existing entities, and Delete operations use delete() or deleteById(). These operations form the foundation of data persistence in your application.

Analogy

CRUD operations are like the basic actions you can perform with a filing cabinet in an office. Create is like adding a new file folder with documents - you organize the information and put it in the right place in the cabinet. Read is like looking up and retrieving files when you need to reference information. Update is like taking out an existing file, making changes to the documents inside, and putting it back in the same location. Delete is like removing a file folder completely when you no longer need it. Just as these filing operations keep your office organized and functional, CRUD operations keep your database organized and your application functional. The filing cabinet (database) stays consistent, you can always find what you're looking for, and you can modify information as your business needs change.

Examples

Create - Adding new records:

User newUser = new User("Alice", "alice@example.com");
User savedUser = userRepository.save(newUser);  // INSERT into database
System.out.println("Created user with ID: " + savedUser.getId());

Read - Finding records:

Optional<User> user = userRepository.findById(1L);  // SELECT by ID
if (user.isPresent()) {
    System.out.println("Found: " + user.get().getName());
}

List<User> allUsers = userRepository.findAll();  // SELECT all records

Update - Modifying existing records:

User existingUser = userRepository.findById(1L).orElse(null);
if (existingUser != null) {
    existingUser.setName("Alice Smith");  // Modify the object
    userRepository.save(existingUser);    // UPDATE in database
}

Delete - Removing records:

userRepository.deleteById(1L);  // DELETE by ID

User userToDelete = userRepository.findById(2L).orElse(null);
if (userToDelete != null) {
    userRepository.delete(userToDelete);  // DELETE by entity
}

Query Methods

Definition

Query methods in Spring Data JPA allow you to create custom database queries by simply defining method names that follow specific naming conventions. Spring Data automatically generates the appropriate SQL based on the method name, entity properties, and parameters. You can find records by any field using patterns like findBy, filter with conditions like And/Or, sort results with OrderBy, and limit results with keywords like First or Top. This approach eliminates the need to write SQL queries for most common search scenarios.

Analogy

Query methods are like having a librarian who understands natural language requests for finding books. Instead of having to learn the complex library catalog system, you can ask in plain English: "Find books by author Shakespeare," "Find books written after 2000 and about programming," or "Find the first 5 books ordered by publication date." The librarian (Spring Data) automatically translates your natural language request into the specific catalog search procedures needed to find exactly what you want. The librarian knows the library's organization system intimately and can efficiently locate items based on any combination of criteria you specify, whether it's author, subject, publication date, or any other attribute in the catalog system.

Examples

Simple property queries:

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);           // WHERE name = ?
    List<User> findByEmail(String email);         // WHERE email = ?
    List<User> findByAgeGreaterThan(int age);     // WHERE age > ?
}

Complex queries with multiple conditions:

List<User> findByNameAndEmail(String name, String email);  // WHERE name = ? AND email = ?
List<User> findByAgeGreaterThanOrNameContaining(int age, String namePattern);
List<User> findByCreatedDateBetween(LocalDate start, LocalDate end);

Sorting and limiting results:

List<User> findByAgeGreaterThanOrderByNameAsc(int age);  // ORDER BY name ASC
List<User> findFirst5ByOrderByCreatedDateDesc();         // LIMIT 5, newest first
Optional<User> findTopByEmailContaining(String domain);  // LIMIT 1

Existence and counting queries:

boolean existsByEmail(String email);        // Check if user exists
long countByAgeGreaterThan(int age);        // Count matching users
void deleteByName(String name);             // Delete matching users

Testing Repositories

Definition

Testing repositories ensures your data access layer works correctly with real database operations. Spring Boot provides @DataJpaTest annotation that sets up an in-memory database, configures JPA, and provides TestEntityManager for test data setup. Repository tests verify that your custom query methods return correct results, entities are saved and retrieved properly, and relationships between entities work as expected. Testing with a real database context catches issues that unit tests with mocks might miss.

Analogy

Testing repositories is like conducting safety drills for a bank vault system. You create a complete replica of the vault (in-memory test database) with all the same security mechanisms and procedures, then practice different scenarios: storing valuable items (saving entities), retrieving specific items (query methods), updating inventory records (entity updates), and removing items (deletions). You test both normal operations and edge cases, like what happens when you try to store duplicate items or retrieve something that doesn't exist. These drills ensure that when the real vault is operational, all procedures work flawlessly and valuable items (your application data) remain safe and accessible. The test environment is isolated from the real vault, so you can safely test destructive operations without risking actual assets.

Examples

Basic repository test setup:

@DataJpaTest
class UserRepositoryTest {
    @Autowired TestEntityManager entityManager;
    @Autowired UserRepository userRepository;
}

Testing save and find operations:

@Test
void shouldSaveAndFindUser() {
    User user = new User("Alice", "alice@example.com");
    User saved = userRepository.save(user);

    Optional<User> found = userRepository.findById(saved.getId());
    assertThat(found).isPresent();
    assertThat(found.get().getName()).isEqualTo("Alice");
}

Testing custom query methods:

@Test
void shouldFindUsersByName() {
    entityManager.persistAndFlush(new User("Alice", "alice@test.com"));
    entityManager.persistAndFlush(new User("Bob", "bob@test.com"));

    List<User> users = userRepository.findByName("Alice");
    assertThat(users).hasSize(1);
    assertThat(users.get(0).getName()).isEqualTo("Alice");
}

Testing update and delete operations:

@Test
void shouldUpdateAndDeleteUser() {
    User user = entityManager.persistAndFlush(new User("Alice", "alice@test.com"));

    user.setName("Alice Smith");
    userRepository.save(user);

    userRepository.deleteById(user.getId());
    assertThat(userRepository.findById(user.getId())).isEmpty();
}

Summary

You've now mastered the fundamentals of data persistence in Spring Boot using JPA and Hibernate. From understanding how JPA provides a standard way to map Java objects to database tables, to creating entities with proper annotations, configuring database connections, and building repositories for data access - you have all the tools needed to integrate databases into your applications. The combination of entity mapping, Spring Data repositories, and automatic CRUD operations eliminates most of the complexity traditionally associated with database programming. With query methods, you can create sophisticated data retrieval logic using simple naming conventions, and proper testing ensures your data layer works reliably. Next, you'll explore advanced JPA features including entity relationships, custom queries, and performance optimization techniques that enable you to build complex, efficient data models for real-world applications.

Programming Challenge

Challenge: Library Management System with JPA

Task: Build a complete library management system using JPA entities, repositories, and proper database integration.

Requirements:

  1. Create entities for the system:
    • Book: id, title, author, isbn, publicationYear, available
    • Member: id, name, email, membershipDate, active status
    • Loan: id, book, member, loanDate, returnDate, returned status
  2. Implement repositories with custom query methods:
    • Find books by author or title (partial match)
    • Find available books
    • Find active members
    • Find current loans (not returned)
    • Find overdue loans (past due date)
  3. Create service layer methods:
    • Register new member
    • Add new book to library
    • Loan book to member
    • Return book
    • Search books by criteria
  4. Configure H2 database for development
  5. Write comprehensive repository tests
  6. Add proper validation annotations

Bonus features:

  • Add audit fields (createdDate, lastModified) to entities
  • Implement pagination for book search results
  • Create statistics queries (most popular books, active loan count)
  • Add book categories with enum mapping
  • Implement soft deletion for books and members

Learning Goals: Practice entity design, repository creation, custom query methods, service layer integration, and comprehensive testing of JPA-based data access in a realistic domain model.