【问题标题】:Support for resource nesting支持资源嵌套
【发布时间】:2023-03-22 11:22:01
【问题描述】:

我想知道,是否可以配置 DataProvider/Resource/List 以支持像 api/users/1/roles 这样的 REST url?

对于 RESTful API,获取某个父实体的子实体是非常常见的用例,但我不知道如何设置 React Admin 并实现这一点。我正在使用基于 OData 规范后端的自定义 DataProvider 构建。

我知道我可以通过过滤api/roles?filter={userId: 1} 或类似的请求来获取某些用户的角色,但我的问题是我的用户和角色处于多对多关系,因此关系引用存储在数据透视表中.换句话说,我在角色表中没有对用户的引用,所以我无法过滤它们。

我是在监督什么,还是有一些我根本看不到的方法?

编辑: REST API 内置在 OData 规范中,它支持与经典数据透视表(或中间表)的多对多关系。此表未在 API 中公开,但在上述 URL 中使用。所以我不能直接作为资源访问它。

用户架构 - 角色关系看起来也很标准。

|----------|    |-----------|     |--------|
| USER     |    | User_Role |     | Role   |
|----------|    |-----------|     |--------|
| Id       |-\  | Id        |   /-| Id     |
| Login    |  \-| UserId    |  /  | Name   |
| Password |    | RoleId    |-/   | Code   |
|----------|    |-----------|     |--------|

【问题讨论】:

  • 您能指定您的数据库设计,以便我更好地理解这个问题吗?我对 react-admin 不熟悉,但我可以告诉你 REST API 的外观。你能改变你的数据库设计吗?

标签: rest react-admin


【解决方案1】:

有一个非常相似的问题。我的solution 更像是一个 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 的一个分支。 我把index.js 改成了case GET_MANY_REFERENCE 下的这个:

      // 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)}`;

所以基本上我只是为target匹配硬编码正则表达式的特殊情况将参数重新映射到url。

ReferenceManyField 通常会导致 dataProvider 调用 api/roles?filter[_nested_users_id]=1,而此修改使 dataProvider 调用 api/users/1/roles。对 react-admin 来说是透明的。

不优雅,但它可以工作并且似乎不会破坏前端的任何东西。

【讨论】:

    【解决方案2】:

    你的问题已经回答 here,但我想告诉你我的解决方法,以便 React-Admin 处理多对多关系。

    正如在提到的答案中所说,您必须扩展 DataProvider 才能获取多对多关系的资源。但是,您需要使用新的 REST 动词,假设您的应用程序某处有 GET_MANY_MANY_REFERENCE。由于不同的 REST 服务/API 可以有不同的路由格式来获取相关资源,因此我没有费心尝试构建新的 DataProvider,我知道这不是一个很好的解决方案,但对于较短的期限来说相当简单。

    我的解决方案是从&lt;ReferenceManyField&gt; 获得灵感,并为多对多关系构建一个新组件&lt;ReferenceManyManyField&gt;。该组件使用fetch API 获取componentDidMount 上的相关记录。 On response 使用响应数据来构建对象,其中一个数据是一个对象,键是记录 ID,值是相应的记录对象,以及一个带有记录 ID 的 ids 数组。这与其他状态变量(如 page、sort、perPage、total)一起传递给子项,以处理数据的分页和排序。请注意,更改 Datagrid 中数据的顺序意味着将向 API 发出新请求。该组件分为控制器和视图,如&lt;ReferencemanyField&gt;,其中控制器获取数据、管理数据并将其传递给子级,而视图接收控制器数据并将其传递给子级渲染其内容。这使我可以在 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 状态中,在此实现中不是。除了组件使应用程序无法离线工作之外,该关系没有保存在任何地方,因为无法获取数据,甚至无法进行排序。

    【讨论】:

    • 相反,我认为您的解决方法是如何实现与 react-admin 的多对多关系。不过,感谢您分享您的想法,这对您了解需要做什么非常有帮助。
    • 无论如何,您介意我快速浏览一下您的解决方案吗?也许我们可以一起想出对已经很棒的 react-admin 的扩展:)
    • 不错的方法!这就是为什么我们严重依赖定制,这样有创意的用户可以做聪明的事情。 :)
    • 添加了我所拥有的实现和一个使用示例。希望它现在有所帮助,但我真的认为它需要更新并更认真地考虑它如何与 React-Admin 一起使用。
    • 是否应该在源代码中公开一个问题来讨论如何完成实现?
    【解决方案3】:

    TL;DR:默认情况下,React Admin 不支持嵌套资源,你必须write a custom data provider。

    这个问题在过去的问题上得到了回答:maremelab/react-admin#261

    详细解答

    React Admin 中的默认数据提供者是ra-data-simple-rest。

    正如其文档中所述,此库不支持嵌套资源,因为它仅使用资源名称和资源 ID 来构建资源 URL:

    为了支持嵌套资源,你必须编写自己的数据提供者。

    嵌套资源支持是recurrentfeaturerequest,但当时核心团队不想处理这种工作量。

    我强烈建议集结你的力量并编写一个外部数据提供者并像ra-data-odata 提供者一样发布它。这将是一个很好的补充,我们将很荣幸为您提供该外部软件包。

    【讨论】:

    • 感谢您的回复,我已经几乎完成了 OData 方言的自定义数据提供程序。为了让它真正发挥作用,我需要实现嵌套资源、更好的多对多参考字段和输入。现在我感到有点气馁,因为我觉得我不能胜任这项任务。正如你所说,这是工作量。我不想承诺任何事情,但我想继续这个很棒的项目,所以最终我将不得不分叉 react-admin 并扩展它,而不是寻找我讨厌做的解决方法。但现在我不得不妥协。再次感谢您的回复:)
    • 是的!你有一个起始示例,一个 OSS 存储库吗?如果您愿意,我们可以帮助您将志愿者重定向到您的仓库。
    • 现在我有私有存储库,但我不介意将它分叉到 Github。现在它有点脏,因为解决方法,但我可以发布我的自定义 odata 提供程序的旧版本,它可以很好地与过滤器、排序以及所有这些东西都期望嵌套资源和多对多关系。对于简单的 OData REST API 应该没问题
    • 0.1 版本是一个好的开始。随意在 GitHub 上创建一个 repo,并创建一个问题,您将在其中 ping 我@Kmaschta
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-09
    • 1970-01-01
    • 2011-01-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多