Apollo Client (React) 触ってみた。 Query編
Apolloのドキュメントを読んだ時のメモ
https://www.apollographql.com/docs/
まずは簡単なgraphQL APIを構築 https://github.com/DaisukeMatsumoto0925/graph_practice
Front Next.ts (codegen) Backend Go (gqlgen)
Apolloの構築
const createApolloClient = () => { return new ApolloClient({ link: new HttpLink({ uri: 'http://localhost:3000/query', }), cache: new InMemoryCache(), }); }; const client = createApolloClient() function MyApp({ Component, pageProps }: AppProps) { return ( <ApolloProvider client={client}> <Component {...pageProps} /> </ApolloProvider> ) }
以上、Reduxの構築に比べると圧倒的コード量の少なさ。
useQuery
https://www.apollographql.com/docs/react/data/queries/
apolloで使えるhooks。 GraphQLのQueryを叩く
以下のようにgaraphQLのAPIを叩いてレスポンスを取得できる。
const { loading, error, data } = useQuery(QUERY_GQL);
そのほかにも様々なコールバック関数が用意されておりかなり使い勝手が良い。 useEffectやssr等で呼び出す必要すらない。
ちなみに graphql code generator というライブラリを使用すると hooksをラッピングするカスタムhookを作成してくれて、さらに便利。 https://www.graphql-code-generator.com/
cache
useQueryが発火した時fetchされた結果は自動的にキャッシュされる。
Whenever Apollo Client fetches query results from your server, it automatically caches those results locally. This makes subsequent executions of the same query extremely fast.
つまり何もする必要はない。
Polling
指定された時間でuseQueryを定期実行してくれる。
const { loading, error, data } = useQuery(TASKS_QUERY, { pollInterval: 500, });
試しにDBを直接更新してみた。
clientではリロードせずにデータがfetchされた。
fetch policy
デフォルトでは cache-first が適用されている。
no-cacheにした場合
const { loading, error, data } = useQuery(TASKS_QUERY, { fetchPolicy: "no-cache" });
cacheには保存されていない。 その他のポリシー https://www.apollographql.com/docs/react/data/queries/#supported-fetch-policies
その他
https://www.apollographql.com/docs/react/data/queries/#usequery-api
その他にもたくさんのAPIが用意されている。
onCompleted
Queryが成功した時のコールバック
const {data, refetch, networkStatus, loading, error} = useTasksQuery({ onCompleted: (data) => {return console.log("result",data)} })
これはかなり使えそう、コールバックにはfetch後のデータが入ってくるが、 それ以外のdom操作でformの中身をリセットしたり、成功時のポップアップを出す場合など
skip
Queryを実行しないようにする。
const {data, refetch, networkStatus, loading, error} = useTasksQuery({ skip: true })
ある条件下では発火させたくない時はこれを使うのが良さそう。
refetch
hooksのresult
名前の通りQueryを再発火させる。 ボタンなどに仕込めばレスポンスを容易に再取得できる。
const {refetch} = useTasksQuery() refetch()
以上が構築〜Query編 次回はmutationを見ていく。
GraphQL ID設計
きっかけ
本業で初めてGraphQLに触れた。 GraphQLでapiを叩く時、IDの仕様に癖があったので調べてみた。
参考
本題
そもそもGraphQlのスキーマ設計は何を基準に行うべきなのか?
GraphQLスキーマ設計ガイド 第2版 - わかめの自動売り場 - BOOTH では
とあるように大手APIの仕様を参考にする。
GitHubのGraphQLAPIは、 Reactのクライアントライブラリである Relay 向けの仕様へ準拠されている。
https://github.com/facebook/relay
つまり フロントエンド側で使うライブラリは ApolloやRelayが有名でそれらに合わせて設計するのが良さそう。
Apollo、RelayにあせてIDを設計すると IDはユニークでグローバルな必要がある。
たとえばUser型のidを123と返すのではなく、User:123や、これを base64 encode したVXNlcjoxMjM=とする。
理由は、ApolloやRelayはキャッシュ機構を持っておりこのidをキーとしているため、 idが重複してしまうとキャッシュが上書きされバグの原因となる。
query FindUser{ user(id: "users: 1"){ id name }
"users: 1"
テーブル名 : ID
このようにテーブル間を跨いでユニークな値にする事で Node の id の値のみから、任意のデータを見つけられるようになる。
UserのIDから検索をしたい時に単にID"1"のみを指定した場合どのテーブルかを判別できない。
以上
Go 埋め込み
きっかけ
こちらの記事でクリーンアーキテクチャを勉強していたときに埋め込みについてさらっと使用されていたので自分なりに深堀ってみた。 https://qiita.com/hirotakan/items/698c1f5773a3cca6193e
type UserRepository struct { SqlHandler } func (repo *UserRepository) Store(u domain.User) (id int, err error) { result, err := repo.Execute( "INSERT INTO users (first_name, last_name) VALUES (?,?)", u.FirstName, u.LastName, ) if err != nil { return } id64, err := result.LastInsertId() if err != nil { return } id = int(id64) return }
type SqlHandler interface { Execute(string, ...interface{}) (Result, error) Query(string, ...interface{}) (Row, error) }
UserRepository というstructに構造体にSqlHandler というinterfaceを埋め込んでいる。
UserRepositoryではSqlHandlerのメソッドを使えるようになる。
Goでの埋め込み
参考: https://qiita.com/momotaro98/items/4f6e2facc40a3f37c3c3
interfaceにはstructを埋め込めない
structにはstructを埋め込める interfaceにはinterfaceを埋め込める structにはinterfaceを埋め込める。
埋め込みのメリット
実装を省略できる為 DRY である。
埋め込みの注意点
参考: https://qiita.com/shibukawa/items/16acb36e94cfe3b02aa1 Goの埋め込みで継承はできない。
「名前なしでアクセスできるメンバー変数」というシンタックスシュガーと考えるべきです。
あくまでもDRYの為
埋め込み元と埋め込み先がのメソッドが重複した場合
埋め込み先のメソッドが先に優先される。
あるメソッドは埋め込み元と同じ実装で あるメソッドは独自のメソッドを実装したいときに使用する。
以上。
Go migrateion goose setup
Goのマイグレーションツール
goose
Go製のmigration ツールでは人気のある方?
業務でも採用されていたので使ってみました。
情報にバラつきがありセットアップまでに時間がかかりました。
goose
bitbucket版 と github版があります。
github版 https://github.com/pressly/goose
同じかと思いきや、微妙に挙動が違うので要注意です。
今回はbitbucket版を使用します。
install
bitbucketを使用します。 githubと間違わないよう注意です。
go get bitbucket.org/liamstask/goose/cmd/goose
configure
db/dbconf.yml デフォルトでこのパスを見にいくみたいです。
development: driver: mysql open: $DB_URL production: driver: mysql open: $DB_URL
% echo $DB_URL root:password@tcp(db:3306)/sample_pj
{username}:{password}@tcp({host}:3306/{db_name}
上記の形式で指定します。 私の場合はdocker-composeを使用している為 hostがdb(コンテナ名)を指定しています。
docker-compose
app: environment: DB_URL: root:password@tcp(db:3306)/sample_pj db: environment: MYSQL_DATABASE: sample_pj MYSQL_ROOT_PASSWORD: password
バージョンによって書き方が違うみたいです。
commands
% goose goose is a database migration management system for Go projects. Usage: goose [options] <subcommand> [subcommand options] Options: -env string which DB environment to use (default "development") -path string folder containing db info (default "db") -pgschema string which postgres-schema to migrate (default = none) Commands: up Migrate the DB to the most recent version available down Roll back the version by 1 redo Re-run the latest migration status dump the migration status for the current DB create Create the scaffolding for a new migration dbversion Print the current version of the database
create
マイグレーションファイルの生成 db/migrations/ 配下にmigrationファイルが生成されます。
github版だとなぜかルートにマイグレーションファイルが生成されます。
goose create AddSomeColumns sql
sqlを指定しない場合はgoファイルで生成されます。
migrationfile
db/migrations/20210526144142_AddSomeColumns.sql 生成されたファイルに以下を追加
-- +goose Up -- SQL in section 'Up' is executed when this migration is applied CREATE TABLE samples ( id int UNSIGNED AUTO_INCREMENT PRIMARY KEY, first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, super_user int(8) UNSIGNED, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- +goose Down -- SQL section 'Down' is executed when this migration is rolled back DROP TABLE IF EXISTS samples;
migration
% goose up
これでマイグレーションされます。
逆にdownすると
% goose down
テーブルは削除されます。
またup or down 時に goose_db_versionというテーブルが生成され 変更のたびにversionが記録されていきます。
以上です。
bitbucket製とgithub製、configファイルの書き方に苦戦しました。
他にもコマンドがあるのでもう少し触ってみようと思います。
Go API開発の環境構築 テンプレート アーキテクチャ、ホットリロード、ロギング、Docker
GoでAPIを作るときのテンプレです。 ホットリロード、ロギング、コンテナ、DB、まとめて構築していきます。
以下の技術を使用します。
- Docker docker-compose
- ホットリロードライブラリair https://github.com/cosmtrek/air
- フレームワークgin https://github.com/gin-gonic/gin
- ORM gorm v1 https://gorm.io/
- db mysql
ディレクトリ構成です。
├── Dockerfile ├── air.toml //airの設定 ├── config │ ├── config.go │ └── development.yml ├── db │ └── my.cnf ├── docker-compose.yml ├── entry.sh ├── go.mod ├── go.sum ├── main.go ├── src │ └── infrastructure │ ├── db │ │ └── sqlhandler.go │ ├── logging │ │ └── logging.go │ └── router │ └── router.go └── tmp └── main
クリーンアーキテクチャでの実装を前提としています。
Dockerfile
Dockerfile
FROM golang:latest as development WORKDIR /app VOLUME /app ADD . /app ENV GO_ENV=development RUN apt-get update && apt-get -y install default-mysql-client RUN go get github.com/cosmtrek/air CMD ["./entry.sh"]
docker-compose側でマルチビルドしたいので as developmentとします。 開発環境用なので環境変数に developmentを渡します。 buildに必要な mysql clientと airをインストールします。
docker-compose
version: '3.7' services: app: build: context: . dockerfile: Dockerfile target: development ports: - "8080:8080" volumes: - "./:/app" security_opt: - seccomp:unconfined depends_on: - db environment: DB_PORT: 3306 DB_NAME: sample_pj DB_USER: root DB_PASSWORD: password db: platform: linux/x86_64 #for m1Tip image: mysql:8.0.25 environment: MYSQL_DATABASE: sample_pj MYSQL_ROOT_PASSWORD: password ports: - "3306:3306" volumes: - ./data/mysql:/var/lib/mysql:cached - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
m1チップのMac Bookではplatform: linux/x86_64を指定する必要があります。
entry.sh
#!/bin/sh set -e $GOPATH/bin/air -c air.toml
set -e https://qiita.com/youcune/items/fcfb4ad3d7c1edf9dc96#set--e -cオプション はコンフィグファイルの指定https://github.com/cosmtrek/air#usage
air.toml
root = "." tmp_dir = "tmp" [build] cmd = "go build -o ./tmp/main ./main.go" bin = "tmp/main"
次にGoのコードを書いていきます。
config/config.go
package config import ( "fmt" "io/ioutil" "log" "os" "sync" "github.com/caarlos0/env" "gopkg.in/yaml.v2" ) type Config struct{ Port string Db Db } type Db struct { Port string `env:"DB_PORT"` Name string `env:"DB_NAME"` User string `env:"DB_USER"` Password string `env:"DB_PASSWORD"` } var config *Config var once sync.Once func Get() *Config { return config } func init() { once.Do(func(){ config = &Config{} goenv := os.Getenv("GO_ENV") filepath := fmt.Sprintf("./config/%v.yml", goenv) buf, err := ioutil.ReadFile(filepath) if err != nil { log.Fatalln("failed to load config yaml err:", err) } fmt.Println("config:", config) err = yaml.Unmarshal(buf, config) if err != nil { log.Fatalln("failed to Unmarshal yaml err: ", err) } if err := env.Parse(&config.Db); err != nil { fmt.Printf("failed to load Db error: %s", err) } }) }
ここでは環境変数の設定を行います。 環境変数はDockerfileとdocker-composeの両方から渡しています。 os.Getenvで読み込む GO_ENVはDockerfile env アノテーションで読み込むDB_ はdocker-composeからです。
ioutil.ReadFileでconfig配下のymlファイルを読み込みます。
development.yml
port: 8080
これでGO_ENV環境変数毎にymlを切り替えます。
次にsrc配下のGoファイルを作っていきます。 今回はクリーンアーキテクチャを前提にFrameworks & Drivers に当たるinfrastructureの部分を実装していきます。
src/infrastructure/db/sqlhandler.go
package db import ( "fmt" "log" "github.com/username/sample_pj/config" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) type SQLHandler struct { Conn *gorm.DB } func New() *SQLHandler { connectionString := genConnectString() fmt.Println("connectionString: ", connectionString) conn, err := gorm.Open("mysql", connectionString) if err != nil { log.Fatal("db connection error: ", err) } fmt.Println("Connection OK") conn.LogMode(true) // debug mode sqlHandler := &SQLHandler{ Conn: conn, } return sqlHandler } func genConnectString() string { conf := config.Get() USER := conf.Db.User PASSWORD := conf.Db.Password PROTOCOL := fmt.Sprintf("tcp(db:%s)", conf.Db.Port) DBNAME := conf.Db.Name QUERY := "?charset=utf8mb4&parseTime=True&loc=Local" return USER + ":" + PASSWORD + "@" + PROTOCOL + "/" + DBNAME + QUERY }
*gorm.DBを返します。
src/infrastructure/logging/logging.go
package logging import ( "fmt" "io" "log" "os" ) type Logger struct { F *log.Logger I *log.Logger D *log.Logger E *log.Logger } type NullWriter struct{} func (n NullWriter) Write(p []byte) (int, error) { return 0, nil } func NewLogger(env string) *Logger { var fatalWriter io.Writer = os.Stderr var infoWriter io.Writer = os.Stdout var errorWriter io.Writer = os.Stderr var debugWriter io.Writer = os.Stdout if env != "development" { debugWriter = NullWriter{} } return &Logger{ F: log.New(fatalWriter, "[FATAL] ", log.Llongfile|log.Ldate|log.Lmicroseconds), E: log.New(errorWriter, "[ERROR] ", log.Llongfile|log.Ldate|log.Lmicroseconds), I: log.New(infoWriter, "[INFO] ", log.Llongfile|log.Ldate|log.Lmicroseconds), D: log.New(debugWriter, "[DEBUG] ", log.Llongfile|log.Ldate|log.Lmicroseconds), } } func (l *Logger) Info(v ...interface{}) { l.I.Output(2, fmt.Sprint(v...)) } func (l *Logger) Infof(format string, v ...interface{}) { l.I.Output(2, fmt.Sprintf(format, v...)) } func (l *Logger) Debug(v ...interface{}) { l.D.Output(2, fmt.Sprint(v...)) } func (l *Logger) Debugf(format string, v ...interface{}) { l.D.Output(2, fmt.Sprintf(format, v...)) } func (l *Logger) Error(v ...interface{}) { l.E.Output(2, fmt.Sprint(v...)) } func (l *Logger) Errorf(format string, v ...interface{}) { l.E.Output(2, fmt.Sprintf(format, v...)) } func (l *Logger) Fatal(v ...interface{}) { l.F.Output(2, fmt.Sprint(v...)) os.Exit(1) } func (l *Logger) Fatalf(format string, v ...interface{}) { l.F.Output(2, fmt.Sprintf(format, v...)) os.Exit(1) }
標準logライブラリのマッパーです。 外部パッケージでlogger.Debug("debug")のように使用できます。 可変引数についてhttps://golang.org/ref/spec#Passing_arguments_to_..._parameters logについて https://waman.hatenablog.com/entry/2017/09/29/011614
src/infrastructure/router/router.go
package router import ( "net/http" "github.com/username/sample_pj/config" "github.com/username/sample_pj/src/infrastructure/db" "github.com/gin-gonic/gin" ) func Dispatch(sqlhandler *db.SQLHandler) { conf := config.Get() r := gin.Default() r.GET("/hello", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{ "message": "hello", }) }) r.Run(":" + conf.Port) }
/hello にアクセスした時, {"message": "hello"} を返します。
main.go
package main import ( "github.com/username/sample_pj/src/infrastructure/db" "github.com/username/sample_pj/src/infrastructure/logging" "github.com/username/sample_pj/src/infrastructure/router" ) func main() { logger := logging.NewLogger("development") logger.Debug("debug log") sqlhandler := db.New() defer sqlhandler.Conn.Close() router.Dispatch(sqlhandler) }
docker-compose up
以上。