ネストされたミューテーション
ひ 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を作成するためのミューテーションを定義します。
このミューテーションはinput
という単一の引数をとり、これは作成したいPostのデータを含みます。
最初の引数title
は、Post
モデルが持つ値で、DBのカラムに対応付来ます。
2つ目の引数author
は、Post
モデルに定義されたリレーションのメソッドと同じ名前です。
ネストされたBelongsTo
リレーションは下記の操作を受け付けます。
connect
は、既存のモデルに接続します。create
は、新しいリレーション先のモデルを作って、アタッチします。update
は、既存のモデルをアタッチします。upsert
は、新規、または既存のモデルをアタッチします。disconnect
は、関連するモデルを切断します。delete
は、関連するモデルとの関連づけを削除します。
disconnect
とdelete
は両方とも、更新のコンテキストではあまり意味を持ちません。
上記の中から必要なものだけをinput
に定義することで、どのオーペレーションが可能かを指定できます。
以下では、関連するUser
モデルに対して、3つの操作を公開しています。
input CreateUserBelongsTo {
connect: ID
create: CreateUserInput
update: UpdateUserInput
upsert: UpsertUserInput
}
最後に、新しいユーザを認めるためのinput
を定義します。
新しいモデルを作って、既存のモデルに紐づけて合い場合は、紐づけたいモデルのIDを渡せば良いです。
mutation {
createPost(input: { title: "My new Post", author: { connect: 123 } }) {
id
author {
name
}
}
}
Lighthouseは新しいPost
を作って、それをUser
に関連づけます。
関連するモデルがまだ存在していない場合、新規に作成することもできます。
下記の例ではUserをcreate
で生成しています。
mutation {
createPost(
input: { title: "My new Post", author: { create: { name: "Gina" } } }
) {
id
author {
id
}
}
}
アップデートのミューテーションを発行する時に、ユーザーがリレーションを削除できるようにすることも可能です。
disconnect
とdelete
はどちらも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
}
実際に実行するためには、disconnect
と delete
にtrueと見做される値を渡す必要があります。
この構造が選ばれたのは、BelongsToMany
リレーションシップを更新する際に一貫性があるのと、クエリストリングがほとんど静的で変数値ををもとに動作を制御することができるためです。
mutation UpdatePost($disconnectAuthor: Boolean) {
updatePost(
input: {
id: 1
title: "An updated title"
author: { disconnect: $disconnectAuthor }
}
) {
title
author {
name
}
}
}
author
リレーションは、変数$disconnectAuthor
がtrue
の時のみで、false
やnull
が渡されたときは変更されません。
upsert
を発行する際に、update
と同じネストされたオペレーションを公開できます。
新しいモデルが作成された場合は無視されます。
mutation UpdatePost($disconnectAuthor: Boolean) {
upsertPost(
input: {
id: 1
title: "An updated or created title"
author: { disconnect: $disconnectAuthor }
}
) {
id
title
author {
name
}
}
}
MorphTo
ネストされたMprphToのミューテーション側の基本的な構造はBelongsToと似ています。
関連するモデルのid
とtype
の両方を持っているインプット型を要求する点が、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
.
This mutation takes a single argument input
that contains values
of the User
itself and its associated Phone
model.
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
.
This mutation takes a single argument input
that contains values
of the User
itself and its associated Post
models.
Now, we can expose an operation that allows us to directly create new posts
right when we create the User
.
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
}
}
}
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.