コンテンツにスキップ

ネストされたミューテーション

ひ Lighthouseはモデルとモデルに関連付いたリレーションの作成、更新、削除を全て一回のミューテーションで実行できます。これはネストされたarg resolverメカニズムによって可能となっています。

戻り値に対する型指定の要求

これを行うためには、Lighthouseが検出できるよう、リレーションのメソッドにに戻り値型の定義が必要です。

use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    public function user(): BelongsTo // これはOK
    {
        return $this->belongsTo(User::class);
    }

    public function comments() // これはNG
    {
        return $this->hasMany(Comment::class);
    }
}

部分的な失敗

デフォルトでは、全てのミューテーションはDBのトランザクションでラップされます。 もし、ネストされた更新操作が1つでも失敗した場合、全体のミューテーションは中止され、DBへの書き込みは何も行われません。

この振る舞いはconfigで変更できます.

多態型のリレーションに対する制限

GraphQLのSpecificationではぜんざいではまだ、多態型の入力型をサポートしていないため、多態型のリレーションに対して利用できる機能は限定されます。

関連する異なるモデルが持つ可能性のある属性を渡すために必要となる、異なる型を含むようは引数は持てません。 今のところは、入力型が関連するモデル間で変化しない場合のみに対応しています。(例えばIDを介して繋がったリレーションの、切断や削除です)

さらなる議論についてはこのissueを参照してください。

BelongsTo

BelongsToのリレーションをを取り扱いたい場合は、まず、Postを作成するためのミューテーションを定義します。

type Mutation {
  createPost(input: CreatePostInput! @spread): Post @create
}

このミューテーションはinputという単一の引数をとり、これは作成したいPostのデータを含みます。

input CreatePostInput {
  title: String!
  author: CreateUserBelongsTo
}

最初の引数titleは、Postモデルが持つ値で、DBのカラムに対応付来ます。

2つ目の引数authorは、Postモデルに定義されたリレーションのメソッドと同じ名前です。

ネストされたBelongsToリレーションは下記の操作を受け付けます。

  • connect は、既存のモデルに接続します。
  • create は、新しいリレーション先のモデルを作って、アタッチします。
  • update は、既存のモデルをアタッチします。
  • upsert は、新規、または既存のモデルをアタッチします。
  • disconnect は、関連するモデルを切断します。
  • delete は、関連するモデルとの関連づけを削除します。

disconnectdeleteは両方とも、更新のコンテキストではあまり意味を持ちません。 上記の中から必要なものだけをinputに定義することで、どのオーペレーションが可能かを指定できます。 以下では、関連するUserモデルに対して、3つの操作を公開しています。

input CreateUserBelongsTo {
  connect: ID
  create: CreateUserInput
  update: UpdateUserInput
  upsert: UpsertUserInput
}

最後に、新しいユーザを認めるためのinputを定義します。

input CreateUserInput {
  name: String!
}

新しいモデルを作って、既存のモデルに紐づけて合い場合は、紐づけたいモデルのIDを渡せば良いです。

mutation {
  createPost(input: { title: "My new Post", author: { connect: 123 } }) {
    id
    author {
      name
    }
  }
}

Lighthouseは新しいPostを作って、それをUserに関連づけます。

{
  "data": {
    "createPost": {
      "id": 456,
      "author": {
        "name": "Herbert"
      }
    }
  }
}

関連するモデルがまだ存在していない場合、新規に作成することもできます。 下記の例ではUserをcreateで生成しています。

mutation {
  createPost(
    input: { title: "My new Post", author: { create: { name: "Gina" } } }
  ) {
    id
    author {
      id
    }
  }
}
{
  "data": {
    "createPost": {
      "id": 456,
      "author": {
        "id": 55
      }
    }
  }
}

アップデートのミューテーションを発行する時に、ユーザーがリレーションを削除できるようにすることも可能です。 disconnectdeleteはどちらもauthorへの関連付けを削除します。ですが、deleteはauthorモデル自体も削除します。

type Mutation {
  updatePost(input: UpdatePostInput! @spread): Post @update
}

input UpdatePostInput {
  id: ID!
  title: String
  author: UpdateUserBelongsTo
}

input UpdateUserBelongsTo {
  connect: ID
  create: CreateUserInput
  update: UpdateUserInput
  upsert: UpdateUserInput
  disconnect: Boolean
  delete: Boolean
}

実際に実行するためには、disconnectdelete にtrueと見做される値を渡す必要があります。 この構造が選ばれたのは、BelongsToManyリレーションシップを更新する際に一貫性があるのと、クエリストリングがほとんど静的で変数値ををもとに動作を制御することができるためです。

