Repositories & CrudRepository
Learn how extending JpaRepository or CrudRepository gives you working save, find, update, and delete methods without writing any implementation code.
Introduction
You now have a real @Entity backed by a real database table, but you still need a way to save, retrieve, update, and delete rows from Java code. Writing that data access code by hand — JDBC connections, PreparedStatements, ResultSet mapping — is exactly the kind of repetitive work Spring Data JPA exists to eliminate. This lesson introduces repositories, the interfaces that give you fully working data access with almost no code.
- How to define a repository interface for an entity.
- The difference between CrudRepository and JpaRepository.
- How to use a repository from a controller.
- How Spring Data JPA generates queries from method names alone.
The Repository Interface
A Spring Data JPA repository is just an interface — you don't write an implementation class at all. Spring generates one automatically at startup, backed by Hibernate, purely based on the interface you declare.
import org.springframework.data.repository.CrudRepository;import org.springframework.stereotype.Repository;
@Repositorypublic interface StudentRepository extends CrudRepository<Student, Long> {}The two generic type parameters are the entity type (Student) and the type of its primary key (Long). That single line already gives you methods like save, findById, findAll, count, and deleteById — fully implemented, with zero SQL written by you.
CrudRepository vs JpaRepository
Spring Data offers a small hierarchy of repository interfaces. CrudRepository provides the basic operations; JpaRepository extends it with JPA-specific and pagination-friendly additions.
| Interface | Adds |
|---|---|
| CrudRepository<T, ID> | save, findById, findAll, count, deleteById, existsById |
| PagingAndSortingRepository<T, ID> | findAll(Pageable) and findAll(Sort) for paging and sorting |
| JpaRepository<T, ID> | Everything above, plus flush(), batch deletes, and JPA-specific extras like saveAndFlush |
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentRepository extends JpaRepository<Student, Long> {}In practice, most Spring Boot projects extend JpaRepository directly rather than CrudRepository, simply because it includes everything CrudRepository does plus pagination and JPA-specific conveniences you'll almost certainly want eventually.
Using a Repository in a Controller
Inject the repository into your controller with constructor injection, then replace the in-memory ArrayList logic from Lesson 15 with real repository calls.
@RestController@RequestMapping("/api/students")public class StudentController {
private final StudentRepository studentRepository;
public StudentController(StudentRepository studentRepository) { this.studentRepository = studentRepository; }
@PostMapping public ResponseEntity<Student> createStudent(@RequestBody Student student) { Student saved = studentRepository.save(student); return ResponseEntity.status(HttpStatus.CREATED).body(saved); }
@GetMapping public ResponseEntity<List<Student>> getAllStudents() { List<Student> students = (List<Student>) studentRepository.findAll(); return ResponseEntity.ok(students); }
@GetMapping("/{id}") public ResponseEntity<Student> getStudentById(@PathVariable Long id) { return studentRepository.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); }
@DeleteMapping("/{id}") public ResponseEntity<Void> deleteStudent(@PathVariable Long id) { if (!studentRepository.existsById(id)) { return ResponseEntity.notFound().build(); } studentRepository.deleteById(id); return ResponseEntity.noContent().build(); }}Click Run to see what this code prints.
Derived Query Methods
Spring Data JPA can generate query implementations directly from a method's name, as long as it follows a recognized pattern. You simply declare the method signature on the repository interface — no @Query annotation or SQL required.
public interface StudentRepository extends JpaRepository<Student, Long> {
List<Student> findByName(String name);
List<Student> findByEmailContaining(String keyword);
boolean existsByEmail(String email);}Click Run to see what this code prints.
Common Mistakes
- Extending CrudRepository and then manually casting findAll()'s Iterable result awkwardly — extending JpaRepository returns a List directly instead.
- Misspelling a field name in a derived query method, such as findByEmial, which fails at application startup with a clear but easily missed error.
- Calling save() expecting it to always insert — if the entity's id is already set to an existing value, save() performs an update instead.
- Forgetting @Repository is optional but helpful for exception translation clarity — Spring Data still detects the interface without it via component scanning of repository interfaces.
Best Practices
- Prefer JpaRepository over CrudRepository for new projects, since it's a strict superset with pagination support.
- Use derived query methods for simple lookups, and reserve @Query (covered later) for anything more complex than a couple of conditions.
- Keep repository interfaces focused on one entity each, matching the one-repository-per-aggregate convention most Spring Data JPA projects follow.
- Let exceptions from the repository propagate to a centralized error handler rather than catching generic exceptions in every controller method.
Frequently Asked Questions
No. Spring Data JPA generates the implementation automatically at application startup using a dynamic proxy — you only ever write the interface.
It parses the method name according to a defined grammar — findBy, then a property name (matched against the entity's fields), optionally combined with keywords like Containing, And, OrderBy, and so on.
findById always returns at most one result, so Optional<T> models the possibility of nothing being found. Name-based lookups can naturally match multiple rows, so they return a List<T> instead.
Yes, using the @Query annotation with JPQL or native SQL. That's typically the right move once a method name would need more than two or three conditions to express.
Key Takeaways
- A Spring Data repository is an interface; Spring generates its implementation automatically.
- CrudRepository provides basic CRUD methods; JpaRepository extends it with pagination and JPA-specific extras, and is the usual default choice.
- Repositories are injected into controllers or services just like any other Spring bean.
- Derived query methods let you get working custom queries just by naming a method correctly, with no SQL required.
Summary
Your controller now talks to a real database through a repository, with almost no data access code written by hand. Next, you'll model a relationship between two entities — Student and Course — using JPA's @OneToMany and @ManyToOne annotations.