Welcome to one of the most useful and popular topics in Spring Data — working with repositories! In this lecture we'll go over what repositories are, why they matter, how CrudRepository and JpaRepository differ, how they make your life easier, and of course how to plug them into your app. This stuff will be handy not only for your school projects but also in real work — repositories are the backbone of database interaction in Spring.
What is a repository?
A repository (Repository) in this context is the application layer responsible for talking to the database. It abstracts basic operations like creating, reading, updating and deleting data (the so-called CRUD operations).
If in the past interacting with a database meant writing SQL by hand, Spring Data makes that process way easier. You don't have to "get your hands dirty" with SQL anymore (though nothing stops you if you want to). Instead, repositories let you focus on business logic and leave the routine work to Spring.
CrudRepository and JpaRepository: differences
Spring Data provides several interfaces for DB access, but two are used most often:
CrudRepository
CrudRepository is the basic interface for performing CRUD operations. Here are its main methods:
public interface CrudRepository<T, ID> {
<S extends T> S save(S entity); // Save (or update) an entity
Optional<T> findById(ID id); // Find an entity by id
boolean existsById(ID id); // Check if a record exists
Iterable<T> findAll(); // Get all entities
void deleteById(ID id); // Delete by id
void delete(T entity); // Delete the provided entity
}
When to use: if you only need basic CRUD operations.
JpaRepository
JpaRepository extends CrudRepository and adds extra features:
- Pagination (breaking large lists into pages — like Google: 10 results per page)
- Sorting of data
- JPA-specific methods like
flush()for managing the persistence context
Example of pagination:
// Get the first 20 users, sorted by name
Page<User> users = repository.findAll(PageRequest.of(0, 20, Sort.by("name")));
Examples of methods from JpaRepository:
public interface JpaRepository<T, ID> extends CrudRepository<T, ID> {
List<T> findAll(Sort sort); // Sort results
Page<T> findAll(Pageable pageable); // Paginate results
void flush(); // Flush changes to the database
<S extends T> S saveAndFlush(S entity); // Save and immediately flush the context
}
When to use: if you need extra DAO-level features like sorting or pagination.
Using repositories in practice
Now that the theory is a bit clearer, let's switch to practice! We'll work with an app that already has a User entity.
1. Entity User
Our User class is a simple entity that will store user data:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
2. Creating the repository
Now let's create the repository interface. Spring Data will do the rest for us:
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// You can add custom methods here
}
That's it! The repository is ready. Now we have access to all CRUD operations provided by JpaRepository.
Performing CRUD operations
Let's use our repository for basic operations.
Saving a user
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(String name, String email) {
User user = new User(name, email);
return userRepository.save(user);
}
}
Getting a user by ID
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
Updating a user's data
public User updateUser(Long id, String name, String email) {
User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
user.setName(name);
user.setEmail(email);
return userRepository.save(user);
}
Deleting a user
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
Using it in a REST controller Let's create a controller that uses our UserService:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.createUser(user.getName(), user.getEmail());
return new ResponseEntity<>(savedUser, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(user -> new ResponseEntity<>(user, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
User updatedUser = userService.updateUser(id, user.getName(), user.getEmail());
return new ResponseEntity<>(updatedUser, HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Custom repository methods
Sometimes the standard operations aren't enough and you need to write your own. For example, finding a user by name.
Implementation using method name
List<User> findByName(String name);
Spring Data "magically" understands that you want to find users by name, thanks to method naming conventions.
Implementation using @Query
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
Pagination — if you have lots of data, you can return it in small chunks:
Page<User> findAll(Pageable pageable);
Usage:
Pageable pageable = PageRequest.of(0, 10, Sort.by("name"));
Page<User> page = userRepository.findAll(pageable);
Possible errors and how to avoid them
Here are a few common mistakes to keep in mind:
- Error "No EntityManager with actual transaction": don't forget to add the
@Transactionalannotation to service methods that modify data. - Missing no-arg constructor in the entity: make sure entity classes have a public no-argument constructor.
- Lazy loading issues (LazyInitializationException): be careful with lazy loading and try to use
FetchType.LAZYonly where it's necessary.
Why does all this matter?
Repositories are a great way to focus on application logic without wasting time on boilerplate. Working with repositories is faster, safer, and way easier than writing SQL queries by hand.
In real-world work you'll be using repositories constantly, whether in a simple CRUD app or in a complex microservices system with dozens of databases.
GO TO FULL VERSION