1. Overview

In this short tutorial, we’ll discuss how to create a read-only Spring Data Repository.

It’s sometimes necessary to read data out of a database without having to modify it. In this case, having a read-only Repository interface would be perfect.

It’ll provide the ability to read data without the risk of anyone changing it.

2. Extending Repository

Let’s begin with a Spring Boot project that includes the spring-boot-starter-data-jpa dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.4.3</version>
</dependency>

Included in this dependency is Spring Data’s popular CrudRepository interface, which comes with methods for all the basic CRUD operations (create, read, update, delete) that most applications need. However, it includes several methods that modify data, and we need a repository that only has the ability to read data.

CrudRepository actually extends another interface called Repository. We can also extend this interface to fit our needs.

Let’s create a new interface that extends Repository:

@NoRepositoryBean
public interface ReadOnlyRepository<T, ID> extends Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
}

Here, we’ve only defined two read-only methods. The entity that is accessed by this repository will be safe from any modification.

It is also important to note that we must use the @NoRepositoryBean annotation to tell Spring that we want this repository to remain generic. This allows us to reuse our read-only repository for as many different entities as we want.

Next, we’ll see how to tie an entity to our new ReadOnlyRepository.

3. Extending ReadOnlyRepository

Let’s assume we have a simple Book entity that we would like to access:

@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;
    private String author;
    private String title;

    //getters and setters
}

Now that we have a persistable entity, we can create a repository interface that inherits from our ReadOnlyRepository:

public interface BookReadOnlyRepository extends ReadOnlyRepository<Book, Long> {
    List<Book> findByAuthor(String author);
    List<Book> findByTitle(String title);
}

In addition to the two methods that it inherits, we’ve added two more Book-specific read-only methods: findByAuthor() and findByTitle(). In total, this repository has access to four read-only methods.

Finally, let’s write a test that will ensure the functionality of our BookReadOnlyRepository:

@Test
public void givenBooks_whenUsingReadOnlyRepository_thenGetThem() {
    Book aChristmasCarolCharlesDickens = new Book();
    aChristmasCarolCharlesDickens.setTitle("A Christmas Carol");
    aChristmasCarolCharlesDickens.setAuthor("Charles Dickens");
    bookRepository.save(aChristmasCarolCharlesDickens);

    Book greatExpectationsCharlesDickens = new Book();
    greatExpectationsCharlesDickens.setTitle("Great Expectations");
    greatExpectationsCharlesDickens.setAuthor("Charles Dickens");
    bookRepository.save(greatExpectationsCharlesDickens);

    Book greatExpectationsKathyAcker = new Book();
    greatExpectationsKathyAcker.setTitle("Great Expectations");
    greatExpectationsKathyAcker.setAuthor("Kathy Acker");
    bookRepository.save(greatExpectationsKathyAcker);

    List<Book> charlesDickensBooks = bookReadOnlyRepository.findByAuthor("Charles Dickens");
    Assertions.assertEquals(2, charlesDickensBooks.size());

    List<Book> greatExpectationsBooks = bookReadOnlyRepository.findByTitle("Great Expectations");
    Assertions.assertEquals(2, greatExpectationsBooks.size());

    List<Book> allBooks = bookReadOnlyRepository.findAll();
    Assertions.assertEquals(3, allBooks.size());
    
    Long bookId = allBooks.get(0).getId();
    Book book = bookReadOnlyRepository.findById(bookId).orElseThrow(NoSuchElementException::new);
    Assertions.assertNotNull(book);
}

In order to save the books into the database before reading them back out, we created a BookRepository that extends CrudRepository in the test scope. This repository is not needed in the main project scope but was necessary for this test.

public interface BookRepository extends CrudRepository<Book, Long> {}

We were able to test all four of our read-only methods and can now reuse the ReadOnlyRepository interface for other entities.

4. Conclusion

We learned how to extend Spring Data’s Repository interface in order to create a reusable read-only repository. After that, we tied it to a simple Book entity and wrote a test that proved its functionality works as we would expect.

As always, a working example of this code can be found over on GitHub.