支持资源嵌套

时间:2019-01-18 13:30:10

标签: rest react-admin

我想知道,是否可以将DataProvider / Resource / List配置为支持诸如api/users/1/roles之类的REST网址?

对于RESTful API,获取某些父实体的子代是非常常见的用例,但是我无法弄清楚它如何设置React Admin并实现这一点。我正在OData规范后端上使用自定义DataProvider。

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

我正在监督某些事情,还是有一些我根本看不到的方法?

编辑: REST API是基于OData规范构建的,它支持与经典数据透视表(或中间表)的多对多关系。该表未在API中公开,但在上面的网址中使用。因此,我无法直接将其作为资源访问。

用户模式-角色关系看起来也很标准。

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

3 个答案:

答案 0 :(得分:5)

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

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

详细答案

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

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

Simple REST Data Provider

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

嵌套资源支持是recurrent feature request,但当时,核心团队不想处理这些工作。

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

答案 1 :(得分:4)

您的问题已经回答here,但是我想向您介绍我的解决方法,以便React-Admin与多对多关系进行工作。

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

我的解决方案是从<ReferenceManyField>那里获得灵感,并为多对多关系建立了一个新的组件<ReferenceManyManyField>。该组件使用fetch API获取componentDidMount上的相关记录。在响应时,使用响应数据将对象构建为一个数据,该数据是一个对象,其键为记录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状态下,在此实现中不是。该关系不会保存在组件之外的任何地方,从而使应用程序无法离线运行,因为无法获取数据,甚至无法订购。

答案 2 :(得分:0)

有一个非常相似的问题。我的solution有点像黑客,但是如果您想要启用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)}`;

因此,基本上,我只是将特殊情况下的参数重新映射到url,其中target与硬编码的正则表达式匹配。

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

不太优雅,但它可以正常工作,而且前端似乎没有任何损坏。