Lab: Spring Boot Data JPA with PostgreSQL
By the end of this lab, you’ll understand how object-relational mapping works in Spring Boot applications and gain practical experience implementing data persistence.
Learning Objectives
- Understand the core concepts of Spring Data JPA
- Connect a Spring Boot application to PostgreSQL
- Create entity classes and repositories
- Implement CRUD operations using JPA repositories
- Test data persistence operations
Prerequisites
- Basic Java programming knowledge
- Familiarity with Spring Boot fundamentals
- PostgreSQL installed locally or Docker for running PostgreSQL in a container
- IDE (IntelliJ IDEA, Eclipse, or VS Code with Spring extensions)
- Maven or Gradle
Section 1: Setup and Configuration
Step 1.1: Create a Spring Boot Project
- Go to Spring Initializr
- Configure the project:
- Project: Maven
- Language: Java
- Spring Boot: 3.1.x (or latest stable version)
- Group: com.example
- Artifact: student-management
- Dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Lombok (optional, but helpful)
- Generate and download the project
- Extract and import the project into your IDE
Step 1.2: Configure PostgreSQL Database
- Create a PostgreSQL database named
student_db
# Using psql command-line
psql -U postgres
CREATE DATABASE student_db;
Or using Docker:
# Run PostgreSQL in Docker
docker run --name postgres-db -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=student_db -p 5432:5432 -d postgres
Step 1.3: Configure Database Connection
Open src/main/resources/application.properties
and add:
# Database configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/student_db
spring.datasource.username=postgres
spring.datasource.password=postgres
# JPA/Hibernate properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
The properties explained:
spring.datasource.*
: Defines connection parameters to your PostgreSQL databasespring.jpa.hibernate.ddl-auto=update
: Automatically updates database schema based on entity classesspring.jpa.show-sql=true
: Shows generated SQL in logs (helpful for learning)
Section 2: Creating the Domain Model
Step 2.1: Create a Student Entity
Create a new package com.example.studentmanagement.model
and add a Student.java
class:
package com.example.studentmanagement.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
// If using Lombok, include these:
// import lombok.AllArgsConstructor;
// import lombok.Data;
// import lombok.NoArgsConstructor;
@Entity
@Table(name = "students")
// If using Lombok, uncomment these:
// @Data
// @NoArgsConstructor
// @AllArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private int age;
private String major;
// Constructor, getters, and setters
// (Not needed if using Lombok's @Data, @NoArgsConstructor, and @AllArgsConstructor)
public Student() {
}
public Student(String firstName, String lastName, String email, int age, String major) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
this.major = major;
}
// Getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", age=" + age +
", major='" + major + '\'' +
'}';
}
}
Key annotations explained:
@Entity
: Marks this class as a JPA entity, which will be mapped to a database table@Table(name = "students")
: Specifies the table name in the database@Id
: Marks the primary key field@GeneratedValue
: Defines how the primary key should be generated
Section 3: Creating JPA Repository
Step 3.1: Create a StudentRepository Interface
Create a new package com.example.studentmanagement.repository
and add a StudentRepository.java
interface:
package com.example.studentmanagement.repository;
import com.example.studentmanagement.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Custom finder methods
List<Student> findByMajor(String major);
List<Student> findByAgeLessThan(int age);
List<Student> findByLastNameContaining(String namePattern);
// Custom JPQL query
@Query("SELECT s FROM Student s WHERE s.age >= :minAge AND s.age <= :maxAge")
List<Student> findStudentsInAgeRange(int minAge, int maxAge);
}
The repository interface explained:
- Extends
JpaRepository<Student, Long>
where:Student
is the entity typeLong
is the type of the primary key
- Spring Data JPA automatically implements basic CRUD operations
- Custom finder methods follow a naming convention that Spring translates into SQL queries
@Query
annotation allows for custom JPQL queries
Section 4: Creating a Service Layer
Step 4.1: Create a StudentService Interface
Create a new package com.example.studentmanagement.service
and add a StudentService.java
interface:
package com.example.studentmanagement.service;
import com.example.studentmanagement.model.Student;
import java.util.List;
import java.util.Optional;
public interface StudentService {
Student saveStudent(Student student);
List<Student> getAllStudents();
Optional<Student> getStudentById(Long id);
Student updateStudent(Student student);
void deleteStudent(Long id);
List<Student> getStudentsByMajor(String major);
List<Student> getStudentsYoungerThan(int age);
List<Student> getStudentsByLastNamePattern(String pattern);
List<Student> getStudentsInAgeRange(int minAge, int maxAge);
}
Step 4.2: Implement the StudentService
Create a StudentServiceImpl.java
class:
package com.example.studentmanagement.service;
import com.example.studentmanagement.model.Student;
import com.example.studentmanagement.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StudentServiceImpl implements StudentService {
private final StudentRepository studentRepository;
@Autowired
public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
@Override
public Student saveStudent(Student student) {
return studentRepository.save(student);
}
@Override
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
@Override
public Optional<Student> getStudentById(Long id) {
return studentRepository.findById(id);
}
@Override
public Student updateStudent(Student student) {
// First check if student exists
if (studentRepository.existsById(student.getId())) {
return studentRepository.save(student);
}
throw new RuntimeException("Student with ID " + student.getId() + " not found");
}
@Override
public void deleteStudent(Long id) {
studentRepository.deleteById(id);
}
@Override
public List<Student> getStudentsByMajor(String major) {
return studentRepository.findByMajor(major);
}
@Override
public List<Student> getStudentsYoungerThan(int age) {
return studentRepository.findByAgeLessThan(age);
}
@Override
public List<Student> getStudentsByLastNamePattern(String pattern) {
return studentRepository.findByLastNameContaining(pattern);
}
@Override
public List<Student> getStudentsInAgeRange(int minAge, int maxAge) {
return studentRepository.findStudentsInAgeRange(minAge, maxAge);
}
}
The service implementation:
- Uses
@Service
to mark it as a Spring service component - Uses constructor-based dependency injection to get the repository
- Delegates most operations to the repository
- Adds business logic where needed (like checking if a student exists before updating)
Section 5: Creating the REST Controller
Step 5.1: Create a StudentController
Create a new package com.example.studentmanagement.controller
and add a StudentController.java
class:
package com.example.studentmanagement.controller;
import com.example.studentmanagement.model.Student;
import com.example.studentmanagement.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService studentService;
@Autowired
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
public ResponseEntity<Student> createStudent(@RequestBody Student student) {
Student savedStudent = studentService.saveStudent(student);
return new ResponseEntity<>(savedStudent, HttpStatus.CREATED);
}
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
List<Student> students = studentService.getAllStudents();
return new ResponseEntity<>(students, HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
return studentService.getStudentById(id)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(@PathVariable Long id, @RequestBody Student student) {
student.setId(id);
try {
Student updatedStudent = studentService.updateStudent(student);
return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {
studentService.deleteStudent(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@GetMapping("/major/{major}")
public ResponseEntity<List<Student>> getStudentsByMajor(@PathVariable String major) {
List<Student> students = studentService.getStudentsByMajor(major);
return new ResponseEntity<>(students, HttpStatus.OK);
}
@GetMapping("/younger-than/{age}")
public ResponseEntity<List<Student>> getStudentsYoungerThan(@PathVariable int age) {
List<Student> students = studentService.getStudentsYoungerThan(age);
return new ResponseEntity<>(students, HttpStatus.OK);
}
@GetMapping("/by-lastname/{pattern}")
public ResponseEntity<List<Student>> getStudentsByLastNamePattern(@PathVariable String pattern) {
List<Student> students = studentService.getStudentsByLastNamePattern(pattern);
return new ResponseEntity<>(students, HttpStatus.OK);
}
@GetMapping("/age-range")
public ResponseEntity<List<Student>> getStudentsInAgeRange(
@RequestParam int minAge, @RequestParam int maxAge) {
List<Student> students = studentService.getStudentsInAgeRange(minAge, maxAge);
return new ResponseEntity<>(students, HttpStatus.OK);
}
}
The controller explained:
@RestController
: Combines@Controller
and@ResponseBody
to create a RESTful controller@RequestMapping("/api/students")
: Base URL for all endpoints in this controller- Implements all CRUD operations using appropriate HTTP methods
- Returns appropriate HTTP status codes for different outcomes
Section 6: Running and Testing the Application
Step 6.1: Run the Application
Run the main application class:
package com.example.studentmanagement;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentManagementApplication {
public static void main(String[] args) {
SpringApplication.run(StudentManagementApplication.class, args);
}
}
Step 6.2: Create Test Data
Let’s create a class to initialize some test data. Create a new class DataInitializer.java
in the main package:
package com.example.studentmanagement;
import com.example.studentmanagement.model.Student;
import com.example.studentmanagement.repository.StudentRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataInitializer {
@Bean
CommandLineRunner initDatabase(StudentRepository repository) {
return args -> {
// Only add test data if repository is empty
if (repository.count() == 0) {
repository.save(new Student("John", "Doe", "[email protected]", 20, "Computer Science"));
repository.save(new Student("Jane", "Smith", "[email protected]", 22, "Mathematics"));
repository.save(new Student("Alice", "Johnson", "[email protected]", 19, "Computer Science"));
repository.save(new Student("Bob", "Williams", "[email protected]", 21, "Physics"));
repository.save(new Student("Charlie", "Brown", "[email protected]", 23, "Mathematics"));
System.out.println("Sample data initialized!");
}
};
}
}
Step 6.3: Test the REST API
Use tools like Postman, cURL, or simply your web browser to test the API endpoints.
Example cURL commands:
- Get all students:
curl -X GET http://localhost:8080/api/students
- Get a student by ID:
curl -X GET http://localhost:8080/api/students/1
- Create a new student:
curl -X POST http://localhost:8080/api/students \
-H "Content-Type: application/json" \
-d '{"firstName":"David","lastName":"Miller","email":"[email protected]","age":24,"major":"Psychology"}'
- Update a student:
curl -X PUT http://localhost:8080/api/students/1 \
-H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Doe","email":"[email protected]","age":21,"major":"Computer Science"}'
- Delete a student:
curl -X DELETE http://localhost:8080/api/students/3
- Get students by major:
curl -X GET http://localhost:8080/api/students/major/Computer%20Science
- Get students in age range:
curl -X GET "http://localhost:8080/api/students/age-range?minAge=20&maxAge=22"
Lab Questions and Exercises
Now that you’ve completed building the application, try to answer the following questions to solidify your understanding:
-
What is the purpose of the
@Entity
annotation? How does it relate to database tables? -
Explain what happens when Spring Data JPA encounters a method like
findByMajor(String major)
in a repository interface. -
How does Spring Boot know how to connect to your PostgreSQL database?
-
What is the difference between
spring.jpa.hibernate.ddl-auto=update
andspring.jpa.hibernate.ddl-auto=create
? -
Add a new entity class called
Course
with fields forid
,name
,code
, andcredits
. Then create:
- A many-to-many relationship between
Student
andCourse
- A repository interface for the
Course
entity - Service layer methods to add/remove students from courses
- Controller endpoints to manage courses and student enrollment
-
Implement validation for student data (e.g., email format, age range) using Bean Validation (JSR 380).
-
Add pagination and sorting support to the endpoints that return multiple students.
Additional Resources
- Spring Data JPA Documentation
- Hibernate ORM Documentation
- PostgreSQL Documentation
- REST API Design Best Practices
Conclusion
In this lab, you’ve learned how to use Spring Boot Data JPA to implement data persistence with PostgreSQL. You’ve created entity classes, repositories, services, and REST controllers to manage student data. This foundational knowledge can be applied to build more complex applications with sophisticated data models and relationships.
Remember that Spring Data JPA greatly simplifies database operations by providing a high-level abstraction over JPA. This allows you to focus on your business logic rather than writing boilerplate code for data access.
By Wahid Hamdi