Spring Data JPA Introduction
The repository abstraction Spring Data JPA adds over plain JPA, with a complete JpaRepository example.
What Spring Data JPA adds over plain JPA/Hibernate
Plain JPA (with Hibernate as the implementation, covered in the Hibernate track) already removes a lot of manual JDBC boilerplate — you work with entities and an EntityManager instead of hand-written SQL and ResultSet mapping. But even with JPA, every project ends up hand-writing near-identical boilerplate: a "find all", a "find by id", a "save", a "delete" method, repeated for every entity.
Spring Data JPA adds a repository abstraction on top of JPA: you declare an interface describing what data access you need, and Spring generates a working implementation at runtime — no method bodies, no boilerplate DAO classes.
public interface UserRepository extends JpaRepository<User, Long> {
}
That's a complete, fully working repository. JpaRepository<User, Long> (entity type, ID type) already provides save, findById, findAll, deleteById, count, and more, all implemented for you.
A complete entity + repository example
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
private String fullName;
protected User() {
// required by JPA
}
public User(String email, String fullName) {
this.email = email;
this.fullName = fullName;
}
public Long getId() { return id; }
public String getEmail() { return email; }
public String getFullName() { return fullName; }
}
public interface UserRepository extends JpaRepository<User, Long> {
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User register(String email, String fullName) {
return userRepository.save(new User(email, fullName));
}
public List<User> allUsers() {
return userRepository.findAll();
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
}
No implementation class was written for UserRepository — at startup, Spring Data JPA scans for interfaces extending JpaRepository (or its parents, CrudRepository/PagingAndSortingRepository) and generates a proxy implementation backed by an EntityManager, registered as a bean automatically.
What JpaRepository gives you for free
| Method | What it does |
|---|---|
save(entity) |
Inserts a new row, or updates an existing one if the entity already has an ID |
findById(id) |
Returns Optional<T> — present if a row with that ID exists |
findAll() |
Returns every row as a List<T> |
deleteById(id) |
Deletes the row with that ID |
count() |
Returns the total row count |
existsById(id) |
Returns boolean without loading the full entity |
JpaRepository extends PagingAndSortingRepository (pagination/sorting) which itself extends CrudRepository (the basic CRUD operations above) — the hierarchy exists so you can depend on a narrower interface if you only need basic CRUD, but in practice most applications just extend JpaRepository directly, since it includes everything the parents provide plus a few JPA-specific batch operations.
Common mistakes
- Writing a manual DAO implementation class "just in case," duplicating what
JpaRepositoryalready provides for free. - Forgetting the no-argument protected/package-private constructor JPA requires to instantiate entities via reflection — a purely-
@AllArgsConstructorentity with no default constructor will fail at runtime. - Calling
save()expecting it to always insert — it inserts when the entity's ID is null/unset, and updates when the ID matches an existing row, based on whetherfindByIdfor that ID would return a result. - Treating the repository interface as if it needs any implementation code at all — the entire point of Spring Data JPA is that the interface is the complete contract.