mutation UpdatePost($disconnectAuthor: Boolean) {
  updatePost(
    input: {
      id: 1
      title: "An updated title"
      author: { disconnect: $disconnectAuthor }
    }
  ) {
    title
    author {
      name
    }
  }
}

authorリレーションは、変数$disconnectAuthortrueの時のみで、falsenullが渡されたときは変更されません。

{
  "data": {
    "updatePost": {
      "id": 1,
      "title": "An updated title",
      "author": null
    }
  }
}

upsertを発行する際に、updateと同じネストされたオペレーションを公開できます。 新しいモデルが作成された場合は無視されます。

mutation UpdatePost($disconnectAuthor: Boolean) {
  upsertPost(
    input: {
      id: 1
      title: "An updated or created title"
      author: { disconnect: $disconnectAuthor }
    }
  ) {
    id
    title
    author {
      name
    }
  }
}
{
  "data": {
    "upsertPost": {
      "id": 1,
      "title": "An updated or created title",
      "author": null
    }
  }
}

MorphTo

ネストされたMprphToのミューテーション側の基本的な構造はBelongsToと似ています。 関連するモデルのidtypeの両方を持っているインプット型を要求する点が、connectとの一番の違いです。

type Task {
  name: String
}

type Image {
  url: String
  imageable: Task @morphTo
}

type Mutation {
  updateImage(input: UpdateImageInput! @spread): Image @update
}

input UpdateImageInput {
  id: ID!
  url: String
  imageable: UpdateImageableMorphTo
}

input UpdateImageableMorphTo {
  connect: ConnectImageableInput
  disconnect: Boolean
  delete: Boolean
}

input ConnectImageableInput {
  type: String!
  id: ID!
}

既存のモデルを関連付けるには、connect を使用します。

mutation {
  createImage(
    input: {
      url: "https://cats.example/cute"
      imageable: { connect: { type: "App\\Models\\Task", id: 1 } }
    }
  ) {
    id
    url
    imageable {
      id
      name
    }
  }
}

disconnectオペレーションは、現在関連付けられているモデルを切り離すためにあります。

mutation {
  updateImage(
    input: {
      id: 1
      url: "https://dogs.example/supercute"
      imageable: { disconnect: true }
    }
  ) {
    url
    imageable {
      id
      name
    }
  }
}

deleteオペレーションは、現在関連付けられているモデルを切り離して、さらに削除します。

mutation {
  upsertImage(
    input: {
      id: 1
      url: "https://bizniz.example/serious"
      imageable: { delete: true }
    }
  ) {
    url
    imageable {
      id
      name
    }
  }
}

HasOne

The counterpart to a BelongsTo relationship can be HasOne. We will start off by defining a mutation to update a User.

type Mutation {
  updateUser(input: UpdateUserInput! @spread): User @update
}

This mutation takes a single argument input that contains values of the User itself and its associated Phone model.

input UpdateUserInput {
  id: ID!
  name: String
  phone: UpdatePhoneHasOne
}

Now, we can expose operations that allows us to update the users phone.

input UpdatePhoneHasOne {
  create: CreatePhoneInput
  update: UpdatePhoneInput
  upsert: UpsertPhoneInput
  delete: ID
}

input CreatePhoneInput {
  number: String!
}

input UpdatePhoneInput {
  id: ID!
  number: String
}

input UpsertPhoneInput {
  id: ID
  number: String
}

We can now update the User and their phone in one request.

mutation {
  updateUser(
    input: {
      id: 4
      name: "Donald"
      phone: { update: { id: 92, number: "+12 345 6789" } }
    }
  ) {
    id
  }
}

MorphOne

Works exactly like HasOne

HasMany

Another possible counterpart to a BelongsTo relationship is HasMany. We will start off by defining a mutation to create an User.

type Mutation {
  createUser(input: CreateUserInput! @spread): User @create
}

This mutation takes a single argument input that contains values of the User itself and its associated Post models.

input CreateUserInput {
  name: String!
  posts: CreatePostsHasMany
}

Now, we can expose an operation that allows us to directly create new posts right when we create the User.

input CreatePostsHasMany {
  create: [CreatePostInput!]!
}

input CreatePostInput {
  title: String!
}

We can now create a User and some posts with it in one request.

mutation {
  createUser(
    input: {
      name: "Phil"
      posts: {
        create: [
          { title: "Phils first post" }
          { title: "Awesome second post" }
        ]
      }
    }
  ) {
    id
    posts {
      id
    }
  }
}
{
  "data": {
    "createUser": {
      "id": 23,
      "posts": [
        {
          "id": 434
        },
        {
          "id": 435
        }
      ]
    }
  }
}

When updating a User, further nested operations become possible. It is up to you which ones you want to expose through the schema definition.

The following example covers the full range of possible operations:

type Mutation {
  updateUser(input: UpdateUserInput! @spread): User @update
}

