- UI组件
- 前端
【免费下载链接】ant-design-blazor
🌈A rich set of enterprise-class UI components based on Ant Design and Blazor.
导读
本文介绍 Ant Design Blazor 表格组件(Table)的RelationColumn(关联列)实验性能力:当表格中的某一列需要根据外键字段(如UserId)展示关联数据(如用户名)时,框架会自动收集所有需要加载的关联 ID,在数据加载完成后统一批量加载、自动去重、零反射取值,从根源上消除 N+1 查询问题。读完本文,你将掌握RelationComponentBase<TItem, TData>基类、[RelationColumn]特性标注、共享缓存三种使用姿势,并能结合源码理解其批量加载与渲染的完整调用链,直接在你的 Blazor 表格项目中落地。
一、它解决什么问题:N+1 查询与关联数据展示
在常规的表格开发中,如果某一列要根据外键展示关联信息,最常见的写法是在每行渲染时单独查询一次关联数据:
- 表格有 100 行数据;
- 每行渲染时执行一次关联查询(如根据
UserId查用户名); - 最终产生1 + 100 次数据库查询,即典型的 N+1 问题。
Ant Design Blazor 的 RelationColumn 提供了一种自动化方案:表格数据加载完成后,框架一次性收集当前页所有行需要的外键值集合,只执行一次(或少数几次)批量查询,然后把结果缓存起来供每一行渲染时读取。官方文档中给出了四个典型使用场景:
- 表格列需要展示关联数据(例如通过用户 ID 展示用户名);
- 需要批量加载关联数据以避免 N+1 查询;
- 需要让多个表格共享同一份关联数据缓存;
- 需要对关联数据的加载与渲染逻辑进行灵活控制。
从源码注释看,该能力定位为Experimental(实验性)特性(见 site/AntDesign.Docs/Demos/Experimental/TableRelationColumn/doc/index.en-US.md 的 front-matter),意味着 API 可能随版本演进调整,生产使用前建议关注版本更新。
二、核心特性与架构概览
RelationColumn 的核心特性可以概括为五点:
| 特性 | 说明 |
|---|---|
| 批量加载(Batch Loading) | 自动收集所有需要加载的关联 ID,一次批量加载,避免 N+1 查询 |
| 自动去重(Auto Deduplication) | 智能去重,同一个 ID 只会被加载一次 |
| 零反射(Zero Reflection) | 使用委托(delegate)访问字段值,避免反射开销 |
| 共享缓存(Shared Cache) | 通过RelationCache参数跨表格共享关联数据缓存 |
| 三种使用方式 | 支持 C# 类、Razor 组件、特性标注三种模式 |
整个机制涉及三个核心源码文件:
- RelationComponentBase.cs:所有关联组件的抽象基类,封装了批量加载、渲染与缓存逻辑;
- IRelationComponent.cs:框架内部使用的非泛型与泛型接口,让 Table 可以统一管理不同类型的关联组件;
- RelationColumnAttribute.cs:特性标注方式的核心,负责校验组件类型并动态生成渲染片段。
以 C# 类方式实现为例,最小结构如下(完整示例见 RelationComponentBase.cs 的 XML 注释):
public class UserRelation : RelationComponentBase<Order, int> { [Inject] private IUserService UserService { get; set; } private Dictionary<int, User> _userCache = new(); protected override async Task OnLoadBatch(IEnumerable<int> userIds) { var users = await UserService.GetUsersByIdsAsync(userIds); _userCache = users.ToDictionary(u => u.Id); } protected override RenderFragment RenderContent(int userId, Order order) { return builder => { if (_userCache.TryGetValue(userId, out var user)) builder.AddContent(0, user.Name); }; } }三、三种使用方式详解
方式一:C# 类继承RelationComponentBase<TItem, TData>
创建一个继承RelationComponentBase<Order, int>的类——第一个泛型参数TItem是表格行数据类型,第二个泛型参数TData是列字段值(外键)类型:
public class UserNameRelation : RelationComponentBase<Order, int> { protected override Task OnLoadBatch(IEnumerable<int> userIds) { // 批量加载用户数据,例如:UserService.GetUsersByIdsAsync(userIds) return Task.CompletedTask; } protected override RenderFragment RenderContent(int userId, Order order) { // 渲染单元格内容 return builder => builder.AddContent(0, "Username"); } }在表格中通过PropertyColumn的RelationContent区域使用:
<PropertyColumn Property="c=>c.UserId" Title="User"> <RelationContent> <UserNameRelation /> </RelationContent> </PropertyColumn>值得说明的是,RelationContent是 Column.razor.cs 中定义的RenderFragment类型参数。从 Column.razor 的渲染逻辑可以看到,当单元格存在关联组件时(CurrentRelationComponent != null),单元格内容优先由关联组件渲染,其次才是CellRender、ChildContent与默认格式化文本(见 Column.razor 的CellContent方法)。
方式二:Razor 组件
创建一个 Razor 文件,同样继承RelationComponentBase<TItem, TData>。Razor 方式的优势在于:无需重写RenderContent方法,直接在标记中编写 UI,并通过CurrentFieldValue、CurrentRowData访问当前单元格的外键值与整行数据:
@inherits RelationComponentBase<Employee, int> @if (departments.TryGetValue(CurrentFieldValue, out var dept)) { <Tag>@dept.Name</Tag> } @code { private Dictionary<int, Department> departments = new(); protected override Task OnLoadBatch(IEnumerable<int> ids) { // 批量加载部门数据 return Task.CompletedTask; } }其底层原理在 RelationComponentBase.cs:基类提供的默认渲染实现会把fieldValue与rowData分别写入CurrentFieldValue和CurrentRowData,再调用由 Razor 编译器生成的BuildRenderTree来渲染模板。
Razor 组件还天然支持泛型TItem,让同一个关联组件可以跨不同类型的表格复用。仓库演示 Basic.razor 就展示了这一点:同一个UserNameRelation(见 Shared/UserNameRelation.razor,其中声明了@typeparam TItem)既被UserNameRelation<Order>用来显示订单表的用户信息,也被UserNameRelation<Employee>用来显示员工表的部门信息。
方式三:[RelationColumn]特性标注(最简洁)
在实体属性上直接标注[RelationColumn],Table 会在初始化阶段自动创建对应关联组件,无需手写任何ChildContent:
public class Product { [RelationColumn(typeof(CategoryNameRelation))] public int CategoryId { get; set; } }配套的关联组件依然继承RelationComponentBase<Product, int>(完整示例见 demo/Attribute.razor)。使用时表格代码与普通列无异:
<Table TItem="Product" DataSource="@products"> <PropertyColumn Property="c=>c.ProductId" Title="Product ID" /> <PropertyColumn Property="c=>c.CategoryId" Title="Category" /> ... </Table>自动装配的关键逻辑位于 Column.razor.cs:在表头初始化阶段,若ChildContent为空且字段表达式存在,框架会取出字段成员上的RelationColumnAttribute并调用CreateRelationComponentContent()生成可复用的RenderFragment。这意味着渲染片段只创建一次,并在所有行之间复用。
四、API 参考
RelationComponentBase<TItem, TData>
属性
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| CurrentRowData | (仅 Razor 组件方式)当前行数据 | TItem | - |
| CurrentFieldValue | (仅 Razor 组件方式)当前字段值 | TData | - |
需要说明的是,这两个属性“仅在渲染期间有效”,即只在RenderContent被调用时被赋值(见 RelationComponentBase.cs 的源码注释)。
此外,基类还通过级联参数注入了三个内部能力:
SharedCache(级联参数名RelationCache):ConcurrentDictionary<string, object>类型的共享缓存,由 Table 提供、可被多个关联组件共享,官方建议在加载数据前先查缓存以避免重复加载;Column:当前关联列组件,可访问GetValue委托、标题等列配置;Table:当前表格组件,可访问数据源等表格配置。
方法
| 方法 | 说明 | 参数 | 返回 |
|---|---|---|---|
| OnLoadBatch | 批量加载关联数据(简化版) | IEnumerable<TData> fieldValues | Task |
| OnLoadBatch | 批量加载关联数据(完整版) | IEnumerable<TItem> items, QueryModel queryModel | Task |
| RenderContent | 渲染单元格内容 | TData fieldValue, TItem item | RenderFragment |
| GetFieldValue | 获取指定行的字段值 | TItem item | TData |
两个OnLoadBatch重载的关系值得展开:完整版重载(RelationComponentBase.cs)的默认实现会遍历数据源、调用GetFieldValue取出字段值、Distinct()去重后,转调简化版OnLoadBatch。因此:
- 大多数场景只需重写简化版;
- 当你需要访问整行数据、或需要感知分页/排序/筛选信息(
QueryModel中的PageIndex、PageSize、SortModel、FilterModel)时,再重写完整版。
GetFieldValue的“零反射”体现在 RelationComponentBase.cs:它直接调用列内部预编译的GetItemValueExpression<TItem>()(rowData)委托来取值,而不是用反射读取属性。
RelationColumnAttribute
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| ComponentType | 关联组件类型 | Type | - |
| Parameters | 组件参数(可选) | string[] | null |
关于Parameters有两点源码级细节(见 RelationColumnAttribute.cs):
- 它的实际类型是
Dictionary<string, object>,键为参数名、值为参数值,例如new Dictionary<string, object> { ["Size"] = 50, ["ShowName"] = true }; - 构造时会校验
ComponentType必须实现IRelationComponent接口,否则抛出ArgumentException; - 参数值在生成渲染片段时会按目标参数类型自动转换(支持
bool、int、long、double、string等基本类型),且渲染片段按“组件类型 + 参数”生成缓存键,避免重复创建(RelationColumnAttribute.cs)。
五、性能优化机制:从源码看批量加载与共享缓存
RelationColumn 的性能优化不是空话,四条优化路径都能在源码中找到对应实现:
1. 批量加载 + 并行执行。在 Table.razor.cs 的LoadRelationDataAsync方法中,Table 在每次数据加载/刷新完成后:
- 遍历所有列定义,收集注册的关联组件;
- 清空共享缓存
RelationDataCache; - 为每个组件调用
SetDataSource(_showItems, _currentQueryModel)传入当前页数据源; - 将每个组件的
OnLoadBatchAsync()任务放入数组,用Task.WhenAll并行等待全部加载完成; - 最后调用
ForceReRender()统一重绘。
该方法在 Table.razor.cs 处由数据源变更流程异步触发,无需手动调用——这正对应文档 Notes 中的说明:“OnLoadBatch在表格数据加载后自动调用,无需手动触发”。
2. 自动去重。完整版OnLoadBatch默认实现对字段值执行Distinct()(见 RelationComponentBase.cs),相同 ID 只加载一次。
3. 零反射。GetFieldValue通过列预编译委托取值(RelationComponentBase.cs)。
4. 共享缓存。Table 通过 Table.razor 中的<CascadingValue Name="RelationCache" Value="@RelationDataCache" IsFixed>把ConcurrentDictionary<string, object>级联给所有关联组件。官方推荐在OnLoadBatch中先查SharedCache再加载:
protected override async Task OnLoadBatch(IEnumerable<int> userIds) { var uncachedIds = userIds.Where(id => !SharedCache.ContainsKey($"User_{id}")).ToList(); if (uncachedIds.Any()) { var users = await UserService.GetUsersByIdsAsync(uncachedIds); foreach (var user in users) { SharedCache[$"User_{user.Id}"] = user; } } }仓库演示 Shared/UserMultiFieldRelation.razor 更进一步展示了共享缓存的进阶用法:用一个静态SemaphoreSlim加锁防止并发重复加载,并在获取锁后“二次检查”缓存。该组件还被同一个表格的三个列(用户名、地址、邮箱)共同使用(见 demo/MultiColumn.razor),一次加载、多处展示,同时通过DisplayField参数控制各列显示哪个字段——这是“多列共用一次加载数据”的典型范例。
六、数据刷新后的状态同步
关联数据加载是异步的,加载完成后如何让表格重新渲染?答案在基类的StateHasChanged方法(RelationComponentBase.cs):
protected void StateHasChanged() { if (_hasPendingQueuedRender) return; if (_hasNeverRendered || ShouldRender()) { _hasPendingQueuedRender = true; try { // 触发 Table 重新渲染所有行和单元格 Table?.Refresh(); } ... } }即:关联组件自身不直接渲染(基类SetParametersAsync中明确注释“渲染完全由 Table 通过 RenderContent 控制”,见 RelationComponentBase.cs),而是通过Table.Refresh()让整个表格重绘。这也是为什么异步批量加载完成后,所有行的单元格都能拿到最新缓存数据。在UserMultiFieldRelation.razor演示中,加载完成后调用StateHasChanged()以立即刷新界面。
另外,基类实现了IComponent接口并在OnInitialized中通过Column is IColumnInternal columnInternal调用columnInternal.SetRelationComponent(this)(RelationComponentBase.cs)完成注册;列侧则通过IColumnInternal接口(见 IColumnInternal.cs)对外暴露GetRelationComponent()/SetRelationComponent(),把关联组件与列绑定起来。
七、使用建议与注意事项
结合官方文档 Notes 与仓库演示,给出以下实践建议:
- 不要手动触发
OnLoadBatch:它由 Table 在数据加载完成后自动统一调用; - Razor 方式不用重写
RenderContent:直接在 Razor 标记中写 UI,通过CurrentFieldValue/CurrentRowData取数; - 特性标注最简洁但能力有限:
[RelationColumn]无需手写ChildContent,适合简单文本展示场景;需要复杂渲染或多列复用逻辑时,优先用 Razor 组件方式; - 建议在组件字段中缓存关联数据:避免重复加载;多表格场景下进一步使用
SharedCache跨表共享; - 批量查询优先:官方源码注释明确建议使用
IN查询或数据源的批量 API,把“按 ID 逐条查询”变成“按 ID 集合一次查询”; - 关注实验性状态:该特性位于 Experimental 分类下,API 可能在后续版本调整,升级时留意 changelog。
对于大数据量场景,仓库还提供了虚拟化表格与关联列结合的演示(demo/Virtualization.razor):在开启EnableVirtualization的远程表格中,UserNameRelation组件同样只对未缓存的 ID 发起请求,验证了批量加载与缓存机制在滚动加载场景下的可用性。
结语
RelationColumn 把“按行查询关联数据”的惯用写法,收敛为“按页批量加载 + 去重 + 共享缓存 + 委托取值”的框架级能力:开发者只需继承RelationComponentBase<TItem, TData>(C# 类或 Razor 组件)或在属性上标注[RelationColumn],即可获得自动批量加载与统一渲染,彻底摆脱 N+1 查询。理解其背后LoadRelationDataAsync+Task.WhenAll的调用链、GetFieldValue的委托取值与RelationCache的级联共享,能帮助你在真实项目中写出既简洁又高性能的关联列。
- UI组件
- 前端
【免费下载链接】ant-design-blazor
🌈A rich set of enterprise-class UI components based on Ant Design and Blazor.
相关推荐
ant-design-blazor Table 关联列(RelationColumn)实战:批量加载关联数据,彻底告别 N+1 查询
ant design blazor Table 关联列(RelationColumn)实战:批量加载关联数据,彻底告别 N+1 查询 Table 关联数据自动加
前端UI组件设计系统Ant Design Blazor Table 关联列(RelationColumn):自动批量加载关联数据,彻底告别 N+1 查询
Ant Design Blazor Table 关联列(RelationColumn):自动批量加载关联数据,彻底告别 N+1 查询 导读 在业务表格中,"通过
前端UI组件设计系统ant-design-blazor Table 关联列(RelationColumn)基本用法:共享泛型关联组件解决 N+1 查询
ant design blazor Table 关联列(RelationColumn)基本用法:共享泛型关联组件解决 N+1 查询 Table 关联列(Rela
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考