Mutations

直到目前为止,我们只与GraphQL端点进行交互,以执行获取数据的查询。 在本指南中,你将学习如何使用 Relay 实现 mutations – 包括对数据存储的写入以及任何已更改字段的获取的操作。

一个完整的例子 #

在深入了解 mutation API 之前, 我们来看一个完整的例子。在这里,我们子类 Relay.Mutation 创建一个我们可以用来喜欢故事的自定义 mutation。

class LikeStoryMutation extends Relay.Mutation {
  // This method should return a GraphQL operation that represents
  // the mutation to be performed. This presumes that the server
  // implements a mutation type named ‘likeStory’.
  getMutation() {
    return Relay.QL`mutation {likeStory}`;
  }
  // Use this method to prepare the variables that will be used as
  // input to the mutation. Our ‘likeStory’ mutation takes exactly
  // one variable as input – the ID of the story to like.
  getVariables() {
    return {storyID: this.props.story.id};
  }
  // Use this method to design a ‘fat query’ – one that represents every
  // field in your data model that could change as a result of this mutation.
  // Liking a story could affect the likers count, the sentence that
  // summarizes who has liked a story, and the fact that the viewer likes the
  // story or not. Relay will intersect this query with a ‘tracked query’
  // that represents the data that your application actually uses, and
  // instruct the server to include only those fields in its response.
  getFatQuery() {
    return Relay.QL`
      fragment on LikeStoryPayload {
        story {
          likers {
            count,
          },
          likeSentence,
          viewerDoesLike,
        },
      }
    `;
  }
  // These configurations advise Relay on how to handle the LikeStoryPayload
  // returned by the server. Here, we tell Relay to use the payload to
  // change the fields of a record it already has in the store. The
  // key-value pairs of ‘fieldIDs’ associate field names in the payload
  // with the ID of the record that we want updated.
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {
        story: this.props.story.id,
      },
    }];
  }
  // This mutation has a hard dependency on the story's ID. We specify this
  // dependency declaratively here as a GraphQL query fragment. Relay will
  // use this fragment to ensure that the story's ID is available wherever
  // this mutation is used.
  static fragments = {
    story: () => Relay.QL`
      fragment on Story {
        id,
      }
    `,
  };
}

下面是 LikeButton 组件使用 mutation 的示例:

class LikeButton extends React.Component {
  _handleLike = () => {
    // To perform a mutation, pass an instance of one to
    // `this.props.relay.commitUpdate`
    this.props.relay.commitUpdate(
      new LikeStoryMutation({story: this.props.story})
    );
  }
  render() {
    return (
      <div>
        {this.props.story.viewerDoesLike
          ? 'You like this'
          : <button onClick={this._handleLike}>Like this</button>
        }
      </div>
    );
  }
}

module.exports = Relay.createContainer(LikeButton, {
  fragments: {
    // You can compose a mutation's query fragments like you would those
    // of any other RelayContainer. This ensures that the data depended
    // upon by the mutation will be fetched and ready for use.
    story: () => Relay.QL`
      fragment on Story {
        viewerDoesLike,
        ${LikeStoryMutation.getFragment('story')},
      }
    `,
  },
});

在这个特殊的例子中,LikeButton 关心的唯一是 viewerDoesLike. 该字段将构成跟踪查询的一部分, Relay 将与 fat 查询相结合 LikeStoryMutation 确定要作为服务器响应有效载荷作为突变的一部分的字段。应用程序中其他部分的组件可能也对 likers count,或是 like sentence 相关。由于这些字段会自动地被加进 Relay 的跟踪查询, LikeButton 不需要明确请求它们。

Mutation props #

我们传递给 mutation 的构造函数的 props 可以在它的实体方法由 this.props 调用。 就像在 Relay 容器中使用组件一样, 已经定义了相应片段的 props 将由 Relay 使用查询数据填充:

class LikeStoryMutation extends Relay.Mutation {
  static fragments = {
    story: () => Relay.QL`
      fragment on Story {
        id,
        viewerDoesLike,
      }
    `,
  };
  getMutation() {
    // Here, viewerDoesLike is guaranteed to be available.
    // We can use it to make this mutation polymorphic.
    return this.props.story.viewerDoesLike
      ? Relay.QL`mutation {unlikeStory}`
      : Relay.QL`mutation {likeStory}`;
  }
  /* ... */
}

