是否需强制所有数据库返回数据通过实体以执行业务规则?
解决DDD中查询绕过聚合根业务逻辑的问题
你的核心矛盾是:为了查询效率独立实现了查询逻辑,但导致聚合根中签名字段强制必填的业务规则没有被应用,又不想重复代码或放弃查询独立性。以下是几个符合DDD原则的解决方案:
方案1:提取业务逻辑为独立纯函数
把FormField中计算required字段的逻辑抽离成一个独立的纯函数,让聚合根和查询逻辑都复用这个函数,从根源上避免重复代码。
步骤1:提取纯函数
// 可放在领域层工具类或单独文件中 export function calculateFormFieldRequired(type: FormFieldType, originalRequired: boolean): boolean { return type === 'SIGNATURE' ? true : originalRequired; }
步骤2:修改聚合根构造函数
protected _id: string private _name: string private _type: FormFieldType private _options: FormFieldOption[] private _required: boolean constructor(data: FormFieldData) { super(data) this._id = data.id this._name = data.name this._type = data.type this._options = data.options.map(option => new FormFieldOption(option)) // 复用提取的规则函数 this._required = calculateFormFieldRequired(data.type, data.required) }
步骤3:修改查询逻辑应用规则
在查询映射DTO时,对form.fields的required字段重新计算:
// Map data to DTO return { id: data.id, formId: data.formId, patientId: data.patientId, submitted: data.submitted, responses, form: { ...data.form, fields: data.form.fields.map(field => ({ ...field, required: calculateFormFieldRequired(field.type, field.required) })) }, updatedAt: data.updatedAt, }
该方案优势:
- 严格遵循DRY原则,逻辑仅维护一处
- 聚合根与查询职责不受影响,查询依然保持独立高效
- 纯函数易于测试,无需依赖任何领域对象
方案2:使用领域装配器(Assembler)处理DTO转换
创建专门的领域服务类,负责将查询返回的原始数据转换为符合领域规则的DTO,把业务规则应用逻辑集中在装配器中。
实现装配器
export class PatientFormAssembler { static toDTO(rawData: typeof data): PatientFormDTO { const responses: FormResponseDTO[] = rawData.responses.reduce((acc: FormResponseDTO[], response) => { if (isPatientResponseFormFieldType(response.formField.type)) { const value = patientResponseFormFieldValueExtractor[response.formField.type](response) const res: FormResponseDTO = { id: response.id, formFieldId: response.formFieldId, value, } acc.push(res) } return acc }, []) // 应用签名字段必填规则 const processedFields = rawData.form.fields.map(field => ({ ...field, required: field.type === 'SIGNATURE' ? true : field.required })) return { id: rawData.id, formId: rawData.formId, patientId: rawData.patientId, submitted: rawData.submitted, responses, form: { ...rawData.form, fields: processedFields }, updatedAt: rawData.updatedAt, } } }
修改查询类调用装配器
async run(params: GetPatientFormParams): Promise<PatientFormDTO | null> { if (!params.practiceId) throw new Error('Missing practiceId') const { patientFormId, practiceId } = params const data = await this.prisma.patientForm.findUnique({ where: { id: patientFormId, practiceId }, include: { /* 原include内容保持不变 */ } }) if (!data) return null // 调用装配器完成DTO转换 return PatientFormAssembler.toDTO(data) }
该方案优势:
- 把DTO转换与规则应用逻辑从查询类剥离,查询类仅负责数据获取,职责更单一
- 装配器属于领域层,确保领域规则在DTO转换过程中被正确应用
- 聚合根可复用装配器中的规则函数(若将规则进一步抽离)
方案3:数据库层面添加兜底约束(辅助措施)
作为上述方案的补充,可在数据库层面添加约束,确保SIGNATURE类型的formField的required字段始终为true,避免数据不一致:
Prisma Schema示例
model FormField { id String @id @default(uuid()) type FormFieldType required Boolean // 其他字段... // 添加检查约束 @@check(type != 'SIGNATURE' OR required = true) }
注意:该方案仅作为兜底,不能替代领域层规则应用——领域规则可能随业务变化调整,数据库约束灵活性较差。
内容的提问来源于stack exchange,提问作者J Doe
相关产品推荐
相关产品推荐

