支持资源嵌套
Support for resource nesting
我想知道,是否可以配置 DataProvider/Resource/List 以支持像 api/users/1/roles
这样的 REST url?
对于 RESTful API 获取某些父实体的子实体是非常常见的用例,但我不知道如何设置 React Admin 并实现此目的。我正在使用基于 OData 规范后端的自定义 DataProvider。
我知道我可以通过 api/roles?filter={userId: 1}
或类似的过滤请求获得特定用户的角色,但我的问题是我的用户和角色处于多对多关系中,因此关系引用是存储在数据透视表 table 中。换句话说,我没有参考角色 table 中的用户,所以我无法过滤它们。
我是在监督什么还是有一些我根本看不到的方法?
编辑:
REST API 内置于 OData 规范中,它支持与经典枢轴(或中间枢轴)table 的多对多关系。此 table 未在 API 中公开,但在上述 url 中使用。所以我无法直接将其作为资源访问。
用户模式 - 角色关系看起来也很标准。
|----------| |-----------| |--------|
| USER | | User_Role | | Role |
|----------| |-----------| |--------|
| Id |-\ | Id | /-| Id |
| Login | \-| UserId | / | Name |
| Password | | RoleId |-/ | Code |
|----------| |-----------| |--------|
TL;DR: 默认情况下,React Admin 不支持嵌套资源,你必须 write a custom data provider.
这个问题在过去的一期中得到了回答:maremelab/react-admin#261
详细解答
React Admin 中的默认数据提供者是 ra-data-simple-rest
。
如其文档中所述,此库不支持嵌套资源,因为它仅使用资源名称和资源 ID 来构建资源 URL:
为了支持嵌套资源,你必须自己编写数据提供者。
嵌套资源支持是一项 recurrent feature request,但当时核心团队不想处理这项工作。
我强烈建议集结力量编写一个外部数据提供程序并像ra-data-odata
提供程序一样发布。这将是一个很好的补充,我们很荣幸能帮助您处理该外部包。
你的问题已经回答 ,但我想告诉你我的解决方法,以便 React-Admin 处理多对多关系。
如上述答案所述,您必须扩展 DataProvider 才能获取多对多关系的资源。但是,您需要使用新的 REST 动词,让我们假设 GET_MANY_MANY_REFERENCE
在您的应用程序的某处。由于不同的 REST services/API 可以有不同的路由格式来获取相关资源,我没有费心去尝试构建一个新的 DataProvider,我知道这不是一个很好的解决方案,但对于短期限来说相当简单。
我的解决方案是从 <ReferenceManyField>
中汲取灵感,并为多对多关系构建一个新组件 <ReferenceManyManyField>
。此组件使用 fetch API 在 componentDidMount
上获取相关记录。 On response 使用响应数据构建对象,一个数据是一个对象,其键是记录 id,并为相应的记录对象赋值,以及一个包含记录 id 的 ids 数组。这与其他状态变量(如 page、sort、perPage、total)一起传递给子级,以处理数据的分页和排序。请注意,更改 Datagrid 中数据的顺序意味着将向 API 发出新请求。该组件分为控制器和视图,如 <ReferencemanyField>
,其中控制器获取数据、管理数据并将其传递给子视图,视图接收控制器数据并将其传递给子视图呈现其内容。这使我可以在 Datagrid 上呈现多对多关系数据,即使有一些限制,它也是一个要聚合到我的项目的组件,并且仅在我必须更改某些内容时才与我当前的 API 一起使用该字段,但就目前而言,它可以工作并且可以在我的应用程序中重复使用。
实施细则如下:
//ReferenceManyManyField
export const ReferenceManyManyField = ({children, ...prop}) => {
if(React.Children.count(children) !== 1) {
throw new Error( '<ReferenceManyField> only accepts a single child (like <Datagrid>)' )
}
return <ReferenceManyManyFieldController {...props}>
{controllerProps => (<ReferenceManyManyFieldView
{...props}
{...{children, ...controllerProps}} /> )}
</ReferenceManyManyFieldController>
//ReferenceManyManyFieldController
class ReferenceManyManyFieldController extends Component {
constructor(props){
super(props)
//State to manage sorting and pagination, <ReferecemanyField> uses some props from react-redux
//I discarded react-redux for simplicity/control however in the final solution react-redux might be incorporated
this.state = {
sort: props.sort,
page: 1,
perPage: props.perPage,
total: 0
}
}
componentWillMount() {
this.fetchRelated()
}
//This could be a call to your custom dataProvider with a new REST verb
fetchRelated({ record, resource, reference, showNotification, fetchStart, fetchEnd } = this.props){
//fetchStart and fetchEnd are methods that signal an operation is being made and make active/deactivate loading indicator, dataProvider or sagas should do this
fetchStart()
dataProvider(GET_LIST,`${resource}/${record.id}/${reference}`,{
sort: this.state.sort,
pagination: {
page: this.state.page,
perPage: this.state.perPage
}
})
.then(response => {
const ids = []
const data = response.data.reduce((acc, record) => {
ids.push(record.id)
return {...acc, [record.id]: record}
}, {})
this.setState({data, ids, total:response.total})
})
.catch(e => {
console.error(e)
showNotification('ra.notification.http_error')
})
.finally(fetchEnd)
}
//Set methods are here to manage pagination and ordering,
//again <ReferenceManyField> uses react-redux to manage this
setSort = field => {
const order =
this.state.sort.field === field &&
this.state.sort.order === 'ASC'
? 'DESC'
: 'ASC';
this.setState({ sort: { field, order } }, this.fetchRelated);
};
setPage = page => this.setState({ page }, this.fetchRelated);
setPerPage = perPage => this.setState({ perPage }, this.fetchRelated);
render(){
const { resource, reference, children, basePath } = this.props
const { page, perPage, total } = this.state;
//Changed basePath to be reference name so in children can nest other resources, not sure why the use of replace, maybe to maintain plurals, don't remember
const referenceBasePath = basePath.replace(resource, reference);
return children({
currentSort: this.state.sort,
data: this.state.data,
ids: this.state.ids,
isLoading: typeof this.state.ids === 'undefined',
page,
perPage,
referenceBasePath,
setPage: this.setPage,
setPerPage: this.setPerPage,
setSort: this.setSort,
total
})
}
}
ReferenceManyManyFieldController.defaultProps = {
perPage: 25,
sort: {field: 'id', order: 'DESC'}
}
//ReferenceManyManyFieldView
export const ReferenceManyManyFieldView = ({
children,
classes = {},
className,
currentSort,
data,
ids,
isLoading,
page,
pagination,
perPage,
reference,
referenceBasePath,
setPerPage,
setPage,
setSort,
total
}) => (
isLoading ?
<LinearProgress className={classes.progress} />
:
<Fragment>
{React.cloneElement(children, {
className,
resource: reference,
ids,
data,
basePath: referenceBasePath,
currentSort,
setSort,
total
})}
{pagination && React.cloneElement(pagination, {
page,
perPage,
setPage,
setPerPage,
total
})}
</Fragment>
);
//Assuming the question example, the presentation of many-to-many relationship would be something like
const UserShow = ({...props}) => (
<Show {...props}>
<TabbedShowLayout>
<Tab label='User Roles'>
<ReferenceManyManyField source='users' reference='roles' addLabel={false} pagination={<Pagination/>}>
<Datagrid>
<TextField source='name'/>
<TextField source='code'/>
</Datagrid>
</ReferenceManyManyField>
</Tab>
</TabbedShowLayout>
</Show>
)
//Used <TabbedShowLayout> because is what I use in my project, not sure if works under <Show> or <SimpleShowLayout>, but I think it work since I use it in other contexts
我认为实现可以改进并与 React-Admin 更兼容。在其他参考字段中,数据提取存储在 react-redux 状态中,在此实现中不是。除了组件之外,该关系不会保存在任何地方,这使得应用程序无法离线工作,因为无法获取数据,甚至无法订购。
有一个非常相似的问题。我的 更像是 hack,但如果您只想启用 ReferenceManyField
,则实施起来会更简单一些。只有dataProvider
需要修改:
我在这里重复针对当前问题修改的解决方案:
使用股票ReferenceManyField
:
<Show {...props}>
<TabbedShowLayout>
<Tab label="Roles">
<ReferenceManyField reference="roles" target="_nested_users_id" pagination={<Pagination/>} >
<Datagrid>
<TextField source="role" />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
然后我修改了我的 dataProvider,它是 ra-jsonapi-client 的一个分支。
我把case GET_MANY_REFERENCE
下的index.js
改成了:
// Add the reference id to the filter params.
query[`filter[${params.target}]`] = params.id;
url = `${apiUrl}/${resource}?${stringify(query)}`;
对此:
// Add the reference id to the filter params.
let refResource;
const match = /_nested_(.*)_id/g.exec(params.target);
if (match != null) {
refResource = `${match[1]}/${params.id}/${resource}`;
} else {
query[`filter[${params.target}]`] = params.id;
refResource = resource;
}
url = `${apiUrl}/${refResource}?${stringify(query)}`;
所以基本上我只是将参数重新映射到 url 以应对 target
匹配硬编码正则表达式的特殊情况。
ReferenceManyField
通常会导致 dataProvider 调用 api/roles?filter[_nested_users_id]=1
,而此修改会使 dataProvider 调用 api/users/1/roles
。它对 react-admin.
是透明的
不够优雅,但可以正常工作,而且似乎不会破坏前端的任何东西。
我想知道,是否可以配置 DataProvider/Resource/List 以支持像 api/users/1/roles
这样的 REST url?
对于 RESTful API 获取某些父实体的子实体是非常常见的用例,但我不知道如何设置 React Admin 并实现此目的。我正在使用基于 OData 规范后端的自定义 DataProvider。
我知道我可以通过 api/roles?filter={userId: 1}
或类似的过滤请求获得特定用户的角色,但我的问题是我的用户和角色处于多对多关系中,因此关系引用是存储在数据透视表 table 中。换句话说,我没有参考角色 table 中的用户,所以我无法过滤它们。
我是在监督什么还是有一些我根本看不到的方法?
编辑: REST API 内置于 OData 规范中,它支持与经典枢轴(或中间枢轴)table 的多对多关系。此 table 未在 API 中公开,但在上述 url 中使用。所以我无法直接将其作为资源访问。
用户模式 - 角色关系看起来也很标准。
|----------| |-----------| |--------|
| USER | | User_Role | | Role |
|----------| |-----------| |--------|
| Id |-\ | Id | /-| Id |
| Login | \-| UserId | / | Name |
| Password | | RoleId |-/ | Code |
|----------| |-----------| |--------|
TL;DR: 默认情况下,React Admin 不支持嵌套资源,你必须 write a custom data provider.
这个问题在过去的一期中得到了回答:maremelab/react-admin#261
详细解答
React Admin 中的默认数据提供者是 ra-data-simple-rest
。
如其文档中所述,此库不支持嵌套资源,因为它仅使用资源名称和资源 ID 来构建资源 URL:
为了支持嵌套资源,你必须自己编写数据提供者。
嵌套资源支持是一项 recurrent feature request,但当时核心团队不想处理这项工作。
我强烈建议集结力量编写一个外部数据提供程序并像ra-data-odata
提供程序一样发布。这将是一个很好的补充,我们很荣幸能帮助您处理该外部包。
你的问题已经回答
如上述答案所述,您必须扩展 DataProvider 才能获取多对多关系的资源。但是,您需要使用新的 REST 动词,让我们假设 GET_MANY_MANY_REFERENCE
在您的应用程序的某处。由于不同的 REST services/API 可以有不同的路由格式来获取相关资源,我没有费心去尝试构建一个新的 DataProvider,我知道这不是一个很好的解决方案,但对于短期限来说相当简单。
我的解决方案是从 <ReferenceManyField>
中汲取灵感,并为多对多关系构建一个新组件 <ReferenceManyManyField>
。此组件使用 fetch API 在 componentDidMount
上获取相关记录。 On response 使用响应数据构建对象,一个数据是一个对象,其键是记录 id,并为相应的记录对象赋值,以及一个包含记录 id 的 ids 数组。这与其他状态变量(如 page、sort、perPage、total)一起传递给子级,以处理数据的分页和排序。请注意,更改 Datagrid 中数据的顺序意味着将向 API 发出新请求。该组件分为控制器和视图,如 <ReferencemanyField>
,其中控制器获取数据、管理数据并将其传递给子视图,视图接收控制器数据并将其传递给子视图呈现其内容。这使我可以在 Datagrid 上呈现多对多关系数据,即使有一些限制,它也是一个要聚合到我的项目的组件,并且仅在我必须更改某些内容时才与我当前的 API 一起使用该字段,但就目前而言,它可以工作并且可以在我的应用程序中重复使用。
实施细则如下:
//ReferenceManyManyField
export const ReferenceManyManyField = ({children, ...prop}) => {
if(React.Children.count(children) !== 1) {
throw new Error( '<ReferenceManyField> only accepts a single child (like <Datagrid>)' )
}
return <ReferenceManyManyFieldController {...props}>
{controllerProps => (<ReferenceManyManyFieldView
{...props}
{...{children, ...controllerProps}} /> )}
</ReferenceManyManyFieldController>
//ReferenceManyManyFieldController
class ReferenceManyManyFieldController extends Component {
constructor(props){
super(props)
//State to manage sorting and pagination, <ReferecemanyField> uses some props from react-redux
//I discarded react-redux for simplicity/control however in the final solution react-redux might be incorporated
this.state = {
sort: props.sort,
page: 1,
perPage: props.perPage,
total: 0
}
}
componentWillMount() {
this.fetchRelated()
}
//This could be a call to your custom dataProvider with a new REST verb
fetchRelated({ record, resource, reference, showNotification, fetchStart, fetchEnd } = this.props){
//fetchStart and fetchEnd are methods that signal an operation is being made and make active/deactivate loading indicator, dataProvider or sagas should do this
fetchStart()
dataProvider(GET_LIST,`${resource}/${record.id}/${reference}`,{
sort: this.state.sort,
pagination: {
page: this.state.page,
perPage: this.state.perPage
}
})
.then(response => {
const ids = []
const data = response.data.reduce((acc, record) => {
ids.push(record.id)
return {...acc, [record.id]: record}
}, {})
this.setState({data, ids, total:response.total})
})
.catch(e => {
console.error(e)
showNotification('ra.notification.http_error')
})
.finally(fetchEnd)
}
//Set methods are here to manage pagination and ordering,
//again <ReferenceManyField> uses react-redux to manage this
setSort = field => {
const order =
this.state.sort.field === field &&
this.state.sort.order === 'ASC'
? 'DESC'
: 'ASC';
this.setState({ sort: { field, order } }, this.fetchRelated);
};
setPage = page => this.setState({ page }, this.fetchRelated);
setPerPage = perPage => this.setState({ perPage }, this.fetchRelated);
render(){
const { resource, reference, children, basePath } = this.props
const { page, perPage, total } = this.state;
//Changed basePath to be reference name so in children can nest other resources, not sure why the use of replace, maybe to maintain plurals, don't remember
const referenceBasePath = basePath.replace(resource, reference);
return children({
currentSort: this.state.sort,
data: this.state.data,
ids: this.state.ids,
isLoading: typeof this.state.ids === 'undefined',
page,
perPage,
referenceBasePath,
setPage: this.setPage,
setPerPage: this.setPerPage,
setSort: this.setSort,
total
})
}
}
ReferenceManyManyFieldController.defaultProps = {
perPage: 25,
sort: {field: 'id', order: 'DESC'}
}
//ReferenceManyManyFieldView
export const ReferenceManyManyFieldView = ({
children,
classes = {},
className,
currentSort,
data,
ids,
isLoading,
page,
pagination,
perPage,
reference,
referenceBasePath,
setPerPage,
setPage,
setSort,
total
}) => (
isLoading ?
<LinearProgress className={classes.progress} />
:
<Fragment>
{React.cloneElement(children, {
className,
resource: reference,
ids,
data,
basePath: referenceBasePath,
currentSort,
setSort,
total
})}
{pagination && React.cloneElement(pagination, {
page,
perPage,
setPage,
setPerPage,
total
})}
</Fragment>
);
//Assuming the question example, the presentation of many-to-many relationship would be something like
const UserShow = ({...props}) => (
<Show {...props}>
<TabbedShowLayout>
<Tab label='User Roles'>
<ReferenceManyManyField source='users' reference='roles' addLabel={false} pagination={<Pagination/>}>
<Datagrid>
<TextField source='name'/>
<TextField source='code'/>
</Datagrid>
</ReferenceManyManyField>
</Tab>
</TabbedShowLayout>
</Show>
)
//Used <TabbedShowLayout> because is what I use in my project, not sure if works under <Show> or <SimpleShowLayout>, but I think it work since I use it in other contexts
我认为实现可以改进并与 React-Admin 更兼容。在其他参考字段中,数据提取存储在 react-redux 状态中,在此实现中不是。除了组件之外,该关系不会保存在任何地方,这使得应用程序无法离线工作,因为无法获取数据,甚至无法订购。
有一个非常相似的问题。我的 ReferenceManyField
,则实施起来会更简单一些。只有dataProvider
需要修改:
我在这里重复针对当前问题修改的解决方案:
使用股票ReferenceManyField
:
<Show {...props}>
<TabbedShowLayout>
<Tab label="Roles">
<ReferenceManyField reference="roles" target="_nested_users_id" pagination={<Pagination/>} >
<Datagrid>
<TextField source="role" />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
然后我修改了我的 dataProvider,它是 ra-jsonapi-client 的一个分支。
我把case GET_MANY_REFERENCE
下的index.js
改成了:
// Add the reference id to the filter params.
query[`filter[${params.target}]`] = params.id;
url = `${apiUrl}/${resource}?${stringify(query)}`;
对此:
// Add the reference id to the filter params.
let refResource;
const match = /_nested_(.*)_id/g.exec(params.target);
if (match != null) {
refResource = `${match[1]}/${params.id}/${resource}`;
} else {
query[`filter[${params.target}]`] = params.id;
refResource = resource;
}
url = `${apiUrl}/${refResource}?${stringify(query)}`;
所以基本上我只是将参数重新映射到 url 以应对 target
匹配硬编码正则表达式的特殊情况。
ReferenceManyField
通常会导致 dataProvider 调用 api/roles?filter[_nested_users_id]=1
,而此修改会使 dataProvider 调用 api/users/1/roles
。它对 react-admin.
不够优雅,但可以正常工作,而且似乎不会破坏前端的任何东西。