1. 概述

在本篇教程中,我们将探讨如何在 Spring Data 中创建一个只读 Repository

在某些场景下,我们只需要从数据库中读取数据,而不需要对其进行任何修改。此时,使用一个只读的 Repository 接口会非常合适。

它能提供完整的数据读取能力,同时避免数据被意外修改的风险 ✅。

2. 继承 Repository 接口

我们从一个包含 spring-boot-starter-data-jpa 依赖的 Spring Boot 项目开始:

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

这个依赖中包含了 Spring Data 提供的 CrudRepository 接口,它封装了常用的 CRUD 操作。但问题在于,它也包含了修改数据的方法(如 save、delete 等),而这正是我们要避免的。

CrudRepository 实际上继承自一个更基础的接口:Repository。我们可以直接继承 Repository,来自定义只读行为。

下面是我们自定义的只读接口:

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

这里我们只定义了两个只读方法。✅ 通过这种方式,任何使用该接口的实体都将无法被修改

另外,使用 @NoRepositoryBean 注解非常重要,它告诉 Spring 不要为这个接口创建具体的实现 Bean。这样我们就可以将这个只读接口复用到多个实体上

接下来,我们看看如何将这个接口与具体实体绑定。

3. 使用 ReadOnlyRepository

假设我们有一个简单的实体类 Book

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

    // getters and setters
}

接着,我们创建一个继承自 ReadOnlyRepository 的接口:

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

除了继承的 findByIdfindAll,我们又增加了两个只读的查询方法:findByAuthor()findByTitle()。✅ 这样,该 Repository 总共拥有了四个只读方法。

我们写一个测试用例来验证其功能:

@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);
}

⚠️ 注意:为了在测试中插入数据,我们额外创建了一个普通的 BookRepository(继承自 CrudRepository):

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

这个 Repository 只用于测试,项目主代码中并不需要它。

通过这个测试,我们验证了所有四个只读方法都能正常工作,并且可以将 ReadOnlyRepository 复用到其他实体上。

4. 小结

本教程中我们学习了如何继承 Spring Data 的 Repository 接口来创建一个通用的只读 Repository,并通过 Book 实体进行了演示和测试。

这种做法简单粗暴,非常适合在需要严格控制写操作的场景下使用 ✅。

完整代码示例可以在 GitHub 上找到:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-data-2


原始标题:Creating a Read-Only Repository with Spring Data