片段变量 #

就像使用 Relay 容器一样, we 我们可以根据以前的变量和运行时环境,为 mutation的片段构建器初始化变量。

class RentMovieMutation extends Relay.Mutation {
  static initialVariables = {
    format: 'hd',
    lang: 'en-CA',
  };
  static prepareVariables = (prevVariables) => {
    var overrideVariables = {};
    if (navigator.language) {
      overrideVariables.lang = navigator.language;
    }
    var formatPreference = localStorage.getItem('formatPreference');
    if (formatPreference) {
      overrideVariables.format = formatPreference;
    }
    return {...prevVariables, ...overrideVariables};
  };
  static fragments = {
    // Now we can use the variables we've prepared to fetch movies
    // appropriate for the viewer's locale and preferences
    movie: () => Relay.QL`
      fragment on Movie {
        posterImage(lang: $lang) { url },
        trailerVideo(format: $format, lang: $lang) { url },
      }
    `,
  };
}

fat 查询 #

改变系统中一个东西,可能会造成其他东西接着改变的连锁反应。想象一个我们可以用来接受好友邀请的 mutation。这可能造成很大的影响:

  • 两人的好友数都会增加
  • 表示新好友的边将被添加到观众的 friends 连接中
  • 表示观众的边将被添加到新好友的 friends 连接中
  • 观众与请求者的友谊状态将会改变

设计一个 fat 查询,涵盖可能改变的每一个可能的领域:

class AcceptFriendRequestMutation extends Relay.Mutation {
  getFatQuery() {
    // This presumes that the server-side implementation of this mutation
    // returns a payload of type `AcceptFriendRequestPayload` that exposes
    // `friendEdge`, `friendRequester`, and `viewer` fields.
    return Relay.QL`
      fragment on AcceptFriendRequestPayload {
        friendEdge,
        friendRequester {
          friends,
          friendshipStatusWithViewer,
        },
        viewer {
          friends,
        },
      }
    `;
  }
}

这个 fat 查询看起来像任何其他GraphQL查询,有一个重要的区别。我们知道这些字段中的一些是非标量的(如 friendEdgefriends) 但是注意到我们没有通过子查询命名他们的任何子项。以这种方式,我们向 Relay 指出,由于这种 mutation,这些非标量场下的 任何事情 都可能会改变。

附注

设计 fat 查询时,请考虑由于 mutation 而可能更改的所有 数据 – 而不仅仅是应用程序当前正在使用的数据。我们不需要担心过度提取; 这个查询永远不会执行,而不必首先将其与我们的应用程序实际需要的数据的“跟踪查询”相结合。如果我们在 fat 查询中省略字段,则可能会在将来添加具有新数据依赖关系的视图或在现有视图中添加新数据依赖关系时,观察数据不一致。

Mutator 配置 #

我们需要给出关于如何使用每个 mutation 的响应有效载荷来更新客户端存储的 Relay 指令。 我们通过配置一种或多种以下 mutation 类型的 mutation 来实现:

FIELDS_CHANGE #

可以通过DataID与客户端存储中的一个或多个记录相关联的有效内容中的任何字段将与存储中的记录合并。

参数 #

  • fieldIDs: {[fieldName: string]: DataID | Array<DataID>}

    A map between a fieldName 响应中的一个和存储中的一个或多个DataID 之间的映射。

示例 #

class RenameDocumentMutation extends Relay.Mutation {
  // This mutation declares a dependency on a document's ID
  static fragments = {
    document: () => Relay.QL`fragment on Document { id }`,
  };
  // We know that only the document's name can change as a result
  // of this mutation, and specify it here in the fat query.
  getFatQuery() {
    return Relay.QL`
      fragment on RenameDocumentMutationPayload { updatedDocument { name } }
    `;
  }
  getVariables() {
    return {id: this.props.document.id, newName: this.props.newName};
  }
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      // Correlate the `updatedDocument` field in the response
      // with the DataID of the record we would like updated.
      fieldIDs: {updatedDocument: this.props.document.id},
    }];
  }
  /* ... */
}

