You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

是否需强制所有数据库返回数据通过实体以执行业务规则?

解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 05:23:18