FacebookããªãŒãã³ãœãŒã¹ã®ã«ããŽãªã«ç§»è¡ããåŸãéå»æ°å¹Žã«ãããGraphQLãã¯ãããžãŒã¯éåžžã«äººæ°ããããŸããã æ¬æžã®ç¿»èš³è
ã§ããæ¬æ¥çºè¡šãã翻蚳è
ã¯ãNode.jsç°å¢ã§GraphQLã䜿çšããããšããŸãããã圌èªèº«ã®çµéšããããã®æè¡ã¯ãã®åªããæ©èœãšã·ã³ãã«ãã«ããã誀ã£ãŠããã»ã©æ³šç®ãããªããšç¢ºä¿¡ããŠãããšè¿°ã¹ãŠããŸãã æè¿ã圌ã¯æ°ãããããžã§ã¯ãã«åŸäºããŠããéã«ãNode.jsããGolangã«åãæ¿ããŸããã ãã®åŸã圌ã¯GolangãšGraphQLã®ã³ã©ãã¬ãŒã·ã§ã³ããã¹ãããããšã«ããŸããã
äºåæ
å ±
å
¬åŒã®GraphQLå®çŸ©ããããããAPIã®ã¯ãšãªèšèªã§ãããæ¢åã®ããŒã¿ã«å¯ŸããŠãã®ãããªã¯ãšãªãå®è¡ããããã®ã©ã³ã¿ã€ã ã§ããããšãããããŸãã GraphQLã¯ãç¹å®ã®APIã®ããŒã¿ã®å®å
šã§ç解å¯èœãªèª¬æãæäŸãã顧客ãå¿
èŠãªæ
å ±ãæ£ç¢ºã«èŠæ±ããããšãå¯èœã«ããŸãã
Golangçšã®GraphQLã©ã€ãã©ãªã¯å€ããããŸããã ç¹ã«ã
Thunder ã
graphql ã
graphql-go ãããã³
gqlgenã®ãããªã©ã€ãã©ãªããã¹ãã
ãŸãã ã ç§ãè©Šããäžã§äžçªè¯ãã£ãã®ã¯gqlgenã©ã€ãã©ãªãŒã ã£ãããšã«æ³šæããŠãã ããã
gqlgenã©ã€ãã©ãªã¯ãŸã ããŒã¿çã§ããããã®è³æã®å·çæç¹ã§ã¯ããŒãžã§ã³
0.7.2ã§ãã ã ã©ã€ãã©ãªã¯æ¥éã«é²åããŠããŸãã
ããã§ã¯ããã®éçºèšç»ã«ã€ããŠèª¿ã¹ãããšãã§ããŸãã çŸåšãgqlgenã®å
¬åŒã¹ãã³ãµãŒã¯99designsãããžã§ã¯ãã§ããã€ãŸãããã®ã©ã€ãã©ãªã¯ããããã以åãããããã«é«éã«éçºãããŸãã ãã®ã©ã€ãã©ãªã®äž»ãªéçºè
ã¯
vektahãš
neelanceã§ãããããã«neelanceã¯graphql-goã©ã€ãã©ãªã§åäœããŸãã
GraphQLã®åºæ¬çãªç¥èãããããšãåæã«ãgqlgenã©ã€ãã©ãªã«ã€ããŠè©±ããŸãããã
Gqlgenã®æ©èœ
gqlgenã®èª¬æã§ã¯ãGolangã§å³å¯ã«åæå®ãããGraphQLãµãŒããŒãè¿
éã«äœæããããã®ã©ã€ãã©ãªã§ãã以åã®ããŒãžã§ã³ã確èªã§ããŸãã ãã®ãã¬ãŒãºã¯ããã®ã©ã€ãã©ãªã§äœæ¥ãããšãã«
map[string]interface{}
ãããªãã®ã«åºãããããšããªãããšãæå³ãããããéåžžã«ææã ãšæãããŸãã
ããã«ããã®ã©ã€ãã©ãªã¯ãããŒã¿ã¹ããŒãã«åºã¥ãã¢ãããŒãã䜿çšããŸãã ã€ãŸããAPIã¯GraphQL
ã¹ããŒãå®çŸ©èšèªã䜿çšããŠèšè¿°ãããŸãã ãã®èšèªã«ã¯ãGraphQLã³ãŒããèªåçã«äœæããç¬èªã®åŒ·åãªã³ãŒãçæããŒã«ããããŸãã ãã®å Žåãããã°ã©ãã¯å¯Ÿå¿ããã€ã³ã¿ãŒãã§ã€ã¹ã¡ãœããã®åºæ¬ããžãã¯ã®ã¿ãå®è£
ã§ããŸãã
ãã®èšäºã¯2ã€ã®ããŒãã«åãããŠããŸãã 1ã€ç®ã¯åºæ¬çãªäœæ¥æ¹æ³ã2ã€ç®ã¯é«åºŠãªæ¹æ³ã§ãã
äž»ãªäœæ¥æ¹æ³ïŒã»ããã¢ãããããŒã¿ã®åä¿¡ãšå€æŽã®ãªã¯ãšã¹ãããµãã¹ã¯ãªãã·ã§ã³
å®éšçãªã¢ããªã±ãŒã·ã§ã³ãšããŠããŠãŒã¶ãŒãåç»ãå
¬éããããã¹ã¯ãªãŒã³ã·ã§ãããã¬ãã¥ãŒãè¿œå ããããåç»ãæ€çŽ¢ããããä»ã®é²ç»ã«é¢é£ããé²ç»ã®ãªã¹ãã衚瀺ãããã§ãããµã€ãã䜿çšããŸãã ãã®ãããžã§ã¯ãã®äœæ¥ãå§ããŸãããïŒ
mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/
ãããžã§ã¯ãã®ã«ãŒããã£ã¬ã¯ããªã«æ¬¡ã®ããŒã¿ã¹ããŒããã¡ã€ã«ïŒ
schema.graphql
ïŒãäœæããŸãã
type User { id: ID! name: String! email: String! } type Video { id: ID! name: String! description: String! user: User! url: String! createdAt: Timestamp! screenshots: [Screenshot] related(limit: Int = 25, offset: Int = 0): [Video!]! } type Screenshot { id: ID! videoId: ID! url: String! } input NewVideo { name: String! description: String! userId: ID! url: String! } type Mutation { createVideo(input: NewVideo!): Video! } type Query { Videos(limit: Int = 25, offset: Int = 0): [Video!]! } scalar Timestamp
ããã§ã¯ãåºæ¬çãªããŒã¿ã¢ãã«ããµã€ãã«æ°ãããããªãã¡ã€ã«ãå
¬éããããã«äœ¿çšããã1ã€ã®ãã¥ãŒ
Mutation
ïŒ
Mutation
ãããŒã¿å€æŽã®ãªã¯ãšã¹ãã®èª¬æïŒãããã³ãã¹ãŠã®ãããªãã¡ã€ã«ã®ãªã¹ããååŸããããã®1ã€ã®ã¯ãšãªïŒ
Query
ïŒã«ã€ããŠèª¬æããŸãã GraphQLã¹ããŒãã«ã€ããŠè©³ããã¯
ãã¡ããã芧ãã ãã ã ããã«ãããã§ã¯ãç¬èªã®ã¹ã«ã©ãŒããŒã¿åã®1ã€ã宣èšããŸããã GraphQLã«ãã5ã€ã®æšæºã¹ã«ã©ãŒããŒã¿
å ïŒ
Int
ã
Float
ã
String
ã
Boolean
ããã³
ID
ïŒã«æºè¶³ããŠããŸããã
ç¬èªã®åã䜿çšããå¿
èŠãããå Žåã
schema.graphql
ïŒãã®å Žåããã®åã¯
Timestamp
ïŒã§å®£èšããã³ãŒãã§å®çŸ©ãæäŸã§ããŸãã gqlgenã©ã€ãã©ãªã䜿çšããå Žåã¯ãç¬èªã®ãã¹ãŠã®ã¹ã«ã©ãŒåã®ããŒã·ã£ãªã³ã°ããã³ã¢ã³ããŒã·ã£ãªã³ã°ã®ã¡ãœãããæäŸããgqlgen.ymlã䜿çšããŠãããã³ã°ãæ§æããå¿
èŠããããŸãã
ã©ã€ãã©ãªã®ææ°ããŒãžã§ã³ã§ã¯ã1ã€ã®éèŠãªå€æŽããã£ãããšã«æ³šæããŠãã ããã ã€ãŸããã³ã³ãã€ã«ããããã€ããªãã¡ã€ã«ãžã®äŸåé¢ä¿ã¯åé€ãããŸããã ãããã£ãŠããããžã§ã¯ãã«æ¬¡ã®ã³ã³ãã³ãã®
scripts/gqlgen.go
ãè¿œå ããå¿
èŠããã
scripts/gqlgen.go
ã
ãã®åŸã
dep
ãåæåããå¿
èŠããããŸãïŒ
dep init
次ã«ãã©ã€ãã©ãªã®ã³ãŒãçææ©èœãå©çšããŸãã ãããã¯ãã¹ãŠã®éå±ãªå®åã³ãŒããäœæããããšãå¯èœã«ããŸãããå®å
šã«é¢çœããªããšã¯èšããŸããã èªåã³ãŒãçæã¡ã«ããºã ãéå§ããã«ã¯ã次ã®ã³ãã³ããå®è¡ããŸãã
go run scripts/gqlgen.go init
å®è¡ã®çµæã次ã®ãã¡ã€ã«ãäœæãããŸãã
gqlgen.yml
ïŒã³ãŒãçæã管çããããã®æ§æãã¡ã€ã«ã
generated.go
ïŒçæãããã³ãŒãã
models_gen.go
ïŒæäŸãããã¹ããŒãã®ãã¹ãŠã®ã¢ãã«ãšããŒã¿åã
resolver.go
ïŒããã°ã©ããŒãäœæããã³ãŒãã§ãã
server/server.go
ïŒGraphQLãµãŒããŒãèµ·åããããã®http.Handlerãæã€ãšã³ããªãã€ã³ãã
ã¿ã€ã
Video
çšã«çæãããã¢ãã«ïŒãã¡ã€ã«
generated_video.go
ïŒãèŠãŠãã ããïŒ
type Video struct { ID string `json:"id"` Name string `json:"name"` User User `json:"user"` URL string `json:"url"` CreatedAt string `json:"createdAt"` Screenshots []*Screenshot `json:"screenshots"` Related []Video `json:"related"` }
ããã§ã¯ã
ID
ãæååã§ããã
CreatedAt
ãæååã§ããããšãããããŸãã ãã®ä»ã®é¢é£ã¢ãã«ã¯ããã«å¿ããŠæ§æãããŸãã ãã ããå®éã®ã¢ããªã±ãŒã·ã§ã³ã§ã¯ããã¯å¿
èŠãããŸããã ä»»æã®ã¿ã€ãã®SQLããŒã¿ã䜿çšããŠããå Žåãããšãã°ã䜿çšããããŒã¿ããŒã¹ã«å¿ããŠã
ID
ãã£ãŒã«ãã¯
int
ãŸãã¯
int64
ãŸãã
ããšãã°ããã®ãã¢ã¢ããªã±ãŒã·ã§ã³ã§ã¯PostgreSQLã䜿çšããŠãããããåœç¶ã
ID
ãã£ãŒã«ãã¯
int
åã§ã
CreatedAt
ãã£ãŒã«ãã¯
time.Time
åã§ãã
time.Time
ãŸãã ããã¯ãç¬èªã®ã¢ãã«ãå®çŸ©ããæ°ããã¢ãã«ãçæãã代ããã«ã¢ãã«ã䜿çšããå¿
èŠãããããšãgqlgenã«äŒããå¿
èŠããããšããäºå®ã«ã€ãªãããŸãã
models.go
ãã¡ã€ã«ã®å
容ã¯æ¬¡ã®
models.go
ã§ãã
type Video struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` User User `json:"user"` URL string `json:"url"` CreatedAt time.Time `json:"createdAt"` Related []Video }
ã©ã€ãã©ãªã«ãããã®ã¢ãã«ïŒ
gqlgen.yml
ãã¡ã€ã«ïŒã䜿çšããããã«
gqlgen.yml
ãŸãã
schema: - schema.graphql exec: filename: generated.go model: filename: models_gen.go resolver: filename: resolver.go type: Resolver models: Video: model: github.com/ridhamtarpara/go-graphql-demo/api.Video ID: model: github.com/ridhamtarpara/go-graphql-demo/api.ID Timestamp: model: github.com/ridhamtarpara/go-graphql-demo/api.Timestamp
ããã®ãã¹ãŠã®ãã€ã³ãã¯ã
gqlgen.yml
ãã¡ã€ã«ã§ããŒã·ã£ãªã³ã°ããã³ã¢ã³ããŒã·ã£ãªã³ã°ãšãããã³ã°ã®ããã®ã¡ãœãããåãã
ID
ãš
Timestamp
ç¬èªã®å®çŸ©ãæã£ãŠããããšã§ãã ãŠãŒã¶ãŒãæååã
ID
ãšããŠæäŸããã®ã§ã
UnmarshalID()
ã¡ãœããã¯ãã®æååãæŽæ°ã«å€æããŸãã å¿çãéä¿¡ãããšãã
MarshalID()
ã¡ãœããã¯æ°å€ãæååã«å€æããŸãã åãããšã
Timestamp
ã§ããããã°ã©ããŒã«ãã£ãŠå®£èšãããä»ã®ã¹ã«ã©ãŒåã§ãèµ·ãããŸãã
次ã«ãã¢ããªã±ãŒã·ã§ã³ããžãã¯ãå®è£
ããŸãã
resolver.go
ãã¡ã€ã«ãéããçªç¶å€ç°ãšã¯ãšãªã®èª¬æãè¿œå ããŸãã ãã§ã«èªåçæããããã³ãã¬ãŒãã³ãŒãããããæå³ãåããå¿
èŠããããŸãã ãã®ãã¡ã€ã«ã®ã³ãŒãã¯æ¬¡ã®ãšããã§ãã
func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) { newVideo := api.Video{ URL: input.URL, Name: input.Name, CreatedAt: time.Now().UTC(), } rows, err := dal.LogAndQuery(r.db, "INSERT INTO videos (name, url, user_id, created_at) VALUES($1, $2, $3, $4) RETURNING id", input.Name, input.URL, input.UserID, newVideo.CreatedAt) defer rows.Close() if err != nil || !rows.Next() { return api.Video{}, err } if err := rows.Scan(&newVideo.ID); err != nil { errors.DebugPrintf(err) if errors.IsForeignKeyError(err) { return api.Video{}, errors.UserNotExist } return api.Video{}, errors.InternalServerError } return newVideo, nil } func (r *queryResolver) Videos(ctx context.Context, limit *int, offset *int) ([]api.Video, error) { var video api.Video var videos []api.Video rows, err := dal.LogAndQuery(r.db, "SELECT id, name, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2", limit, offset) defer rows.Close(); if err != nil { errors.DebugPrintf(err) return nil, errors.InternalServerError } for rows.Next() { if err := rows.Scan(&video.ID, &video.Name, &video.URL, &video.CreatedAt, &video.UserID); err != nil { errors.DebugPrintf(err) return nil, errors.InternalServerError } videos = append(videos, video) } return videos, nil }
ããã§ã¯ãçªç¶å€ç°ããã¹ãããŸãããã
çªç¶å€ç°createVideoããŸãããïŒ ãããããªããŠãŒã¶ãŒæ
å ±ïŒ
user
ãªããžã§ã¯ãïŒã«äœããªãã®ã§ããïŒ GraphQLã䜿çšããå Žåããé
延ãïŒé
延ïŒããã³ã貪欲ãïŒç±å¿ïŒããŒãã«é¡äŒŒããæŠå¿µãé©çšå¯èœã§ãã ãã®ã·ã¹ãã ã¯æ¡åŒµå¯èœã§ãããããã©ã®ãã£ãŒã«ãã«ã貪欲ã«ãå
¥åããå¿
èŠããããã©ã®ãã£ãŒã«ãã«ãé
延ããæå®ããå¿
èŠããããŸãã
ç§ã¯ãgqlgenã§äœæ¥ãããšãã«é©çšããã次ã®ããŽãŒã«ãã³ã«ãŒã«ããæ±ãçµç¹ã®ããŒã ã«ææ¡ããŸããããã¯ã©ã€ã¢ã³ãããèŠæ±ãããå Žåã«ã®ã¿ããŒãããå¿
èŠãããã¢ãã«ãã£ãŒã«ãã«å«ããªãã§ãã ãããã
ãã®å Žåãã¯ã©ã€ã¢ã³ãããããã®ãã£ãŒã«ããèŠæ±ããå Žåã«ã®ã¿ãé¢é£ãããããªã¯ãªããã«é¢ããããŒã¿ïŒããã³ãŠãŒã¶ãŒæ
å ±ãïŒãããŠã³ããŒãããå¿
èŠããããŸãã ãããããããã®ãã£ãŒã«ããã¢ãã«ã«å«ãããããgqlgenã¯ããããªã«é¢ããæ
å ±ãåä¿¡ããããšã§ãã®ããŒã¿ãæäŸãããšæ³å®ããŠããŸãã ãã®çµæã空ã®æ§é ãåŸãããŸãã
æã
ãç¹å®ã®ã¿ã€ãã®ããŒã¿ãæ¯åå¿
èŠã«ãªãããšããããããå¥ã®ãªã¯ãšã¹ãã䜿çšããŠããŠã³ããŒãããããšã¯å®çšçã§ã¯ãããŸããã ãã®ãããããã©ãŒãã³ã¹ãåäžãããããã«ãSQLçµåãªã©ã䜿çšã§ããŸãã äžåºŠïŒãã ããããã§æ€èšããäŸã«ã¯é©çšãããŸããïŒãã¡ã¿ããŒã¿ããããªãšãšãã«ã¢ããããŒãããå¿
èŠããããŸããã ãããã®ãšã³ãã£ãã£ã¯ç°ãªãå Žæã«ä¿åãããŠããŸããã ãã®çµæãã·ã¹ãã ããããªã®ããŠã³ããŒãèŠæ±ãåä¿¡ããå Žåãã¡ã¿ããŒã¿ãååŸããããã«å¥ã®èŠæ±ãè¡ãå¿
èŠããããŸããã ããããç§ã¯ãã®èŠä»¶ãç¥ã£ãŠããã®ã§ïŒã€ãŸããã¯ã©ã€ã¢ã³ãåŽã§ã¯åžžã«ãããªãšãã®ã¡ã¿ããŒã¿ã®äž¡æ¹ãå¿
èŠã§ããããšãç¥ã£ãŠããŸããïŒã欲匵ããªããŠã³ããŒãæè¡ã䜿çšããŠããã©ãŒãã³ã¹ãåäžãããããšã奜ã¿ãŸããã
ã¢ãã«ãæžãçŽããŠãgqlgenã³ãŒããå床çæããŸãããã 話ãè€éã«ããªãããã«ã
user
ãã£ãŒã«ãïŒ
models.go
ãã¡ã€ã«ïŒã®ã¡ãœããã®ã¿ãèšè¿°ããŸãã
type Video struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` UserID int `json:"-"` URL string `json:"url"` CreatedAt time.Time `json:"createdAt"` }
UserID
ãè¿œå ãã
User
æ§é ãåé€ããŸããã 次ã«ãã³ãŒããåçæããŸãã
go run scripts/gqlgen.go -v
ãã®ã³ãã³ãã®ãããã§ãæªå®çŸ©ã®æ§é ã解決ããããã«æ¬¡ã®ã€ã³ã¿ãŒãã§ãŒã¹ã¡ãœãããäœæãããŸãã ããã«ããªãŸã«ããŒïŒ
generated.go
ãã¡ã€ã«ïŒã§ä»¥äžã決å®ããå¿
èŠããããŸãã
type VideoResolver interface { User(ctx context.Context, obj *api.Video) (api.User, error) Screenshots(ctx context.Context, obj *api.Video) ([]*api.Screenshot, error) Related(ctx context.Context, obj *api.Video, limit *int, offset *int) ([]api.Video, error) }
å®çŸ©ã¯æ¬¡ã®ãšããã§ãïŒ
resolver.go
ãã¡ã€ã«ïŒïŒ
func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { rows, _ := dal.LogAndQuery(r.db,"SELECT id, name, email FROM users where id = $1", obj.UserID) defer rows.Close() if !rows.Next() { return api.User{}, nil } var user api.User if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil { errors.DebugPrintf(err) return api.User{}, errors.InternalServerError } return user, nil }
ããã§ãçªç¶å€ç°ãã¹ãã®çµæã¯æ¬¡ã®ããã«ãªããŸãã
çªç¶å€ç°createVideoå
ã»ã©èª¬æããã®ã¯ãGraphQLã®åºç€ã§ããããã§ã«ãã¹ã¿ãŒããŠããã®ã§ãæ¢ã«ç¬èªã®ãã®ãäœæã§ããŸãã ãã ããGraphQLãšGolangã®å®éšã«çªå
¥ããåã«ãããã§è¡ã£ãŠããããšã«çŽæ¥é¢é£ãããµãã¹ã¯ãªãã·ã§ã³ã«ã€ããŠèª¬æããŠãããšäŸ¿å©ã§ãã
âãµãã¹ã¯ãªãã·ã§ã³
GraphQLã¯ããªã¢ã«ã¿ã€ã ã§çºçããããŒã¿å€æŽããµãã¹ã¯ã©ã€ãããæ©èœãæäŸããŸãã gqlgenã©ã€ãã©ãªã䜿çšãããšããªã¢ã«ã¿ã€ã ã§Webãœã±ããã䜿çšããŠããµãã¹ã¯ãªãã·ã§ã³ã€ãã³ããåŠçã§ããŸãã
ãµãã¹ã¯ãªãã·ã§ã³ã¯
schema.graphql
ãã¡ã€ã«ã§èª¬æããå¿
èŠããããŸãã ãããªå
¬éã€ãã³ãã®ãµãã¹ã¯ãªãã·ã§ã³ã®èª¬æã¯æ¬¡ã®ãšããã§ãã
type Subscription { videoPublished: Video! }
次ã«ãèªåã³ãŒãçæãå床å®è¡ããŸãã
go run scripts/gqlgen.go -v
æ¢ã«è¿°ã¹ãããã«ã
generated.go
ãã¡ã€ã«ã§ã®ã³ãŒãã®èªåäœæäžã«ãèªèæ©èœã«å®è£
ããå¿
èŠãããã€ã³ã¿ãŒãã§ãŒã¹ãäœæãããŸãã ãã®å Žåã次ã®ããã«ãªããŸãïŒ
resolver.go
ãã¡ã€ã«ïŒïŒ
var videoPublishedChannel map[string]chan api.Video func init() { videoPublishedChannel = map[string]chan api.Video{} } type subscriptionResolver struct{ *Resolver } func (r *subscriptionResolver) VideoPublished(ctx context.Context) (<-chan api.Video, error) { id := randx.String(8) videoEvent := make(chan api.Video, 1) go func() { <-ctx.Done() }() videoPublishedChannel[id] = videoEvent return videoEvent, nil } func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) {
ããã§ãæ°ãããããªãäœæãããšãã«ãã€ãã³ããããªã¬ãŒããå¿
èŠããããŸãã ãã®äŸã§ã¯ãããã¯
for _, observer := range videoPublishedChannel
ãŸãã
ããããµãã¹ã¯ãªãã·ã§ã³ããã§ãã¯ããŸãããã
ãµãã¹ã¯ãªãã·ã§ã³ã確èªãããã¡ãããGraphQLã«ã¯ç¹å®ã®äŸ¡å€ã®ããæ©èœããããŸããã圌ããèšãããã«ãããããèŒãã®ã¯éã ãã§ã¯ãããŸããã ã€ãŸããGraphQLã䜿çšãã人ã¯ãæ¿èªããªã¯ãšã¹ãã®è€éãããã£ãã·ã¥ãN + 1ãªã¯ãšã¹ãã®åé¡ãã¯ãšãªå®è¡é床ã®å¶éãªã©ãåŠçããå¿
èŠããããšããäºå®ã«ã€ããŠè©±ããŠããã ããããªããšãGraphQLã䜿çšããŠéçºãããã·ã¹ãã ã®ããã©ãŒãã³ã¹ã倧å¹
ã«äœäžããå¯èœæ§ããããŸãã
é«åºŠãªææ³ïŒèªèšŒãããŒã¿ããŒããŒãã¯ãšãªã®è€éã
ãã®ãããªããã¥ã¢ã«ãèªããã³ã«ãããããç¿åŸããããšã§ãç¹å®ã®æè¡ã«ã€ããŠç¥ã£ãŠããå¿
èŠã®ãããã¹ãŠã®ããšãåŠã³ãè€éãªåé¡ã解決ããèœåãåŸãããšãã§ããŸãã
ããããèªåã®ãããžã§ã¯ãã§äœæ¥ãéå§ãããšãéåžžããµãŒããŒãšã©ãŒã®ããã«èŠãããäºæãã¬ç¶æ³ãçºçããŸãã çµæãšããŠããããè¡ãã«ã¯ãããæè¿å®å
šã«ç解ã§ãããšæããããã®ããããã調æ»ããå¿
èŠããããŸãã ãã®åãããã¥ã¢ã«ã§ããããåé¿ã§ããããšãé¡ã£ãŠããŸãã ãã®ããããã®ã»ã¯ã·ã§ã³ã§ã¯ãGraphQLãæäœããããã®é«åºŠãªãã¯ããã¯ã«ã€ããŠèª¬æããŸãã
âèªèšŒ
REST APIã䜿çšããå Žåãç¹å®ã®ãšã³ããã€ã³ãã䜿çšããå ŽåãèªèšŒã·ã¹ãã ãšæšæºã®æ¿èªããŒã«ããããŸãã ãã ããGraphQLã䜿çšããå Žåã1ã€ã®ãšã³ããã€ã³ãã®ã¿ã䜿çšããããããã¹ããŒããã£ã¬ã¯ãã£ãã䜿çšããŠèªèšŒã¿ã¹ã¯ã解決ã§ããŸãã
schema.graphql
ãã¡ã€ã«ã次ã®ããã«ç·šéããŸãã
type Mutation { createVideo(input: NewVideo!): Video! @isAuthenticated } directive @isAuthenticated on FIELD_DEFINITION
isAuthenticated
ãã£ã¬ã¯ãã£ããäœæãã
createVideo
ãµãã¹ã¯ãªãã·ã§ã³ã«é©çšããŸããã 次ã®èªåã³ãŒãçæã»ãã·ã§ã³ã®åŸããã®ãã£ã¬ã¯ãã£ãã®å®çŸ©ãå®çŸ©ããå¿
èŠããããŸãã çŸåšããã£ã¬ã¯ãã£ãã¯ã€ã³ã¿ãŒãã§ãŒã¹ã®åœ¢åŒã§ã¯ãªããæ§é ã®ã¡ãœããã®åœ¢åŒã§å®è£
ãããŠãããããããããèšè¿°ããå¿
èŠããããŸãã
server.go
ãã¡ã€ã«ã«ããèªåçæã³ãŒããç·šéãã
server.go
ãã¡ã€ã«ã®GraphQLæ§æãè¿ãã¡ãœãããäœæããŸããã
resolver.go
ãã¡ã€ã«ã¯æ¬¡ã®ãšããã§ãã
func NewRootResolvers(db *sql.DB) Config { c := Config{ Resolvers: &Resolver{ db: db, }, }
server.go
ãã¡ã€ã«ã¯æ¬¡ã®
server.go
ã§ãã
rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db) ) ) http.Handle("/query", auth.AuthMiddleware(rootHandler))
ã³ã³ããã¹ããããŠãŒã¶ãŒ
ID
ãèªã¿åããŸãã ããã¯ãããããšæããŸãããïŒ ãã®æå³ã¯ã©ã®ããã«æèã«ãªã£ãã®ã§ããããïŒ å®éã®ãšãããgqlgenã¯å®è£
ã¬ãã«ã§ã®ã¿ãªã¯ãšã¹ãã³ã³ããã¹ããæäŸãããããã¬ã³ã°ãã€ã¶ãŒãŸãã¯ãã£ã¬ã¯ãã£ãã§ããããŒãCookieãªã©ã®HTTPãªã¯ãšã¹ãããŒã¿ãèªã¿åãæ¹æ³ã¯ãããŸããã ãã®çµæãç¬èªã®äžéã¡ã«ããºã ãã·ã¹ãã ã«è¿œå ãããã®ããŒã¿ãåä¿¡ããŠââã³ã³ããã¹ãã«å
¥ããå¿
èŠããããŸãã
次ã«ããªã¯ãšã¹ãããèªèšŒããŒã¿ãååŸããŠæ€èšŒããããã®ç¬èªã®äžéèªèšŒã¡ã«ããºã ã説æããå¿
èŠããããŸãã
ããã§ã¯ããžãã¯ã¯å®çŸ©ãããŠããŸããã 代ããã«ãèš±å¯ããŒã¿ã®å Žåããã¢ã³ã¹ãã¬ãŒã·ã§ã³ã®ç®çã§ããŠãŒã¶ãŒ
ID
åã«ããã«æž¡ãããŸãã ãã®ã¡ã«ããºã ã¯ã
server.go
ã§æ°ããæ§æèªã¿èŸŒã¿ã¡ãœãããšçµã¿åããããŸãã
ããã§ããã£ã¬ã¯ãã£ãã®èª¬æãçã«ããªã£ãŠããŸãã ããã«ãŠã§ã¢ã³ãŒãã§èš±å¯ãããŠããªããŠãŒã¶ãŒãªã¯ãšã¹ãã¯åŠçãããŸããããã®ãããªãªã¯ãšã¹ãã¯ãã£ã¬ã¯ãã£ãã«ãã£ãŠåŠçãããããã§ãã å€èŠ³ã¯æ¬¡ã®ãšããã§ãã
æš©éã®ãªããŠãŒã¶ãŒãšé£æºããèš±å¯ããããŠãŒã¶ãŒãšé£æºããã¹ããŒããã£ã¬ã¯ãã£ãã䜿çšããå ŽåãåŒæ°ãæž¡ãããšãã§ããŸãã
directive @hasRole(role: Role!) on FIELD_DEFINITION enum Role { ADMIN USER }
âããŒã¿ããŒããŒ
ããã¯ãã¹ãŠéåžžã«èå³æ·±ãããã«æããŸãã å¿
èŠãªãšãã«ããŒã¿ãããŠã³ããŒãããŸãã ã¯ã©ã€ã¢ã³ãã«ã¯ããŒã¿ã管çããæ©èœããããå¿
èŠãªãã®ã¯ã¹ãã¬ãŒãžããååŸãããŸãã ãããããã¹ãŠã«äŸ¡æ ŒããããŸãã
ãããã®æ©äŒã«æ¯æã代䟡ã¯ãããã§ããïŒ ãã¹ãŠã®åç»ã®ããŠã³ããŒããã°ãã芧ãã ããã ã€ãŸãã8ã€ã®ãããªãš5人ã®ãŠãŒã¶ãŒããããšããããšã§ãã
query{ Videos(limit: 10){ name user{ name } } }
ãããªã®ããŠã³ããŒãã®è©³çŽ° Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1
ããã§äœãèµ·ãã£ãŠããŸããïŒ 9ã€ã®ãªã¯ãšã¹ããããã®ã¯ãªãã§ããïŒ1ã€ã®ãªã¯ãšã¹ãããããªããŒãã«ã«é¢é£ä»ãããã8ã€ã®ãªã¯ãšã¹ãããŠãŒã¶ãŒããŒãã«ã«é¢é£ä»ããããŠããŸãïŒïŒ ã²ã©ãã§ããã æ¢åã®APIãããã«çœ®ãæããå¿
èŠããããšæã£ããšããç§ã®å¿ã¯ã»ãŒåæ¢ããŸãã...確ãã«ãããŒã¿ããŒããŒã¯ãã®åé¡ã«å®å
šã«å¯ŸåŠã§ããŸãã
ããã¯N + 1åé¡ãšåŒã°ãããã¹ãŠã®ããŒã¿ãååŸããã¯ãšãªã1ã€ãããããŒã¿ïŒNïŒããšã«ããŒã¿ããŒã¹ãžã®å¥ã®ã¯ãšãªãååšãããšããäºå®ã«ã€ããŠè©±ããŸãã
ããã¯ãããã©ãŒãã³ã¹ãšãªãœãŒã¹ã«é¢ããŠéåžžã«æ·±å»ãªåé¡ã§ãããããã®èŠæ±ã¯äžŠè¡ããŠããŸãããã·ã¹ãã ãªãœãŒã¹ãæ¶è²»ããŸãã
ãã®åé¡ã解決ããããã«ãgqlgenã©ã€ãã©ãªã®äœæè
ã®
dataloadenã©ã€ãã©ãªã䜿çšããŸãã ãã®ã©ã€ãã©ãªã䜿çšãããšãGoã³ãŒããçæã§ããŸãã ãŸãã
User
ãšã³ãã£ãã£ã®ããŒã¿ããŒããŒãçæããŸãã
go get github.com/vektah/dataloaden dataloaden github.com/ridhamtarpara/go-graphql-demo/api.User
Fetch
ã
LoadAll
Prime
ãªã©ã®ã¡ãœãããæã€ãã¡ã€ã«
userloader_gen.go
èªç±ã«äœ¿çšã§ããŸãã
次ã«ãäžè¬çãªçµæãååŸããããã«ã
Fetch
ã¡ãœããïŒ
dataloader.go
ãã¡ã€ã«ïŒãå®çŸ©ããå¿
èŠããããŸãã
func DataloaderMiddleware(db *sql.DB, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userloader := UserLoader{ wait : 1 * time.Millisecond, maxBatch: 100, fetch: func(ids []int) ([]*api.User, []error) { var sqlQuery string if len(ids) == 1 { sqlQuery = "SELECT id, name, email from users WHERE id = ?" } else { sqlQuery = "SELECT id, name, email from users WHERE id IN (?)" } sqlQuery, arguments, err := sqlx.In(sqlQuery, ids) if err != nil { log.Println(err) } sqlQuery = sqlx.Rebind(sqlx.DOLLAR, sqlQuery) rows, err := dal.LogAndQuery(db, sqlQuery, arguments...) defer rows.Close(); if err != nil { log.Println(err) } userById := map[int]*api.User{} for rows.Next() { user:= api.User{} if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil { errors.DebugPrintf(err) return nil, []error{errors.InternalServerError} } userById[user.ID] = &user } users := make([]*api.User, len(ids)) for i, id := range ids { users[i] = userById[id] i++ } return users, nil }, } ctx := context.WithValue(r.Context(), CtxKey, &userloader) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) }
ããã§ã¯1ããªç§åŸ
æ©ããŸãã ãªã¯ãšã¹ããå®è¡ããåã«ãæ倧100ãªã¯ãšã¹ãã®ããã±ãŒãžã§ãªã¯ãšã¹ããåéããŸãã ããã§ãããŒããŒã¯åãŠãŒã¶ãŒã«å¯ŸããŠåå¥ã«ãªã¯ãšã¹ããå®è¡ãã代ããã«ãããŒã¿ããŒã¹ã«ã¢ã¯ã»ã¹ããåã«æå®ãããæéåŸ
æ©ããŸãã 次ã«ãããŒã¿ããŒããŒïŒ
resolver.go
ãã¡ã€ã«ïŒã䜿çšããèŠæ±ã䜿çšããŠèªèãšã³ãžã³ããžãã¯ãåæ§æããå€æŽããå¿
èŠããããŸãã
func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { user, err := ctx.Value(dataloaders.CtxKey).(*dataloaders.UserLoader).Load(obj.UserID) return *user, err }
äžèšã®ãããªç¶æ³ã§ããã°ãã©ã®ããã«èŠãããã次ã«ç€ºããŸãã
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Dataloader: User : SELECT id, name, email from users WHERE id IN ($1, $2, $3, $4, $5)
ããã§ã¯2ã€ã®ããŒã¿ããŒã¹ã¯ãšãªã®ã¿ãå®è¡ããããã®çµæã誰ããæºè¶³ããŠããŸãã 8ã€ã®ãããªã®ããŒã¿ãèŠæ±ãããŸããã5ã€ã®ãŠãŒã¶ãŒèå¥åã®ã¿ãèŠæ±ã«éä¿¡ãããããšã«æ³šæããŠãã ããã , .
â
GraphQL API , . , API DOS-.
, .
Video
, . GraphQL
Video
. . â .
, â :
{ Videos(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 100, offset: 0){ name url } } } } }
100, . (, , ) , .
gqlgen , . , (
handler.ComplexityLimit(300)
) GraphQL (300 ). , (
server.go
):
rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db)), handler.ComplexityLimit(300) ), )
, , . 12. , , , ( , , , , ).
resolver.go
:
func NewRootResolvers(db *sql.DB) Config { c := Config{ Resolvers: &Resolver{ db: db, }, }
, , .
, ,
related
. , , , , .
ãŸãšã
, ,
GitHub . . , , .
芪æãªãèªè
ïŒ GraphQL , Go?