models: ID:# The GraphQL type ID is backed by model: -github.com/99designs/gqlgen/graphql.IntID# An go integer -github.com/99designs/gqlgen/graphql.ID# or a go string
$ mkdir gqlgen-todos $ cd gqlgen-todos $ go mod init github.com/[username]/gqlgen-todos $ go get github.com/99designs/gqlgen
构建服务器
创建项目框架
1
$ go run github.com/99designs/gqlgen init
这将创建我们建议的项目布局。 如果需要,可以在gqlgen.yml中修改这些路径。
1 2 3 4 5 6 7 8 9 10 11 12
├── go.mod ├── go.sum ├── gqlgen.yml - The gqlgen config file, knobs for controlling the generated code. ├── graph │ ├── generated - A package that only contains the generated runtime │ │ └── generated.go │ ├── model - A package for all your graph models, generated or otherwise │ │ └── models_gen.go │ ├── resolver.go - The root graph resolver type. This file wont get regenerated │ ├── schema.graphqls - Some schema. You can split the schema into as many graphql files as you like │ └── schema.resolvers.go - the resolver implementation for schema.graphql └── server.go - The entry point to your app. Customize it however you see fit
# Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: -graph/*.graphqls
# Where should the generated server code go? exec: filename:graph/generated/generated.go package:generated
# Enable Apollo federation support federation: filename:graph/generated/federation.go package:generated
# Where should any generated models go? model: filename:graph/model/models_gen.go package:model
# Where should the resolver implementations go? resolver: layout:follow-schema dir:graph package:graph filename_template:"{name}.resolvers.go"
# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models # struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing # omit_slice_element_pointers: false
# Optional: set to speed up generation time by not performing a final validation pass. # skip_validation: true
# gqlgen will search for any type names in the schema in these go packages # if they match it will use them, otherwise it will generate them. autobind: -"github.com/your/app/graph/model"
# This section declares type mapping between the GraphQL and go type systems # # The first line in each type will be used as defaults for resolver arguments and # modelgen, the others will be allowed when binding to fields. Configure them to # your liking models: ID: model: -github.com/99designs/gqlgen/graphql.ID -github.com/99designs/gqlgen/graphql.Int -github.com/99designs/gqlgen/graphql.Int64 -github.com/99designs/gqlgen/graphql.Int32 Int: model: -github.com/99designs/gqlgen/graphql.Int -github.com/99designs/gqlgen/graphql.Int64 -github.com/99designs/gqlgen/graphql.Int3
一切都有默认值,因此可以根据需要添加。
带指令的内联配置
gqlgen附带了一些内置指令,这使得管理接线更加容易。
要开始使用它们,您首先需要定义它们:
1 2 3 4 5 6 7 8 9
directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
funcApplyChanges(changes map[string]interface{}, to interface{})error { dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ ErrorUnused: true, TagName: "json", Result: to, ZeroFields: true, // This is needed to get mapstructure to call the gqlgen unmarshaler func for custom scalars (eg Date) DecodeHook: func(a reflect.Type, b reflect.Type, v interface{}) (interface{}, error) { if reflect.PtrTo(b).Implements(reflect.TypeOf((*graphql.Unmarshaler)(nil)).Elem()) { resultType := reflect.New(b) result := resultType.MethodByName("UnmarshalGQL").Call([]reflect.Value{reflect.ValueOf(v)}) err, _ := result[0].Interface().(error) return resultType.Elem().Interface(), err }
func(r *todoResolver) UserRaw(ctx context.Context, obj *model.Todo) (*model.User, error) { res := db.LogAndQuery(r.Conn, "SELECT id, name FROM dataloader_example.user WHERE id = ?", obj.UserID) defer res.Close()
if !res.Next() { returnnil, nil } var user model.User if err := res.Scan(&user.ID, &user.Name); err != nil { panic(err) } return &user, nil }
注意:我将在此处使用go的低级sql.DB。 所有这一切都将与您喜欢的ORM一起使用。
查询执行程序将调用Query.Todos解析器,该解析器select * from todo,并返回N个todos。 然后,对于每个待办事项,同时调用Todo_user解析程序,SELECT * from USER where id = todo.user_id。
SELECT id, todo, user_id FROM todo SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ? SELECT id, name FROMuserWHERE id = ?
go get github.com/vektah/dataloaden mkdir dataloader cd dataloader go run github.com/vektah/dataloaden UserLoader int *gqlgen-tutorials/dataloader/graph/model.User
funcMiddleware(conn *sql.DB, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), loadersKey, &Loaders{ UserById: UserLoader{ maxBatch: 100, wait: 1 * time.Millisecond, fetch: func(ids []int) ([]*model.User, []error) { placeholders := make([]string, len(ids)) args := make([]interface{}, len(ids)) for i := 0; i < len(ids); i++ { placeholders[i] = "?" args[i] = i }
res := db.LogAndQuery(conn, "SELECT id, name from dataloader_example.user WHERE id IN ("+strings.Join(placeholders, ",")+")", args..., ) defer res.Close()
userById := map[int]*model.User{} for res.Next() { user := model.User{} err := res.Scan(&user.ID, &user.Name) if err != nil { panic(err) } userById[user.ID] = &user }
users := make([]*model.User, len(ids)) for i, id := range ids { users[i] = userById[id] }
funcmain() { cfg, err := config.LoadConfigFromDefaultLocations() if err != nil { fmt.Fprintln(os.Stderr, "failed to load config", err.Error()) os.Exit(2) }
err = api.Generate(cfg, api.AddPlugin(yourplugin.New()), // This is the magic line ) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(3) } }
funcmain() { c := Config{ Resolvers: &resolvers{} } srv := handler.NewDefaultServer(blog.NewExecutableSchema(c)) srv.Use(extension.FixedComplexityLimit(5)) // This line is key r.Handle("/query", srv) }
// UnmarshalGQL implements the graphql.Unmarshaler interface func(y *YesNo) UnmarshalGQL(v interface{}) error { yes, ok := v.(string) if !ok { return fmt.Errorf("YesNo must be a string") }
// These two methods are required for gqlgen to resolve the internal id-only wrapper structs. // This boilerplate might be removed in a future version of gqlgen that can no-op id only nodes. func(r *entityResolver) FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) { return &model.Product{ Upc: upc, }, nil }
// Here we implement the stitched part of this service, returning reviews for a product. Of course normally you would // go back to the database, but we are just making some data up here. func(r *productResolver) Reviews(ctx context.Context, obj *model.Product) ([]*model.Review, error) { switch obj.Upc { case"top-1": return []*model.Review{{ Body: "A highly effective form of birth control.", }}, nil
case"top-2": return []*model.Review{{ Body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.", }}, nil
case"top-3": return []*model.Review{{ Body: "This is the last straw. Hat you will wear. 11/10", }}, nil
} returnnil, nil }
func(r *userResolver) Reviews(ctx context.Context, obj *model.User) ([]*model.Review, error) { if obj.ID == "1234" { return []*model.Review{{ Body: "Has an odd fascination with hats.", }}, nil } returnnil, nil }
{ "data":{ "me":{ "username":"Me", "reviews":[ { "body":"A highly effective form of birth control.", "product":{ "name":"Trilby", "upc":"top-1" } }, { "body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.", "product":{ "name":"Trilby", "upc":"top-1" } } ] } } }
// A private key for context that only this package can access. This is important // to prevent collisions between different context uses var userCtxKey = &contextKey{"user"} type contextKey struct { name string }
// A stand-in for our database backed user object type User struct { Name string IsAdmin bool }
// Middleware decodes the share session cookie and packs the session into context funcMiddleware(db *sql.DB)func(http.Handler) http.Handler { returnfunc(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie("auth-cookie")
// Allow unauthenticated users in if err != nil || c == nil { next.ServeHTTP(w, r) return }
// get the user from the database user := getUserByID(db, userId)
// put it in context ctx := context.WithValue(r.Context(), userCtxKey, user)
// and call the next with our new context r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } }
// ForContext finds the user from the context. REQUIRES Middleware to have run. funcForContext(ctx context.Context) *User { raw, _ := ctx.Value(userCtxKey).(*User) return raw }
// Add CORS middleware around every request // See https://github.com/rs/cors for full option listing router.Use(cors.New(cors.Options{ AllowedOrigins: []string{"http://localhost:8080"}, AllowCredentials: true, Debug: true, }).Handler)
srv := handler.NewDefaultServer(starwars.NewExecutableSchema(starwars.NewResolver())) srv.AddTransport(&transport.Websocket{ Upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request)bool { // Check against your desired domains here return r.Host == "example.org" }, ReadBufferSize: 1024, WriteBufferSize: 1024, }, })
// Defining the Graphql handler funcgraphqlHandler() gin.HandlerFunc { // NewExecutableSchema and Config are in the generated.go file // Resolver is in the resolver.go file h := handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: &Resolver{}}))