Single endpoint cùng GraphQL (Backend Go)
graphql
3
Go
41
restful
10
White

Thach Le viết ngày 22/07/2017

GraphQL được tạo ra bởi Facebook vào năm 2012, production ready năm 2016 nhưng hiện tại vẫn chưa phổ biến lắm với cộng đồng developers, đặc biệt là Việt Nam.

Đọc về nó lâu rồi nhưng chưa có cơ hội sử dụng. Nhân tiện có một khách hàng muốn xài GraphQL nên team mình đã apply vào luôn. Trong quá trình dùng thì thấy khá kute phô mai que nên muốn share ít kiến thức tìm hiểu được.

alt text

Problem

Để hiểu được concreate problem của GraphQL thì phải nói tới REST.

REST

Chắc hẳn mọi người đều đã làm việc với REST rồi.
Có rất nhiều thứ để nói về REST, mình sẽ tóm gọn lại một cách dễ hiểu.
REST hay Representational state transfer là tập hợp những architecture principles, qui định cách thức clients interact với server, giúp application có thể manage tài nguyên.

Citizen của REST là "resource". Khi bạn muốn retreive dữ liệu resource thì bạn dùng GET, khi muốn insert thì dùng POST... kiểu vậy.

Một điểm quan trọng đó là REST stateless và cacheable.

alt text

REST drawbacks

Ứng dụng mình chia làm 2 phần, backend là Go và frontend là React. Mình bắt đầu gặp những vấn đề khi ứng dụng grows:

  • Số lượng API endpoint quá nhiều, đôi lúc sẽ phải cầm cái id này chạy tới endpoint này kết quả, sau đó cầm tập id được response chạy tới endpoint khác để lấy cái thật sự cần.
  • Rất khó để define ra được một chuẩn chung của dữ liệu trả về, vì đôi lúc với một model, client có thể lúc thì cần fields này, lúc thì cần những fields khác.
  • Khi mà đổi một cái gì đó, ví dụ schema, thì không biết những chỗ nào bị impact.

GraphQL for the sake

GraphQL là một internal project của Facebook, sau đó được opensourced. Nó là một query language cho phép clients construct resource mà bạn muốn server trả về.

Ví dụ:

{
  me {
    name
  }
}

Đoạn query trên muốn khi gọi API thì lấy tên của chính mình. Kết quả trả về:

{
  "me": {
    "name": "Runi"
  }
}

Bạn có thể nhận ra query của nó gần như là một JSON mà bị missing value, và kết quả trả về là JSON với format đúng như client requests.

REST vs GraphQL

Thật ra so sánh 2 thằng này thì không hợp lý lắm, point ở đây là so sánh 2 thằng này "in practice".

Giống:

  • Đều là send data over HTTP request.
  • Core idea đều là resource.
  • Output cuối cùng là JSON là như nhau.
  • Đều có cách phân biệt giữa write/read data.

Khác:

  • REST coupled giữa cách bạn define data và cách bạn retrieve nó. Ví dụ /songs/1. GraphQL separates endpoint và cách bạn lấy data.
  • REST định nghĩa những thông tin resource ở server, clients chỉ make a call. GraphQL cho phép client đưa lên một datashape, nhiệm vụ server phải trả về đúng thông tin như vậy.
  • Khi bạn muốn fetch nhiều related data bạn phải gọi multiple request ở REST. GraphQL cho phép bạn traverse entry point để lấy data bằng single request. (hacky way ở REST là tạo 1 endpoint mới gom response lại, too painful).
  • Thay đổi read/write ở REST bằng http method, GraphQL bằng query.

Well, tới đây chắc các bạn cũng có một quick overview về GraphQL rồi. Mình sẽ apply vào Go backend xem thế nào.

Integrate backend Go

Mình đã thử dùng GraphQL trên elixir và Ruby. Phải thừa nhận là vì dynamic language nên code ... rất sướng tay.
Trên Go thì hơi trâu bò một chút.

Bài toán: Mình sẽ tạo một endpoint GraphQL, dùng nó lấy random một bài hát trong database, và tạo một song mới.

Mình sử dụng github.com/graphql-go/graphql làm GraphQL implementation.

Vì GraphQL idiomatic là chỉ dùng single endpoint, nên mình chỉ serve:

http.Handle("/graphql", graphHandler)

