Add id manually while using GraphQL

Hi,

Assuimng my scheme is:

type Person {
    id: ID!
    name: String @search(by: [hash])
    friends: [Person] @hasInverse(field: friends)
}

Is there a way to define IDs to objects in GraphQL like the following in graphql±:

{
    "set":[
        {
            "uid": "_:p1",
            "dgraph.type": "Person",
            "Person.name": "John",
            "Person.friends": [
                {
                    "uid": "_:p2"
                },
                {
                    "uid": "_:p3"
                }
            ]
        },
        {
            "uid":"_:p2",
            "dgraph.type":"Person",
            "Person.name": "Doe"
        },
        {
            "uid": "_:p3",
            "dgraph.type": "Person",
            "Person.name": "Jane",
            "Person.friends": [
                {
                    "uid":"_:p1"
                }
            ]
        }
    ]
}

Also, what is the right way to insert the following data using GraphQL and the scheme I supplied above?
Notice that the ‘friends’ field has the @hasInverse (or @revers in DQL)
My guess was the one below but it doesn’t has the same result:

mutation {
  addPerson(
    input: [
      {
        name: "Or"
        friends: [
          { name: "Michal", friends: [{ name: "Or" }] }
          { name: "Elior" }
        ]
      }
    ]
  ) {
    person {
      name
      friends {
        name
      }
    }
  }
}

Thanks

To set custom id, you can use @id directive.

type Person {
  id: String! @id
}

Refer: Documentation on @Id

1 Like

Thanks, I meant how can I add an id in the data not in the scheme.
In DQL for example it is possible to do this:

