1. 引言

在进行领域驱动设计(DDD)建模时,使用 Spring Data JPA 作为数据访问层的抽象是一个非常成熟且高效的选择。它不仅能大幅减少样板代码,还能通过约定优于配置的方式自动实现常见数据库操作。

如果你刚接触 Spring Data JPA,建议先阅读 《使用 Spring Data JPA 构建持久层》 这类入门教程打好基础。

本文重点讲解如何通过 仓库片段(repository fragments) 构建可组合仓库(composable repositories)——这是一种将功能拆分、再灵活组装到主仓库中的高级用法,特别适合复杂业务场景下的代码组织。

2. Maven 依赖

可组合仓库的功能从 Spring 5 开始正式支持,需引入以下核心依赖:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.7.11</version>
</dependency>

同时,为了运行数据访问逻辑,你还得配置一个数据源。开发和测试阶段推荐使用 H2 这类内存数据库,启动快、无需外部依赖,参考:《Spring 测试中配置独立数据源》

3. 背景知识

3.1. Hibernate 作为 JPA 实现

Spring Data JPA 本身只是一个抽象层,默认底层使用 Hibernate 作为 JPA 实现。但两者不能混为一谈:

  • Spring Data JPA:提供统一的 Repository 接口规范
  • Hibernate:是 JPA 规范的一种具体实现

你可以轻松替换底层实现,比如改用 EclipseLink,详见 《Spring 集成 EclipseLink 实战》

3.2. 默认仓库接口

大多数 CRUD、分页、排序操作根本不需要手写 SQL。

只需定义一个继承 JpaRepository 的接口即可:

public interface LocationRepository extends JpaRepository<Location, Long> {
}

就这么简单,你就拥有了对 Location 实体的完整增删改查能力。

更酷的是,Spring Data 支持方法名自动解析生成查询语句,例如:

public interface StoreRepository extends JpaRepository<Store, Long> {
    List<Store> findStoreByLocationId(Long locationId);
}

框架会自动解析 findStoreByLocationId 并生成对应的 JPQL 查询,无需注解或 XML 配置。

3.3. 自定义仓库

当内置方法不够用时,可以通过“接口 + 实现类”的方式扩展功能,这种模式叫 仓库片段(fragment)

举个例子,我们想给 ItemTypeRepository 添加自定义删除逻辑:

public interface ItemTypeRepository 
    extends JpaRepository<ItemType, Long>, CustomItemTypeRepository {
}

其中 CustomItemTypeRepository 是自定义接口:

public interface CustomItemTypeRepository {
    void deleteCustomById(ItemType entity);
}

实现类必须命名为 XXXImpl(默认后缀),Spring 才能自动扫描到:

public class CustomItemTypeRepositoryImpl implements CustomItemTypeRepository {
 
    @Autowired
    private EntityManager entityManager;

    @Override
    public void deleteCustomById(ItemType itemType) {
        entityManager.remove(itemType);
    }
}

⚠️ 注意:实现类后缀必须是 Impl,否则不会被自动装配。

当然,你也可以自定义后缀,通过 XML 配置:

<repositories base-package="com.baeldung.repository" repository-impl-postfix="CustomImpl" />

或者使用注解方式:

@EnableJpaRepositories(
  basePackages = "com.baeldung.repository", 
  repositoryImplementationPostfix = "CustomImpl")

这样实现类就可以叫 CustomItemTypeRepositoryCustomImpl 了。

4. 使用多个片段组合仓库

在过去,一个仓库只能关联一个自定义实现类,导致所有扩展逻辑被迫塞进同一个 XXXImpl 中,代码臃肿不堪——典型的“上帝类”陷阱 ❌。

Spring 5 之后,✅ 支持一个仓库继承多个片段接口,每个片段都有自己的实现类,真正做到高内聚、低耦合。

来看个实际例子:

我们定义两个功能片段:

public interface CustomItemTypeRepository {
    void deleteCustom(ItemType entity);
    void findThenDelete(Long id);
}

public interface CustomItemRepository {
    Item findItemById(Long id);
    void deleteCustom(Item entity);
    void findThenDelete(Long id);
}

然后在一个主仓库中同时继承它们:

public interface ItemTypeRepository 
    extends JpaRepository<ItemType, Long>, 
            CustomItemTypeRepository, 
            CustomItemRepository {
}

✅ 效果:ItemTypeRepository 现在拥有了来自两个片段的所有方法,代码清晰、职责分明。

5. 处理方法冲突(歧义问题)

多个片段中如果存在同名同参的方法,Spring 怎么知道调用哪一个?

答案是:按接口继承顺序决定优先级

比如上面两个片段都有 findThenDelete(Long id) 方法:

// 继承顺序决定实现来源
extends CustomItemTypeRepository, CustomItemRepository  → 调用前者
extends CustomItemRepository, CustomItemTypeRepository  → 调用后者

也就是说,谁排前面,就用谁的实现。

我们可以通过单元测试验证这一点:

@Test
public void givenItemAndItemTypeWhenDeleteThenItemTypeDeleted() {
    Optional<ItemType> itemType = composedRepository.findById(1L);
    assertTrue(itemType.isPresent());

    Item item = composedRepository.findItemById(2L);
    assertNotNull(item);

    // 调用的是 CustomItemTypeRepository 的实现
    composedRepository.findThenDelete(1L);

    Optional<ItemType> sameItemType = composedRepository.findById(1L);
    assertFalse(sameItemType.isPresent()); // 类型被删

    Item sameItem = composedRepository.findItemById(2L);
    assertNotNull(sameItem); // 商品还在
}

⚠️ 踩坑提醒:这种优先级机制虽然简单粗暴,但在多人协作项目中容易引发隐性 Bug。建议:

  • 尽量避免多个片段定义相同方法
  • 如果必须共用,应在文档中明确说明行为来源
  • 或统一抽取到公共基类中

6. 总结

Spring Data JPA 不仅让基础数据操作变得极其简单,还通过可组合仓库机制提供了强大的扩展能力。

关键要点回顾:

  • ✅ 仓库片段 = 接口 + Impl 类(默认后缀)
  • ✅ 一个仓库可继承多个片段,实现功能聚合
  • ✅ 方法冲突时,继承顺序决定实现优先级
  • ✅ 合理使用可避免“胖仓库”问题,提升代码可维护性

合理运用这一特性,能让你的持久层代码更加模块化、易测试、易复用,尤其是在大型系统中优势尤为明显。


原始标题:Spring Data Composable Repositories | Baeldung