Trong đó

func  graphHandler(w http.ResponseWriter, r *http.Request) {
    var schema, _ = graphql.NewSchema(graphql.SchemaConfig{
    Query:    query,
    Mutation: mutation,
    })

    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: r.URL.Query().Get("query"),
    })

    json.NewEncoder(w).Encode(result)
}

Trong GraphQL, query = read và mutation = write.

Hàm trên có thể hiểu, graphql handler sẽ đọc vào một cái schema, parse query string từ client gửi lên để lấy datashape, sau đó query data từ nơi nào đó trả về.

Query

Giờ mình sẽ construct query object để lấy bài hát.

var query = graphql.NewObject(graphql.ObjectConfig{
        Name: "Query",
        Fields: graphql.Fields{
            "song": &graphql.Field{
                Type: songType, 
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    song, err := getRandomSong()
                    if err != nil {
                        logrus.Errorf("failed to random song, err = %v", err)
                        return nil, err
                    }

                    return song, nil
                },
            },
        },
    })



var songType = graphql.NewObject(graphql.ObjectConfig{
    Name:        "Song",
    Description: "Song contains some information",
    Fields: graphql.Fields{
        "id": &graphql.Field{
            Type:        graphql.ID,
            Description: "Song's id",
        },
        "title": &graphql.Field{
            Type:        graphql.String,
            Description: "The title of the song.",
        },
        "artist": &graphql.Field{
            Type:        graphql.String,
            Description: "The artist of the song.",
        },
    },
})

Có 2 thứ ở đây cần chú ý:

  • Type của object bạn muốn trả về, đoạn code trên là songType qui định datashape mà client có thể query để lấy được.
  • Resolve function: Khi bạn viết một schema cho GraphQL, bạn phải viết resolve function cho nó. GraphQL execution engine sẽ invoke function này khi data thật sự được queried.

Như vậy với đoạn code trên, nếu không có gì xảy ra thì khi make request với query như sau:

query {
    song {
        id
        title
        artist
    }
}

sẽ được kết quả

{
    "song": {
        "id": 1,
        "title": "Chieu Hom Ay",
        "artist": "Jaykii"
    }
}

Match exactly với schema chúng ta vừa định nghĩa.

Vậy còn nếu muốn random một bài hát, mà có theo tag do client gửi lên thì sao?

GraphQL hỗ trợ arguments, ta sẽ sửa lại code schema:

var query = graphql.NewObject(graphql.ObjectConfig{
        Name: "Query",
        Fields: graphql.Fields{
            "song": &graphql.Field{
                Type: songType, 
                Args: graphql.FieldConfigArgument{
                    "tag": &graphql.ArgumentConfig{
                        Type: graphql.String,
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    tag := p.Args["tag"].(string)
                    song, err := getRandomSongByTag(tag)
                    if err != nil {
                        logrus.Errorf("failed to random song by tag, err = %v", err)
                        return nil, err
                    }

                    return song, nil
                },
            },
        },
    })

Vậy query string của chúng ta sẽ thay đổi một chút thành:

query {
    song(tag: "us") {
        id
        title
        artist
    }
}

Kết quả sẽ random ra 1 bài hát có tag là "us":

{
    "song": {
        "id": 12,
        "title": "Leave out all the reset",
        "artist": "Linkin Park"
    }
}

Mutation

GraphQL tư tưởng là dùng để ease for querying data là chính, nhưng với một data platform hoàn chỉnh thì phải hỗ trợ cả việc modify data trên server.

Bên REST không khuyến khích bạn modify data bằng GET request. Nhưng thật ra bạn vẫn làm được (nhét hết lên url params chẳng hạn, có điều url thì có limit length). Bên GraphQL cũng vậy, bạn thậm chí có thể write data bằng query, nhưng nó ko đúng convention. GraphQL cung cấp mutation để làm chuyện này.

Ví dụ bạn muốn tạo một bài hát mới, chúng ta sẽ viết một song mutation:

var mutation = graphql.NewObject(graphql.ObjectConfig{
    Name: "Mutation",
    Fields: graphql.Fields{
        "createSong": &graphql.Field{
            Type:        songInputType,
            Description: "Create new song",
            Args: graphql.FieldConfigArgument{
                "title": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
                "artist": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String),
                },
            },
            Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                title, _ := params.Args["title"].(string)
                artist, _ := params.Args["artist"].(string)

                song := &domain.Song{
                        Title: title,
                        Artist: artist,
                   }

                err := saveSong(song)
                if err != nil {
                    logrus.Errorf("failed to save song, err = %v", err)
                    return nil, err
                }

                return song, nil
            },
        },
    },
})