NODE_DELETE #

给定父节点,连接和响应有效负载中的一个或多个DataID,Relay将从连接中删除节点,并从存储中删除关联的记录。

参数 #

  • parentName: string

    响应中的字段名称,表示连接的父级

  • parentID?: string

    包含连接的父节点的DataID。这个参数是可选的。

  • connectionName: string

    表示连接的响应中的字段名称

  • deletedIDFieldName: string

    响应中包含已删除节点的DataID的字段名称

示例 #

class DestroyShipMutation extends Relay.Mutation {
  // This mutation declares a dependency on an enemy ship's ID
  // and the ID of the faction that ship belongs to.
  static fragments = {
    ship: () => Relay.QL`fragment on Ship { id, faction { id } }`,
  };
  // Destroying a ship will remove it from a faction's fleet, so we
  // specify the faction's ships connection as part of the fat query.
  getFatQuery() {
    return Relay.QL`
      fragment on DestroyShipMutationPayload {
        destroyedShipID,
        faction { ships },
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'NODE_DELETE',
      parentName: 'faction',
      parentID: this.props.ship.faction.id,
      connectionName: 'ships',
      deletedIDFieldName: 'destroyedShipID',
    }];
  }
  /* ... */
}

RANGE_ADD #

给定一个父级, 一个连接,并且响应有效负载中新创建的边的名称Relay将将该节点添加到该存储,并根据指定的范围行为将其附加到该连接。

参数 #

  • parentName: string

    响应中的字段名称,表示连接的父级

  • parentID?: string

    包含连接的父节点的DataID。这个参数是可选的。

  • connectionName: string

    表示连接的响应中的字段名称

  • edgeName: string

    响应中的字段名称表示新创建的边

  • rangeBehaviors: {[call: string]: GraphQLMutatorConstants.RANGE_OPERATIONS} | (connectionArgs: {[argName: string]: string}) => $Enum<GraphQLMutatorConstants.RANGE_OPERATIONS>

    一个 printed、dot-separated 依字母顺序的 GraphQL 调用 以及我们想要 Relay 在这些 调用 的影响下添加新的边到连接的行为之间的 map 或是一个接收一个连接参数的数组,回传那个行为的函数。

例如, rangeBehaviors 可以这样写:

const rangeBehaviors = {
  // When the ships connection is not under the influence
  // of any call, append the ship to the end of the connection
  '': 'append',
  // Prepend the ship, wherever the connection is sorted by age
  'orderby(newest)': 'prepend',
};

或者这样,结果相同:

const rangeBehaviors = ({orderby}) => {
  if (orderby === 'newest') {
    return 'prepend';
  } else {
    return 'append';
  }
};

行为可以是一个 'append', 'ignore', 'prepend', 'refetch', 或 'remove'

示例 #

class IntroduceShipMutation extends Relay.Mutation {
  // This mutation declares a dependency on the faction
  // into which this ship is to be introduced.
  static fragments = {
    faction: () => Relay.QL`fragment on Faction { id }`,
  };
  // Introducing a ship will add it to a faction's fleet, so we
  // specify the faction's ships connection as part of the fat query.
  getFatQuery() {
    return Relay.QL`
      fragment on IntroduceShipPayload {
        faction { ships },
        newShipEdge,
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'RANGE_ADD',
      parentName: 'faction',
      parentID: this.props.faction.id,
      connectionName: 'ships',
      edgeName: 'newShipEdge',
      rangeBehaviors: {
        // When the ships connection is not under the influence
        // of any call, append the ship to the end of the connection
        '': 'append',
        // Prepend the ship, wherever the connection is sorted by age
        'orderby(newest)': 'prepend',
      },
    }];
  }
  /* ... */
}

RANGE_DELETE #

给定一个连接,响应有效负载中的一个或多个DataID以及父连接之间的路径,Relay将从连接中删除节点,但将关联的记录保留在存储中。

参数 #

  • deletedIDFieldName: string | Array<string>

    响应中包含已删除节点的DataID的字段名称,或从该连接中删除节点的路径

  • pathToConnection: Array<string>

    包含父级和连接之间的字段名称的数组,包括父级和连接

