こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、GraphQLでSearch機能を実装します。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
前回
Search機能
GraphQLを使って、Search機能を実装しましょう。
ルートの共通化
GraphQLはエンドポイントを集約できるので、新しくルートを追加する必要はありません。ここがAPI開発においてメリットになる部分かと思います。
ただし、共通で使える名前に変更しておきましょう。
1 2 3 4 5 6 7 8 9 10 |
// cmd/api/routes.go ... func (app *application) routes() http.Handler { ... router.HandlerFunc(http.MethodPost, "/v1/graphql", app.moviesGraphQL) ... return app.enableCORS(router) } |
スキーマーの追加
Go側でスキーマーを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// cmd/api/graphql.go ... var ( movies []*models.Movie // graphql schema definition fields = graphql.Fields{ ... "search": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Search movies by title", Args: graphql.FieldConfigArgument{ "titleContains": &graphql.ArgumentConfig{ Type: graphql.String, }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { var theList []*models.Movie search, ok := params.Args["titleContains"].(string) if ok { for _, currentMovie := range movies { if strings.Contains(currentMovie.Title, search) { log.Println("Fond one") theList = append(theList, currentMovie) } } } return theList, nil }, }, } movieType = graphql.NewObject( ... ) ) |
searchスキーマーを追加しました。リクエスト中に「titleContains」が存在すれば、検索処理を実行します。
Searchエリアの追加
GraphQLコンポーネントに、Searchエリアを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// components/GraphQL.tsx ... import Input from './form-components/Input' const GraphQL = (props: any) => { ... const [searchTerm, setSearchTerm] = useState("") ... return ( <Fragment> <h2>GraphQL</h2> <hr /> <Input title={"Search"} type={"text"} name={"search"} value={searchTerm} handleChange={setSearchTerm} /> <div className="list-group"> ... </div> </Fragment> ) } export default GraphQL |

Searchクエリの発行
APIへクエリを発行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
// components/GraphQL.tsx ... const GraphQL = (props: any) => { ... const performList = () => { const payload = ` { list { id title runtime year description } } ` graphQLRequest(payload, "list") } const performSearch = () => { const payload = ` { search(titleContains: "${searchTerm}") { id title runtime year description } } ` graphQLRequest(payload, "search") } useEffect(() => { ( () => { if (searchTerm === "") { performList() } else { performSearch() } } )() }, [searchTerm]) const graphQLRequest = async (payload: string, type: string) => { const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql', payload, config) .then((response) => { let theList switch (type) { case 'list': theList = response.data.data.list break case 'search': theList = response.data.data.search break } if (theList.length) { setMovies(theList) } else { setMovies([]) } setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } ... return ( ... ) export default GraphQL |
前回実装したListクエリと同じくPOSTを使うので、POSTリクエストを関数(graphQLRequest)として外だしし共通化しました。
GraphQLを利用することで「クエリ」のみを気にすれば良くなるので、結構使いやすくなりますね。
また、useStateを使ってSearch KeyをStateで管理しています。このキーに変更があった時、自動的にperformSearch関数がよばれるようにしました。
また、キーが空の場合は全件検索にしています。
検証
Search機能を検証してみましょう。
OKですね。
次回
次回は、GraphQLでAPIへのリクエスト&データの取得処理を実装しましょう。
記事まとめ
参考書籍
ソースコード
graphql.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
// cmd/api/graphql.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strings" "github.com/graphql-go/graphql" ) var ( movies []*models.Movie // graphql schema definition fields = graphql.Fields{ "movie": &graphql.Field{ Type: movieType, Description: "Get movie by id", Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.Int, }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(int) if ok { for _, movie := range movies { if movie.ID == id { return movie, nil } } } return nil, nil }, }, "list": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Get all movies", Resolve: func(params graphql.ResolveParams) (interface{}, error) { return movies, nil }, }, "search": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Search movies by title", Args: graphql.FieldConfigArgument{ "titleContains": &graphql.ArgumentConfig{ Type: graphql.String, }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { var theList []*models.Movie search, ok := params.Args["titleContains"].(string) if ok { for _, currentMovie := range movies { if strings.Contains(currentMovie.Title, search) { log.Println("Fond one") theList = append(theList, currentMovie) } } } return theList, nil }, }, } movieType = graphql.NewObject( graphql.ObjectConfig{ Name: "Movie", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.Int, }, "title": &graphql.Field{ Type: graphql.String, }, "description": &graphql.Field{ Type: graphql.String, }, "year": &graphql.Field{ Type: graphql.Int, }, "release_date": &graphql.Field{ Type: graphql.DateTime, }, "runtime": &graphql.Field{ Type: graphql.Int, }, "rating": &graphql.Field{ Type: graphql.Int, }, "mpaa_rating": &graphql.Field{ Type: graphql.String, }, "created_at": &graphql.Field{ Type: graphql.DateTime, }, "updated_at": &graphql.Field{ Type: graphql.DateTime, }, }, }, ) ) func (app *application) moviesGraphQL(w http.ResponseWriter, r *http.Request) { movies, _ = app.models.DB.All() q, _ := io.ReadAll(r.Body) query := string(q) log.Println(query) rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields} schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} schema, err := graphql.NewSchema(schemaConfig) if err != nil { app.errorJSON(w, errors.New("failed to create schema")) log.Println(err) return } params := graphql.Params{Schema: schema, RequestString: query} resp := graphql.Do(params) if len(resp.Errors) > 0 { app.errorJSON(w, errors.New(fmt.Sprintf("failed: %+v", resp.Errors))) } j, _ := json.MarshalIndent(resp, "", " ") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(j) } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// cmd/api/routes.go package main import ( "context" "net/http" "github.com/julienschmidt/httprouter" "github.com/justinas/alice" ) func (app *application) wrap(next http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := context.WithValue(r.Context(), "params", ps) next.ServeHTTP(w, r.WithContext(ctx)) } } func (app *application) routes() http.Handler { router := httprouter.New() secure := alice.New(app.checkToken) router.HandlerFunc(http.MethodGet, "/status", app.statusHandler) router.HandlerFunc(http.MethodPost, "/v1/graphql", app.moviesGraphQL) router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovie) router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenres) router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres) router.POST("/v1/admin/editmovie", app.wrap(secure.ThenFunc(app.editMovie))) router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie) return app.enableCORS(router) } |
GraphQL.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
// components/GraphQL.tsx import { Fragment, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Movie } from '../models/movie' import axios from 'axios' import Input from './form-components/Input' const GraphQL = (props: any) => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [searchTerm, setSearchTerm] = useState("") const performList = () => { const payload = ` { list { id title runtime year description } } ` graphQLRequest(payload, "list") } const performSearch = () => { const payload = ` { search(titleContains: "${searchTerm}") { id title runtime year description } } ` graphQLRequest(payload, "search") } useEffect(() => { ( () => { if (searchTerm === "") { performList() } else { performSearch() } } )() }, [searchTerm]) const graphQLRequest = async (payload: string, type: string) => { const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql', payload, config) .then((response) => { let theList switch (type) { case 'list': theList = response.data.data.list break case 'search': theList = response.data.data.search break } if (theList.length) { setMovies(theList) } else { setMovies([]) } setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>GraphQL</h2> <hr /> <Input title={"Search"} type={"text"} name={"search"} value={searchTerm} handleChange={setSearchTerm} /> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/movies/${m.id}`} className="list-group-item list-group-item-action" > <strong>{m.title}</strong> </Link> ) })} </div> </Fragment> ) } export default GraphQL |
コメントを残す
コメントを投稿するにはログインしてください。