var songInputType = graphql.NewInputObject(graphql.InputObjectConfig{
    Name:        "Song",
    Description: "Song inputs",
    Fields: graphql.InputObjectConfigFieldMap{
        "title": &graphql.InputObjectFieldConfig{
            Type:        graphql.String,
            Description: "The title of the song.",
        },
        "artist": &graphql.InputObjectFieldConfig{
            Type:        graphql.String,
            Description: "The artist of the song.",
        },
    },
})

GraphQL phân biệt kiểu Input và Output. Các bạn có thể hiểu đơn giản là Output => cho những thứ để export ra và Input là các giá trị được truyền vào. Output và Input cùng một model nhưng có thể có schema khác nhau, Output có thể chứa nhiều complex data type hơn.

Trong ví dụ trên là songInputType sẽ là datashape của các parameters mà clients sẽ gửi lên. Khi đó mutation của chúng ta sẽ là:

mutation {
    createSong(title: "Co em cho", artist: "Min") {
        id
    }
}

Tức là chúng ta đang muốn tạo một bài hát mới với given title + artist, sau khi tạo xong thì trả về id.
Kết quả mong đợi sẽ là

{
    "createSong": {
        "id: 16
    }
}

Như vậy là chúng ta đã biết cách để read/write data với GraphQL.

Combo với frontend

Đây là backend, vậy frontend thì chúng ta có gì?

Ngoài ra các bạn nào chưa muốn chuyển giao công nghệ kịp cả hai platform, thì có thể chơi thằng này join monster. Đại loại là một query planner, sinh ra optimal sql query. Bạn có interface cho GraphQL (viết frontend sướng quá chừng), backend thì đỡ phải cài lại query. :3.

Vã quá thì bạn nào xài posgres có thể chơi postgraphql bụp 1 cái tự instropect schema, tự gen API GraphQL luôn.

Tổng kết

GraphQL rất thích hợp khi mà product của bạn có các clients cần flexible response format, lúc thì cần như thế này, lúc cần như thế kia mà không cần backend phải thay đổi.
Ngoài ra GraphQL cũng giúp drops TCP requests + network round trip với single endpoint.

Với GraphQL cách approach của bạn sẽ natural hơn, tức là suy nghĩ cái mình cần trả về, thay vì suy nghĩ cách lấy đầu tiên. Điều này có thể speed up development.

alt text
Q: GraphQL có drawbacks không?
A: Có chứ, khá nhiều đấy. Một số ví dụ như: không thể versioned, painful khi xử lý upload hay validation, cache các kiểu không safe (DataLoader)
Q: Vậy xài chung REST và GraphQL được không?
A: Hoàn toàn CÓ. Hai thằng này có thể bổ sung cho những khuyết điểm của nhau. Đôi bạn cùng tiến.

Bài viết được dẫn từ Runiblog

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Thach Le

6 bài viết.
102 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
39 5
Dạo này Rust đang nổi lên như một thế lực khiến một hispter như mình không thể không để tâm. Sau vài ngày dig deeper vào Rust, mình cho rằng Rust l...
Thach Le viết 1 năm trước
39 5
White
36 2
Chắc các bạn cũng không lạ lẫm gì Go nữa, Go là một ngôn ngữ backend được phát triển bởi Google. (Ảnh) Một vài điểm mạnh nổi bật trong Go: Stati...
Thach Le viết 11 tháng trước
36 2
White
18 4
Messenger bots của facebook ra cũng khá lâu rồi. Đã có nhiều người sử dụng cho page facebook của mình. Bài này của mình sẽ hướng dẫn các bạn viết 1...
Thach Le viết hơn 1 năm trước
18 4
Bài viết liên quan
White
7 2
Makefile thực hiện một số thao tác thường dùng trong Go Khi làm project Go mình thường tạo một file Makefile dạng này: Lưu ý nhớ thay thành tên m...
Huy Trần viết hơn 1 năm trước
7 2
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
6 bài viết.
102 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!