I usually develop backend with Ruby on Rails so I'm not familier with golang but I have studied it and created a GraphQL API with Gin + Gorm + GraphQL.
I will explain about entire flow. Next, I will talk about stack point I was working in.
This is the repository.
gin-gorm-gqlgen-sample
Technologies I used
Followings are technologies I used this time,
- Docker
- Dep (package manager)
- Gin (WAF)
- Gorm (ORM)
- gqlgen (GraphQL Library)
Gin
Gin is one of the Go language WAF. It have got the most star on github in go lang WAF(as far as I know).
Gorm
Gorm is Go Lang ORM. According to web articles, its interface is like Rails's ActiveRecord.
I am familliar with Rails so I choose this library.
Dep
Dep is pacakge manager. I need to "go get" to install some libraries but it seems like disturbing my Dockerfile.
So I used dep
gqlgen
gqlgen is GraphQL server library. There are some graphql libraries for golang such as graphql-go/graphql.
But I selected gqlgen becuase I really like a point of generating code after I defined schema.
If you use gqlgen, you can proceed your development with following procedure.
- Define Schema
- Write logic about query in resolvers
In this process, you can focus on writing logic and you don't need to write boring code because gqlgen genearate code based on your schema.
Entire Flow
The gqlgen sample in the document is baout Todo List. If you will read "Getting Start", you start it easily.
But, I need to do something when you use it with Gorm or you develop CRUD. So, I will explain those way, So, I will explain those way.
Install gqlgen with dep
You can install gqlgen by "go get" but if you use dep, you have to prepare wrapper scripts to generate boilerplate.
scripts/gqlgen.go
package main
import "github.com/99designs/gqlgen/cmd"
func main() {
cmd.Execute()
}
If you do this,
$go run scripts/gqlgen.go init
you will be able to execute it like this.
When you finished to write scripts, make files you need on gqlgen.
$go run scripts/gqlgen.go init
Gqlgen will generate followings files.
- gqlgen.yml ・・・ Configure file. you write the configure fo generated code.
- genereated.go ・・・ When you define schema and execute scripts, this file will be generatted. Must not edit manually because it is generated.
- models_gen.go ・・・ This file is also generated from schema. Must not edit manually because it is generated.
- resolver.go ・・・ This is generated if you yet to create this name file.
- server/server.go ・・・ Entrypoint.
When you generate code, excecute following command and install pacakges.
dep ensure
Create Entry Point for Gin
I have to prepare entry point for gin if you want to use gin.
prepare two endpoints.( for playground and main)
import (
"github.com/99designs/gqlgen/handler"
"github.com/gin-gonic/gin"
)
// Defining the Graphql handler
func graphqlHandler() gin.HandlerFunc {
h := handler.GraphQL(NewExecutableSchema(Config{Resolvers: &Resolver{}}))
return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
// Defining the Playground handler
func playgroundHandler() gin.HandlerFunc {
h := handler.Playground("GraphQL", "/query")
return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
func main() {
// Setting up Gin
r := gin.Default()
r.POST("/query", graphqlHandler())
r.GET("/", playgroundHandler())
r.Run()
}
Prepare Gorm Model
Next, I will define model which are used on schema definition.
I put it on internal/models according to go language standard layout.
// user.go
package models
import (
"time"
)
type User struct {
ID int
Name string
Todos []Todo
CreatedAt time.Time
UpdatedAt time.Time
}
// todo.go
package models
import (
"time"
)
type Todo struct {
ID int
Text string
Done bool
UserID int
User User
CreatedAt time.Time
UpdatedAt time.Time
}
Certainly I think it is preferd to wirte it with embed (please refer to Gorm documents)
but errors occur when I excecuted. I wrote all attributes..(I would like to know better way..)
// todo.go
package models
import (
"time"
)
type Todo struct {
gorm.Model
Text string
Done bool
UserID int
User User
}
If you finish to define your model, you have to wirte mapping of model in gqlgen.yml.
Followings is definition in this case.
models:
Todo:
model: gin_graphql/internal/models.Todo
User:
model: gin_graphql/internal/models.User
Define Schema
What I introduced until now is to prepare to develop in schema drriven, I'll to write the code and logic for api from now.
type Todo {
id: Int!
text: String!
done: Boolean!
userID: Int!
user: User!
createdAt: Time!
updatedAt: Time!
}
type User {
id: Int!
name: String!
createdAt: Time!
updatedAt: Time!
}
type Query {
todos: [Todo!]!
users: [User!]!
todo(input: FetchTodo): Todo!
}
input NewTodo {
text: String!
userId: Int!
}
input EditTodo {
id: Int!
text: String!
}
input NewUser {
name: String!
}
input FetchTodo {
id: Int!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
updateTodo(input: EditTodo!): Todo!
deleteTodo(input: Int!): Todo!
createUser(input: NewUser!): User!
}
scalar Time
Above is schema.graphql to realize CRUD. You have to use graphql syntax to write this.
Processes to change resouce is wrriten in Mutations and Processes to read it is wrriten in Query.
And Types you need is also defined in schema.graphql.
Until now, you got reday to generate code and you start to edit resolver.In resolver, IO is defined by schema.graphql and it is generated automatically.
So you focus on writing logic between input and output.
Implemntaion Resolver
In resolver, write code to get data from database or create some records.
You have to know that resolver is not overwritten when it is already exist.
Following is resolver I wrote. I can't explain all because it is about gorm and bored for us.
type Resolver struct {
}
func (r *Resolver) Mutation() MutationResolver {
return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}
type mutationResolver struct{ *Resolver }
func (r *mutationResolver) CreateUser(ctx context.Context, input NewUser) (*models.User, error) {
user := models.User{
Name: input.Name,
}
db.Create(&user)
return &user, nil
}
func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*models.Todo, error) {
todo := models.Todo{
Text: input.Text,
UserID: input.UserID,
Done: false,
}
db.Create(&todo)
return &todo, nil
}
func (r *mutationResolver) UpdateTodo(ctx context.Context, input EditTodo) (*models.Todo, error) {
todo := models.Todo{ID: input.ID}
db.First(&todo)
todo.Text = input.Text
db.Model(&models.Todo{}).Update(&todo)
return &todo, nil
}
func (r *mutationResolver) DeleteTodo(ctx context.Context, input int) (*models.Todo, error) {
todo := models.Todo{
ID: input,
}
db.First(&todo)
db.Delete(&todo)
return &todo, nil
}
type queryResolver struct{ *Resolver }
func (r *queryResolver) Todo(ctx context.Context, input *FetchTodo) (*models.Todo, error) {
var todo models.Todo
db.Preload("User").First(&todo, input.ID)
return &todo, nil
}
func (r *queryResolver) Todos(ctx context.Context) ([]models.Todo, error) {
var todos []models.Todo
db.Preload("User").Find(&todos)
fmt.Println(todos[0].User)
return todos, nil
}
func (r *queryResolver) Users(ctx context.Context) ([]models.User, error) {
var users []models.User
db.Find(&users)
return users, nil
}
Run
Finally, you can run your code if you finish process I introduced.
The entrypoint of the repositry I prepared is cmd/app/main.go so you can run following command.
ENV=development go run cmd/app/main.go
We should pass env in this command because I implemented it to load configuration by env.
When you success to run the server, you can try it on http://localhost:8080.
Stack Points
Next, I will write about points I stacked while I was building.
How can I define type of Time like CreatedAt and Updated?
Schema Types are only 5.
- ID
- Int
- Float
- String
- Boolean
So we have to define types we need such as Date.
When I create this sample, I'm stack here but I found gqlgen provide Time Type so I can use it.
You only do like this as above schema definition.
scalar Time
After you declare the type, the type is mapped to Go Embeded tim.Time Type.
Gqlgen support Map, Upload and Any type so if you want to use their type, only declare it with scalar declaration.
Error Occured in Gorm Model while I generate code.
This problem is not resolved. I got away with it by workaround though..
I defined a model with gorm embed type but error occured during generation...
type User {
gorm.Model
name
}
I got away with writing all attributes but it is not good.
If anyone know better way, I would like to teach me.
Get association model while I get list
This is about not GraphQL but Gorm. I often get list which include associated model so I tried it in this sample, too.
That is simpler than I expeceted. All you need is to add fields to struct.
type Todo {
id: Int!
text: String!
done: Boolean!
userID: Int!
user: User!
createdAt: Time!
updatedAt: Time!
}
For example, when you define Todo model, you can do that by adding userId and user fields to Todo model.
db.Preload("User").Find(&todo, input.ID)
And add this line to resolver.
You already write preload clasue so you already solved N+1 problem.
This is sql when I execute.You can make sure that users query is preloaded.
(/app/src/gin_graphql/resolver.go:65)
[2019-05-14 15:27:07] [0.90ms] SELECT * FROM `todos` WHERE (`todos`.`id` = 2) ORDER BY `todos`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/app/src/gin_graphql/resolver.go:65)
[2019-05-14 15:27:07] [2.65ms] SELECT * FROM `users` WHERE (`id` IN (1)) ORDER BY `users`.`id` ASC
[1 rows affected or returned ]
Wrrap Up
I tryied to build hot stacks of Web API and Graphql.
I currently feel the limit of REST API while I develop system with it so I feel stronger the good points of graphql.
In REST API, When I create new endpoint, I have to write code to request that endpoint in client-side.
In GraphQL, if you define schema once, you can fetch necessary resouce anytime.
When I develop api in REST, I think how I don't depned on frontend but I often need to change code of server-side code when the front is changed.
I think such problems will be solved by GraphQL (many people say so though..)
Certainly, I learn it deeply and I would know that drawbacks and I can never know that now but I can feel that potential.
If you are familiar with REST or are sick of creating Web API, try GraphQL
Thank you,
Top comments (0)