Advanced Java & Spring Boot · Lesson 7 of 16 · Updated ·
Spring Data JPA and Hibernate — Entities, Repositories and Queries
Writing JDBC for every table (lesson 2) is repetitive. JPA (Jakarta Persistence API) lets you work with Java objects while Hibernate (the JPA implementation Spring Boot uses) writes the SQL. Spring Data JPA goes further: you write an interface and get the implementation for free.
1. An entity
// File: course-api/src/main/java/com/skills101/courses/course/Course.java
package com.skills101.courses.course;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private int fee; // in rupees
private int seats;
protected Course() {
// JPA needs a no-argument constructor
}
public Course(String title, int fee, int seats) {
this.title = title;
this.fee = fee;
this.seats = seats;
}
public Long getId() { return id; }
public String getTitle() { return title; }
public int getFee() { return fee; }
public int getSeats() { return seats; }
public void update(String title, int fee, int seats) {
this.title = title;
this.fee = fee;
this.seats = seats;
}
}
| Annotation | Meaning |
|---|---|
@Entity | this class is stored in a table (named course) |
@Id | the primary key |
@GeneratedValue(strategy = IDENTITY) | the database generates ids (auto-increment) |
@Column(unique = true, nullable = false) | column rules (see Student.email) |
@Table, @Transient, @Enumerated | table name/constraints, skip a field, store enums |
JPA needs a no-argument constructor (it can be protected) and must be able to read the fields. Notice there's no setter for id — the database owns it.
2. A repository: no implementation needed
// File: course-api/src/main/java/com/skills101/courses/course/CourseRepository.java
package com.skills101.courses.course;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface CourseRepository extends JpaRepository<Course, Long> {
// Spring Data writes the SQL from the method name
List<Course> findByFeeLessThanEqualOrderByFeeAsc(int maxFee);
boolean existsByTitleIgnoreCase(String title);
// or write JPQL yourself (it uses class and field names, not table names)
@Query("select c from Course c where lower(c.title) like lower(concat('%', :word, '%'))")
List<Course> search(String word);
}
JpaRepository<Course, Long> already gives you save, findById, findAll, existsById, deleteById, count, paging and sorting.
Derived queries: Spring Data reads the method name and writes the query:
| Method | Generated WHERE |
|---|---|
findByTitle(String t) | title = ? |
findByFeeLessThanEqualOrderByFeeAsc(int max) | fee <= ? order by fee asc |
existsByTitleIgnoreCase(String t) | upper(title) = upper(?) |
findByTitleContainingIgnoreCase(String w) | upper(title) like %?% |
countByCourseId(Long id) | count(*) … course_id = ? |
For anything complex, write JPQL in @Query — it uses entity and field names (Course, c.title), not table names. Native SQL is possible with nativeQuery = true.
3. Proving it works: a @DataJpaTest
// File: course-api/src/test/java/com/skills101/courses/course/CourseRepositoryTest.java
package com.skills101.courses.course;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
// Starts only JPA + an in-memory database; each test is rolled back afterwards
@DataJpaTest
class CourseRepositoryTest {
@Autowired
CourseRepository repository;
@Test
void derivedQueriesWork() {
repository.save(new Course("Python Basics", 1999, 50));
repository.save(new Course("Java Full Stack", 4999, 30));
repository.save(new Course("Git Basics", 0, 100));
assertThat(repository.findByFeeLessThanEqualOrderByFeeAsc(2000))
.extracting(Course::getTitle)
.containsExactly("Git Basics", "Python Basics");
assertThat(repository.existsByTitleIgnoreCase("java full stack")).isTrue();
assertThat(repository.search("basics")).hasSize(2);
}
}
This test passes in our build: free courses first (Git Basics, then Python Basics at ₹1999), the case-insensitive check finds "java full stack", and the JPQL search finds both "basics" courses.
4. What Hibernate does
- Generates SQL from entities and method calls (
spring.jpa.show-sql=trueprints it while learning). - Dirty checking: inside a transaction, change a loaded entity's fields and the UPDATE runs automatically at commit — that's why
CourseService.updatenever callssave. - First-level cache: loading the same id twice in one transaction hits the database once.
- Schema generation with
ddl-auto(fine for learning; use Flyway/Liquibase migrations for real apps).
5. Entity states
| State | Meaning |
|---|---|
| Transient | new Course(...) — not known to JPA yet |
| Managed | saved or loaded in the current transaction; changes are tracked |
| Detached | the transaction ended; changes are no longer tracked |
| Removed | scheduled for deletion |
Common mistakes
- No
@Id→ startup error "No identifier specified for entity". - Using table/column names in JPQL instead of entity/field names.
- A typo in a derived method name (
findByTitel) → startup error "No property 'titel' found". - Making entities
finalor without a no-arg constructor.
Quick recap
@Entity+@Idmap a class to a table; Hibernate writes the SQL.JpaRepositorygives CRUD; derived query names and@Query(JPQL) give the rest.- Inside a transaction, managed entities are saved automatically (dirty checking).
- Test repositories with
@DataJpaTest.