示例 #

class RemoveTagMutation extends Relay.Mutation {
  // This mutation declares a dependency on the
  // todo from which this tag is being removed.
  static fragments = {
    todo: () => Relay.QL`fragment on Todo { id }`,
  };
  // Removing a tag from a todo will affect its tags connection
  // so we specify it here as part of the fat query.
  getFatQuery() {
    return Relay.QL`
      fragment on RemoveTagMutationPayload {
        todo { tags },
        removedTagIDs,
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'RANGE_DELETE',
      deletedIDFieldName: 'removedTagIDs',
      pathToConnection: ['todo', 'tags'],
    }];
  }
  /* ... */
}

REQUIRED_CHILDREN #

REQUIRED_CHILDREN 配置用于将附加的子项附加到 mutation 查询中。 你可能会需要使用这个,例如,要获取在一个 mutation 建立的新对象上的字段 (而Relay 通常不会尝试去获取的,因为它先前没有为那个对象获取任何东西)。

作为 REQUIRED_CHILDREN 配置而被获取的数据不会被写入客户端的 存储,不过你可以在传递进去 commitUpdate()onSuccess 回调添加对其处理的程序代码:

this.props.relay.commitUpdate(
  new CreateCouponMutation(),
  {
    onSuccess: response => this.setState({
      couponCount: response.coupons.length,
    }),
  }
);

参数 #

  • children: Array<RelayQuery.Node>

示例 #

class CreateCouponMutation extends Relay.Mutation<Props> {
  getMutation() {
    return Relay.QL`mutation {
      create_coupon(data: $input)
    }`;
  }

  getFatQuery() {
    return Relay.QL`
      // Note the use of `pattern: true` here to show that this
      // connection field is to be used for pattern-matching only
      // (to determine what to fetch) and that Relay shouldn't
      // require the usual connection arguments like (`first` etc)
      // to be present.
      fragment on CouponCreatePayload @relay(pattern: true) {
        coupons
      }
    `;
  }

  getConfigs() {
    return [{
      // If we haven't shown the coupons in the UI at the time the
      // mutation runs, they've never been fetched and the `coupons`
      // field in the fat query would normally be ignored.
      // `REQUIRED_CHILDREN` forces it to be retrieved anyway.
      type: RelayMutationType.REQUIRED_CHILDREN,
      children: [
        Relay.QL`
          fragment on CouponCreatePayload {
            coupons
          }
        `,
      ],
    }];
  }
}

积极更新 #

目前为止,我们执行的所有 mutation 在更新客户端存储之前等待了服务器的响应。 Relay 提供一个机会给我们基于我们期望在 mutation 成功的情况下服务器的响应,去构造一个相同的积极响应。

让我们对上面 LikeStoryMutation 的例子做出积极的回应:

class LikeStoryMutation extends Relay.Mutation {
  /* ... */
  // Here's the fat query from before
  getFatQuery() {
    return Relay.QL`
      fragment on LikeStoryPayload {
        story {
          likers {
            count,
          },
          likeSentence,
          viewerDoesLike,
        },
      }
    `;
  }
  // Let's craft an optimistic response that mimics the shape of the
  // LikeStoryPayload, as well as the values we expect to receive.
  getOptimisticResponse() {
    return {
      story: {
        id: this.props.story.id,
        likers: {
          count: this.props.story.likers.count + (this.props.story.viewerDoesLike ? -1 : 1),
        },
        viewerDoesLike: !this.props.story.viewerDoesLike,
      },
    };
  }
  // To be able to increment the likers count, and flip the viewerDoesLike
  // bit, we need to ensure that those pieces of data will be available to
  // this mutation, in addition to the ID of the story.
  static fragments = {
    story: () => Relay.QL`
      fragment on Story {
        id,
        likers { count },
        viewerDoesLike,
      }
    `,
  };
  /* ... */
}

你不需要模拟完整的响应载荷。在这里,我们已经尝试过。因为要在客户端上处理在地化很困难。当服务器响应时,Relay 会把它的载荷当作真实来源,不过在这期间,这个积极响应会立刻被应用,让使得用我们产品的用户能在做了一个动作后立即获得反馈。