input UpdateUserInput {
  id: ID!
  name: String
  posts: UpdatePostsHasMany
}

input UpdatePostsHasMany {
  create: [CreatePostInput!]
  update: [UpdatePostInput!]
  upsert: [UpsertPostInput!]
  delete: [ID!]
}

input CreatePostInput {
  title: String!
}

input UpdatePostInput {
  id: ID!
  title: String
}

input UpsertPostInput {
  id: ID!
  title: String
}
mutation {
  updateUser(
    input: {
      id: 3
      name: "Phillip"
      posts: {
        create: [{ title: "A new post" }]
        update: [{ id: 45, title: "This post is updated" }]
        delete: [8]
      }
    }
  ) {
    id
    posts {
      id
    }
  }
}

The behaviour for upsert is a mix between updating and creating, it will produce the needed action regardless of whether the model exists or not.

MorphMany

Works exactly like Has Many.

BelongsToMany

A belongs to many relation allows you to create new related models as well as attaching existing ones.

type Mutation {
  createPost(input: CreatePostInput! @spread): Post @create
}

input CreatePostInput {
  title: String!
  authors: CreateAuthorBelongsToMany
}

input CreateAuthorBelongsToMany {
  create: [CreateAuthorInput!]
  upsert: [UpsertAuthorInput!]
  connect: [ID!]
  sync: [ID!]
}

input CreateAuthorInput {
  name: String!
}

input UpsertAuthorInput {
  id: ID!
  name: String!
}

Just pass the ID of the models you want to associate or their full information to create a new relation.

mutation {
  createPost(
    input: {
      title: "My new Post"
      authors: {
        create: [{ name: "Herbert" }]
        upsert: [{ id: 2000, name: "Newton" }]
        connect: [123]
      }
    }
  ) {
    id
    authors {
      name
    }
  }
}

Lighthouse will detect the relationship and attach, update or create it.

{
  "data": {
    "createPost": {
      "id": 456,
      "authors": [
        {
          "id": 165,
          "name": "Herbert"
        },
        {
          "id": 2000,
          "name": "Newton"
        },
        {
          "id": 123,
          "name": "Franz"
        }
      ]
    }
  }
}

It is also possible to use the sync operation to ensure only the given IDs will be contained withing the relation.

mutation {
  createPost(input: { title: "My new Post", authors: { sync: [123] } }) {
    id
    authors {
      name
    }
  }
}

Updates on BelongsToMany relations may expose additional nested operations:

input UpdateAuthorBelongsToMany {
  create: [CreateAuthorInput!]
  connect: [ID!]
  update: [UpdateAuthorInput!]
  upsert: [UpsertAuthorInput!]
  sync: [ID!]
  syncWithoutDetaching: [ID!]
  delete: [ID!]
  disconnect: [ID!]
}

Storing Pivot Data

It is common that many-to-many relations store some extra data in pivot tables. Suppose we want to track what movies a user has seen. In addition to connecting the two entities, we want to store how well they liked it:

type User {
  id: ID!
  seenMovies: [Movie!] @belongsToMany
}

type Movie {
  id: ID!
  pivot: UserMoviePivot
}

type UserMoviePivot {
  "How well did the user like the movie?"
  rating: String
}

Laravel's sync(), syncWithoutDetaching() or connect() methods allow you to pass an array where the keys are IDs of related models and the values are pivot data.

Lighthouse exposes this capability through the nested operations on many-to-many relations. Instead of passing just a list of ids, you can define an input type that also contains pivot data. It must contain a field called id to contain the ID of the related model, all other fields will be inserted into the pivot table.

type Mutation {
  updateUser(input: UpdateUserInput! @spread): User @update
}

input UpdateUserInput {
  id: ID!
  seenMovies: UpdateUserSeenMovies
}

input UpdateUserSeenMovies {
  connect: [ConnectUserSeenMovie!]
}

input ConnectUserSeenMovie {
  id: ID!
  rating: String
}

You can now pass along pivot data when connecting users to movies:

mutation {
  updateUser(
    input: {
      id: 1
      seenMovies: { connect: [{ id: 6, rating: "A perfect 5/7" }, { id: 23 }] }
    }
  ) {
    id
    seenMovies {
      id
      pivot {
        rating
      }
    }
  }
}

You will get the following response:

{
  "data": {
    "updateUser": {
      "id": 1,
      "seenMovies": [
        {
          "id": 6,
          "pivot": {
            "rating": "A perfect 5/7"
          }
        },
        {
          "id": 20,
          "pivot": {
            "rating": null
          }
        }
      ]
    }
  }
}

It is also possible to use the sync and syncWithoutDetaching operations.

MorphToMany

Works exactly like BelongsToMany.