By now I’m hoping that you’ve had a chance to look at my previous tutorial titled, Using GraphQL with Golang and a NoSQL Database, which is more or less a quick start for using GraphQL with Couchbase. There, we saw how to create a GraphQL schema that allowed read-only queries and mutations against the data. However, the queries were not efficiently designed around potential data relationships.
We’re going to see how to handle and define relationships between data models using GraphQL, Couchbase, and the Go programming language.
In the previous example we had created two GraphQL objects around accounts and blogs that looked like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
accountType := graphql.NewObject(graphql.ObjectConfig{ Name: "Account", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.String, }, "firstname": &graphql.Field{ Type: graphql.String, }, "lastname": &graphql.Field{ Type: graphql.String, }, }, }) blogType := graphql.NewObject(graphql.ObjectConfig{ Name: "Blog", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.String, }, "account": &graphql.Field{ Type: graphql.String, }, "title": &graphql.Field{ Type: graphql.String, }, "content": &graphql.Field{ Type: graphql.String, }, }, }) |
In the above example, we had assumed that the account
field in the blogType
object was a key for the accountType
object. By doing this we were able to run queries like the following:
1 2 3 4 5 6 7 8 9 10 |
{ account(id: "12345") { firstname, lastname } blogs(account: "12345") { title, content } } |
The above approach works, but we have to keep passing around variables in our GraphQL query.
Alright, the schema didn’t really have anything to do with the above query. The query object is responsible for how the query performs, not the schema itself.
Remember, we had defined queries that leveraged Couchbase like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
"accounts": &graphql.Field{ Type: graphql.NewList(accountType), Resolve: func(p graphql.ResolveParams) (interface{}, error) { query := gocb.NewN1qlQuery("SELECT META(account).id, account.* FROM example AS account WHERE account.type = 'account'") rows, err := bucket.ExecuteN1qlQuery(query, nil) if err != nil { return nil, err } var accounts []Account var row Account for rows.Next(&row) { accounts = append(accounts, row) } return accounts, nil }, }, |
So where am I going with this?
Instead of creating queries for every interaction that we want to accomplish, it is easier to bake it into the GraphQL model. In other words, instead of referencing models by a string key, why not just reference another model?
Take the following modification to the blogType
object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
blogType := graphql.NewObject(graphql.ObjectConfig{ Name: "Blog", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.String, }, "account": &graphql.Field{ Type: accountType, Resolve: func(params graphql.ResolveParams) (interface{}, error) { blog := params.Source.(Blog) var account Account account.ID = blog.Account _, err := bucket.Get(blog.Account, &account) if err != nil { return nil, err } return account, nil }, }, "title": &graphql.Field{ Type: graphql.String, }, "content": &graphql.Field{ Type: graphql.String, }, }, }) |
We are still defining each of our possible API fields, but we are also defining a Resolve
function for the account
field. Inside the Resolve
function we can get the data from the parent object, being the blogType
object, and use it to query for account data when requested.
The GraphQL query we saw previously can become the following:
1 2 3 4 5 6 7 8 9 10 |
{ blog(account: "12345") { account { firstname, lastname }, title, content } } |
Alright so the above query may not exactly have the same meaning as the previous, but for this example it does. We’re saying we want to get a particular blog by account. Instead of providing the id for the account, we can just get the information through that Resolve
function that we had created.
Does this approach make queries unnecessary? No, it doesn’t because there may be a need to query in different fashions. It did however allow us to do a type of JOIN
operation with GraphQL.
Couchbase has N1QL, so wouldn’t a JOIN
operation be better via the database and not in the backend? I mean, even though we’ve created NoSQL relationships with GraphQL, we’re still doing numerous requests against the database. I would argue that it would be better for GraphQL to JOIN at the database level, not the application level. The fewer the requests, the faster the application, right?
Let’s modify the blogs
query in our rootQuery
object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
"blogs": &graphql.Field{ Type: graphql.NewList(blogType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { query := gocb.NewN1qlQuery(` SELECT META(blog).id, blog.title, blog.content, { META(account).id, account.firstname, account.lastname } AS account FROM example AS blog JOIN example AS account ON KEYS blog.account WHERE blog.type = 'blog' `) rows, err := bucket.ExecuteN1qlQuery(query, nil) if err != nil { return nil, err } var blogs []Blog var row Blog for rows.Next(&row) { blogs = append(blogs, row) } return blogs, nil }, }, |
Instead of adding a Resolve
function to the accounts
field on the blogType
object, we’re doing it all via the query. Our blogType
object would look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
blogType := graphql.NewObject(graphql.ObjectConfig{ Name: "Blog", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.String, }, "account": &graphql.Field{ Type: accountType, }, "title": &graphql.Field{ Type: graphql.String, }, "content": &graphql.Field{ Type: graphql.String, }, }, }) |
If we wanted to write a query for this, we could easily do so like the following:
1 2 3 4 5 6 7 8 9 10 |
{ blogs { title, content, account { firstname, lastname } } } |
The above query would return all blogs and the account data associated with them. Instead of doing a query for each field, we’re doing it in a single request with N1QL.
Couchbase N1QL makes this approach more plausible than a relational database as the flexible schema that can be easily modeled with GraphQL.
Conclusion
You just saw how to work with related data types in a Golang application that uses GraphQL and the NoSQL database, Couchbase. With GraphQL relationships, you can create Resolve
functions for each model field or you can create more complicated queries with N1QL and let the database do the heavy lifting. In both scenarios, the user can request exactly what they want without having to worry about a potentially excessive amount of API endpoints.
I encourage you to look at my previous example as it has more depth when it comes to GraphQL with Golang data types and Couchbase. You can even take it further and see another example I wrote titled, Getting Started with GraphQL using Golang.
To learn more about data relationships and using Couchbase with Go, check out the Couchbase Developer Portal.