当业务从单库单表走向多租户、大订单量或历史数据持续增长时,分库分表很快会从“性能优化选项”变成数据模型的一部分。ORM Bee 3.0.0.7 的一个重要变化,是将多表关联与 Sharding 分片放到同一个使用场景中考虑,同时继续保留简单接口、原生分页、自定义 SQL 和 MongoDB ORM 等能力。
这类能力的价值不在于让开发者少写几行 SQL,而在于让分片后的数据访问仍然能够被阅读、检查和维护。尤其是在 AI 生成代码越来越普遍的情况下,代码量越大,越需要一个边界清晰、调用方式直接的 ORM 层。
多表关联为什么会放大分片难度
单表分片通常只需要回答一个问题:根据哪个字段计算分片键。例如订单表按 tenant_id 或 user_id 路由到不同数据表。
多表关联则多了几个约束:
- 关联表是否使用相同的分片键。
- JOIN 是否能够在同一个数据库节点内完成。
- 分页、排序和聚合是否需要跨节点合并。
- 一次请求是否会被路由到多个库,导致查询放大。
- 自定义 SQL 是否明确写出了分片条件。
可以这样理解:如果 orders 和 order_items 都按 tenant_id 分片,并且一次查询带有确定的租户条件,那么关联查询更容易落在同一个分片节点;如果两个表使用不同分片键,ORM 或应用层就可能需要执行多次查询,再自行合并结果,性能和一致性都会更难控制。
因此,多表分片不是简单地给表名加一个后缀。数据模型、路由规则和查询写法必须一起设计。
ORM Bee 适合解决什么问题
根据给出的版本摘要,ORM Bee 的定位比较务实:
- 提供简单直接的 ORM 接口,减少重复的数据访问代码。
- 支持原生分页,适合常见列表查询。
- 同时支持面向对象操作和自定义 SQL。
- 支持分库分表 Sharding。
- 支持 MongoDB ORM,并允许项目中同时使用多种数据库。
这种组合适合“简单功能快速开发,复杂功能保留 SQL 控制力”的团队。用户、配置、基础 CRUD 等模块可以使用对象化接口;报表、复杂 JOIN、批量查询或对执行计划敏感的场景,则可以切换到自定义 SQL。
需要注意的是,ORM 支持分片并不意味着所有跨分片查询都会自动变得高效。跨分片排序、全局分页、COUNT、GROUP BY 和跨库 JOIN 仍然需要根据实际路由能力进行验证。框架降低的是接入成本,不能替代分片键设计。
一个可改造的分片设计示例
下面是一个最小化示例。假设订单系统按 tenant_id 分片,订单主表和明细表使用同一个分片键。配置字段名称用于表达设计意图,具体配置键请以项目中 ORM Bee 3.0.0.7 的实际 API 和文档为准。
# sharding-example.yml
sharding:
shard-key: tenant_id
databases:
- name: order_db_0
url: jdbc:mysql://127.0.0.1:3306/order_0
- name: order_db_1
url: jdbc:mysql://127.0.0.1:3306/order_1
tables:
orders:
actual-pattern: order_db_${tenant_id % 2}.orders_${order_id % 8}
order_items:
actual-pattern: order_db_${tenant_id % 2}.order_items_${order_id % 8}
建表时,让关联表保留相同的路由字段:
CREATE TABLE orders_0 (
order_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (order_id),
KEY idx_orders_tenant_created (tenant_id, created_at)
);
CREATE TABLE order_items_0 (
item_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
sku VARCHAR(64) NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (item_id),
KEY idx_items_tenant_order (tenant_id, order_id)
);
查询时必须显式带上分片键。下面的 Java 代码是可改造的服务层示例,OrderRepository 代表 ORM Bee 中对应的对象化或 SQL 查询接口:
public final class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Page<OrderSummary> listOrders(long tenantId, int page, int size) {
if (page < 1 || size < 1 || size > 200) {
throw new IllegalArgumentException("invalid pagination");
}
// tenantId is the shard key. Keep it in every sharded-table query.
return orderRepository.queryPage(
"""
SELECT o.order_id, o.status, o.created_at,
COUNT(i.item_id) AS item_count
FROM orders o
LEFT JOIN order_items i
ON i.tenant_id = o.tenant_id
AND i.order_id = o.order_id
WHERE o.tenant_id = :tenantId
GROUP BY o.order_id, o.status, o.created_at
ORDER BY o.created_at DESC
""",
Map.of("tenantId", tenantId),
page,
size
);
}
}
这个示例有三个关键点:
orders和order_items使用同一个tenant_id。- JOIN 条件中同时带上
tenant_id和业务关联字段order_id。 - 分页查询使用稳定排序字段,避免数据变化时出现重复或遗漏。
如果查询没有分片键,例如只按 status = 'PAID' 查询,系统可能需要广播到多个分片。这样的查询不一定错误,但应当在接口设计阶段明确它的成本,并考虑维护汇总表、搜索索引或按租户拆分请求。
面向对象和自定义 SQL 如何共存
分片项目不适合把所有查询都强行封装成复杂的通用 DAO。可以按查询类型划分:
- CRUD、单条查询和简单条件组合使用 ORM 对象接口。
- 多表关联、复杂投影和报表查询使用自定义 SQL。
- 需要跨分片的数据分析任务交给异步任务或专门的分析存储。
- MongoDB 用于文档型数据,但不要默认把关系型订单和文档型数据放进同一个事务边界。
无论使用哪种接口,都建议把分片键作为方法参数,而不是藏在全局线程变量或隐式上下文中。例如,使用 listOrders(tenantId, page, size) 比使用一个没有租户参数的 listOrders(page, size) 更容易 Review,也更不容易误发起全库扫描。
AI 生成代码时,这一点尤其重要。代码评审可以直接检查:每个分片表查询是否携带分片键、是否存在没有边界的分页、是否把跨分片 JOIN 当成普通 JOIN 使用。
上线前的检查清单
- 明确每张表的分片键、分片算法和扩容策略。
- 让高频关联表尽量共享分片键。
- 为分片键、关联键和排序字段建立匹配索引。
- 测试单分片查询、广播查询和跨分片查询三种路径。
- 验证原生分页在排序、COUNT 和数据新增场景下的行为。
- 检查自定义 SQL 中是否遗漏分片键或引入隐式全表扫描。
- 为数据库连接池、慢查询日志和路由结果建立可观测性。
- 对 MongoDB 和关系型数据库分别定义一致性边界与失败重试策略。
ORM Bee 3.0.0.7 的多表关联分片能力,适合从单库架构逐步演进的业务。但采用时应把它看成一套数据访问约束,而不是一个只需打开开关的功能。先固定分片键和关联模型,再选择 ORM 接口或自定义 SQL,通常能让代码更短,也让后续排查更直接。