[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-28":26,"\u002Farticles\u002Fdesign-pattern-28\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-28\u002Fadjacent":72,"\u002Fcategories":76,"render-design-pattern-28":102},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113935015761940481","design-pattern-28","设计模式修炼之路 ㉘ 组合实体模式——一窝小对象，一次网络往返","一窝小对象一次网络往返、读放大与 N+1、一次提交就是事务边界、依赖对象没有远程身份、JPA 聚合根转世","2113897628365230081",1,3270,7543,19,"2025-04-07T11:55:37","> J2EE 模式第三站。\n\n㉗ 的订单实体只有三个字段，一次远程调用就传输完毕——这是实体世界的\"好养型\"。可真实的领域模型里，\"客户\"这种实体由地址、信用、联系方式、订单历史一堆小对象组成；远程场景下，每个小对象都可能是独立的网络往返。\n\nEJB 时代这句话是字面意义的：entity bean 本身就是远程对象，对象图原样铺到远程，调用图就跟着变成远程调用图——看一个客户，四次网络往返起步；搬个家再提个额，两次独立提交，第二次失败，数据库里一半新一半旧。\n\n**把一窝依赖对象捆成一个粗粒度实体，统一加载、统一提交、统一管理生命周期**——组合实体模式，J2EE 八式里的打包员。\n\n## 一、组合实体模式是什么\n\nJ2EE 蓝皮书的定义大意：组合实体（Composite Entity）是一个**粗粒度实体**，它管理一组**细粒度依赖对象**（dependent objects）的生命周期——加载、修改、提交，全部以它为边界；远程调用方只见它这一个入口，永远不直接接触依赖对象。\n\n三个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Composite Entity（组合实体）\u003C\u002Ftd>\u003Ctd>粗粒度入口：一次加载、一次提交整组依赖对象\u003C\u002Ftd>\u003Ctd>CustomerEntity\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Dependent Object（依赖对象）\u003C\u002Ftd>\u003Ctd>细粒度 POJO，躲在实体身后，没有远程身份\u003C\u002Ftd>\u003Ctd>Address \u002F Credit \u002F Contact\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Coarse Grained Object（粗粒度对象）\u003C\u002Ftd>\u003Ctd>经典分层里实际持有依赖对象的一层；单组依赖时与实体合并\u003C\u002Ftd>\u003Ctd>本文并入 CustomerEntity\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话理解分工：**依赖对象说业务话（\"提额不能超现有额度一半\"），组合实体说边界话（\"要进来整窝一起进，要出去整窝一起出\"），远程世界只见这一个门。**\n\n注意它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——诞生动机很具体：细粒度 entity bean 在分布式环境里是性能灾难，蓝皮书用\"粗粒度\"给实体世界立了新规矩。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **网络往返从 N+1 到 1**：反面教材里看一个客户四次往返，组合实体一次——读放大直接消失。\n2. **写有事务边界**：整组要么全落库、要么全不动；\"搬家成功提额失败\"的一半新一半旧不可能出现。\n3. **客户端不再懂内部构成**：客户由哪几块组成、先加载谁后加载谁，是实体自己的家事——封装回到了实体世界。\n4. **依赖对象可以自由演化**：明天给客户加\"会员等级\"，只动实体内部，远程调用方一行不改。\n\n**缺点：**\n\n1. **一次加载可能过重**：用不到的字段也跟着来——懒加载能缓解，但懒加载会重新引入网络往返（第五节 Hibernate N+1 见分晓），尺度得自己拿捏。\n2. **并发冲突面变大**：两个人同时改同一个客户的不同部分，在整组边界上排队等锁——捆得越粗，锁面越大。\n3. **常需配套传输对象**：组合实体住在服务器上，客户端最终要的是可序列化的数据快照——这对黄金搭档 ㉝ 再展开。\n\n## 三、什么时候用\n\n判断标准：**远程边界 + 领域对象是一张图（主对象带一群小对象）+ 这群小对象常被整体使用**——三条都占，就值得捆。\n\n- **远程实体访问**：EJB、RMI——本模式出生场景\n- **一次业务操作天然跨多个小对象**：搬家 + 提额、改账单地址 + 改通知偏好——分散提交没有事务语义\n- **跨服务按聚合取数**：微服务间想要\"完整的客户\"，与其开一堆 GET 端点，不如一个聚合接口\n\n反面对照：**本地进程内**——直接用 ⑨ 组合模式的对象图就好，粗粒度是替远程边界付的成本，本地享受不到收益只剩缺点；**对象图巨大且访问模式差异大**——客户 + 全部订单历史（几万条），整体加载是灾难，该拆成两个聚合、按需另取。\n\n## 四、实现方式：搬家 + 提额\n\n场景：客户\"张三\"由名字、地址、信用、联系方式组成。业务操作：**搬家 + 提额**——一次操作动两个小对象，正是组合实体的主场。\n\n### 4.1 反面教材：细粒度远程访问\n\n先看依赖对象——纯 POJO，业务规则住自己身上（这一半是对的，问题出在访问方式）：\n\n```java\npublic class Credit {\n\n    private final int score;\n    private int creditLimit;\n\n    public void raiseLimit(int delta) {\n        if (delta \u003C= 0) {\n            throw new IllegalArgumentException(\"提额幅度必须为正数\");\n        }\n        if (delta > creditLimit \u002F 2) {\n            throw new IllegalArgumentException(\"单次提额不能超过现有额度的 50%\");\n        }\n        creditLimit += delta;\n    }\n    \u002F\u002F format\u002Fparse：模拟数据库一列的存取，略\n}\n```\n\n模拟远程持久层：业务层在另一台服务器上，**每个方法调用 = 一次网络往返**。它提供两套接口——细粒度（每个小对象单独读写）和整行（一次读写整组）：\n\n```java\npublic class CustomerDataStore {\n\n    \u002F\u002F ---------- 细粒度接口：一个小对象 = 一次网络往返 ----------\n\n    public String loadName(String id) {\n        roundTrip(\"SELECT name FROM customer WHERE id='\" + id + \"'\");\n        return row(id)[0];\n    }\n\n    public Address loadAddress(String id) { \u002F* 同款，略 *\u002F }\n    public Credit loadCredit(String id) { \u002F* 同款，略 *\u002F }\n    public Contact loadContact(String id) { \u002F* 同款，略 *\u002F }\n\n    public void saveAddress(String id, Address address) {\n        roundTrip(\"UPDATE customer SET address=? WHERE id='\" + id + \"'\");\n        maybeFail();                     \u002F\u002F 模拟网络抖动：写可能失败\n        row(id)[1] = address.format();\n    }\n\n    public void saveCredit(String id, Credit credit) { \u002F* 同款，略 *\u002F }\n\n    private void roundTrip(String sql) {\n        System.out.println(\"      [网络往返] \" + sql + \" ... 220ms\");\n    }\n}\n```\n\n再看表示层门户代码的日常——**错不在门户程序员，他拿到的接口长这样，只能这么写**：\n\n```java\npublic class BadPortal {\n\n    private final CustomerDataStore store;\n\n    public void showCustomer(String id) {\n        String name = store.loadName(id);\n        Address address = store.loadAddress(id);\n        Credit credit = store.loadCredit(id);\n        Contact contact = store.loadContact(id);\n        System.out.println(\"      \" + name + \" | \" + address + \" | 信用分 \" + credit.getScore()\n                + \" 额度 \" + credit.getCreditLimit() + \"元 | \" + contact.getPhone());\n    }\n\n    \u002F** 一次业务操作 = 搬家 + 提额。细粒度接口下被拆成三次远程调用、两次独立提交 *\u002F\n    public void moveAndRaise(String id, String street, String city, String zip, int delta) {\n        store.saveAddress(id, new Address(street, city, zip));   \u002F\u002F 远程写 1\n        Credit credit = store.loadCredit(id);                    \u002F\u002F 远程读\n        credit.raiseLimit(delta);\n        store.saveCredit(id, credit);                            \u002F\u002F 远程写 2：失败就惨了\n    }\n}\n```\n\n运行输出：\n\n```\n========== 反面教材：细粒度远程访问 ==========\n-- 场景 1：查看一个客户 --\n      [网络往返] SELECT name FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT address FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT contact FROM customer WHERE id='C001' ... 220ms\n      张三 | 人民路 1 号（杭州） | 信用分 650 额度 50000元 | 13800000001\n   小结：看 1 个客户 = 4 次网络往返（读放大）\n\n-- 场景 2：搬家 + 提额（一次业务操作）--\n      [网络往返] UPDATE customer SET address=? WHERE id='C001' ... 220ms\n      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\n      [网络往返] UPDATE customer SET credit=? WHERE id='C001' ... 220ms\n   [门户] 第二次远程写失败：网络抖动：提交超时\n\n-- 事后核查（又是 4 次往返）--\n      [网络往返] SELECT name FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT address FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT contact FROM customer WHERE id='C001' ... 220ms\n      张三 | 西湖大道 88 号（杭州） | 信用分 650 额度 50000元 | 13800000001\n   小结：地址是新的、额度是旧的——一半新一半旧，没有事务边界\n```\n\n三个病灶全齐：**读放大**（看 1 个客户 4 次往返，EJB 时代 220ms 一次，近一秒耗在网络上）；**写不一致**（搬家成功、提额失败，数据库里一半新一半旧，还没法重试——重试等于再搬一次家）；**拼装逻辑散落**（\"客户由哪几块组成\"这个知识被逼着写在每个调用方里）。\n\n### 4.2 正解：组合实体\n\n**CustomerEntity**：一个粗粒度对象管住整组依赖对象——加载时组装、修改时在内存、提交时打包：\n\n```java\npublic class CustomerEntity {\n\n    private final String id;\n    private final CustomerDataStore store;\n\n    private String name;\n    private Address address;   \u002F\u002F 依赖对象 1\n    private Credit credit;     \u002F\u002F 依赖对象 2\n    private Contact contact;   \u002F\u002F 依赖对象 3\n\n    \u002F** 一次网络往返，整组依赖对象全部就位 *\u002F\n    public void load() {\n        String[] row = store.loadRow(id);\n        name = row[0];\n        address = Address.parse(row[1]);\n        credit = Credit.parse(row[2]);\n        contact = Contact.parse(row[3]);\n    }\n\n    \u002F** 一次网络往返，整组原子提交——要么全落库，要么全不动 *\u002F\n    public void commit() {\n        store.saveRow(id, new String[]{name, address.format(), credit.format(), contact.format()});\n    }\n\n    public void move(String street, String city, String zip) {\n        address = new Address(street, city, zip);\n    }\n\n    public void raiseCreditLimit(int delta) {\n        credit.raiseLimit(delta);\n    }\n\n    public String describe() { \u002F* 一行拼好整组，略 *\u002F }\n}\n```\n\n**调用方的画风**，只见一个门：\n\n```java\nCustomerEntity customer = new CustomerEntity(\"C001\", store);\n\ncustomer.load();                                  \u002F\u002F 1 次往返，整组就位\ncustomer.move(\"西湖大道 88 号\", \"杭州\", \"310000\");  \u002F\u002F 改内存，不碰网络\ncustomer.raiseCreditLimit(5000);                  \u002F\u002F 改内存，不碰网络\ncustomer.commit();                                \u002F\u002F 1 次往返，整组提交\n```\n\n运行输出（故意让第一次提交失败）：\n\n```\n========== 正解：组合实体 ==========\n-- 加载客户 C001 --\n      [网络往返] SELECT * FROM customer WHERE id='C001' ... 220ms\n   客户视图：张三 | 人民路 1 号（杭州） | 信用分 650 额度 50000元 | 13800000001\n   小结：1 次网络往返，整组依赖对象就位\n\n-- 业务操作：搬家 + 提额 --\n      [网络往返] UPDATE customer SET name=?,address=?,credit=?,contact=? WHERE id='C001' ... 220ms\n   [组合实体] 提交失败：网络抖动：提交超时——整组未落库，重试一次\n      [网络往返] UPDATE customer SET name=?,address=?,credit=?,contact=? WHERE id='C001' ... 220ms\n   提交后：张三 | 西湖大道 88 号（杭州） | 信用分 650 额度 55000元 | 13800000001\n   小结：一次加载、一次提交，业务操作有了事务边界\n```\n\n三处对照读出含金量：加载**一次往返整组就位**（反面教材四次）；搬家和提额在**内存里完成**，全程零网络；提交失败后**整组原封不动**，重试提交的是同一个完整操作——反面教材那个\"一半新一半旧\"的僵局，在这里结构上就不可能发生。\n\n### 4.3 三处细节\n\n**细节一：依赖对象没有远程身份。** 回头数数：Address、Credit、Contact 从头到尾没有被远程直接访问过——远程世界只见 CustomerEntity 一个门。这是组合实体的铁律：**依赖对象的可见性止步于实体边界**。业务规则（提额上限 50%）住在依赖对象身上，不管谁经手，规则绕不过去。\n\n**细节二：一次提交就是事务边界。** 反面教材的病灶二，根治靠的不是\"加重试\"而是\"改提交单位\"：细粒度提交时重试等于重复副作用（再搬一次家）；整组提交时重试是安全重放（提交失败 = 什么都没发生，重放同一个完整提交——真实世界还要配幂等键，防\"超时但其实成功\"的悬案）。经典 EJB 里容器管理的事务（CMT）直接以实体边界为事务边界——**实体边界 = 事务边界 = 锁边界**，一体三面，这也是缺点二\"锁面变大\"的来源。\n\n**细节三：经典结构还能再分一层，加载策略还有两派。** 蓝皮书完整版是 CompositeEntity → CoarseGrainedObject → DependentObject 三层：实体背后还有一个\"粗粒度对象\"实际持有依赖对象。实践里一个实体只带一组依赖对象时，实体自己就是粗粒度对象（本文做法），带多组时才值得分层。加载策略则分饿汉\u002F懒汉两派：本文 load() 是饿汉——一次全来，最省往返；懒汉是首次访问某个依赖对象才加载它——省流量但重新引入网络往返，第五节会看到它在 ORM 里的著名翻车现场。\n\n## 五、在框架里找它\n\n1. **JPA \u002F Hibernate 的实体关联**：@OneToMany、@ElementCollection 把对象图映射成表关系；fetch = EAGER 就是饿汉组合实体，LAZY 就是懒汉。而 Hibernate 著名的 **N+1 查询问题**——懒加载一对多，遍历时每行触发一次查询——正是反面教材的读放大在 ORM 里的转世。解药 JOIN FETCH、@EntityGraph，本质都是把数据访问掰回\"一次往返、整组就位\"。\n\n2. **Spring Data 的聚合根仓库**：官方文档明说 Repository 应该定义在**聚合根**上，OrderItem 这样的从属对象不配拥有自己的 Repository——外部想改订单项？先拿订单聚合，再从聚合内部改。这就是 DDD 聚合根思想：一个聚合一个根，根管整组，**事务一致性以聚合为单位**——组合实体模式的学院派后代，连\"依赖对象不对外\"这条铁律都原样继承。\n\n3. **MyBatis 的聚合映射**：resultMap 里的 `\u003Cassociation>`、`\u003Ccollection>`，一次 JOIN 把客户、地址、联系方式映射成完整对象图——\"一次查询组装整组\"在 SQL 映射层的实现，连本文 format\u002Fparse 手工拼装的动作都替你自动化了。\n\n4. **GraphQL**：读侧同类问题的当代解——REST 时代看一个页面开 N 个端点，GraphQL 一次请求声明整棵需要的树。注意方向相反：组合实体是**服务器**定义打包粒度，GraphQL 是**客户端**声明要什么——但\"别让我为一张图跑 N 趟\"的诉求，三十年没变过。\n\n## 六、组合实体 vs 组合模式 vs 传输对象——\"捆东西\"三兄弟对账\n\n组合实体和 ⑨ 组合模式都姓\"组合\"，和 ㉝ 传输对象都干\"打包\"——摊开对账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>组合模式（⑨）\u003C\u002Fth>\u003Cth>传输对象（㉝）\u003C\u002Fth>\u003Cth>组合实体（㉘）\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>捆的是什么\u003C\u002Ftd>\u003Ctd>**结构**：部分与整体同一接口\u003C\u002Ftd>\u003Ctd>**数据**：一次网络搬一坨字段\u003C\u002Ftd>\u003Ctd>**生命周期**：加载、提交以组为单位\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>结构形态\u003C\u002Ftd>\u003Ctd>树形，可递归嵌套\u003C\u002Ftd>\u003Ctd>扁平快照，通常只读\u003C\u002Ftd>\u003Ctd>一主多仆，主对象是唯一入口\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>捆完给谁看\u003C\u002Ftd>\u003Ctd>调用方（统一遍历）\u003C\u002Ftd>\u003Ctd>网络对面的客户端\u003C\u002Ftd>\u003Ctd>远程调用方只见主对象\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>出身\u003C\u002Ftd>\u003Ctd>GoF 1994\u003C\u002Ftd>\u003Ctd>J2EE 蓝皮书\u003C\u002Ftd>\u003Ctd>J2EE 蓝皮书\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>一句话\u003C\u002Ftd>\u003Ctd>树里树外一个样\u003C\u002Ftd>\u003Ctd>一次打包一份快照\u003C\u002Ftd>\u003Ctd>要进整窝进，要出整窝出\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n⑨ 和 ㉘ 名字最容易混：**⑨ 捆的是结构（对外统一接口），㉘ 捆的是生命周期（对内统一管理）**——一个组合实体内部用什么结构组织依赖对象，随便，⑨ 甚至可以当实现细节用上。㉝ 和 ㉘ 则是黄金搭档：组合实体住在服务器上管生命周期，传输对象负责把数据快照递到客户端手里——本篇 describe() 已经隐约有它的影子，㉝ 正式展开。\n\n## 写在最后\n\n- 组合实体 = 远程边界上的粗粒度对象：一窝依赖对象，一次加载、一次提交\n- 疗效：读放大（N+1）变一次往返；写散沙变事务边界；客户端不再被迫懂内部构成\n- 代价：加载变重、锁面变大——这笔钱只为远程边界付，本地进程别硬套\n- 铁律：依赖对象没有远程身份，业务规则住在依赖对象身上\n- 当代转世：JPA 聚合映射、Spring Data 按聚合根建仓、DDD 聚合根——N+1 问题在 ORM 时代等你\n- 三兄弟对账：⑨ 捆结构、㉝ 捆数据、㉘ 捆生命周期——名字像，账要分清\n\n组合实体把\"一次加载、一次提交\"管得漂漂亮亮——可回头看看 CustomerEntity：load() 里拆字符串、commit() 里拼字符串，SELECT、UPDATE 的痕迹全漏进实体里了。业务对象不该懂数据库，这坨\"怎么存\"的细节谁来收走？——数据访问对象模式，下一篇见。\n","\u003Cblockquote>\n \u003Cp>J2EE 模式第三站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉗ 的订单实体只有三个字段，一次远程调用就传输完毕——这是实体世界的\"好养型\"。可真实的领域模型里，\"客户\"这种实体由地址、信用、联系方式、订单历史一堆小对象组成；远程场景下，每个小对象都可能是独立的网络往返。\u003C\u002Fp>\n\u003Cp>EJB 时代这句话是字面意义的：entity bean 本身就是远程对象，对象图原样铺到远程，调用图就跟着变成远程调用图——看一个客户，四次网络往返起步；搬个家再提个额，两次独立提交，第二次失败，数据库里一半新一半旧。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>把一窝依赖对象捆成一个粗粒度实体，统一加载、统一提交、统一管理生命周期\u003C\u002Fstrong>——组合实体模式，J2EE 八式里的打包员。\u003C\u002Fp>\n\u003Ch2>一、组合实体模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：组合实体（Composite Entity）是一个\u003Cstrong>粗粒度实体\u003C\u002Fstrong>，它管理一组\u003Cstrong>细粒度依赖对象\u003C\u002Fstrong>（dependent objects）的生命周期——加载、修改、提交，全部以它为边界；远程调用方只见它这一个入口，永远不直接接触依赖对象。\u003C\u002Fp>\n\u003Cp>三个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Composite Entity（组合实体）\u003C\u002Ftd>\n   \u003Ctd>粗粒度入口：一次加载、一次提交整组依赖对象\u003C\u002Ftd>\n   \u003Ctd>CustomerEntity\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Dependent Object（依赖对象）\u003C\u002Ftd>\n   \u003Ctd>细粒度 POJO，躲在实体身后，没有远程身份\u003C\u002Ftd>\n   \u003Ctd>Address \u002F Credit \u002F Contact\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Coarse Grained Object（粗粒度对象）\u003C\u002Ftd>\n   \u003Ctd>经典分层里实际持有依赖对象的一层；单组依赖时与实体合并\u003C\u002Ftd>\n   \u003Ctd>本文并入 CustomerEntity\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>依赖对象说业务话（\"提额不能超现有额度一半\"），组合实体说边界话（\"要进来整窝一起进，要出去整窝一起出\"），远程世界只见这一个门。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>注意它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——诞生动机很具体：细粒度 entity bean 在分布式环境里是性能灾难，蓝皮书用\"粗粒度\"给实体世界立了新规矩。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>网络往返从 N+1 到 1\u003C\u002Fstrong>：反面教材里看一个客户四次往返，组合实体一次——读放大直接消失。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>写有事务边界\u003C\u002Fstrong>：整组要么全落库、要么全不动；\"搬家成功提额失败\"的一半新一半旧不可能出现。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>客户端不再懂内部构成\u003C\u002Fstrong>：客户由哪几块组成、先加载谁后加载谁，是实体自己的家事——封装回到了实体世界。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>依赖对象可以自由演化\u003C\u002Fstrong>：明天给客户加\"会员等级\"，只动实体内部，远程调用方一行不改。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>一次加载可能过重\u003C\u002Fstrong>：用不到的字段也跟着来——懒加载能缓解，但懒加载会重新引入网络往返（第五节 Hibernate N+1 见分晓），尺度得自己拿捏。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>并发冲突面变大\u003C\u002Fstrong>：两个人同时改同一个客户的不同部分，在整组边界上排队等锁——捆得越粗，锁面越大。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>常需配套传输对象\u003C\u002Fstrong>：组合实体住在服务器上，客户端最终要的是可序列化的数据快照——这对黄金搭档 ㉝ 再展开。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>远程边界 + 领域对象是一张图（主对象带一群小对象）+ 这群小对象常被整体使用\u003C\u002Fstrong>——三条都占，就值得捆。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>远程实体访问\u003C\u002Fstrong>：EJB、RMI——本模式出生场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>一次业务操作天然跨多个小对象\u003C\u002Fstrong>：搬家 + 提额、改账单地址 + 改通知偏好——分散提交没有事务语义\u003C\u002Fli>\n \u003Cli>\u003Cstrong>跨服务按聚合取数\u003C\u002Fstrong>：微服务间想要\"完整的客户\"，与其开一堆 GET 端点，不如一个聚合接口\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>本地进程内\u003C\u002Fstrong>——直接用 ⑨ 组合模式的对象图就好，粗粒度是替远程边界付的成本，本地享受不到收益只剩缺点；\u003Cstrong>对象图巨大且访问模式差异大\u003C\u002Fstrong>——客户 + 全部订单历史（几万条），整体加载是灾难，该拆成两个聚合、按需另取。\u003C\u002Fp>\n\u003Ch2>四、实现方式：搬家 + 提额\u003C\u002Fh2>\n\u003Cp>场景：客户\"张三\"由名字、地址、信用、联系方式组成。业务操作：\u003Cstrong>搬家 + 提额\u003C\u002Fstrong>——一次操作动两个小对象，正是组合实体的主场。\u003C\u002Fp>\n\u003Ch3>4.1 反面教材：细粒度远程访问\u003C\u002Fh3>\n\u003Cp>先看依赖对象——纯 POJO，业务规则住自己身上（这一半是对的，问题出在访问方式）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Credit {\n\n    private final int score;\n    private int creditLimit;\n\n    public void raiseLimit(int delta) {\n        if (delta &lt;= 0) {\n            throw new IllegalArgumentException(\"提额幅度必须为正数\");\n        }\n        if (delta &gt; creditLimit \u002F 2) {\n            throw new IllegalArgumentException(\"单次提额不能超过现有额度的 50%\");\n        }\n        creditLimit += delta;\n    }\n    \u002F\u002F format\u002Fparse：模拟数据库一列的存取，略\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>模拟远程持久层：业务层在另一台服务器上，\u003Cstrong>每个方法调用 = 一次网络往返\u003C\u002Fstrong>。它提供两套接口——细粒度（每个小对象单独读写）和整行（一次读写整组）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class CustomerDataStore {\n\n    \u002F\u002F ---------- 细粒度接口：一个小对象 = 一次网络往返 ----------\n\n    public String loadName(String id) {\n        roundTrip(\"SELECT name FROM customer WHERE id='\" + id + \"'\");\n        return row(id)[0];\n    }\n\n    public Address loadAddress(String id) { \u002F* 同款，略 *\u002F }\n    public Credit loadCredit(String id) { \u002F* 同款，略 *\u002F }\n    public Contact loadContact(String id) { \u002F* 同款，略 *\u002F }\n\n    public void saveAddress(String id, Address address) {\n        roundTrip(\"UPDATE customer SET address=? WHERE id='\" + id + \"'\");\n        maybeFail();                     \u002F\u002F 模拟网络抖动：写可能失败\n        row(id)[1] = address.format();\n    }\n\n    public void saveCredit(String id, Credit credit) { \u002F* 同款，略 *\u002F }\n\n    private void roundTrip(String sql) {\n        System.out.println(\"      [网络往返] \" + sql + \" ... 220ms\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>再看表示层门户代码的日常——\u003Cstrong>错不在门户程序员，他拿到的接口长这样，只能这么写\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadPortal {\n\n    private final CustomerDataStore store;\n\n    public void showCustomer(String id) {\n        String name = store.loadName(id);\n        Address address = store.loadAddress(id);\n        Credit credit = store.loadCredit(id);\n        Contact contact = store.loadContact(id);\n        System.out.println(\"      \" + name + \" | \" + address + \" | 信用分 \" + credit.getScore()\n                + \" 额度 \" + credit.getCreditLimit() + \"元 | \" + contact.getPhone());\n    }\n\n    \u002F** 一次业务操作 = 搬家 + 提额。细粒度接口下被拆成三次远程调用、两次独立提交 *\u002F\n    public void moveAndRaise(String id, String street, String city, String zip, int delta) {\n        store.saveAddress(id, new Address(street, city, zip));   \u002F\u002F 远程写 1\n        Credit credit = store.loadCredit(id);                    \u002F\u002F 远程读\n        credit.raiseLimit(delta);\n        store.saveCredit(id, credit);                            \u002F\u002F 远程写 2：失败就惨了\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：细粒度远程访问 ==========\n-- 场景 1：查看一个客户 --\n      [网络往返] SELECT name FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT address FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT contact FROM customer WHERE id='C001' ... 220ms\n      张三 | 人民路 1 号（杭州） | 信用分 650 额度 50000元 | 13800000001\n   小结：看 1 个客户 = 4 次网络往返（读放大）\n\n-- 场景 2：搬家 + 提额（一次业务操作）--\n      [网络往返] UPDATE customer SET address=? WHERE id='C001' ... 220ms\n      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\n      [网络往返] UPDATE customer SET credit=? WHERE id='C001' ... 220ms\n   [门户] 第二次远程写失败：网络抖动：提交超时\n\n-- 事后核查（又是 4 次往返）--\n      [网络往返] SELECT name FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT address FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\n      [网络往返] SELECT contact FROM customer WHERE id='C001' ... 220ms\n      张三 | 西湖大道 88 号（杭州） | 信用分 650 额度 50000元 | 13800000001\n   小结：地址是新的、额度是旧的——一半新一半旧，没有事务边界\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个病灶全齐：\u003Cstrong>读放大\u003C\u002Fstrong>（看 1 个客户 4 次往返，EJB 时代 220ms 一次，近一秒耗在网络上）；\u003Cstrong>写不一致\u003C\u002Fstrong>（搬家成功、提额失败，数据库里一半新一半旧，还没法重试——重试等于再搬一次家）；\u003Cstrong>拼装逻辑散落\u003C\u002Fstrong>（\"客户由哪几块组成\"这个知识被逼着写在每个调用方里）。\u003C\u002Fp>\n\u003Ch3>4.2 正解：组合实体\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>CustomerEntity\u003C\u002Fstrong>：一个粗粒度对象管住整组依赖对象——加载时组装、修改时在内存、提交时打包：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class CustomerEntity {\n\n    private final String id;\n    private final CustomerDataStore store;\n\n    private String name;\n    private Address address;   \u002F\u002F 依赖对象 1\n    private Credit credit;     \u002F\u002F 依赖对象 2\n    private Contact contact;   \u002F\u002F 依赖对象 3\n\n    \u002F** 一次网络往返，整组依赖对象全部就位 *\u002F\n    public void load() {\n        String[] row = store.loadRow(id);\n        name = row[0];\n        address = Address.parse(row[1]);\n        credit = Credit.parse(row[2]);\n        contact = Contact.parse(row[3]);\n    }\n\n    \u002F** 一次网络往返，整组原子提交——要么全落库，要么全不动 *\u002F\n    public void commit() {\n        store.saveRow(id, new String[]{name, address.format(), credit.format(), contact.format()});\n    }\n\n    public void move(String street, String city, String zip) {\n        address = new Address(street, city, zip);\n    }\n\n    public void raiseCreditLimit(int delta) {\n        credit.raiseLimit(delta);\n    }\n\n    public String describe() { \u002F* 一行拼好整组，略 *\u002F }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>调用方的画风\u003C\u002Fstrong>，只见一个门：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">CustomerEntity customer = new CustomerEntity(\"C001\", store);\n\ncustomer.load();                                  \u002F\u002F 1 次往返，整组就位\ncustomer.move(\"西湖大道 88 号\", \"杭州\", \"310000\");  \u002F\u002F 改内存，不碰网络\ncustomer.raiseCreditLimit(5000);                  \u002F\u002F 改内存，不碰网络\ncustomer.commit();                                \u002F\u002F 1 次往返，整组提交\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出（故意让第一次提交失败）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：组合实体 ==========\n-- 加载客户 C001 --\n      [网络往返] SELECT * FROM customer WHERE id='C001' ... 220ms\n   客户视图：张三 | 人民路 1 号（杭州） | 信用分 650 额度 50000元 | 13800000001\n   小结：1 次网络往返，整组依赖对象就位\n\n-- 业务操作：搬家 + 提额 --\n      [网络往返] UPDATE customer SET name=?,address=?,credit=?,contact=? WHERE id='C001' ... 220ms\n   [组合实体] 提交失败：网络抖动：提交超时——整组未落库，重试一次\n      [网络往返] UPDATE customer SET name=?,address=?,credit=?,contact=? WHERE id='C001' ... 220ms\n   提交后：张三 | 西湖大道 88 号（杭州） | 信用分 650 额度 55000元 | 13800000001\n   小结：一次加载、一次提交，业务操作有了事务边界\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处对照读出含金量：加载\u003Cstrong>一次往返整组就位\u003C\u002Fstrong>（反面教材四次）；搬家和提额在\u003Cstrong>内存里完成\u003C\u002Fstrong>，全程零网络；提交失败后\u003Cstrong>整组原封不动\u003C\u002Fstrong>，重试提交的是同一个完整操作——反面教材那个\"一半新一半旧\"的僵局，在这里结构上就不可能发生。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：依赖对象没有远程身份。\u003C\u002Fstrong> 回头数数：Address、Credit、Contact 从头到尾没有被远程直接访问过——远程世界只见 CustomerEntity 一个门。这是组合实体的铁律：\u003Cstrong>依赖对象的可见性止步于实体边界\u003C\u002Fstrong>。业务规则（提额上限 50%）住在依赖对象身上，不管谁经手，规则绕不过去。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：一次提交就是事务边界。\u003C\u002Fstrong> 反面教材的病灶二，根治靠的不是\"加重试\"而是\"改提交单位\"：细粒度提交时重试等于重复副作用（再搬一次家）；整组提交时重试是安全重放（提交失败 = 什么都没发生，重放同一个完整提交——真实世界还要配幂等键，防\"超时但其实成功\"的悬案）。经典 EJB 里容器管理的事务（CMT）直接以实体边界为事务边界——\u003Cstrong>实体边界 = 事务边界 = 锁边界\u003C\u002Fstrong>，一体三面，这也是缺点二\"锁面变大\"的来源。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：经典结构还能再分一层，加载策略还有两派。\u003C\u002Fstrong> 蓝皮书完整版是 CompositeEntity → CoarseGrainedObject → DependentObject 三层：实体背后还有一个\"粗粒度对象\"实际持有依赖对象。实践里一个实体只带一组依赖对象时，实体自己就是粗粒度对象（本文做法），带多组时才值得分层。加载策略则分饿汉\u002F懒汉两派：本文 load() 是饿汉——一次全来，最省往返；懒汉是首次访问某个依赖对象才加载它——省流量但重新引入网络往返，第五节会看到它在 ORM 里的著名翻车现场。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>JPA \u002F Hibernate 的实体关联\u003C\u002Fstrong>：@OneToMany、@ElementCollection 把对象图映射成表关系；fetch = EAGER 就是饿汉组合实体，LAZY 就是懒汉。而 Hibernate 著名的 \u003Cstrong>N+1 查询问题\u003C\u002Fstrong>——懒加载一对多，遍历时每行触发一次查询——正是反面教材的读放大在 ORM 里的转世。解药 JOIN FETCH、@EntityGraph，本质都是把数据访问掰回\"一次往返、整组就位\"。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring Data 的聚合根仓库\u003C\u002Fstrong>：官方文档明说 Repository 应该定义在\u003Cstrong>聚合根\u003C\u002Fstrong>上，OrderItem 这样的从属对象不配拥有自己的 Repository——外部想改订单项？先拿订单聚合，再从聚合内部改。这就是 DDD 聚合根思想：一个聚合一个根，根管整组，\u003Cstrong>事务一致性以聚合为单位\u003C\u002Fstrong>——组合实体模式的学院派后代，连\"依赖对象不对外\"这条铁律都原样继承。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>MyBatis 的聚合映射\u003C\u002Fstrong>：resultMap 里的 \u003Ccode>&lt;association&gt;\u003C\u002Fcode>、\u003Ccode>&lt;collection&gt;\u003C\u002Fcode>，一次 JOIN 把客户、地址、联系方式映射成完整对象图——\"一次查询组装整组\"在 SQL 映射层的实现，连本文 format\u002Fparse 手工拼装的动作都替你自动化了。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>GraphQL\u003C\u002Fstrong>：读侧同类问题的当代解——REST 时代看一个页面开 N 个端点，GraphQL 一次请求声明整棵需要的树。注意方向相反：组合实体是\u003Cstrong>服务器\u003C\u002Fstrong>定义打包粒度，GraphQL 是\u003Cstrong>客户端\u003C\u002Fstrong>声明要什么——但\"别让我为一张图跑 N 趟\"的诉求，三十年没变过。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、组合实体 vs 组合模式 vs 传输对象——\"捆东西\"三兄弟对账\u003C\u002Fh2>\n\u003Cp>组合实体和 ⑨ 组合模式都姓\"组合\"，和 ㉝ 传输对象都干\"打包\"——摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>组合模式（⑨）\u003C\u002Fth>\n   \u003Cth>传输对象（㉝）\u003C\u002Fth>\n   \u003Cth>组合实体（㉘）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>捆的是什么\u003C\u002Ftd>\n   \u003Ctd>**结构**：部分与整体同一接口\u003C\u002Ftd>\n   \u003Ctd>**数据**：一次网络搬一坨字段\u003C\u002Ftd>\n   \u003Ctd>**生命周期**：加载、提交以组为单位\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>结构形态\u003C\u002Ftd>\n   \u003Ctd>树形，可递归嵌套\u003C\u002Ftd>\n   \u003Ctd>扁平快照，通常只读\u003C\u002Ftd>\n   \u003Ctd>一主多仆，主对象是唯一入口\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>捆完给谁看\u003C\u002Ftd>\n   \u003Ctd>调用方（统一遍历）\u003C\u002Ftd>\n   \u003Ctd>网络对面的客户端\u003C\u002Ftd>\n   \u003Ctd>远程调用方只见主对象\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>出身\u003C\u002Ftd>\n   \u003Ctd>GoF 1994\u003C\u002Ftd>\n   \u003Ctd>J2EE 蓝皮书\u003C\u002Ftd>\n   \u003Ctd>J2EE 蓝皮书\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>一句话\u003C\u002Ftd>\n   \u003Ctd>树里树外一个样\u003C\u002Ftd>\n   \u003Ctd>一次打包一份快照\u003C\u002Ftd>\n   \u003Ctd>要进整窝进，要出整窝出\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>⑨ 和 ㉘ 名字最容易混：\u003Cstrong>⑨ 捆的是结构（对外统一接口），㉘ 捆的是生命周期（对内统一管理）\u003C\u002Fstrong>——一个组合实体内部用什么结构组织依赖对象，随便，⑨ 甚至可以当实现细节用上。㉝ 和 ㉘ 则是黄金搭档：组合实体住在服务器上管生命周期，传输对象负责把数据快照递到客户端手里——本篇 describe() 已经隐约有它的影子，㉝ 正式展开。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>组合实体 = 远程边界上的粗粒度对象：一窝依赖对象，一次加载、一次提交\u003C\u002Fli>\n \u003Cli>疗效：读放大（N+1）变一次往返；写散沙变事务边界；客户端不再被迫懂内部构成\u003C\u002Fli>\n \u003Cli>代价：加载变重、锁面变大——这笔钱只为远程边界付，本地进程别硬套\u003C\u002Fli>\n \u003Cli>铁律：依赖对象没有远程身份，业务规则住在依赖对象身上\u003C\u002Fli>\n \u003Cli>当代转世：JPA 聚合映射、Spring Data 按聚合根建仓、DDD 聚合根——N+1 问题在 ORM 时代等你\u003C\u002Fli>\n \u003Cli>三兄弟对账：⑨ 捆结构、㉝ 捆数据、㉘ 捆生命周期——名字像，账要分清\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>组合实体把\"一次加载、一次提交\"管得漂漂亮亮——可回头看看 CustomerEntity：load() 里拆字符串、commit() 里拼字符串，SELECT、UPDATE 的痕迹全漏进实体里了。业务对象不该懂数据库，这坨\"怎么存\"的细节谁来收走？——数据访问对象模式，下一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113935321048551425","design-pattern-33","设计模式修炼之路 ㉝ 传输对象模式——一趟网络，带走整份数据","J2EE 模式第八站，也是 J2EE 八式的收官一站。",2516,8059,21,"2025-07-09T21:16:41",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113935074440253441","design-pattern-29","设计模式修炼之路 ㉙ 数据访问对象模式——业务对象不该懂数据库","业务对象不该懂数据库、接口只说业务语言、换实现不换业务、单测不连库、Spring Data 工业化 DAO、DAO 与 Repository 对账",3913,6821,18,"2025-04-15T11:27:29",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113934922031828994","design-pattern-27","设计模式修炼之路 ㉗ 业务代表模式——替表示层挡住远程的脏活","替表示层挡住远程脏活、查找+重试+异常翻译一肩挑、重试风暴、外观\u002F代理\u002F业务代表三兄弟对账、Feign 转世",1811,6147,16,"2025-03-25T21:44:13",{"code":4,"message":5,"data":73},{"prev":74,"next":75},{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},{"code":4,"message":5,"data":77},[78,84,89,95],{"id":79,"name":80,"slug":81,"description":82,"sortOrder":4,"articleCount":83},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":85,"slug":86,"description":87,"sortOrder":4,"articleCount":88},"设计模式","design-pattern","设计模式系列","34",{"id":90,"name":91,"slug":92,"description":93,"sortOrder":33,"articleCount":94},"1946000000000000002","随笔","essay","生活与思考","0",{"id":96,"name":97,"slug":98,"description":99,"sortOrder":100,"articleCount":101},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":103,"toc":104},"\u003Cblockquote>\n \u003Cp>J2EE 模式第三站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉗ 的订单实体只有三个字段，一次远程调用就传输完毕——这是实体世界的\"好养型\"。可真实的领域模型里，\"客户\"这种实体由地址、信用、联系方式、订单历史一堆小对象组成；远程场景下，每个小对象都可能是独立的网络往返。\u003C\u002Fp>\n\u003Cp>EJB 时代这句话是字面意义的：entity bean 本身就是远程对象，对象图原样铺到远程，调用图就跟着变成远程调用图——看一个客户，四次网络往返起步；搬个家再提个额，两次独立提交，第二次失败，数据库里一半新一半旧。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>把一窝依赖对象捆成一个粗粒度实体，统一加载、统一提交、统一管理生命周期\u003C\u002Fstrong>——组合实体模式，J2EE 八式里的打包员。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、组合实体模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：组合实体（Composite Entity）是一个\u003Cstrong>粗粒度实体\u003C\u002Fstrong>，它管理一组\u003Cstrong>细粒度依赖对象\u003C\u002Fstrong>（dependent objects）的生命周期——加载、修改、提交，全部以它为边界；远程调用方只见它这一个入口，永远不直接接触依赖对象。\u003C\u002Fp>\n\u003Cp>三个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Composite Entity（组合实体）\u003C\u002Ftd>\n   \u003Ctd>粗粒度入口：一次加载、一次提交整组依赖对象\u003C\u002Ftd>\n   \u003Ctd>CustomerEntity\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Dependent Object（依赖对象）\u003C\u002Ftd>\n   \u003Ctd>细粒度 POJO，躲在实体身后，没有远程身份\u003C\u002Ftd>\n   \u003Ctd>Address \u002F Credit \u002F Contact\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Coarse Grained Object（粗粒度对象）\u003C\u002Ftd>\n   \u003Ctd>经典分层里实际持有依赖对象的一层；单组依赖时与实体合并\u003C\u002Ftd>\n   \u003Ctd>本文并入 CustomerEntity\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>依赖对象说业务话（\"提额不能超现有额度一半\"），组合实体说边界话（\"要进来整窝一起进，要出去整窝一起出\"），远程世界只见这一个门。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>注意它同样不是 GoF 二十三式，是 J2EE 蓝皮书成员——诞生动机很具体：细粒度 entity bean 在分布式环境里是性能灾难，蓝皮书用\"粗粒度\"给实体世界立了新规矩。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>网络往返从 N+1 到 1\u003C\u002Fstrong>：反面教材里看一个客户四次往返，组合实体一次——读放大直接消失。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>写有事务边界\u003C\u002Fstrong>：整组要么全落库、要么全不动；\"搬家成功提额失败\"的一半新一半旧不可能出现。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>客户端不再懂内部构成\u003C\u002Fstrong>：客户由哪几块组成、先加载谁后加载谁，是实体自己的家事——封装回到了实体世界。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>依赖对象可以自由演化\u003C\u002Fstrong>：明天给客户加\"会员等级\"，只动实体内部，远程调用方一行不改。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>一次加载可能过重\u003C\u002Fstrong>：用不到的字段也跟着来——懒加载能缓解，但懒加载会重新引入网络往返（第五节 Hibernate N+1 见分晓），尺度得自己拿捏。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>并发冲突面变大\u003C\u002Fstrong>：两个人同时改同一个客户的不同部分，在整组边界上排队等锁——捆得越粗，锁面越大。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>常需配套传输对象\u003C\u002Fstrong>：组合实体住在服务器上，客户端最终要的是可序列化的数据快照——这对黄金搭档 ㉝ 再展开。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>远程边界 + 领域对象是一张图（主对象带一群小对象）+ 这群小对象常被整体使用\u003C\u002Fstrong>——三条都占，就值得捆。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>远程实体访问\u003C\u002Fstrong>：EJB、RMI——本模式出生场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>一次业务操作天然跨多个小对象\u003C\u002Fstrong>：搬家 + 提额、改账单地址 + 改通知偏好——分散提交没有事务语义\u003C\u002Fli>\n \u003Cli>\u003Cstrong>跨服务按聚合取数\u003C\u002Fstrong>：微服务间想要\"完整的客户\"，与其开一堆 GET 端点，不如一个聚合接口\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>本地进程内\u003C\u002Fstrong>——直接用 ⑨ 组合模式的对象图就好，粗粒度是替远程边界付的成本，本地享受不到收益只剩缺点；\u003Cstrong>对象图巨大且访问模式差异大\u003C\u002Fstrong>——客户 + 全部订单历史（几万条），整体加载是灾难，该拆成两个聚合、按需另取。\u003C\u002Fp>\n\u003Ch2 id=\"toc-4\">四、实现方式：搬家 + 提额\u003C\u002Fh2>\n\u003Cp>场景：客户\"张三\"由名字、地址、信用、联系方式组成。业务操作：\u003Cstrong>搬家 + 提额\u003C\u002Fstrong>——一次操作动两个小对象，正是组合实体的主场。\u003C\u002Fp>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：细粒度远程访问\u003C\u002Fh3>\n\u003Cp>先看依赖对象——纯 POJO，业务规则住自己身上（这一半是对的，问题出在访问方式）：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Credit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> score;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> creditLimit;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> raiseLimit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> delta\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (delta \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"提额幅度必须为正数\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (delta \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> creditLimit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"单次提额不能超过现有额度的 50%\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        creditLimit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> delta;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F format\u002Fparse：模拟数据库一列的存取，略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>模拟远程持久层：业务层在另一台服务器上，\u003Cstrong>每个方法调用 = 一次网络往返\u003C\u002Fstrong>。它提供两套接口——细粒度（每个小对象单独读写）和整行（一次读写整组）：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CustomerDataStore\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F ---------- 细粒度接口：一个小对象 = 一次网络往返 ----------\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SELECT name FROM customer WHERE id='\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"'\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> row\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id)[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">];\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Address \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadAddress\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 同款，略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Credit \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadCredit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 同款，略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Contact \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadContact\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 同款，略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> saveAddress\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, Address \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">address\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"UPDATE customer SET address=? WHERE id='\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"'\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        maybeFail\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();                     \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 模拟网络抖动：写可能失败\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        row\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id)[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">] \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> address.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">format\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> saveCredit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, Credit \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">credit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 同款，略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">sql\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"      [网络往返] \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sql \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" ... 220ms\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>再看表示层门户代码的日常——\u003Cstrong>错不在门户程序员，他拿到的接口长这样，只能这么写\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadPortal\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> CustomerDataStore store;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> showCustomer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Address address \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadAddress\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Credit credit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadCredit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Contact contact \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadContact\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"      \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" | \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> address \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" | 信用分 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> credit.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getScore\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                +\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 额度 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> credit.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getCreditLimit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"元 | \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> contact.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 一次业务操作 = 搬家 + 提额。细粒度接口下被拆成三次远程调用、两次独立提交 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> moveAndRaise\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">street\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">city\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">zip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> delta\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">saveAddress\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Address\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(street, city, zip));   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 远程写 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Credit credit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadCredit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);                    \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 远程读\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        credit.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">raiseLimit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(delta);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">saveCredit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id, credit);                            \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 远程写 2：失败就惨了\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 反面教材：细粒度远程访问 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 场景 1：查看一个客户 --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT name FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT address FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT contact FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      张三 | 人民路 1 号（杭州） | 信用分 650 额度 50000元 | 13800000001\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   小结：看 1 个客户 = 4 次网络往返（读放大）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 场景 2：搬家 + 提额（一次业务操作）--\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] UPDATE customer SET address=? WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] UPDATE customer SET credit=? WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [门户] 第二次远程写失败：网络抖动：提交超时\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 事后核查（又是 4 次往返）--\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT name FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT address FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT credit FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT contact FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      张三 | 西湖大道 88 号（杭州） | 信用分 650 额度 50000元 | 13800000001\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   小结：地址是新的、额度是旧的——一半新一半旧，没有事务边界\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个病灶全齐：\u003Cstrong>读放大\u003C\u002Fstrong>（看 1 个客户 4 次往返，EJB 时代 220ms 一次，近一秒耗在网络上）；\u003Cstrong>写不一致\u003C\u002Fstrong>（搬家成功、提额失败，数据库里一半新一半旧，还没法重试——重试等于再搬一次家）；\u003Cstrong>拼装逻辑散落\u003C\u002Fstrong>（\"客户由哪几块组成\"这个知识被逼着写在每个调用方里）。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：组合实体\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>CustomerEntity\u003C\u002Fstrong>：一个粗粒度对象管住整组依赖对象——加载时组装、修改时在内存、提交时打包：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CustomerEntity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String id;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> CustomerDataStore store;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Address address;   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 依赖对象 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Credit credit;     \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 依赖对象 2\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Contact contact;   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 依赖对象 3\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 一次网络往返，整组依赖对象全部就位 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[] row \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadRow\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">];\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        address \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Address.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">]);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        credit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Credit.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">]);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        contact \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Contact.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">]);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 一次网络往返，整组原子提交——要么全落库，要么全不动 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> commit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">saveRow\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[]{name, address.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">format\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), credit.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">format\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), contact.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">format\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()});\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> move\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">street\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">city\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">zip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        address \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Address\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(street, city, zip);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> raiseCreditLimit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> delta\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        credit.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">raiseLimit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(delta);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">describe\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 一行拼好整组，略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>调用方的画风\u003C\u002Fstrong>，只见一个门：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">CustomerEntity customer \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CustomerEntity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"C001\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, store);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">customer.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">load\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();                                  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 1 次往返，整组就位\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">customer.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">move\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"西湖大道 88 号\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"杭州\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"310000\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 改内存，不碰网络\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">customer.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">raiseCreditLimit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">5000\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);                  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 改内存，不碰网络\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">customer.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">commit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();                                \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 1 次往返，整组提交\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出（故意让第一次提交失败）：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 正解：组合实体 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 加载客户 C001 --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] SELECT * FROM customer WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   客户视图：张三 | 人民路 1 号（杭州） | 信用分 650 额度 50000元 | 13800000001\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   小结：1 次网络往返，整组依赖对象就位\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 业务操作：搬家 + 提额 --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] UPDATE customer SET name=?,address=?,credit=?,contact=? WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [组合实体] 提交失败：网络抖动：提交超时——整组未落库，重试一次\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      [网络往返] UPDATE customer SET name=?,address=?,credit=?,contact=? WHERE id='C001' ... 220ms\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   提交后：张三 | 西湖大道 88 号（杭州） | 信用分 650 额度 55000元 | 13800000001\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   小结：一次加载、一次提交，业务操作有了事务边界\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处对照读出含金量：加载\u003Cstrong>一次往返整组就位\u003C\u002Fstrong>（反面教材四次）；搬家和提额在\u003Cstrong>内存里完成\u003C\u002Fstrong>，全程零网络；提交失败后\u003Cstrong>整组原封不动\u003C\u002Fstrong>，重试提交的是同一个完整操作——反面教材那个\"一半新一半旧\"的僵局，在这里结构上就不可能发生。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：依赖对象没有远程身份。\u003C\u002Fstrong> 回头数数：Address、Credit、Contact 从头到尾没有被远程直接访问过——远程世界只见 CustomerEntity 一个门。这是组合实体的铁律：\u003Cstrong>依赖对象的可见性止步于实体边界\u003C\u002Fstrong>。业务规则（提额上限 50%）住在依赖对象身上，不管谁经手，规则绕不过去。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：一次提交就是事务边界。\u003C\u002Fstrong> 反面教材的病灶二，根治靠的不是\"加重试\"而是\"改提交单位\"：细粒度提交时重试等于重复副作用（再搬一次家）；整组提交时重试是安全重放（提交失败 = 什么都没发生，重放同一个完整提交——真实世界还要配幂等键，防\"超时但其实成功\"的悬案）。经典 EJB 里容器管理的事务（CMT）直接以实体边界为事务边界——\u003Cstrong>实体边界 = 事务边界 = 锁边界\u003C\u002Fstrong>，一体三面，这也是缺点二\"锁面变大\"的来源。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：经典结构还能再分一层，加载策略还有两派。\u003C\u002Fstrong> 蓝皮书完整版是 CompositeEntity → CoarseGrainedObject → DependentObject 三层：实体背后还有一个\"粗粒度对象\"实际持有依赖对象。实践里一个实体只带一组依赖对象时，实体自己就是粗粒度对象（本文做法），带多组时才值得分层。加载策略则分饿汉\u002F懒汉两派：本文 load() 是饿汉——一次全来，最省往返；懒汉是首次访问某个依赖对象才加载它——省流量但重新引入网络往返，第五节会看到它在 ORM 里的著名翻车现场。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>JPA \u002F Hibernate 的实体关联\u003C\u002Fstrong>：@OneToMany、@ElementCollection 把对象图映射成表关系；fetch = EAGER 就是饿汉组合实体，LAZY 就是懒汉。而 Hibernate 著名的 \u003Cstrong>N+1 查询问题\u003C\u002Fstrong>——懒加载一对多，遍历时每行触发一次查询——正是反面教材的读放大在 ORM 里的转世。解药 JOIN FETCH、@EntityGraph，本质都是把数据访问掰回\"一次往返、整组就位\"。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring Data 的聚合根仓库\u003C\u002Fstrong>：官方文档明说 Repository 应该定义在\u003Cstrong>聚合根\u003C\u002Fstrong>上，OrderItem 这样的从属对象不配拥有自己的 Repository——外部想改订单项？先拿订单聚合，再从聚合内部改。这就是 DDD 聚合根思想：一个聚合一个根，根管整组，\u003Cstrong>事务一致性以聚合为单位\u003C\u002Fstrong>——组合实体模式的学院派后代，连\"依赖对象不对外\"这条铁律都原样继承。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>MyBatis 的聚合映射\u003C\u002Fstrong>：resultMap 里的 \u003Ccode>&lt;association&gt;\u003C\u002Fcode>、\u003Ccode>&lt;collection&gt;\u003C\u002Fcode>，一次 JOIN 把客户、地址、联系方式映射成完整对象图——\"一次查询组装整组\"在 SQL 映射层的实现，连本文 format\u002Fparse 手工拼装的动作都替你自动化了。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>GraphQL\u003C\u002Fstrong>：读侧同类问题的当代解——REST 时代看一个页面开 N 个端点，GraphQL 一次请求声明整棵需要的树。注意方向相反：组合实体是\u003Cstrong>服务器\u003C\u002Fstrong>定义打包粒度，GraphQL 是\u003Cstrong>客户端\u003C\u002Fstrong>声明要什么——但\"别让我为一张图跑 N 趟\"的诉求，三十年没变过。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、组合实体 vs 组合模式 vs 传输对象——\"捆东西\"三兄弟对账\u003C\u002Fh2>\n\u003Cp>组合实体和 ⑨ 组合模式都姓\"组合\"，和 ㉝ 传输对象都干\"打包\"——摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>组合模式（⑨）\u003C\u002Fth>\n   \u003Cth>传输对象（㉝）\u003C\u002Fth>\n   \u003Cth>组合实体（㉘）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>捆的是什么\u003C\u002Ftd>\n   \u003Ctd>**结构**：部分与整体同一接口\u003C\u002Ftd>\n   \u003Ctd>**数据**：一次网络搬一坨字段\u003C\u002Ftd>\n   \u003Ctd>**生命周期**：加载、提交以组为单位\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>结构形态\u003C\u002Ftd>\n   \u003Ctd>树形，可递归嵌套\u003C\u002Ftd>\n   \u003Ctd>扁平快照，通常只读\u003C\u002Ftd>\n   \u003Ctd>一主多仆，主对象是唯一入口\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>捆完给谁看\u003C\u002Ftd>\n   \u003Ctd>调用方（统一遍历）\u003C\u002Ftd>\n   \u003Ctd>网络对面的客户端\u003C\u002Ftd>\n   \u003Ctd>远程调用方只见主对象\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>出身\u003C\u002Ftd>\n   \u003Ctd>GoF 1994\u003C\u002Ftd>\n   \u003Ctd>J2EE 蓝皮书\u003C\u002Ftd>\n   \u003Ctd>J2EE 蓝皮书\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>一句话\u003C\u002Ftd>\n   \u003Ctd>树里树外一个样\u003C\u002Ftd>\n   \u003Ctd>一次打包一份快照\u003C\u002Ftd>\n   \u003Ctd>要进整窝进，要出整窝出\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>⑨ 和 ㉘ 名字最容易混：\u003Cstrong>⑨ 捆的是结构（对外统一接口），㉘ 捆的是生命周期（对内统一管理）\u003C\u002Fstrong>——一个组合实体内部用什么结构组织依赖对象，随便，⑨ 甚至可以当实现细节用上。㉝ 和 ㉘ 则是黄金搭档：组合实体住在服务器上管生命周期，传输对象负责把数据快照递到客户端手里——本篇 describe() 已经隐约有它的影子，㉝ 正式展开。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>组合实体 = 远程边界上的粗粒度对象：一窝依赖对象，一次加载、一次提交\u003C\u002Fli>\n \u003Cli>疗效：读放大（N+1）变一次往返；写散沙变事务边界；客户端不再被迫懂内部构成\u003C\u002Fli>\n \u003Cli>代价：加载变重、锁面变大——这笔钱只为远程边界付，本地进程别硬套\u003C\u002Fli>\n \u003Cli>铁律：依赖对象没有远程身份，业务规则住在依赖对象身上\u003C\u002Fli>\n \u003Cli>当代转世：JPA 聚合映射、Spring Data 按聚合根建仓、DDD 聚合根——N+1 问题在 ORM 时代等你\u003C\u002Fli>\n \u003Cli>三兄弟对账：⑨ 捆结构、㉝ 捆数据、㉘ 捆生命周期——名字像，账要分清\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>组合实体把\"一次加载、一次提交\"管得漂漂亮亮——可回头看看 CustomerEntity：load() 里拆字符串、commit() 里拼字符串，SELECT、UPDATE 的痕迹全漏进实体里了。业务对象不该懂数据库，这坨\"怎么存\"的细节谁来收走？——数据访问对象模式，下一篇见。\u003C\u002Fp>",[105,108,111,114,117,121,124,127,130,133],{"id":106,"text":107,"level":100},"toc-1","一、组合实体模式是什么",{"id":109,"text":110,"level":100},"toc-2","二、优点与缺点",{"id":112,"text":113,"level":100},"toc-3","三、什么时候用",{"id":115,"text":116,"level":100},"toc-4","四、实现方式：搬家 + 提额",{"id":118,"text":119,"level":120},"toc-5","4.1 反面教材：细粒度远程访问",3,{"id":122,"text":123,"level":120},"toc-6","4.2 正解：组合实体",{"id":125,"text":126,"level":120},"toc-7","4.3 三处细节",{"id":128,"text":129,"level":100},"toc-8","五、在框架里找它",{"id":131,"text":132,"level":100},"toc-9","六、组合实体 vs 组合模式 vs 传输对象——\"捆东西\"三兄弟对账",{"id":134,"text":135,"level":100},"toc-10","写在最后"]