{
            "uid": "_:p1",
            "dgraph.type": "Person",
            "Person.name": "John",

where _:p1 is being translated into a uid

There is no way to to do this with your schema in graphql generated mutations in a single mutation. You will either need to use xids but that can cause extra work on the front end needing to generate uniqie ids of some sort. If creating two people at once and trying to link them as friends, you may need to do it with a custom DQL which has its draw backs as well.

This doesn’t have the results expected because it created two seaparate ‘Or’ people. The generated mutation is not able to know that the first ‘Or’ is the same as the second ‘Or’ because the name field is not unique constraint.

However, having the hasInverse. You only need to make the link from one to the other and the reverse will be made automatically.

I will look at this a little closer later on my desktop to get the whole concept and maybe a better answer

Thank you.
I tried some other forms of the data and I tuckled something a little bit strange:
That’s the data:

mutation {
  addPerson(
    input: [
      {
        name: "Or"
        friends: [
          { name: "Michal", friends: [{ name: "Justin" }] }
          { name: "Elior" }
        ]
      }
    ]
  ) {
    person {
      name
      friends {
        name
      }
    }
  }
}

That’s my query:

query{
  queryPerson{
    id
    name
    friends{
      name
    }
  }
}

And this is the result

{
  "data": {
    "queryPerson": [
      {
        "id": "0x9c62",
        "name": "Elior",
        "friends": [
          {
            "name": "Or"
          }
        ]
      },
      {
        "id": "0x9c63",
        "name": "Justin",
        "friends": [
          {
            "name": "Michal"
          }
        ]
      },
      {
        "id": "0x9c64",
        "name": "Michal",
        "friends": [
          {
            "name": "Justin"
          }
        ]
      },
      {
        "id": "0x9c65",
        "name": "Or",
        "friends": [
          {
            "name": "Elior"
          },
          {
            "name": "Michal"
          }
        ]
      }
    ]
  }
}

Michal doesn’t have the revers connection with Or.
Can you please explain why?

@MichelDiz any ideas?

1 Like

Graphql requires specification of the traversals, and the following query will get you the other edges as well. Please see if this helps.

query{
  queryPerson{
    id
    name
    friends{
      name
      friends{
        name
      }
    }
  }
}

Response:

{
  "data": {
    "queryPerson": [
      {
        "id": "0x2",
        "name": "Or",
        "friends": [
          {
            "name": "Michal",
            "friends": [
              {
                "name": "Or"
              }
            ]
          }
        ]
      },
      {
        "id": "0x3",
        "name": "Michal",
        "friends": [
          {
            "name": "Or",
            "friends": [
              {
                "name": "Michal"
              }
            ]
          }
        ]
      },
      {
        "id": "0x4",
        "name": "Or",
        "friends": [
          {
            "name": "Michal",
            "friends": [
              {
                "name": "Or"
              }
            ]
          },
          {
            "name": "Elior",
            "friends": [
              {
                "name": "Or"
              }
            ]
          }
        ]
      },
      {
        "id": "0x5",
        "name": "Elior",
        "friends": [
          {
            "name": "Or",
            "friends": [
              {
                "name": "Michal"
              },
              {
                "name": "Elior"
              }
            ]
          }
        ]
      }
    ]
  },
  "extensions": {
    "touched_uids": 32
  }
}

Nope :confused:
The result of the query you suggest is:

{
  "data": {
    "queryPerson": [
      {
        "id": "0xc356",
        "name": "Or",
        "friends": [
          {
            "name": "Michal",
            "friends": [
              {
                "name": "Justin"
              }
            ]
          },
          {
            "name": "Elior",
            "friends": [
              {
                "name": "Or"
              }
            ]
          }
        ]
      },
      {
        "id": "0xc357",
        "name": "Justin",
        "friends": [
          {
            "name": "Michal",
            "friends": [
              {
                "name": "Justin"
              }
            ]
          }
        ]
      },
      {
        "id": "0xc358",
        "name": "Michal",
        "friends": [
          {
            "name": "Justin",
            "friends": [
              {
                "name": "Michal"
              }
            ]
          }
        ]
      },
      {
        "id": "0xc359",
        "name": "Elior",
        "friends": [
          {
            "name": "Or",
            "friends": [
              {
                "name": "Michal"
              },
              {
                "name": "Elior"
              }
            ]
          }
        ]
      }
    ]
  }
}

I’m a bit confused, do you wanna have the “uid” predicate itself on your GraphQL mutation?

The @id directive, as far as I know, is a “custom” way to add a reference/identifier in GraphQL. e.g. username: String! @id this would make “username” unique. That’s similar to the DQL identifier. But the advantage is that your mutation will always run in the same node no matter what. It is almost the same behavior we could do with the upsert block. But I think, we should use the update mutation instead of creating mutation. That’s a usual pattern in GraphQL.

I have experience with GraphQL, but I haven’t stopped to focus on Dgraph’s GraphQL yet, I have used it, but not deeply. It has features that don’t come by default in GraphQL Specs. So, I’m taking it for granted, based only on my old experience with GraphQL. Just the Dgraph’s features I can’t be 100%, by reading the docs, seems logical what I’m stating.

I can be wrong, correct me if so.

Cheers.

@spinelsun, It looks like you found a bug:

If you do this mutation:

On this schema:

And then run this query:

It should have produced the results:

Or <-> Michal
Michal <-> Justin
Or <-> Elior

But if you look at the results we will find:

Or -> Michal
Michal <-> Justin
Or <-> Elior

@MichelDiz, @Pawan I think we have a bug here where the person “Or” is only linked to Michal with a one-way link instead of a two way as it should have been done.

Just tested on Slash: v20.07.1-rc1-12-gbfa6277f

And here is the complete steps to reproduce:

  addPerson(
    input: [
      {
        name: "Or"
        friends: [
          { name: "Michal", friends: [{ name: "Justin" }] }
          { name: "Elior" }
        ]
      }
    ]
  ) {
    person {
      name
      friends {
        name
      }
    }
  }
}
query ReadData {
  queryPerson{
    id
    name
    friends{
      name
      friends{
        name
      }
    }
  }
}
mutation ResetData {
  deletePerson (filter: {}) { numUids }
}

AddData:

{
  "data": {
    "addPerson": {
      "person": [
        {
          "name": "Or",
          "friends": [
            {
              "name": "Michal"
            },
            {
              "name": "Elior"
            }
          ]
        }
      ]
    }
  },
  "extensions": {
    "touched_uids": 8,
    "tracing": {
      "version": 1,
      "startTime": "2020-09-07T14:47:21.526305936Z",
      "endTime": "2020-09-07T14:47:21.538247035Z",
      "duration": 11941104,
      "execution": {
        "resolvers": [
          {
            "path": [
              "addPerson"
            ],
            "parentType": "Mutation",
            "fieldName": "addPerson",
            "returnType": "AddPersonPayload",
            "startOffset": 128337,
            "duration": 11795506,
            "dgraph": [
              {
                "label": "mutation",
                "startOffset": 208815,
                "duration": 4902737
              },
              {
                "label": "query",
                "startOffset": 7524027,
                "duration": 4364265
              }
            ]
          }
        ]
      }
    },
    "queryCost": 1
  }
}

ReadData:

{
  "data": {
    "queryPerson": [
      {
        "id": "0x7",
        "name": "Justin",
        "friends": [
          {
            "name": "Michal",
            "friends": [
              {
                "name": "Justin"
              }
            ]
          }
        ]
      },
      {
        "id": "0x8",
        "name": "Michal",
        "friends": [
          {
            "name": "Justin",
            "friends": [
              {
                "name": "Michal"
              }
            ]
          }
        ]
      },
      {
        "id": "0x9",
        "name": "Or",
        "friends": [
          {
            "name": "Michal",
            "friends": [
              {
                "name": "Justin"
              }
            ]
          },
          {
            "name": "Elior",
            "friends": [
              {
                "name": "Or"
              }
            ]
          }
        ]
      },
      {
        "id": "0xa",
        "name": "Elior",
        "friends": [
          {
            "name": "Or",
            "friends": [
              {
                "name": "Michal"
              },
              {
                "name": "Elior"
              }
            ]
          }
        ]
      }
    ]
  },
  "extensions": {
    "touched_uids": 32,
    "tracing": {
      "version": 1,
      "startTime": "2020-09-07T14:48:46.961435556Z",
      "endTime": "2020-09-07T14:48:46.96342668Z",
      "duration": 1991133,
      "execution": {
        "resolvers": [
          {
            "path": [
              "queryPerson"
            ],
            "parentType": "Query",
            "fieldName": "queryPerson",
            "returnType": "[Person]",
            "startOffset": 146977,
            "duration": 1800338,
            "dgraph": [
              {
                "label": "query",
                "startOffset": 195516,
                "duration": 1678721
              }
            ]
          }
        ]
      }
    },
    "queryCost": 1
  }
}

ResetData:

{
  "data": {
    "deletePerson": {
      "numUids": 4
    }
  },
  "extensions": {
    "touched_uids": 8,
    "tracing": {
      "version": 1,
      "startTime": "2020-09-07T14:49:59.435734133Z",
      "endTime": "2020-09-07T14:49:59.444221073Z",
      "duration": 8486954,
      "execution": {
        "resolvers": [
          {
            "path": [
              "deletePerson"
            ],
            "parentType": "Mutation",
            "fieldName": "deletePerson",
            "returnType": "DeletePersonPayload",
            "startOffset": 135600,
            "duration": 8331000,
            "dgraph": [
              {
                "label": "mutation",
                "startOffset": 180332,
                "duration": 5844148
              },
              {
                "label": "query",
                "startOffset": 8447288,
                "duration": 6132
              }
            ]
          }
        ]
      }
    },
    "queryCost": 1
  }
}
1 Like

Hi @spinelsun, looks like I used a different mutation. I am seeing a similar result to what you are seeing too and it might be a bug.

I think the intended concept is to have a similar experience to using blank nodes in Dgraph in the add mutation. The OP does not want to use custom IDs but rather use Dgraph’s IDs but just have a way to define links between them in a single addMutation. To expand on the example for better proof of concept lets say we wanted to create these new people as friends:

Or <-> Michal
Or <-> Justin
Or <-> Elior
Michal <-> Justin

This becomes complicated with a single mutation because you have to stop and think of where to begin the mutation from and how to nest everything listing each item only once to make the desired links. There is actually no way to nest the above making the desired links in a single addMutation without listing at least one of the Persons twice. Here is different ways I tried to arrange them:

