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.

Java
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

Java
@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; }
}
Java
public interface UserRepository extends JpaRepository<User, Long> {
}
Java
@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 JpaRepository already provides for free.
  • Forgetting the no-argument protected/package-private constructor JPA requires to instantiate entities via reflection — a purely-@AllArgsConstructor entity 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 whether findById for 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.