别再让JSON字段毁了你的业务代码:从阿里商品中台案例看领域模型与数据模型的正确分工

张开发
2026/4/20 19:36:47 15 分钟阅读
别再让JSON字段毁了你的业务代码:从阿里商品中台案例看领域模型与数据模型的正确分工
领域模型与数据模型的分工艺术从阿里商品中台实践看架构设计的本质记得三年前接手一个电商促销系统重构时我发现前任开发者将所有营销规则都塞进了一个名为promotion_rules的JSON字段里。当需要增加限购地区功能时团队直接在JSON里追加了region_restrictions数组而业务代码中遍布着类似JSON.parse(rule).region_restrictions.includes(userProvince)的判断——这种将数据存储结构直接暴露给业务逻辑的做法让系统在半年内变成了无人敢动的祖传代码。1. 领域模型与数据模型的本质差异1.1 领域模型业务语义的精确表达领域模型是解决业务问题的概念框架其核心价值在于显性化业务语义。在商品价格管控场景中一个良好的领域模型应该像这样public class PriceRule { private ListPriceRange ranges; private ApprovalStrategy strategy; public boolean isPriceValid(BigDecimal price) { return ranges.stream().anyMatch(range - range.contains(price)); } } public class PriceRange { private BigDecimal min; private BigDecimal max; private RangeType type; // AUTO_APPROVE, AUTO_BLOCK等 }这种建模方式明确表达了价格规则由多个区间组成每个区间有特定类型的业务语义。当产品经理提出某些商品需要人工复核时开发者可以自然地扩展ApprovalStrategy枚举而不是在JSON里添加need_manual_review字段。1.2 数据模型存储效率的极致追求数据模型的核心诉求是存储效率和访问性能。同样价格管控场景数据库设计可能简化为字段名类型描述idbigint主键rule_namevarchar规则名称conditionjson价格区间配置modified_byvarchar最后修改人架构师笔记优秀的存储设计应该像乐高积木——通过有限的基础结构组合出无限可能。但业务代码不应该直接拼装这些积木。2. 混淆模型的典型反模式2.1 JSON字段滥用综合症在商品中心系统里我们经常见到这样的灵活设计CREATE TABLE product_extend ( product_id BIGINT PRIMARY KEY, features JSON COMMENT 商品扩展属性 );初期看似便捷的方案随着业务发展会出现三大致命伤可读性灾难代码中充斥着feature.get(pre_sale).get(deposit_amount)这样的魔法字符串验证缺失无法在数据库层约束JSON结构的有效性查询低效即便现代数据库支持JSON查询性能也远不及结构化字段2.2 垂直扩展表的陷阱阿里商品中台的auction_extend表设计确实是扩展性典范字段名类型示例值auction_idbigint123456extend_keyvarcharpresale_infoextend_valuetext{start:1630000}但当业务代码直接操作这些扩展字段时就会出现前文提到的面向字段编程现象。更糟糕的是这种模式会像病毒一样扩散——当第一个团队开始用getFeature(presale_info)其他团队会迅速效仿。3. 模型转换的架构实践3.1 网关模式Gateway的威力COLA架构提出的Gateway模式正是解决这类问题的银弹。以商品服务为例// 数据访问层 public interface ProductGateway { Product getById(Long id); } // 实现层 public class ProductGatewayImpl implements ProductGateway { public Product getById(Long id) { ProductDO productDO dao.selectById(id); ListFeatureDO features featureDao.selectByProductId(id); return convertToDomain(productDO, features); } private Product convertToDomain(ProductDO productDO, ListFeatureDO features) { Product product new Product(); product.setId(productDO.getId()); // 将扩展字段转换为领域对象 features.forEach(f - { if (presale_info.equals(f.getKey())) { product.setPresaleInfo(parsePresaleInfo(f.getValue())); } // 其他字段转换... }); return product; } }这种模式带来三个关键优势业务语义隔离领域层完全感知不到底层存储结构变更局部化数据库结构调整只需修改Gateway实现测试友好可以轻松Mock网关进行单元测试3.2 转换策略的演进随着系统复杂度提升我们可以引入更高级的转换策略策略对比表策略类型适用场景实现复杂度典型案例硬编码转换字段少且稳定★☆☆☆☆基础商品信息注解驱动中等规模领域模型★★★☆☆订单核心流程DSL映射高度动态的业务扩展★★★★★营销活动配置元数据驱动SaaS多租户系统★★★★★★电商开放平台性能提示对于高频访问的领域对象可以在Gateway层引入二级缓存缓存转换后的对象。4. 中台架构的平衡之道4.1 阿里商品中台的启示深入分析阿里auction_extend的设计哲学我们可以提炼出三个核心原则存储与业务解耦扩展表只负责存储不定义业务含义元数据描述通过单独的feature_meta表描述扩展字段语义分层治理核心字段结构化存储长尾需求用扩展表这种设计虽然完美解决了不同业务线疯狂加字段的问题但需要配套的架构约束严格禁止业务代码直接查询扩展表为每个业务线提供专用的SDK封装字段访问建立扩展字段的注册和审计机制4.2 现代架构的解决方案在云原生时代我们可以采用更优雅的解决方案// 使用Spring Data的Converter接口 public class ProductConverter implements ConverterProductDO, Product { private FeatureRepository featureRepo; public Product convert(ProductDO source) { Product product new Product(); // 基础字段映射... // 动态扩展字段处理 featureRepo.findByProductId(source.getId()) .forEach(f - product.addFeature( FeatureFactory.create(f.getKey(), f.getValue()) )); return product; } } // 在Repository中注册 public interface ProductRepository extends JpaRepositoryProductDO, Long { Override EntityGraph(attributePaths {features}) OptionalProductDO findById(Long id); }配合Hibernate的TypeDef注解甚至可以做到JSON字段到领域对象的自动映射TypeDef(name jsonb, typeClass JsonBinaryType.class) public class ProductDO { Type(type jsonb) Column(columnDefinition jsonb) private ProductSpec spec; // 自动序列化/反序列化 }5. 实战重构商品评价系统最近主导的一个电商平台重构项目正是运用这些原则将评价系统从字段驱动改造为模型驱动的典型案例。改造前存储设计CREATE TABLE item_review ( id BIGINT PRIMARY KEY, item_id BIGINT, content TEXT, features JSON -- 包含: media_urls, is_anonymous, tag_scores等 );改造后架构领域模型明确定义public class Review { private ReviewId id; private Content content; private Media media; private AnonymousInfo anonymity; private ListTagRating tagRatings; }网关层转换逻辑public class ReviewGatewayImpl implements ReviewGateway { public Review save(Review review) { ReviewDO reviewDO new ReviewDO(); reviewDO.setItemId(review.id().itemId()); reviewDO.setContent(review.content().text()); // 结构化字段 reviewDO.setIsAnonymous(review.anonymity().enabled()); // 动态扩展字段 reviewDO.setFeatures(JsonUtils.toJson(Map.of( media, review.media().urls(), tags, review.tagRatings().stream() .collect(toMap(TagRating::tag, TagRating::score)) ))); return convertToDomain(reviewDao.save(reviewDO)); } }业务代码的蜕变// 改造前 if (review.getFeatures().getBoolean(is_anonymous)) { displayName 匿名用户; } // 改造后 if (review.anonymity().enabled()) { displayName 匿名用户; }这次重构使得新需求的实施时间平均缩短40%因为新增评价维度时只需扩展领域模型业务规则检查集中在值对象方法中存储结构调整对业务代码零影响6. 模型转换的性能优化当系统达到一定规模时模型转换可能成为性能瓶颈。我们在社交平台项目中遇到过网关转换消耗15%CPU的情况最终通过以下方案优化多级缓存策略原始数据缓存缓存ProductDO等数据对象领域对象缓存缓存转换后的Product懒加载代理对不常用字段使用代理模式public class ProductProxy extends Product { private SupplierProductDetails detailsLoader; Override public ProductDetails getDetails() { if (super.getDetails() null) { super.setDetails(detailsLoader.get()); } return super.getDetails(); } }批量转换优化// 低效做法 ListProduct products productIds.stream() .map(gateway::getById) .collect(toList()); // 高效做法 ListProduct products gateway.batchGet(productIds);在Gateway实现内部批量转换可以利用并行处理和缓存机制大幅提升效率。我们的压测显示批量接口的吞吐量可达单条查询的8倍。7. 领域模型的进化策略优秀的领域模型不是一次性设计出来的而是随着业务认知不断演进的。我们采用三段式演进策略初期允许数据模型驱动领域模型快速验证成长期通过DDD的限界上下文划分明确模型边界成熟期引入领域事件保持模型间松耦合典型案例商品库存模型演进阶段数据模型领域模型驱动因素V1单库存字段Item.stock简单SKU管理V2分仓库存表WarehouseInventory多仓发货V3库存流水表快照InventoryAggregate InventoryLog库存预占与审计V4分渠道库存视图ChannelInventory区分线上线下渠道这种渐进式演进确保了我们既不错失早期市场机会又能支撑业务长期发展。关键在于始终保持数据模型与领域模型之间的明确界限——存储结构可以推翻重来但领域模型应该通过适配器平滑迁移。

更多文章