Or.friends: [
  Michal.friends: [
    Justin
  ]
  Justin # listed 2x
  Elior
]
Justin.friends: [
  Or.friends: [
    Elior
    Michal
  ]
  Michal # listed 2x
]
Michal.friends: [
  Justin
  Or.friends: [
    Elior
    Justin # listed 2x
  ]
]
Elior.friends: [
  Or.friends: [
    Michal.friends: [
      Justin
    ]
    Justin # listed 2x
  ]
]

It is just impossible to do in a single mutation without blank nodes (not currently capable) or xids (not wanted, need to have capability of multiple people with the name Justin for example, and don’t want to generate unique UIDs across every UI).

How can we use the update mutation if none of the data we are adding already exists? What we would have to do: Add the people in a addMutation and then capture the ids, link the ids back to the created people, and make a second trip with a updateMutation to create the links. a good graphDB API (added API, because Dgraph supports it, just the generated graphql endpoint does not), should be able to support this without separating the logic into two round trips.


My personal experience hit this road block very early on. Understanding it as just a limitation of the graphql endpoint, I discovered early how to use DQL to insert data using blank nodes. If this was capable to use blank nodes with the graphql endpoint, it would help me as well.

I want to enable users to import data. Some of these imports will contain deep nested relationships like this illustration. I will have to open some DQL, RDF door to do these imports. This could lead to other issues where now in order to make the nested relationships I loose the functionality of the hasInverse logic in the API layer and must manually create the RDF for all of the reverse edges.

Does this help clear up what the OP is wanting and how it could be useful?

I’ll summarize the concepts here.

This "uid": "_:p1" isn’t an UID or ID or Unique identifier. This is a blank node, it is used in the transaction context to ask the Zero group UIDs for that entity. So, totally not possible in the GraphQL context, cuz Blank Nodes are DQL only related.

You should follow the Docs generated by GraphQL itself. When you send a Schema, GraphQL does the documentation about the API right out of the box. You should read it all the time, every change you do to your Schema.

Those are different things. GraphQL doesn’t use the Reverse index.

What result exactly do you expect?

I think that the reason is that “Or” is a parent, and your query asks for Childs. So “Or” isn’t a Child of “Michal”. The inverse doesn’t mean that it will appear for you anyway, you need a way to query inverse. In DQL we use the “tilde” notation. That means, <~friend> will return the inverse ones. You can’t mix the inverse and the real direction on a query result. It could potentially break applications and confuses the user.

nah, feels like a misunderstanding tho. See the comment above.

BTW, do we have a <~friend> notation in GraphQL? I think that the inverse one is used for other proposes. Not to show inversed edges, just to use them. I can be wrong tho.

Not possible for now I guess, would be interesting to have it at GraphQL mutation level. But is possible to do with custom DQL tho.

Just the fact to have edges you are creating “fixed” relations. Blank nodes have other propose.

Hum, by reading this feels like blank nodes in GraphQL seem useful. Cuz you could send big chunks of GraphQL objects in the mutation input, and not relate them via edges. e.g.

Instead of

