这是一个超级复杂的树状结构,如果我们用常见的 RESTful API 进行设计,每个 API 负责请求一种类型的对象,例如用户是一个类型,帖子是另一个类型,那就需要非常多个请求才能把这个页面所需的所有数据拿回来。而且这些请求直接还存在依赖关系,不能平行地发多个请求,例如说在获得帖子数据之前,无法请求评论数据;在获得评论数据之后,才能开始请求评论作者数据;
如何解决这种问题?一个简单粗暴的办法是专门写一个 RESTful API,请求上述树状复杂数据。但很快新问题就会出现。现在 Facebook 想要做一个新的产品,例如说是宠物,然后要在我的页面上显示我的宠物信息,那这个 RESTful API 的实现就要跟着改;
GraphQL 能够很好地解决这个问题,但前提是:数据已经以图的数据结构进行保存!
例如,上面说到的用户、帖子、评论是顶点,而用户跟用户发过的帖子存在边的关系,帖子跟帖子评论存在一对多的边,评论跟评论作者存在一对一的边。这时候如果新产品引入了新的对象类型(也就是顶点类型)和新的边类型,那没有关系。在查询数据时用 GraphQL 描述一下要查询的这些边和顶点就行,不需要去改 API 实现;
第二,并不是所有场景都适用于 GraphQL 的,有些很简单的事情就应该用 RESTful API 来实现;Facebook 内部用户增长部门的很多 API 都还不是 GraphQL,因为没必要迁移到 GraphQL。用户增长部门的 API 处理新用户注册、填写短信验证码之类的事情,这些事情都是围绕着一个用户的具体某项或多项信息发生的,根本没有任何图的概念。可以强行写作 GraphQL,但得不到显著的好处。既然老的 API 早就写好了,需要的时候做一些小改动,但没必要重写;
第三,GraphQL 尽管查询的数据是图状数据结构,但实际获得的数据视图是树状数据结构。每一个 GraphQL 查询或更新都有自己的根节点,然后所有的数据都是从根结点展开出去的。查询后获得的数据如果要在前端重新变回图的状态,那前端就不能简单地缓存查询得到的数据,必须用对用的 GraphQL 存储库,然后通过顶点的 ID 把不同节点之间的某些边重新连接起来;
DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( id serial PRIMARY KEY, name VARCHAR (50) NOT NULL, age INT NOT NULL, profession VARCHAR (50) NOT NULL, friendly BOOLEAN NOT NULL ) ENGINE=InnoDB CHARSET=utf8mb4;
// Db is our database struct used for interacting with the database type Db struct { *sql.DB }
// User shape type User struct { ID int Name string Age int Profession string Friendly bool }
// New makes a new database using the connection string and // returns it, otherwise returns the error funcNew(connString string) (*Db, error) { db, err := sql.Open("mysql", connString) if err != nil { returnnil, err }
// Check that our connection is good if err := db.Ping(); err != nil { returnnil, err }
fmt.Println("mysql connected!")
return &Db{db}, nil }
// ConnString returns a connection string based on the parameters it's given // This would normally also contain the password, however we're not using one funcConnString(host string, port int, user, passwd, dbName string)string { return fmt.Sprintf( "%s:%s@tcp(%s:%d)/%s", user, passwd, host, port, dbName, ) }
// GetUsersByName is called within our user query for graphql func(d *Db) GetUsersByName(name string) []User { // Prepare query, takes a name argument, protects from sql injection stmt, err := d.Prepare("SELECT * FROM go_graphql_db.`users` WHERE name=?") if err != nil { fmt.Println("GetUserByName preparation Err: ", err) }
// Make query with our stmt, passing in name argument rows, err := stmt.Query(name) if err != nil { fmt.Println("GetUserByName Query Err: ", err) }
// Create User struct for holding each row's data var r User // Create slice of Users for our response var users []User // Copy the columns from row into the values pointed at by r (User) for rows.Next() { if err := rows.Scan( &r.ID, &r.Name, &r.Age, &r.Profession, &r.Friendly, ); err != nil { fmt.Println("Error scanning rows: ", err) } users = append(users, r) }
// Root holds a pointer to a graphql object type Root struct { Query *graphql.Object }
// User describes a graphql object containing a User var User = graphql.NewObject( graphql.ObjectConfig{ Name: "User", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.Int, }, "name": &graphql.Field{ Type: graphql.String, }, "age": &graphql.Field{ Type: graphql.Int, }, "profession": &graphql.Field{ Type: graphql.String, }, "friendly": &graphql.Field{ Type: graphql.Boolean, }, }, }, )
// NewRoot returns base query type. This is where we add all the base queries funcNewRoot(db *mapper.Db) *Root { resolver := Resolver{db: db}
// Create a new Root that describes our base query set up. In this // example we have a user query that takes one argument called name root := Root{ Query: graphql.NewObject( graphql.ObjectConfig{ Name: "Query", Fields: graphql.Fields{ "users": &graphql.Field{ Type: graphql.NewList(User), Args: graphql.FieldConfigArgument{ "name": &graphql.ArgumentConfig{ Type: graphql.String, }, }, // Create a resolver holding our database. Resolver can be found in resolvers.go Resolve: resolver.UserResolver, }, }, }, ), } return &root }
// Resolver struct holds a connection to our database type Resolver struct { db *mapper.Db }
// UserResolver resolves our user query through a db call to GetUserByName func(r *Resolver) UserResolver(p graphql.ResolveParams) (interface{}, error) { // Strip the name from arguments and assert that it's a string name, ok := p.Args["name"].(string) if ok { users := r.db.GetUsersByName(name) return users, nil }
// Handler will hold connection to the db as well as handlers type Handler struct { GqlSchema *graphql.Schema }
type reqBody struct { Query string`json:"query"` }
// GraphQL returns an http.HandlerFunc for our /graphql endpoint func(s *Handler) GraphQL() http.HandlerFunc { returnfunc(w http.ResponseWriter, r *http.Request) { // Check to ensure query was provided in the request body if r.Body == nil { http.Error(w, "Must provide graphql query in request body", 400) return }
var rBody reqBody
// Decode the request body into rBody err := json.NewDecoder(r.Body).Decode(&rBody) if err != nil { http.Error(w, "Error parsing JSON request body", 400) }
// Execute graphql query result := gql.ExecuteQuery(rBody.Query, *s.GqlSchema)
// render.JSON comes from the chi/render package and handles // marshalling to json, automatically escaping HTML and setting // the Content-Type as application/json. render.JSON(w, r, result) } }
funcmain() { // Initialize our API and return a pointer to our router for http.ListenAndServe // and a pointer to our db to defer its closing when main() is finished router, db := initializeAPI() defer db.Close()
// Listen on port 4000 and if there's an error log it and exit if err := http.ListenAndServe(":4000", router); err != nil { log.Fatal(err) } }
funcinitializeAPI() (*chi.Mux, *mapper.Db) { // Create a new connection to our pg database db, err := mapper.New( mapper.ConnString("127.0.0.1", 3306, "root", "123456", "go_graphql_db"), ) if err != nil { log.Fatal(err) }
// Create our root query for graphql rootQuery := gql.NewRoot(db) // Create a new graphql schema, passing in the the root query sc, err := graphql.NewSchema( graphql.SchemaConfig{Query: rootQuery.Query}, ) if err != nil { fmt.Println("Error creating schema: ", err) }
// Create a handler struct that holds a pointer to our database as well // as the address of our graphql schema s := handler.Handler{ GqlSchema: &sc, }
// Create a new router router := chi.NewRouter()
// Add some middleware to our router router.Use( render.SetContentType(render.ContentTypeJSON), // set content-type headers as application/json middleware.Logger, // log API request calls middleware.DefaultCompress, // compress results, mostly gzip assets and json middleware.StripSlashes, // match paths with a trailing slash, strip it, and continue routing through the mux middleware.Recoverer, // recover from panics without crashing handler )
// Create the graphql route with a Handler method to handle it router.Post("/graphql", s.GraphQL())