- 前端
- UI组件
【免费下载链接】react-admin
A frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design
React-admin 作为构建在 REST/GraphQL API 之上的前端框架,其关系数据处理能力贯穿于Reference*系列字段组件之中。本文以官方文档 FieldsForRelationships 为骨架,结合 ra-core 与 ra-ui-materialui 的源码实现,系统讲解 one-to-many、many-to-one、one-to-one、many-to-many 四类关系在 react-admin 中的建模与呈现方式,帮助读者掌握<ReferenceManyField>、<ReferenceField>、<ReferenceArrayField>、<ReferenceManyToManyField>、<ReferenceOneField>与<ArrayField>的选择与底层数据获取原理。
关系数据处理的设计思想
React-admin 提供了大量称为Reference 组件的组件来处理记录之间的关系。事实上,react-admin 和dataProvider接口在设计上就是为了方便实现关系功能,例如:
- 展示某篇文章(post)关联的所有评论(comments)
- 展示某篇文章的作者(author)
- 为某篇文章选择作者
- 为某篇文章添加标签
一个关键的设计前提是:react-admin 处理关系的能力与 API 本身能否管理关系无关。只要你能为 API 提供dataProvider,所有关系功能都可以正常工作。关系数据在 API 中如何建模(外键、外键数组、内嵌对象、连接表),决定了你应该使用哪个 Reference 组件——react-admin 提供了多种辅助组件来获取关联记录,具体取决于关系的类型以及 API 的实现方式。
关系类型与组件选型速查
React-admin 将关系归纳为四类,每一类对应不同的数据建模方式和组件选择。
One-To-Many(一对多)
当一个记录拥有多个关联记录时,称为一对多关系。例如,一位作者写过多本书,则authors与books构成一对多关系。获取某位作者的书,可选组件取决于 API 的建模方式:
<ReferenceManyField>:API 使用外键(如每本书有author_id字段)<ReferenceArrayField>:API 使用外键数组(如每位作者有book_ids字段)<ArrayField>:API 内嵌记录数组(如每位作者有books字段)
Many-To-One(多对一)
多对一关系是一对多关系的反方向(如每本书有一位作者)。获取一本书的作者,可选:
<ReferenceField>:API 使用外键(如每本书有author_id字段)- Deep Field Source:API 内嵌关联记录(如每本书有
author字段,值为一个对象)
其他类型的关系通常可以化简为多对一关系。
One-To-One(一对一)
一对一关系(如一本书有一个book_detail)是基数为 1 的一对多关系的特例。获取一本书的详情,可选:
<ReferenceOneField>:API 使用外键(如每条book_detail有book_id字段)<ReferenceField>:API 使用反向外键(如每本书有book_detail_id字段)- Deep Field Source:API 内嵌关联记录(如每本书有
book_detail字段,值为对象)
Many-To-Many(多对多)
多对多关系通常建模为两次连续的一对多关系。例如,一本书由多人合著,可以建模为 book 与 book_authors 的一对多关系,以及 book_authors 与 authors 的一对多关系。获取某位作者的书,可选:
<ReferenceManyToManyField>:API 使用连接表(如book_authors表同时有book_id和author_id字段)<ReferenceArrayField>:API 使用外键数组(如每位作者有book_ids字段,每本书有author_ids字段)<ArrayField>:API 内嵌记录数组(如每位作者有books字段,每本书有authors字段)
Deep Field Source:内嵌对象的直接读取
当多对一关系(如书的作者)以内嵌对象形式实现时,不需要使用任何 Reference 字段——可以直接使用普通字段,并通过复合字段名(compound field name,如"author.first_name")访问。
┌──────────────────┐ │ books │ │------------------│ │ id │ │ author │ │ └ first_name │ │ └ last_name │ │ └ date_of_birth │ │ title │ │ published_at │ └──────────────────┘使用示例:
const BookShow = () => ( <Show> <SimpleShowLayout> <TextField source="title" /> <DateField source="published_at" /> <FunctionField label="Author" render={record => `${record.author.first_name} ${record.author.last_name}`} /> <DateField label="Author DOB" source="author.date_of_birth" /> </SimpleShowLayout> </Show> );这里的source="author.date_of_birth"就是 Deep Field Source 的核心用法:字段路径可以是一个点分路径(dot path),react-admin 内部使用 lodash 的get()从记录对象中取值(可在 useReferenceManyFieldController.ts 的get(record, source)中看到这一取值模式)。这种方式零网络请求、零 dataProvider 调用,适合 API 返回冗余(denormalized)数据的场景。
<ArrayField>:内嵌记录数组
当一对多关系(如某位作者的书)以内嵌对象数组实现时,使用<ArrayField>获取数据。
┌───────────────────────────┐ │ author │ │---------------------------│ │ id │ │ first_name │ │ last_name │ │ date_of_birth │ │ books │ │ └ { title, published_at} │ │ └ { title, published_at} │ │ └ { title, published_at} │ └───────────────────────────┘使用示例:
const AuthorShow = () => ( <Show> <SimpleShowLayout> <TextField source="first_name" /> <TextField source="last_name" /> <DateField source="date_of_birth" /> <ArrayField source="books"> <DataTable> <DataTable.Col source="title" /> <DataTable.Col source="published_at" field={DateField} /> </DataTable> </ArrayField> </SimpleShowLayout> </Show> );<ArrayField>会为内嵌记录创建一个ListContext(其实现ArrayField位于 ArrayField.tsx,委托给 ra-core 的ArrayFieldBase),因此可以在此上下文中使用任何依赖该上下文的组件(<DataTable>、<SimpleList>、<Datagrid>、<SingleFieldList>等)。由于数据已经随父记录返回,<ArrayField>不会发起任何额外的 API 请求。
<ReferenceField>:外键关联单条记录
当多对一关系(如书的作者)使用外键实现时,用<ReferenceField>获取关联记录。
┌──────────────┐ ┌────────────────┐ │ books │ │ authors │ │--------------│ │----------------│ │ id │ ┌───│ id │ │ author_id │╾──┘ │ first_name │ │ title │ │ last_name │ │ published_at │ │ date_of_birth │ └──────────────┘ └────────────────┘使用示例:
const BookShow = () => ( <Show> <SimpleShowLayout> <TextField source="title" /> <DateField source="published_at" /> <ReferenceField label="Author" source="author_id" reference="authors"> <FunctionField render={record => record && `${record.first_name} ${record.last_name}`} /> </ReferenceField> <ReferenceField label="Author DOB" source="author_id" reference="authors"> <DateField source="date_of_birth" /> </ReferenceField> </SimpleShowLayout> </Show> );<ReferenceField>使用当前record(此例中的 book)通过外键author_id读取引用 id,然后调用dataProvider.getOne('authors', { id })获取关联作者。它还会创建一个RecordContext来存放引用记录,因此任何依赖该上下文的组件(<TextField>、<SimpleShowLayout>等)都可以在 children 中使用。
技巧:无需担心在同一个表格中调用两次<ReferenceField>会造成重复请求——react-admin 只会发起一次 API 调用。这一点在 useReference.ts 中可以得到印证:它内部通过useGetManyAggregate携带{ ids: [id] }获取数据,而非直接调用getOne。
列表中的 n+1 问题与请求聚合
仅展示单条记录还不够,更多时候需要为一本书列表展示作者信息:
const BookList = () => ( <List> <DataTable> <DataTable.Col source="title" /> <DataTable.Col source="published_at" field={DateField} /> <DataTable.Col source="author_id" label="Author"> <ReferenceField source="author_id" reference="authors"> <FunctionField render={record => `${record.first_name} ${record.last_name}`} /> </ReferenceField> </DataTable.Col> <DataTable.Col source="author_id" label="Author DOB"> <ReferenceField source="author_id" reference="authors"> <DateField source="date_of_birth" /> </ReferenceField> </DataTable.Col> </DataTable> </List> );如果书列表的每一行都触发一次dataProvider.getOne('authors', { id })调用,当列表有大量行(例如 25 行)时应用会变得非常慢,甚至可能因滥用请求而被 API 封禁——这就是臭名昭著的n+1 问题。
幸运的是,<ReferenceField>会聚合并去重页面中的所有渲染,生成一个优化的请求。在上述示例中,书列表不会发起 n 次getOne调用,而是只发起一次dataProvider.getMany('authors', { ids })调用。其底层实现位于 useGetManyAggregate.ts:callGetManyQueries函数通过batch()将同一事件循环 tick 内的所有getMany调用合并,按resource + meta分组,用union去重合并 ids,最终只调用一次dataProvider.getMany(),再把返回数据按各自的 ids 过滤分发给每个等待的调用方。这既解决了 n+1 问题,又顺带在成功后把每条记录写入getOne缓存,使后续getOne请求可以直接命中缓存。
<ReferenceManyField>:外键关联多条记录
当一对多关系(如某位作者的书)使用外键实现时,用<ReferenceManyField>获取关联记录。
┌────────────────┐ ┌──────────────┐ │ authors │ │ books │ │----------------│ │--------------│ │ id │───┐ │ id │ │ first_name │ └──╼│ author_id │ │ last_name │ │ title │ │ date_of_birth │ │ published_at │ └────────────────┘ └──────────────┘使用示例:
const AuthorShow = () => ( <Show> <SimpleShowLayout> <TextField source="first_name" /> <TextField source="last_name" /> <DateField source="date_of_birth" /> <ReferenceManyField reference="books" target="author_id"> <DataTable> <DataTable.Col source="title" /> <DataTable.Col source="published_at" field={DateField} /> </DataTable> </ReferenceManyField> </SimpleShowLayout> </Show> );<ReferenceManyField>使用当前record(此例中的 author)基于外键字段(author_id)构建书籍列表的过滤器,然后调用dataProvider.getManyReference('books', { target: 'author_id', id: book.id })获取关联书籍。它会创建一个ListContext来存放关联记录,因此任何依赖该上下文的组件(<DataTable>、<SimpleList>、<Datagrid>等)都可以使用。
为什么用 getManyReference 而不是 getList?
对于许多 API 来说,dataProvider.getList()和dataProvider.getManyReference()之间没有区别——后者是前者的特化版本,只是预置了一个filter。但有些 API 将关联记录暴露为子路由,因此需要特殊方法来获取它们。例如,某位作者的书可以通过以下端点暴露:
GET /authors/:id/books这就是<ReferenceManyField>使用getManyReference()方法而非getList()的原因。从源码看,useGetManyReference(useGetManyReference.ts)的查询键为[resource, 'getManyReference', { target, id, pagination, sort, filter, meta }],当target缺失或id为 null 时直接 reject(target and id are required),保证请求参数完整。
常用 Props 与默认值
<ReferenceManyField>还提供一系列可选 props(其默认值定义在 ReferenceManyFieldBase.tsx 与 useReferenceManyFieldController.ts):
| Prop | 默认值 | 说明 |
|---|---|---|
reference | (必填) | 关联资源名,必须是<Admin>下注册的<Resource>之一 |
target | (必填) | 外键字段名(如author_id) |
source | 'id' | 当前记录中用于构建过滤器的字段 |
filter | {} | 附加过滤条件 |
sort | { field: 'id', order: 'DESC' } | 关联记录的排序 |
perPage | 25 | 默认每页展示条数 |
page | 1 | 默认页码 |
debounce | 500 | 过滤器变更的去抖毫秒数 |
pagination | 无 | 分页组件节点 |
empty/error/loading/offline | 无 | 空、错误、加载、离线状态的定制节点 |
render | 无 | 渲染函数,接收ListControllerResult |
使用示例(perPage、sort、filter):
<ReferenceManyField perPage={10} sort={{ field: 'created_at', order: 'DESC' }} filter={{ is_published: true }} reference="comments" target="post_id"> ... </ReferenceManyField>此外,ReferenceManyFieldBase会额外提供ResourceContextProvider(value 为reference)和ListContextProvider,并把data、total、isPending、error等状态暴露给 children,配合 react-query 的placeholderData在翻页时保持旧数据占位。该控制器还内置了选择(selection)、排序(sort)、过滤(filter)与onSelectAll全选逻辑(storeKey默认形如${resource}.${record?.id}.${reference}),这意味着你可以在<ReferenceManyField>内直接使用带批量操作的工具条组件。
<ReferenceArrayField>:外键数组关联多条记录
当一对多关系(如某位作者的书)使用外键数组实现时,用<ReferenceArrayField>获取关联记录。
┌────────────────┐ ┌──────────────┐ │ authors │ │ books │ │----------------│ │--------------│ │ id │ ┌───│ id │ │ first_name │ │ │ title │ │ last_name │ │ │ published_at │ │ date_of_birth │ │ └──────────────┘ │ book_ids │╾──┘ └────────────────┘使用示例:
const AuthorShow = () => ( <Show> <SimpleShowLayout> <TextField source="first_name" /> <TextField source="last_name" /> <DateField source="date_of_birth" /> <ReferenceArrayField reference="books" source="book_ids"> <DataTable> <DataTable.Col source="title" /> <DataTable.Col source="published_at" field={DateField} /> </DataTable> </ReferenceArrayField> </SimpleShowLayout> </Show> );<ReferenceArrayField>读取当前record(此例中的 author)中的book_ids列表,然后调用dataProvider.getMany('books', { ids })获取关联书籍。它同样创建一个ListContext来存放关联记录,因此任何依赖该上下文的组件(<DataTable>、<SimpleList>等)都可以使用。其源码实现位于 ReferenceArrayField.tsx,UI 层委托给 ra-core 的ReferenceArrayFieldBase,内部控制器(useReferenceArrayFieldController.ts)同样是基于useGetManyAggregate实现。
在列表页中的用法
<ReferenceArrayField>也可以用在列表页中:
const AuthorList = () => ( <List> <DataTable> <DataTable.Col source="first_name" /> <DataTable.Col source="last_name" /> <DataTable.Col source="date_of_birth" field={DateField} /> <DataTable.Col label="Books" source="book_ids"> <ReferenceArrayField reference="books" source="book_ids"> <SingleFieldList> <TextField source="title" /> </SingleFieldList> </ReferenceArrayField> </DataTable.Col> </DataTable> </List> );与<ReferenceField>一样,<ReferenceArrayField>会聚合并去重页面中的所有渲染并生成优化请求。因此,对于整个作者列表,只会发起一次dataProvider.getMany('books', { ids })调用——这同样得益于useGetManyAggregate的批量合并机制。
<ReferenceArrayField>的默认perPage为 1000(区别于<ReferenceManyField>的 25),默认按引用顺序(即 ids 数组的顺序)展示结果,也可以通过sort改变顺序,用filter只展示子集:
<ReferenceArrayField perPage={10} sort={{ field: 'name', order: 'ASC' }} filter={{ is_published: true }} reference="categories" source="category_ids"> ... </ReferenceArrayField><ReferenceManyToManyField>:连接表多对多
<ReferenceManyToManyField>是 Enterprise Edition(React Admin Enterprise Edition)字段,用于展示通过连接表(join table)实现的多对多关系——即用两次一对多关系建模多对多。
┌──────────────────┐ ┌──────────────┐ ┌───────────────┐ │ books │ │ book_authors │ │ authors │ │------------------│ │--------------│ │---------------│ │ id │───┐ │ id │ │ id │ │ title │ └──╼│ book_id │ ┌──│ first_name │ │ published_at │ │ author_id │╾──┘ │ last_name │ └──────────────────┘ │ is_public │ │ date_of_birth │ └──────────────┘ └───────────────┘展示某位作者的书:
const AuthorShow = () => ( <Show> <SimpleShowLayout> <TextField source="first_name" /> <TextField source="last_name" /> <DateField source="date_of_birth" /> <ReferenceManyToManyField reference="books" through="book_authors" using="author_id,book_id" > <DataTable> <DataTable.Col source="title" /> <DataTable.Col source="published_at" field={DateField} /> </DataTable> </ReferenceManyToManyField> <EditButton /> </SimpleShowLayout> </Show> );展示一本书的作者:
const BookShow = props => ( <Show> <SimpleShowLayout> <TextField source="title" /> <DateField source="published_at" /> <ReferenceManyToManyField reference="authors" through="book_authors" using="book_id,author_id" > <DataTable> <DataTable.Col label="Author" render={record => `${record.first_name} ${record.last_name}`} /> <DataTable.Col source="date_of_birth" field={DateField} /> </DataTable> </ReferenceManyToManyField> <EditButton /> </SimpleShowLayout> </Show> );三个关键 props 的含义:
reference:目标资源名(要展示的关联资源,如books或authors)through:连接表对应的资源名(如book_authors)using:连接表中关联两个资源的外键字段对,格式为"source_field,target_field"(如"author_id,book_id"),顺序必须与当前记录到目标记录的指向一致
<ReferenceManyToManyField>同样创建一个ListContext来存放关联记录,因此任何依赖该上下文的组件(<DataTable>、<SimpleList>等)都可以使用。该组件位于 Enterprise Edition 的@react-admin/ra-relationships包中,open-source 版本不包含此组件,如需此能力需要引入对应商业包(本文所分析的仓库 packages 目录中同样不包含其 open-source 实现)。
<ReferenceOneField>:一对一外键关联
当一对一关系(如书的详情)使用外键实现时,用<ReferenceOneField>获取关联记录。
┌──────────────┐ ┌──────────────┐ │ books │ │ book_details │ │--------------│ │--------------│ │ id │───┐ │ id │ │ title │ └──╼│ book_id │ │ published_at │ │ genre │ └──────────────┘ │ ISBN │ └──────────────┘使用方式:
const BookShow = () => ( <Show> <SimpleShowLayout> <TextField source="title" /> <DateField source="published_at" /> <ReferenceOneField label="Genre" reference="book_details" target="book_id"> <TextField source="genre" /> </ReferenceOneField> <ReferenceOneField label="ISBN" reference="book_details" target="book_id"> <TextField source="ISBN" /> </ReferenceOneField> </SimpleShowLayout> </Show> );<ReferenceOneField>的行为与<ReferenceManyField>类似:它使用当前record(此例中的 book)基于外键(book_id)为 book_details 构建过滤器,然后调用dataProvider.getManyReference('book_details', { target: 'book_id', id: book.id })获取关联详情,并取第一条。其控制器源码 useReferenceOneFieldController.tsx 正是基于useGetManyReference实现。
<ReferenceOneField>会创建一个RecordContext来存放引用记录,因此任何依赖该上下文的组件(<TextField>、<SimpleShowLayout>等)都可以使用。从 ReferenceOneFieldBase.tsx 可以看到它还支持sort、filter、link、queryOptions等 props,并通过useGetPathForRecord生成指向关联记录详情页的链接。
技巧:与<ReferenceField>一样,可以在同一组件中按需多次调用<ReferenceOneField>,react-admin 只会发起一次dataProvider.getManyReference()调用(相同的查询键会被 react-query 去重)。
对于反向的一对一关系(如关联到传记的作者),可以使用<ReferenceField>实现。
关系数据请求链路小结
从源码层面对比各 Reference 组件的底层数据获取方式,可以帮助开发者理解不同建模下的真实请求形态:
| 组件 | 关系方向 | 数据建模 | 底层 dataProvider 方法 | 上下文 |
|---|---|---|---|---|
<ArrayField> | 一对多 | 内嵌记录数组 | 无(数据已内嵌) | ListContext |
<ReferenceField> | 多对一 | 外键 | getMany(经由useGetManyAggregate,聚合去重) | RecordContext |
<ReferenceManyField> | 一对多 | 外键 | getManyReference | ListContext |
<ReferenceArrayField> | 一对多 | 外键数组 | getMany(聚合去重) | ListContext |
<ReferenceManyToManyField>(EE) | 多对多 | 连接表 | 分步getManyReference/getMany | ListContext |
<ReferenceOneField> | 一对一 | 外键 | getManyReference(取第一条) | RecordContext |
所有数据获取都经过dataProvider抽象层,因此无论底层 API 是 REST 还是 GraphQL,只要dataProvider正确实现了getOne、getMany、getManyReference等方法,关系功能即可正常工作,这也是 react-admin 关系体系“与 API 关系管理能力解耦”的设计精髓。相关的dataProvider方法签名可以参考 DataProviderWriting 与 DataProviderList 文档,各关系的完整 API 参考可进一步阅读 ReferenceField、ReferenceManyField、ReferenceArrayField、ReferenceOneField、ReferenceManyToManyField 与 ArrayField 等独立章节。
- 前端
- UI组件
【免费下载链接】react-admin
A frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design
相关推荐
react-admin ChipField 组件完全指南:用 Material UI Chip 优雅展示标签字段与一对多关系
react admin ChipField 组件完全指南:用 Material UI Chip 优雅展示标签字段与一对多关系 本篇技术指南以 react adm
前端UI组件react-admin 一对一关系编辑组件 `<ReferenceOneInput>` 完整使用指南
react admin 一对一关系编辑组件 <ReferenceOneInput 完整使用指南 <ReferenceOneInput 是 react admin
前端UI组件Refine v5 数据获取完全指南:Data Provider、数据 Hooks 与关系管理实战
Refine v5 数据获取完全指南:Data Provider、数据 Hooks 与关系管理实战 数据获取是所有内部工具与 Admin Panel 应用的核心
前端企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考