(
    input: [
   {
      name: "lucas",
      friendOf: [
         {
            name: "Jorge",
            friendOf: [
               {
                    name: "lucas"
               }
            ]
         }
      ]
   }
  )

It could be like:

addPerson(
    input: [
 {
      uid: "_:Lucas",
      name: "lucas",
      friendOf: [
         {
            uid: "_:Jorge"
         }
      ]
   },
   {
      uid: "_:Jorge",
      name: "lucas",
      friendOf: [
         {
            uid: "_:Lucas"
         }
      ]
   }
    ]
  )

That’s a view of the organization at transaction level. But the @id directive could do the same thing.

One of the entities should exist, and (using the @id directive) Dgraph would create the other one. You just need the root and the other will be linked via custom id.

I have tested it with update mutation. It works, but not as I would like to do.

e.g

mutation {
  updatePerson(
    input: { 
      filter: {
        name: { eq: "Test" }
      }
      set: {
        friends: [
          { name: "Lucas" }, { name: "Deng" }
          { name: "Phillip" }, { name: "Lucas 2" }
        ]
      }
    }
  ) {
    person {
      name
      friends {
        name
      }
    }
  }
}

This mutation above will search for the “Test” entity. And link or create the others.

In my opinion, this update should have a better method. e.g

  addPerson(
    input: [
      {
        byID: "Test"
        friends: [
          { name: "Lucas", friends: [{ name: "Deng" }] }
          { name: "Phillip" }
        ]
      }
    ]
  )

The byID: "..." method would be more clean and direct.

I agree with the Blank node usage could potentially make things easy. But GraphQL is about simple mutations. Your application won’t send huge mutations. In general, who uses GraphQL, won’t add like 10 friends in a single batch. Your front-end doesn’t have this concept of bulk. You could create something like a queue, but feels odd. Front-end applications will do each new relation a single mutation of two edges, not 20, you know?

If you wanna bulk data into Dgraph, you should do it via Liveload. Not GraphQL mutation.

Cheers.

Bahumbug, lol. And this is where we differ. I want to keep the logical restrictions imposed by the graphql endpoint that the liveloader does not honor. We are not talking about importing GBs of data, but rather a users Contact lists from an excel sheet of ~100 contacts, which could contain (as we would have to parse in a script) a link to contacts family, and employer, and friends. These would all be imported at once, and hopefully by a mutation in graphql :wink:

As we start to integrate with other graphDB services I am sure we will start to get data going both ways in JSON format and we would want to make the mutations again with graphql.

Anyways, I think this is stepping away from the OP, but just my feedback.

Is my understanding here correct:

if so, then it is a bug. I think @pawan agrees that it is a bug, as he added the bug tag.

Yeah this looks like a bug as pointed out by @amaster507. I’ll have a deeper look tomorrow and confirm of the cause.

@hasInverse in GraphQL is not implemented using @reverse under the hood. So you don’t need to add the ~ notation to query it. You should be able to query it using the field name.

There is a lot of interesting and useful feedback in this thread which I’ll go through at length this week. Thanks for that!

1 Like

But how to defer a child from a parent relation? Both will be shown as it was nested?

Here is a PR with the fix: fix(GraphQL): Fix squashIntoObject so that results are correctly merged by pawanrawal · Pull Request #6416 · dgraph-io/dgraph · GitHub

I am not if I understand this, could you give me an example that would help me understand better?

2 Likes

Neither I, I’m assuming that the user and you are saying that @hasInverse and @reverse has the same logic. And the only difference between them is that @hasInverse doesn’t uses the reverse index.

Taking @reverse into consideration. When you use the tilde you are basically asking Dgraph for incoming edges/nodes. Does that works the same on GraphQL or the usage of @hasInverse is totally for other propose?

If @hasInverse has the same goal as @reverse, so @hasInverse should have a way to differentiate the directions.

The point is, I don’t know how @hasInverse works, and feels like the user thinks it is 100% like @reverse. But with a different notation.

Update:

The docs says:

@hasInverse is used to setup up two way edges such that adding a edge in one direction automically adds the one in the inverse direction.

This statement is exactly what @reverse do. But @reverse adds in the same edge in a reversed way. And @hasInverse do it to GraphQL types. But feels like it adds an edge automatically, not a “reverse” as we spect. if so, this should be clear to not confuse users with DQL.

GraphQL supports custom directives.
Maybe it will the best thing to add the @reverse directive to the language and translate it in dgraph internals.
neo4j did something similar by declering the directive @relation which is a great idea by itself.
with the @relation you can specify the direction of the relationship and also properties on the relation (similar to facets in dgraph).
I’m adding a link to the concept here: @relation directive

About this, I understand now the delicate difference between the two directives. It is still a bit confusing though. If the docs could contain more concrete examples and explanations on when to use each one of them it would be great.

1 Like

I agree, the docs should add a “reason/goal” for each feature. That way would help to understand the differences easily. Especially when such a feature has homonymous.