领域驱动设计(DDD)学习中的四类核心技术问题咨询
你的判断是对的,重复校验属于领域规则,必须放在领域层。具体实现上,应该用DomainService来封装这个校验逻辑,而非把规则塞进Repository里。
Repository的职责是抽象数据持久化和查询能力,它只需要提供“根据某个领域标识查询是否存在实体”的方法(比如existsByEmail(String email)),而判断“邮箱重复违反注册规则”这类领域逻辑,应该由DomainService来完成——它会调用Repository的查询方法,再根据领域规则抛出异常或返回校验结果。
这样做既符合依赖倒置原则(DomainService依赖抽象的Repository接口),也能保持各层职责单一:Repository只处理数据层面的操作,DomainService专注于领域规则的封装。
完全不需要为了持久化暴露getter方法,破坏实体的封装性。可以通过以下几种方式解决:
- ORM工具直接映射字段:比如JPA支持
@Access(AccessType.FIELD)注解,直接访问实体的私有字段完成持久化,不需要任何getter/setter; - 包级私有访问:给实体的私有字段提供包级私有的读取方法(比如
String getEmail(),不加public修饰符),让同包下的Repository实现类可以访问,但外部无法调用; - 通过构造器重建实体:从数据库读取数据时,用这些数据调用实体的构造器(或工厂方法)重建实体对象,而不是直接给字段赋值,这样既完成了持久化,又保证实体始终处于合法状态。
核心原则是:实体的封装优先,持久化需求不能凌驾于领域对象的完整性之上。
不建议直接用领域对象作为ApplicationService的输入输出,尤其是对外暴露的服务接口,必须用DTO隔离领域层和外部边界。原因很简单:领域对象的结构会随着领域规则迭代变化,而外部接口(比如API、前端)需要稳定的契约,用DTO可以避免领域层的改动直接影响外部。
关于DTO的设计:
- 不是为每个实体创建DTO,而是为每个服务接口的业务场景设计。比如“创建用户”服务的输入DTO只需要包含用户名、邮箱、密码这些必要字段;“查询用户详情”的输出DTO可能包含用户ID、用户名、注册时间等展示字段,它们的结构和User实体不一定完全对应;
- 如果是内部服务之间的调用,偶尔用领域对象传递数据是可以的,但对外暴露的接口必须严格用DTO做隔离。
DDD的数据库设计确实应该围绕聚合展开,聚合根是唯一的持久化入口,确保聚合内的一致性。具体表结构设计:
- 聚合根单独建表:作为聚合的核心,聚合根必须有独立的表,比如
orders表; - 聚合内的实体:通常单独建表,通过外键关联到聚合根(比如
order_items表,用order_id关联orders),但要注意这些表不能被直接操作,所有对订单项的修改必须通过订单聚合根完成,保证聚合内的业务规则被执行; - 值对象:有两种处理方式:
- 嵌入到聚合根表:比如用户的地址值对象,可以在
users表中加province、city、detail等字段直接存储; - 单独建表:如果值对象比较复杂或需要复用,可以单独建表(比如
addresses),但同样要通过聚合根关联,不能单独查询或修改。
- 嵌入到聚合根表:比如用户的地址值对象,可以在
核心是数据库结构要匹配聚合的边界,确保聚合的一致性规则在数据层面也能得到保障,避免出现绕过聚合根直接操作子实体的情况。
内容的提问来源于stack exchange,提问作者dylan.kwon

