Skip to content

A:

JPA Specification(Infrastructure 层):动态数据库查询 + DDD Specification(Domain 层):纯业务规则校验 ,这是我项目里面这样使用specs 的,不过ContentAggregateRepository.java public List<ContentAggregate> findByAuthor(UserId authorId) 这里用到了Domain ContentSpecifications的精确筛选;但是在ActivityAggregateRepository却没有用到ActivitySpecifications,是不是哪里代码写错了啊

Q:

📊 现状分析

✅ ContentAggregateRepository(正确示范)

java
@Override
public List<ContentAggregate> findByAuthor(UserId authorId) {
    // Step 1: 使用 JPA Specification 在数据库层面粗筛
    Specification<PostPO> dbSpec = PostPoSpecs.isNotDeleted()
            .and(PostPoSpecs.byAuthor(authorId.getValue()))
            .and(PostPoSpecs.isPublished());
    
    List<PostPO> postPOs = postRepository.findAll(dbSpec);
    
    // Step 2: 转换为聚合根并使用 Domain Specification 精筛
    List<ContentAggregate> aggregates = postPOs.stream()
            .map(contentConverter::poToAggregate)
            .filter(ContentSpecifications.authorPublished(authorId.getValue())::isSatisfiedBy)  // ✅ 使用 Domain Spec
            .collect(Collectors.toList());
    
    return aggregates;
}

设计思路:

  • ✅ 两层过滤:JPA Specification(粗筛) + Domain Specification(精筛)

  • ✅ 职责清晰:数据库过滤 + 业务规则校验


❌ ActivityAggregateRepository(缺少 Domain Specification)

java
@Override
public ActivitySearchResult searchActivities(
        ActivitySearchCriteria criteria, 
        int page, 
        int size) {
    
    // ✅ Step 1: JPA Specification(数据库过滤)
    Specification<ActivityPO> spec = ActivityPoSpecs.bySearchCriteria(criteria, now);
    Page<ActivityPO> poPage = activityJpaRepository.findAll(spec, pageable);
    
    // ✅ Step 2: PO → Aggregate 转换
    List<ActivityAggregate> activities = poPage.getContent().stream()
        .map(activityConverter::poToAggregate)
        .toList();
    
    // ❌ 缺少 Step 3: Domain Specification 精筛(业务规则校验)
    
    return ActivitySearchResult.builder()...build();
}

问题:

  • ❌ 只有 JPA Specification,没有 Domain Specification

  • ❌ 缺少业务规则的二次校验

  • ❌ 没有充分利用 ActivitySpecifications


🔧 优化方案

方案 1:在 searchActivities 中增加 Domain Specification 精筛

适用场景:需要对搜索结果进行业务规则校验

java
@Override
public ActivitySearchResult searchActivities(
        ActivitySearchCriteria criteria, 
        int page, 
        int size) {
    
    LocalDateTime now = LocalDateTime.now();
    
    // Step 1: JPA Specification(数据库层面粗筛)
    Specification<ActivityPO> spec = ActivityPoSpecs.bySearchCriteria(criteria, now);
    Sort sort = ActivityPoSpecs.buildSort(criteria);
    Pageable pageable = PageRequest.of(page, size, sort);
    
    Page<ActivityPO> poPage = activityJpaRepository.findAll(spec, pageable);
    
    // Step 2: PO → Aggregate 转换
    List<ActivityAggregate> activities = poPage.getContent().stream()
        .map(activityConverter::poToAggregate)
        .toList();
    
    // Step 3: 【新增】Domain Specification 精筛(业务规则校验)
    // 根据业务需求选择合适的 Domain Specification
    ISpecification<ActivityAggregate> domainSpec = buildDomainSpecification(criteria);
    
    List<ActivityAggregate> filteredActivities = activities.stream()
        .filter(domainSpec::isSatisfiedBy)  // 使用 Domain Spec 精筛
        .toList();
    
    // Step 4: 构建结果
    return ActivitySearchResult.builder()
        .activities(filteredActivities)
        .total(poPage.getTotalElements())  // 注意:这里的 total 是数据库查询的总数
        .page(page)
        .size(size)
        .build();
}

/**
 * 根据搜索条件构建 Domain Specification
 * <p>
 * 用于业务规则的二次校验
 */
private ISpecification<ActivityAggregate> buildDomainSpecification(ActivitySearchCriteria criteria) {
    // 基础规格:已发布
    ISpecification<ActivityAggregate> spec = ActivitySpecifications.isPublished();
    
    // 如果需要有名额的活动
    if (Boolean.TRUE.equals(criteria.getHasAvailableSlots())) {
        spec = spec.and(ActivitySpecifications.hasAvailableSlots());
    }
    
    // 如果需要可报名的活动(更严格的业务规则)
    // 例如:已发布 + 有名额 + 未开始 + 未过报名截止时间
    if (Boolean.TRUE.equals(criteria.getOnlyRegistrable())) {
        spec = spec.and(ActivitySpecifications.isRegistrable());
    }
    
    // 如果需要免费活动
    if (Boolean.TRUE.equals(criteria.getOnlyFree())) {
        spec = spec.and(ActivitySpecifications.isFree());
    }
    
    return spec;
}

方案 2:在特定查询方法中使用 Domain Specification

适用场景:某些查询需要复杂的业务规则校验

java
/**
 * 查询推荐活动(使用 Domain Specification 精筛)
 * <p>
 * 业务规则:
 * - 已发布 + 有名额 + 可报名 + 未满员 + 评分高
 */
@Override
public List<ActivityAggregate> findRecommendedActivities(String city, int limit) {
    log.debug("查询推荐活动 - 城市: {}, 限制: {}", city, limit);
    
    try {
        LocalDateTime now = LocalDateTime.now();
        
        // Step 1: JPA Specification(数据库层面粗筛)
        Specification<ActivityPO> dbSpec = ActivityPoSpecs.isNotDeleted()
                .and(ActivityPoSpecs.isPublished())
                .and(ActivityPoSpecs.inCity(city))
                .and(ActivityPoSpecs.hasAvailableSlots());
        
        List<ActivityPO> activityPOs = activityJpaRepository.findAll(dbSpec);
        
        // Step 2: 转换为聚合根
        List<ActivityAggregate> aggregates = activityPOs.stream()
                .map(activityConverter::poToAggregate)
                .toList();
        
        // Step 3: 【使用 Domain Specification 精筛】
        // 业务规则:可报名 + 高质量
        ISpecification<ActivityAggregate> domainSpec = ActivitySpecifications.isRegistrable()
                .and(ActivitySpecifications.hasAvailableSlots());
        
        List<ActivityAggregate> recommendedActivities = aggregates.stream()
                .filter(domainSpec::isSatisfiedBy)  // Domain 层业务规则过滤
                .limit(limit)
                .toList();
        
        log.debug("找到 {} 个推荐活动", recommendedActivities.size());
        return recommendedActivities;
        
    } catch (Exception e) {
        log.error("查询推荐活动失败 - 城市: {}", city, e);
        throw new ActivityRepositoryException("查询推荐活动失败", e);
    }
}

方案 3:在 findBySpecification 中已经实现(保持现状)

你的 ActivityAggregateRepository 已经实现了 findBySpecification 方法:

java
@Override
public List<ActivityAggregate> findBySpecification(ISpecification<ActivityAggregate> specification) {
    // Step 1: 数据库层面基础过滤
    Specification<ActivityPO> baseSpec = ActivityPoSpecs.isNotDeleted();
    List<ActivityPO> allActivities = activityJpaRepository.findAll(baseSpec);
    
    // Step 2: Domain Specification 精筛
    List<ActivityAggregate> aggregates = allActivities.stream()
            .map(activityConverter::poToAggregate)
            .filter(specification::isSatisfiedBy)  // ✅ 使用 Domain Spec
            .toList();
    
    return aggregates;
}

这个方法已经正确使用了 Domain Specification!


🎯 建议的最佳实践

两种使用模式

模式 1:简单查询(只用 JPA Specification)

适用场景:查询条件简单,不需要复杂业务规则校验

java
// 示例:按城市查询活动
@Override
public List<ActivityAggregate> findByCity(String city) {
    Specification<ActivityPO> spec = ActivityPoSpecs.isNotDeleted()
            .and(ActivityPoSpecs.inCity(city));
    
    List<ActivityPO> activityPOs = activityJpaRepository.findAll(spec);
    
    return activityPOs.stream()
            .map(activityConverter::poToAggregate)
            .toList();
}

模式 2:复杂查询(JPA Spec + Domain Spec)

适用场景:需要业务规则校验、推荐算法、复杂筛选

java
// 示例:查询推荐活动(需要业务规则校验)
@Override
public List<ActivityAggregate> findRecommendedActivities(String city) {
    // Step 1: 数据库层面粗筛
    Specification<ActivityPO> dbSpec = ActivityPoSpecs.isNotDeleted()
            .and(ActivityPoSpecs.isPublished())
            .and(ActivityPoSpecs.inCity(city));
    
    List<ActivityPO> activityPOs = activityJpaRepository.findAll(dbSpec);
    
    // Step 2: 转换
    List<ActivityAggregate> aggregates = activityPOs.stream()
            .map(activityConverter::poToAggregate)
            .toList();
    
    // Step 3: Domain Specification 精筛(业务规则)
    ISpecification<ActivityAggregate> domainSpec = ActivitySpecifications.isRegistrable()
            .and(ActivitySpecifications.hasAvailableSlots());
    
    return aggregates.stream()
            .filter(domainSpec::isSatisfiedBy)
            .toList();
}

📋 总结

你的项目现状

RepositoryJPA SpecificationDomain Specification状态
ContentAggregateRepository✅ 使用✅ 使用(精筛)✅ 正确
ActivityAggregateRepository✅ 使用⚠️ 部分使用(只在 findBySpecification 中)⚠️ 可优化

建议

  1. ✅ 保持 findBySpecification 方法(已经正确使用 Domain Spec)

  2. ⚠️ 考虑在 searchActivities 中增加 Domain Spec 精筛(如果需要业务规则校验)

  3. ✅ 在推荐、筛选等场景中使用 Domain Spec(参考 ContentAggregateRepository)

何时使用 Domain Specification?

  • ✅ 需要复杂业务规则校验(如:可报名、推荐算法)

  • ✅ 需要在内存中进行二次筛选(如:根据用户画像推荐)

  • ✅ 需要组合多个业务规则(如:高质量内容 = 推荐 + 热门 + 活跃)

  • ❌ 简单的数据库查询(直接用 JPA Spec 即可)


你的代码没有写错,只是 ActivityAggregateRepository 可以参考 ContentAggregateRepository 的模式,在某些场景下增加 Domain Specification 的使用! 🎯

新增🎯 5 个复杂业务场景设计

场景 1:智能推荐活动(根据用户画像)

场景 2:即将爆满活动提醒(紧急通知)

场景 3:高质量活动筛选(多维度评分)

场景 4:用户适配活动(年龄/性别/技能匹配)

场景 5:活动热度排行(综合指标计算)

Powered by VitePress