CodeGym /Courses /Module 5. Spring /Working with repositories: CrudRepository, JpaRepository

Working with repositories: CrudRepository, JpaRepository

Module 5. Spring
Level 5 , Lesson 3
Available

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 @Transactional annotation 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.LAZY only 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.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION