- 投稿日:2019-04-11T23:47:21+09:00
Goのgoroutine, channelをわかりたい
Goのgoroutine, channelがわからない
マルチスレッドってなんやねん!
go ステートメントってなんやねん!
<- なんやねんこれ!意味不明これをやりましょう → Go by Example
やっていれば、なんとなくわかってくる。かも以下は Go by Exampleを少し変更を加えて実行した例です。
Gorutineとは
Tour of go によると
goroutine (ゴルーチン)は、Goのランタイムに管理される軽量なスレッドです。
まず、スレッドがちゃんと理解してない
スレッド:一連のプログラムの流れ
シングルスレッド:1つのスレッドだけからなるプログラム
マルチスレッド:1つのプログラムで複数のスレッドを同時に実行するマルチスレッド最強やんけ!ずっとこれ使えば小早川セナじゃん!
→ 実際には、しっかり理解して使わないとパフォーマンスが低下したり、デッドロックが生じる可能性がある。Goroutinesを理解しよう
gorutineは、軽量のスレッドである。
goステートメントで関数を指定することで、並行実行される。 ※並列実行ではない。ex) 関数fが並行実行される。
func f(value string) { for i := 0; i < 3; i++ { fmt.Println(value) time.Sleep(3 * time.Second) } } func main() { go f("goroutineを使って実行") f("普通に実行") fmt.Println("done") }こんなふうに出力が混ざった状態になる。
普通に実行 goroutineを使って実行 goroutineを使って実行 普通に実行 普通に実行 goroutineを使って実行 done試したい方どうぞ → The Go Playground
Channelsを理解しよう
チャネルは、並行実行されるgoroutine間を接続するパイプ(トンネル)のイメージ。つまり、並行実行している関数から値を受信する。(あるgoroutineから別のgorutineへ値を渡す。)
make(chan 型)で新しいチャネルを作成できる
channel <- 構文で、チャネルへ値を 送信 します。
<-channel 構文で、チャネルから値を 受信 します
つまり、上の例では、messageというパイプを使って、無名関数からmsgへ"ping"を渡している。
"ping"という値がmessageトンネルを通ってmsgへ届くGoはデフォルトで、送る側と受ける側が準備できるまで、 送受信はブロックされる。
このため、同期処理的なものを書かなくても、"ping"がmsgに渡されるまで、待ってくれるfunc main() { messages := make(chan string) go func() { messages <- "Hello" }() msg := <-messages fmt.Println(msg) }出力
Helloチャネルはバッファとして使える。
- バッファ: 一時的に記憶する場所
バッファが詰まるとチャネルへの送信をブロックする
バッファが空のときは、チャネルの受信をブロックするex)
package main import "fmt" func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println(<-ch) fmt.Println(<-ch) }出力
1 2デッドロックが起こるように書き換え
ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)fatal error: all goroutines are asleep - deadlock!Buffering
バッファリングされたチャネルは、対応する受信側がいなくても決められた量までなら 値を送信することができる
make(chan string, 2)によって2妻でバッファリングするチャネルを作っている。func main() { messages := make(chan string, 2) messages <- "Hello" messages <- "World" fmt.Println(<-messages) fmt.Println(<-messages) }出力
Hello WorldDirections
チャネルを関数の引数として使うと送信か受信のどちらを意図しているか指定(わかりやすく)することができる。
func ping(pings chan<- string, msg string) { pings <- msg } // この `pong` 関数は、1 つ目のチャネルを受信専用で (`pings`)、 // 2 つ目のチャネルを送信専用で (`pongs`) 受け取ります。 func pong(pings <-chan string, pongs chan<- string) { msg := <-pings pongs <- msg } func main() { pings := make(chan string, 1) pongs := make(chan string, 1) ping(pings, "Hello") pong(pings, pongs) fmt.Println(<-pongs) }出力
HelloSelect
selectを利用することで、複数のチャネル操作を待つことができる。
受信したものから、画面に表示される。2つのチャンネルに対して selectをする例
func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(2 * time.Second) c2 <- "two" }() go func() { time.Sleep(1 * time.Second) c1 <- "one" }() for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } }出力
received one received twoTimeouts
selectは最初に受信したものを処理するため、<-Time.Afterのほうが処理がはやければ、そちらの処理が走ります。
selectタイムアウトパターンを使用するためにはチャンネル経由でやりとりする必要がある。func main() { c1 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout 1") } }出力
timeout 1Closing Channels
jobsチャネルをcloseします。
closeするとmoreの値がfalseになり、doneがtrueになります。
チャネルをクローズすることは、もう値を送信しないことを意味し、チャネルの受け手に完了を伝えるのに便利です。func main() { jobs := make(chan int, 5) done := make(chan bool) go func() { for { j, more := <-jobs if more { fmt.Println("received job", j) } else { fmt.Println("received all jobs") done <- true return } } }() for j := 1; j <= 5; j++ { jobs <- j fmt.Println("sent job", j) } close(jobs) fmt.Println("sent all jobs") <-done }出力
sent job 1 sent job 2 received job 1 received job 2 sent job 3 sent job 4 sent job 5 sent all jobs received job 3 received job 4 received job 5 received all jobsTimers
将来のある時点や一定間隔で繰り返し、ある部分を実行したい際に利用する
タイマーは待ち時間を指定すると、その時間にチャネルが処理を実施します。
例では、5秒経過すると一定の処理を実施します。func main() { start := time.Now() timer1 := time.NewTimer(5 * time.Second) <-timer1.C fmt.Println("It's time!") end := time.Now(); fmt.Printf("%f秒\n",(end.Sub(start)).Seconds()) }出力
It's time! 5.001331秒Ticker
ティッカーは一定間隔で何かを実行した際に使用します。
ティッカーは、ticker.Stop()により停止するとそのチャネルから値を受信しなくなるfunc main() { ticker := time.NewTicker(500 * time.Millisecond) go func() { for t := range ticker.C { fmt.Println("Tick at", t) } }() time.Sleep(1600 * time.Millisecond) ticker.Stop() fmt.Println("Ticker stopped") }出力
Tick at 2019-04-11 15:08:11.070217 +0900 JST m=+0.503669133 Tick at 2019-04-11 15:08:11.571622 +0900 JST m=+1.005080664 Tick at 2019-04-11 15:08:12.072034 +0900 JST m=+1.505498210 Ticker stoppedWorker Pools
workerは3つ分並列実行されます。
5つのジョブが送信されるため5秒分のタスクを実行します。
しかし、worker関数のtime.Sleepで5秒分のタスクを実行する似にかかわらず、2秒しかかかりません。
これは、3つのworkerが並列実行しているためです。func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "started job", j) time.Sleep(time.Second) fmt.Println("worker", id, "finished job", j) results <- j * 2 } } func main() { start := time.Now() jobs := make(chan int, 100) results := make(chan int, 100) for w := 1; w <= 3; w++ { go worker(w, jobs, results) } for j := 1; j <= 5; j++ { jobs <- j } close(jobs) for a := 1; a <= 5; a++ { <-results } end := time.Now(); fmt.Printf("%f秒\n",(end.Sub(start)).Seconds()) }出力
worker 3 started job 1 worker 1 started job 2 worker 2 started job 3 worker 1 finished job 2 worker 3 finished job 1 worker 3 started job 4 worker 1 started job 5 worker 2 finished job 3 worker 3 finished job 4 worker 1 finished job 5 2.006739秒Worker Poolまで試せば、並列処理の速さがわかるはず!5秒の処理が2秒に!!
図とかのイメージはあっているのか...?
参考
- 投稿日:2019-04-11T23:33:30+09:00
Cloud Runをたった3ステップでデプロイしてみた (golang)
Cloud Run とは?
Cloud Run is a managed compute platform that enables you to run stateless containers that are invocable via HTTP requests. Cloud Run is serverless
※ https://cloud.google.com/run/
詳しくは割愛するが、Cloud FunctionsやApp Engineと同じようなサーバーレスで動作するもの。
コンテナをdeployするため、GKEから制御することもできる。
※ https://japan.zdnet.com/article/35135525/デプロイしてみた
ちなみに、動作環境は下記コンテナ内に行う。
https://hub.docker.com/r/google/cloud-sdkstep1. gcloudの各種設定
$ gcloud components update $ gcloud components install beta $ gcloud config set run/region us-central1※ 2019/04/11時点では、Cloud Runはbeta.
step2. アプリケーションコードの作成
$ mkdir helloworld-go && cd helloworld-go $ touch helloworld.go Dockerfilehelloworld.gopackage main import ( "fmt" "log" "net/http" "os" ) func handler(w http.ResponseWriter, r *http.Request) { log.Print("Hello world received a request.") target := os.Getenv("TARGET") if target == "" { target = "World" } fmt.Fprintf(w, "Hello %s!\n", target) } func main() { log.Print("Hello world sample started.") http.HandleFunc("/", handler) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }Dockerfile# Use the offical Golang image to create a build artifact. # This is based on Debian and sets the GOPATH to /go. # https://hub.docker.com/_/golang FROM golang:1.12 as builder # Copy local code to the container image. WORKDIR /go/src/github.com/knative/docs/helloworld COPY . . # Build the command inside the container. # (You may fetch or manage dependencies here, # either manually or with a tool like "godep".) RUN CGO_ENABLED=0 GOOS=linux go build -v -o helloworld # Use a Docker multi-stage build to create a lean production image. # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds FROM alpine # Copy the binary to the production image from the builder stage. COPY --from=builder /go/src/github.com/knative/docs/helloworld/helloworld /helloworld # Run the web service on container startup. CMD ["/helloworld"]step3. 登録&デプロイ
$ gcloud builds submit --tag gcr.io/[PROJECT-ID]/helloworld $ gcloud beta run deploy --image gcr.io/[PROJECT-ID]/helloworld感想
普段私は、個人開発をしているときによくつかっている now.shというServerless Deploymentsを使っている。こちらは、v1のときはdockerコンテナを使えていたのだが、v2になると使えなくなってしまった。ただ、無料で簡単にデプロイできるものを選んでいると、こちらのサービスが最善だと感じていた。
しかし、今回のGoogleCloudNext19の発表で、CloudRunというものをBeta版でリリースされたことを知り、早速使ってみた。
何事もなく、今回の手順を進めて一切失敗することなく、3分以内にデプロイまで進めることができた。
これは、なんて楽で便利なんだと感心してしまった。
また、価格テーブルを見ると、CloudFunctionsのようなリクエストによる従量課金制で、月2百万リクエストまで無料だ。個人開発においては、AppEngineのようなインスタンス起動時間による料金設定よりも、こちらの方が断然オトク。
これはもうnow.shをやめて、こっちに乗り換えるっきゃない!!
- 投稿日:2019-04-11T22:06:20+09:00
Go言語でMongoDBのトランザクションを使う
MongoDBのトランザクションをGo言語(1.12.1)から利用してみました。
Go言語からMongoDBのを使うには、公式のドライバとmgoという人気のあるライブラリを使う方法などがありますが、mgoの方は現時点ではトランザクションに対応していないっぽいので、公式ドライバを使った方法を記載します。
トランザクションに対応したMongoDBを用意する
前に書いた記事で一瞬で作成できます
https://qiita.com/jp_ibis/items/c00b9509fac87abb32c6コレクションを作成しておく
MongoDBにはデータをinsertしたら自動でDBとCollectionを作成してくれる機能があるのですが、トランザクション中には使えないようです。事前にDBとCollectionを用意しておきましょう。
MongoDBのシェルを起動してコマンドでCollectionを作成しておきます。上のリンクの記事のようにDockerでMongoDBを立てていた場合は
docker exec -it コンテナ名 mongoのようにしてシェルを起動してください。MongoDBのシェルで、
use 作成するDB名としてDB名を決めて、
db.createCollection("作成するコレクション名")で、空っぽのコレクションを作っておきます。必要なのはこれだけなので、quitでmongoのシェルを抜けておきます。
ライブラリを用意する
go mod を使うのがナウいのですが、VSCodeで補完させようとすると結局 go get も必要なんですよね(´・ω・`)
go get -u go.mongodb.org/mongo-driver/mongo go get -u go.mongodb.org/mongo-driver/bsonコードを書く
ライブラリのimport部
mongoで接続するときにcontextを使用するので一緒にimportしておきます。
import ( "context" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" )MongoDBへの接続
ApplyURIの引数で接続先を指定します。DockerコンテナのMongoDBを使う場合は、コンテナのIPを指定してください。
// MongoDBへの接続 ctx := context.Background() client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://MongoDBのIPアドレス:ポート番号(省略可)")) if err != nil { panic(err) }セッションを使用する
公式ドライバにはやり方が何通りもあってどれを選べばいいのかわかりにくいですが、UseSessionを使うのが一番スッキリするかな、と。UseSessionの実装の中に
defer defaultSess.EndSession(ctx)というコードがあったので、自分でEndSessionを書く必要は無さそうです。// セッション開始 err = client.UseSession(ctx, func(sc mongo.SessionContext) error { // ここにトランザクションの処理を書く }) if err != nil { panic(err) }トランザクションを使用する
UseSessionのコールバック関数の中でトランザクションの処理を記述します。コールバックの引数でもらえるSessionContext型のコンテキストを使用することでトランザクションが実現できます。
// ここからトランザクション開始 err = sc.StartTransaction() if err != nil { return err } // DBとCollection名はここで指定 db := client.Database("DB名") collection := db.Collection("コレクション名") // 適当にInsert文などを。context型を求める引数にSessionContextを使うのが重要 _, err = collection.InsertOne(sc, bson.M{"キー": 値}) if err != nil { // EndSession内でAbortTransactionが呼ばれるので、ここで無理に呼ぶ必要はない。 sc.AbortTransaction(sc) return err } // トランザクション中の処理を確定 return sc.CommitTransaction(sc)まとめたサンプル
package main import ( "context" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func main() { // MongoDBへの接続 ctx := context.Background() client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://172.17.0.2:27017")) if err != nil { panic(err) } // セッション開始 err = client.UseSession(ctx, func(sc mongo.SessionContext) error { // ここからトランザクション開始 err = sc.StartTransaction() if err != nil { return err } // DBとCollection名はここで指定 db := client.Database("my_test_db") collection := db.Collection("my_test_col") // 適当にInsert文などをば。引数にSessionContextを入れているのが重要 _, err = collection.InsertOne(sc, bson.M{"number": "abc"}) if err != nil { // EndSession内でAbortTransactionが呼ばれるので、ここで無理に呼ぶ必要はない。 sc.AbortTransaction(sc) return err } // トランザクション中の処理を確定 return sc.CommitTransaction(sc) }) if err != nil { panic(err) } }おしまい
セッションのところがコールバックでちょっとモヤっとしたけど、JavaScriptみたいに非同期ではないのでまだ使いやすいです。あ、非同期にしたいときはUseSessionのあたりをgoルーチンにするだけです。
- 投稿日:2019-04-11T19:44:10+09:00
GORM でインデックスプリフィクス長を指定した CREATE INDEX 文を発行する
GORM では以下のように struct を元にテーブルを作成することができる。
func createTable(db *gorm.DB, table interface{}) error { if err := db.CreateTable(table).Error; err != nil { return err } return nil } func createBookTable(db *gorm.DB) error { if err := createTable(db, &Book{}); err != nil { return err } return nil } type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text"` }これを実行すると、以下の SQL が発行される。
CREATE TABLE `books` (`id` int AUTO_INCREMENT,`name` varchar(255),`description` text , PRIMARY KEY (`id`)) CREATE INDEX idx_books_name ON `books`(`name`)
text型であるdescriptionにも index を設定したい場合、通常では最大バイト数制限に引っかかる。type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text;index:idx_books_description"` }CREATE TABLE `books` (`id` int AUTO_INCREMENT,`name` varchar(255),`description` text , PRIMARY KEY (`id`)) CREATE INDEX idx_books_name ON `books`(`name`) CREATE INDEX idx_books_description ON `books`(`description`) Error 1170: BLOB/TEXT column 'description' used in key specification without a key length以下のようにインデックスプリフィクス長を指定すると先頭部分のみを使用するインデックスを作成できるが、GORM でどのようにこの SQL 文を発行できるだろうか。
(ref: MySQL 5.6 リファレンスマニュアル 13.1.13 CREATE INDEX 構文)CREATE INDEX `idx_books_description` ON `books`(description(100))struct のタグに指定する
この方法では設定はできないようだ。
type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text;index:idx_books_description(100)"` }このような不正な SQL として作成されてしまう。
CREATE INDEX idx_books_description(100) ON `books`(`description`) Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(100) ON `books`(`description`)' at line 1別途
AddIndex()を呼ぶ一応これでは実現できる。
func createBookTable(db *gorm.DB) error { table := &Book{} if err := createTable(db, table); err != nil { return err } if err := db.Model(table).AddIndex("idx_books_description", "description(100)").Error; err != nil { return err } return nil } type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text"` }…が、struct だけを見てもどのカラムにインデックスが設定されているのか分からず、これではあまり嬉しくない。
独自タグを定義し、別途タグからインデックス情報を読み取る
addExtraIndex()を呼ぶこのような関数を作成する。
func addExtraIndex(db *gorm.DB, table interface{}) error { scope := db.NewScope(table) indices := make(map[string][]ExtraIndex) for _, field := range scope.Fields() { tag, ok := field.TagSettingsGet("EXTRAINDEX") if !ok { continue } extraIndex := strings.Split(tag, ",") if len(extraIndex) != 2 { return errors.Errorf("unsupported extraindex: %s", tag) } name := extraIndex[0] if name == "" { name = fmt.Sprintf("idx_%s_%s", scope.TableName(), field.DBName) } length, err := strconv.Atoi(extraIndex[1]) if err != nil { return errors.Wrapf(err, "invalid extraindex size: %s", tag) } indices[name] = append(indices[name], ExtraIndex{column: field.DBName, length: length}) } for name, index := range indices { columns := make([]string, len(index)) for i, idx := range index { columns[i] = fmt.Sprintf("%s(%d)", idx.column, idx.length) } if err := db.Model(table).AddIndex(name, strings.Join(columns, ",")).Error; err != nil { return err } } return nil } type ExtraIndex struct { column string length int } func createBookTable(db *gorm.DB) error { table := &Book{} if err := createTable(db, table); err != nil { return err } if err := addExtraIndex(db, table); err != nil { return err } return nil }struct にタグを追加する。
type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text;extraindex:idx_books_description,100"` }この
addExtraIndex()を別途呼ぶ事で、追加で CREATE INDEX 文を発行できる。CREATE INDEX idx_books_description ON `books`(description(100))インデックス名を省略するとテーブル名とカラム名から自動で設定される。
type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text;extraindex:,100"` }CREATE INDEX idx_books_description ON `books`(description(100))複合インデックスにも対応。
type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description1 string `gorm:"type:text;extraindex:idx_books_descriptions,100"` Description2 string `gorm:"type:text;extraindex:idx_books_descriptions,100"` }CREATE INDEX idx_books_descriptions ON `books`(description1(100),description2(100)結局
db.CreateTable()とは別に関数を呼ばないといけないのは変わらないが、
struct のタグにDBテーブルのメタ情報が集約されるという点では少しはマシかもしれない。タグ名を
gorm:に相乗りしないようにする
gorm:タグに追加すればfield.TagSettingsGet()でタグの内容が取れるので楽をするためにこれを利用したが、
本来であれば別のタグにしておいたほうが丁寧だとは思う。
タグの取得はreflectパッケージを使用して頑張って取得する事になるが。type Book struct { ID int Name string `gorm:"index:idx_books_name"` Description string `gorm:"type:text" extraindex:"name:idx_books_description;length:100"` }
- 投稿日:2019-04-11T16:56:36+09:00
Prisma
Prisma.ioとは?
- SQLサーバにGraphQLを生やします。
- 今の所、MySQL、PostgreSQL、MongoDBに対応。
- PrismaサーバはDockerコンテナとして起動。
- GraphQLなので、クライアントはHTTPが使えればPrismaサーバを操作できる。
- Prismaサーバの操作をしやすくするPrismaクライアント(GO, TypeScript, JavaScript)を自動生成できる。
- Prisma Adminでブラウザからデータベースの照会、更新などができる。
雑なまとめなので、公式サイトを見てもらったほうがいいと思います。
また、以下の記事がすごく参考になりました。ありがとうございます。
prisma - 最速 GraphQL Server実装 - Qiita
Prisma.ioでGraphQL APIサーバーを楽して作る - Qiita構成
PrismaサーバはSQLサーバのCRUD全てが出来てしまうので、そのまま公開するのは危険。
なので、アプリケーション/APIサーバの層を追加します。この層はPrismaクライアントを使って自前で作るので、APIはGraphQLでなくてもOK。
(REST,gRPCとか)
Database
MySQL。Dockerで構築。prisma initで自動的に作ってくれる。Data Access Layer(Prisma)
Dockerで構築。prisma initで自動的に作ってくれる。Application / API Service
今回はGoで作成。 GraphQLサーバのフレームワークはgqlgenを使用。
最初はホストで直接起動。後からDocker化。Client(ブラウザ)
gqlgenでGraphQL Playgroundを追加できるので、そこから操作。
自分はGraphiQLも使ってます。基本的に公式のチュートリアルに沿って進めますが、所々アレンジ入れてます。
完成後のソースはこちらです。
環境一覧
試した時の環境です。
環境やバージョンが違っても動くとは思います。
- macOS Mojave
- Docker version 18.09.2, build 6247962
- docker-compose version 1.21.2, build a133471
- Node.js v8.15.1 ※prismaインストールのため
公式ではbrewも書いてありますが、途中で以下のエラーに遭遇したので、
npmで入れ直しました。brew tap prisma/prisma brew install prisma Error: Cannot find module 'generate'Step1 Set up Prisma
Prisma インストール
$ npm install -g prisma $ prisma -v prisma/1.30.0 (darwin-x64) node-v8.15.1Prisma init
構築は
GOHOMEのディレクトリ下で構築します。(例: ~/go/src/prisma-hello-world)まず、prismaコマンドで土台を作ります。
途中質問がくるので、以下を選択。
- Create new database
- MySQL
- Go
$ cd ~/go/src $ prisma init prisma-hello-world ? Set up a new Prisma server or deploy to an existing server? Create new database ? What kind of database do you want to deploy to? MySQL ? Select the programming language for the generated Prisma client Prisma Go Client Created 3 new files: prisma.yml Prisma service definition datamodel.prisma GraphQL SDL-based datamodel (foundation for database) docker-compose.yml Docker configuration file Next steps: 1. Open folder: cd prisma-hello-world 2. Start your Prisma server: docker-compose up -d 3. Deploy your Prisma service: prisma deploy 4. Read more about Prisma server: http://bit.ly/prisma-server-overview実行が終わるとファイルがいくつか作成されています。
まず、DBサーバとPrismaサーバを起動するdocker-composeファイル。
docker-compose.ymlversion: '3' services: prisma: image: prismagraphql/prisma:1.30 restart: always ports: - "4466:4466" environment: PRISMA_CONFIG: | port: 4466 # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security # managementApiSecret: my-secret databases: default: connector: mysql host: mysql user: root password: prisma rawAccess: true port: 3306 migrations: true mysql: image: mysql:5.7 restart: always environment: MYSQL_ROOT_PASSWORD: prisma volumes: - mysql:/var/lib/mysql volumes: mysql:データモデル定義のパスや、Prismaクライアントの出力先とかの設定ファイル。
prisma.ymlendpoint: http://localhost:4466 datamodel: datamodel.prisma generate: - generator: go-client output: ./generated/prisma-client/データモデル定義。
datamodel.prismatype User { id: ID! @unique name: String! }起動・デプロイ
まずは初期状態で起動してみます。
$ docker-compose up -d $ prisma deploy以下にアクセスすると、PrismaサーバのGraphQL Playgroundが開きます。
以下のアドレスにアクセスすると、データ管理が出来るPrisma Adminが開きます。
Goクライアントを作成
データが空っぽなので、Goクライアントを実装してデータを追加してみます。
まず、GO MODULESを初期化します。
$ export GO111MODLUE=on $ go initPrismaクライアントを使ってデータを登録するソースを作成します。
index.gopackage main import ( "context" "fmt" prisma "prisma-hello-world/generated/prisma-client" ) func main() { client := prisma.New(nil) ctx := context.TODO() // Create a new user name := "Alice" newUser, err := client.CreateUser(prisma.UserCreateInput{ Name: name, }).Exec(ctx) if err != nil { panic(err) } fmt.Printf("Created new user: %+v\n", newUser) users, err := client.Users(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", users) }実行すると、1件データが登録されます。
IDはcuidに基づいて自動的に振られます。$ go run index.go Created new user: &{ID:cjuc0tk8f001l07165y3waxtt Name:Alice} [{ID:cjuc0tk8f001l07165y3waxtt Name:Alice}]Step2 データモデルの変更
データモデルに項目を追加します。
datamodel.prismatype User { id: ID! @unique email: String @unique name: String! posts: [Post!]! } type Post { id: ID! @unique title: String! published: Boolean! @default(value: "false") author: User }デプロイとPrismaクライアントの更新をします。
$ prisma deploy $ prisma generate
generated/prisma-client/prisma.goに新しいAPIが追加されましたので、これを使ってデータを登録するソースを作成します。index.gopackage main import ( "context" "fmt" prisma "prisma-hello-world/generated/prisma-client" ) func main() { client := prisma.New(nil) ctx := context.TODO() // Create a new user with two posts name := "Bob" email := "bob@prisma.io" title1 := "Join us for GraphQL Conf in 2019" title2 := "Subscribe to GraphQL Weekly for GraphQL news" newUser, err := client.CreateUser(prisma.UserCreateInput{ Name: name, Email: &email, Posts: &prisma.PostCreateManyWithoutAuthorInput{ Create: []prisma.PostCreateWithoutAuthorInput{ prisma.PostCreateWithoutAuthorInput{ Title: title1, }, prisma.PostCreateWithoutAuthorInput{ Title: title2, }, }, }, }).Exec(ctx) if err != nil { panic(err) } fmt.Printf("Created new user: %+v\n", newUser) allUsers, err := client.Users(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", allUsers) allPosts, err := client.Posts(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", allPosts) }実行すると、新しいユーザ
Bobと記事が2件追加されます。$ go run index.go Created new user: &{ID:cjuc1dwo1002207164z8feea9 Email:0xc000093520 Name:Bob} [{ID:cjuc0tk8f001l07165y3waxtt Email:<nil> Name:Alice} {ID:cjuc1dwo1002207164z8feea9 Email:0xc000146320 Name:Bob}] [{ID:cjuc1dwp4002307162oty6fva Title:Join us for GraphQL Conf in 2019 Published:false} {ID:cjuc1dwpv00250716kvvo5xab Title:Subscribe to GraphQL Weekly for GraphQL news Published:false}]次に、登録した記事をemailを指定して検索してみます。
index.gopackage main import ( "context" "fmt" prisma "prisma-hello-world/generated/prisma-client" ) func main() { client := prisma.New(nil) ctx := context.TODO() email := "bob@prisma.io" postsByUser, err := client.User(prisma.UserWhereUniqueInput{ Email: &email, }).Posts(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", postsByUser) }$ go run index.go [{ID:cjuc1dwp4002307162oty6fva Title:Join us for GraphQL Conf in 2019 Published:false} {ID:cjuc1dwpv00250716kvvo5xab Title:Subscribe to GraphQL Weekly for GraphQL news Published:false}]Step3 Build an App
次に、公開用のGraphQLサーバを作っていきます。
まず、gqlgenのパッケージを登録し、初期構築コマンドを入力します。
$ go get github.com/99designs/gqlgen $ go run github.com/99designs/gqlgen init実行すると以下のファイルが出来ます。
gqlgen.yml
gqlgenの設定。自動生成コードの出力先とかを設定。schema.graphql
公開するGrapQLのスキーマ。この定義からコードが自動生成される。generated.go
gqlgenで自動生成されるコード。
自動生成するので、一旦削除。models_gen.go
prisma-clientで作成された構造体を使うので不要。削除。resolver.go
GraphQLのリゾルバ。自分で作る必要があるが、必要な関数などのテンプレートは自動生成してくれる。
自動生成するので、一旦削除。server/server.go
GraphQLサーバ起動のコードごちゃごちゃして来たので、フォルダを作って整理します。
- gqlgen/
- gqlgen.yml
- schema.graphql
- server/server.go
次に、
gqlgenの設定をPrismaに合わせて書き換えます。gqlgen.ymlschema: schema.graphql exec: filename: generated.go models: Post: model: prisma-hello-world/generated/prisma-client.Post User: model: prisma-hello-world/generated/prisma-client.User resolver: filename: resolver.go type: Resolver公開するGraphQLのスキーマを作成します。
schema.graphqltype Query { publishedPosts: [Post!]! post(postId: ID!): Post postsByUser(userId: ID!): [Post!]! } type Mutation { createUser(name: String!): User createDraft(title: String!, userId: ID!): Post publish(postId: ID!): Post } type User { id: ID! email: String name: String! posts: [Post!]! } type Post { id: ID! title: String! published: Boolean! author: User }ファイルが出来たら、
gqlgenを実行してソースを自動生成します。$ cd gqlgen $ go run github.com/99designs/gqlgen以下のファイルが出来ました。
- gqlgen/
- generated.go
- resolver.go
自動生成されたGraphQLのリゾルバは枠しかないので、Prismaクライアントを使って実装していきます。
gqlgen/resolver.go//go:generate go run github.com/99designs/gqlgen package gqlgen import ( "context" "prisma-hello-world/generated/prisma-client" ) type Resolver struct { Prisma *prisma.Client } func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } func (r *Resolver) Post() PostResolver { return &postResolver{r} } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } func (r *Resolver) User() UserResolver { return &userResolver{r} } type mutationResolver struct{ *Resolver } func (r *mutationResolver) CreateUser(ctx context.Context, name string) (*prisma.User, error) { return r.Prisma.CreateUser(prisma.UserCreateInput{ Name: name, }).Exec(ctx) } func (r *mutationResolver) CreateDraft(ctx context.Context, title string, userId string) (*prisma.Post, error) { return r.Prisma.CreatePost(prisma.PostCreateInput{ Title: title, Author: &prisma.UserCreateOneWithoutPostsInput{ Connect: &prisma.UserWhereUniqueInput{ID: &userId}, }, }).Exec(ctx) } func (r *mutationResolver) Publish(ctx context.Context, postId string) (*prisma.Post, error) { published := true return r.Prisma.UpdatePost(prisma.PostUpdateParams{ Where: prisma.PostWhereUniqueInput{ID: &postId}, Data: prisma.PostUpdateInput{Published: &published}, }).Exec(ctx) } type postResolver struct{ *Resolver } func (r *postResolver) Author(ctx context.Context, obj *prisma.Post) (*prisma.User, error) { return r.Prisma.Post(prisma.PostWhereUniqueInput{ID: &obj.ID}).Author().Exec(ctx) } type queryResolver struct{ *Resolver } func (r *queryResolver) PublishedPosts(ctx context.Context) ([]prisma.Post, error) { published := true return r.Prisma.Posts(&prisma.PostsParams{ Where: &prisma.PostWhereInput{Published: &published}, }).Exec(ctx) } func (r *queryResolver) Post(ctx context.Context, postId string) (*prisma.Post, error) { return r.Prisma.Post(prisma.PostWhereUniqueInput{ID: &postId}).Exec(ctx) } func (r *queryResolver) PostsByUser(ctx context.Context, userId string) ([]prisma.Post, error) { return r.Prisma.Posts(&prisma.PostsParams{ Where: &prisma.PostWhereInput{ Author: &prisma.UserWhereInput{ ID: &userId, }}, }).Exec(ctx) } type userResolver struct{ *Resolver } func (r *userResolver) Posts(ctx context.Context, obj *prisma.User) ([]prisma.Post, error) { return r.Prisma.User(prisma.UserWhereUniqueInput{ID: &obj.ID}).Posts(nil).Exec(ctx) }先頭の
//go:generate go run github.com/99designs/gqlgenは、go generateコマンドでgqlgenを実行するためのコメントです。
スキーマを修正したら、以下のコマンドでコードを更新できます。例$ go generate gqlgen/resolver.go次に、アプリ起動部分を作成します。
server/server.gopackage main import ( "log" "net/http" "os" prisma "prisma-hello-world/generated/prisma-client" "prisma-hello-world/gqlgen" "github.com/99designs/gqlgen/handler" ) const defaultPort = "4000" func main() { port := os.Getenv("PORT") if len(port) == 0 { port = defaultPort } client := prisma.New(nil) resolver := gqlgen.Resolver{ Prisma: client, } http.Handle("/", handler.Playground("GraphQL Playground", "/query")) http.Handle("/query", handler.GraphQL(gqlgen.NewExecutableSchema( gqlgen.Config{Resolvers: &resolver}))) log.Printf("Server is running on http://localhost:%s", port) err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal(err) } }できたら、起動してみます。
$ go run server/server.go以下にアクセスすると、GraphQL Playgroundが開きます。
ためしに、ユーザや記事を追加してみます。
query-ユーザ追加mutation { createUser(name: "otanu") { id name } }結果{ "data": { "createUser": { "id": "cjuc3vysh000d0744f8n94vw4", "name": "otanu" } } }query-記事追加mutation { createDraft(title: "テスト", userId: "cjuc3vysh000d0744f8n94vw4") { id title published author { id name } } }結果{ "data": { "createDraft": { "id": "cjuc42km0000j07441ucjddnd", "title": "テスト", "published": false, "author": { "id": "cjuc3vysh000d0744f8n94vw4", "name": "otanu" } } } }query-公開mutation { publish(postId: "cjuc42km0000j07441ucjddnd") { id title published author { name } } }結果{ "data": { "publish": { "id": "cjuc42km0000j07441ucjddnd", "title": "テスト", "published": true, "author": { "name": "otanu" } } } }query-公開記事検索query { publishedPosts { id title } }結果{ "data": { "publishedPosts": [ { "id": "cjuc42km0000j07441ucjddnd", "title": "テスト" } ] } }アプリのDocker化
アプリもDockerComposeでまとめて起動できるように、Docker化していきます。
アプリをDocker化すると、prismaサーバへの接続がlocalhostでは繋がらななくなるので、環境変数
ENDPOINTを追加して、エンドポイントを変更できるようにします。server/server.gopackage main import ( "log" "net/http" "os" prisma "prisma-hello-world/generated/prisma-client" "prisma-hello-world/gqlgen" "github.com/99designs/gqlgen/handler" ) const defaultPort = "4000" func main() { port := os.Getenv("PORT") if len(port) == 0 { port = defaultPort } // 追加 var opt *prisma.Options endpoint := os.Getenv("ENDPOINT") if len(endpoint) != 0 { opt = &prisma.Options{ Endpoint: endpoint, } } client := prisma.New(opt) resolver := gqlgen.Resolver{ Prisma: client, } http.Handle("/", handler.Playground("GraphQL Playground", "/query")) http.Handle("/query", handler.GraphQL(gqlgen.NewExecutableSchema( gqlgen.Config{Resolvers: &resolver}))) log.Printf("Server is running on http://localhost:%s", port) err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal(err) } }次にDockerfileを準備します。
ついでにfreshでホットリロードも追加。DockerfileFROM golang:1.11-alpine AS build_base RUN apk add bash ca-certificates git gcc g++ libc-dev WORKDIR /app COPY go.mod . COPY go.sum . RUN go mod download RUN go get github.com/pilu/fresh COPY . . EXPOSE 4000 CMD cd server; fresh server.goDockerComposeにアプリの設定を追加。
これで、まとめて起動できるようになりました。docker-compose.ymlversion: '3' services: prisma: image: prismagraphql/prisma:1.30 restart: always ports: - "4466:4466" environment: PRISMA_CONFIG: | port: 4466 # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security # managementApiSecret: my-secret databases: default: connector: mysql host: mysql user: root password: prisma rawAccess: true port: 3306 migrations: true mysql: image: mysql:5.7 restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: prisma volumes: - mysql:/var/lib/mysql app: build: context: . dockerfile: ./Dockerfile ports: - "4000:4000" volumes: - .:/app depends_on: - prisma environment: ENDPOINT: http://prisma:4466 volumes: mysql:
- 投稿日:2019-04-11T16:56:36+09:00
Prisma.io + Go のチュートリアル
Prisma.ioとは?
- SQLサーバにGraphQLを生やします。
- 今の所、MySQL、PostgreSQL、MongoDBに対応。
- PrismaサーバはDockerコンテナとして起動。
- GraphQLなので、クライアントはHTTPが使えればPrismaサーバを操作できる。
- Prismaサーバの操作をしやすくするPrismaクライアント(GO, TypeScript, JavaScript)を自動生成できる。
- Prisma Adminでブラウザからデータベースの照会、更新などができる。
雑なまとめなので、公式サイトを見てもらったほうがいいと思います。
また、以下の記事がすごく参考になりました。ありがとうございます。
prisma - 最速 GraphQL Server実装 - Qiita
Prisma.ioでGraphQL APIサーバーを楽して作る - Qiita構成
PrismaサーバはSQLサーバのCRUD全てが出来てしまうので、そのまま公開するのは危険。
なので、アプリケーション/APIサーバの層を追加します。この層はPrismaクライアントを使って自前で作るので、APIはGraphQLでなくてもOK。
(REST,gRPCとか)
Database
MySQL。Dockerで構築。prisma initで自動的に作ってくれる。Data Access Layer(Prisma)
Dockerで構築。prisma initで自動的に作ってくれる。Application / API Service
今回はGoで作成。 GraphQLサーバのフレームワークはgqlgenを使用。
最初はホストで直接起動。後からDocker化。Client(ブラウザ)
gqlgenでGraphQL Playgroundを追加できるので、そこから操作。
自分はGraphiQLも使ってます。基本的に公式のチュートリアルに沿って進めますが、所々アレンジ入れてます。
完成後のソースはこちらです。
環境一覧
試した時の環境です。
環境やバージョンが違っても動くとは思います。
- macOS Mojave
- Docker version 18.09.2, build 6247962
- docker-compose version 1.21.2, build a133471
- Node.js v8.15.1 ※prismaインストールのため
公式ではbrewも書いてありますが、途中で以下のエラーに遭遇したので、
npmで入れ直しました。brew tap prisma/prisma brew install prisma Error: Cannot find module 'generate'Step1 Set up Prisma
Prisma インストール
$ npm install -g prisma $ prisma -v prisma/1.30.0 (darwin-x64) node-v8.15.1Prisma init
構築は
GOHOMEのディレクトリ下で構築します。(例: ~/go/src/prisma-hello-world)まず、prismaコマンドで土台を作ります。
途中質問がくるので、以下を選択。
- Create new database
- MySQL
- Go
$ cd ~/go/src $ prisma init prisma-hello-world ? Set up a new Prisma server or deploy to an existing server? Create new database ? What kind of database do you want to deploy to? MySQL ? Select the programming language for the generated Prisma client Prisma Go Client Created 3 new files: prisma.yml Prisma service definition datamodel.prisma GraphQL SDL-based datamodel (foundation for database) docker-compose.yml Docker configuration file Next steps: 1. Open folder: cd prisma-hello-world 2. Start your Prisma server: docker-compose up -d 3. Deploy your Prisma service: prisma deploy 4. Read more about Prisma server: http://bit.ly/prisma-server-overview実行が終わるとファイルがいくつか作成されています。
まず、DBサーバとPrismaサーバを起動するdocker-composeファイル。
docker-compose.ymlversion: '3' services: prisma: image: prismagraphql/prisma:1.30 restart: always ports: - "4466:4466" environment: PRISMA_CONFIG: | port: 4466 # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security # managementApiSecret: my-secret databases: default: connector: mysql host: mysql user: root password: prisma rawAccess: true port: 3306 migrations: true mysql: image: mysql:5.7 restart: always environment: MYSQL_ROOT_PASSWORD: prisma volumes: - mysql:/var/lib/mysql volumes: mysql:データモデル定義のパスや、Prismaクライアントの出力先とかの設定ファイル。
prisma.ymlendpoint: http://localhost:4466 datamodel: datamodel.prisma generate: - generator: go-client output: ./generated/prisma-client/データモデル定義。
datamodel.prismatype User { id: ID! @unique name: String! }起動・デプロイ
まずは初期状態で起動してみます。
$ docker-compose up -d $ prisma deploy以下にアクセスすると、PrismaサーバのGraphQL Playgroundが開きます。
以下のアドレスにアクセスすると、データ管理が出来るPrisma Adminが開きます。
Goクライアントを作成
データが空っぽなので、Goクライアントを実装してデータを追加してみます。
まず、GO MODULESを初期化します。
$ export GO111MODLUE=on $ go initPrismaクライアントを使ってデータを登録するソースを作成します。
index.gopackage main import ( "context" "fmt" prisma "prisma-hello-world/generated/prisma-client" ) func main() { client := prisma.New(nil) ctx := context.TODO() // Create a new user name := "Alice" newUser, err := client.CreateUser(prisma.UserCreateInput{ Name: name, }).Exec(ctx) if err != nil { panic(err) } fmt.Printf("Created new user: %+v\n", newUser) users, err := client.Users(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", users) }実行すると、1件データが登録されます。
IDはcuidに基づいて自動的に振られます。$ go run index.go Created new user: &{ID:cjuc0tk8f001l07165y3waxtt Name:Alice} [{ID:cjuc0tk8f001l07165y3waxtt Name:Alice}]Step2 データモデルの変更
データモデルに項目を追加します。
datamodel.prismatype User { id: ID! @unique email: String @unique name: String! posts: [Post!]! } type Post { id: ID! @unique title: String! published: Boolean! @default(value: "false") author: User }デプロイとPrismaクライアントの更新をします。
$ prisma deploy $ prisma generate
generated/prisma-client/prisma.goに新しいAPIが追加されましたので、これを使ってデータを登録するソースを作成します。index.gopackage main import ( "context" "fmt" prisma "prisma-hello-world/generated/prisma-client" ) func main() { client := prisma.New(nil) ctx := context.TODO() // Create a new user with two posts name := "Bob" email := "bob@prisma.io" title1 := "Join us for GraphQL Conf in 2019" title2 := "Subscribe to GraphQL Weekly for GraphQL news" newUser, err := client.CreateUser(prisma.UserCreateInput{ Name: name, Email: &email, Posts: &prisma.PostCreateManyWithoutAuthorInput{ Create: []prisma.PostCreateWithoutAuthorInput{ prisma.PostCreateWithoutAuthorInput{ Title: title1, }, prisma.PostCreateWithoutAuthorInput{ Title: title2, }, }, }, }).Exec(ctx) if err != nil { panic(err) } fmt.Printf("Created new user: %+v\n", newUser) allUsers, err := client.Users(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", allUsers) allPosts, err := client.Posts(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", allPosts) }実行すると、新しいユーザ
Bobと記事が2件追加されます。$ go run index.go Created new user: &{ID:cjuc1dwo1002207164z8feea9 Email:0xc000093520 Name:Bob} [{ID:cjuc0tk8f001l07165y3waxtt Email:<nil> Name:Alice} {ID:cjuc1dwo1002207164z8feea9 Email:0xc000146320 Name:Bob}] [{ID:cjuc1dwp4002307162oty6fva Title:Join us for GraphQL Conf in 2019 Published:false} {ID:cjuc1dwpv00250716kvvo5xab Title:Subscribe to GraphQL Weekly for GraphQL news Published:false}]次に、登録した記事をemailを指定して検索してみます。
index.gopackage main import ( "context" "fmt" prisma "prisma-hello-world/generated/prisma-client" ) func main() { client := prisma.New(nil) ctx := context.TODO() email := "bob@prisma.io" postsByUser, err := client.User(prisma.UserWhereUniqueInput{ Email: &email, }).Posts(nil).Exec(ctx) if err != nil { panic(err) } fmt.Printf("%+v\n", postsByUser) }$ go run index.go [{ID:cjuc1dwp4002307162oty6fva Title:Join us for GraphQL Conf in 2019 Published:false} {ID:cjuc1dwpv00250716kvvo5xab Title:Subscribe to GraphQL Weekly for GraphQL news Published:false}]Step3 Build an App
次に、公開用のGraphQLサーバを作っていきます。
まず、gqlgenのパッケージを登録し、初期構築コマンドを入力します。
$ go get github.com/99designs/gqlgen $ go run github.com/99designs/gqlgen init実行すると以下のファイルが出来ます。
gqlgen.yml
gqlgenの設定。自動生成コードの出力先とかを設定。schema.graphql
公開するGrapQLのスキーマ。この定義からコードが自動生成される。generated.go
gqlgenで自動生成されるコード。
自動生成するので、一旦削除。models_gen.go
prisma-clientで作成された構造体を使うので不要。削除。resolver.go
GraphQLのリゾルバ。自分で作る必要があるが、必要な関数などのテンプレートは自動生成してくれる。
自動生成するので、一旦削除。server/server.go
GraphQLサーバ起動のコードごちゃごちゃして来たので、フォルダを作って整理します。
- gqlgen/
- gqlgen.yml
- schema.graphql
- server/server.go
次に、
gqlgenの設定をPrismaに合わせて書き換えます。gqlgen.ymlschema: schema.graphql exec: filename: generated.go models: Post: model: prisma-hello-world/generated/prisma-client.Post User: model: prisma-hello-world/generated/prisma-client.User resolver: filename: resolver.go type: Resolver公開するGraphQLのスキーマを作成します。
schema.graphqltype Query { publishedPosts: [Post!]! post(postId: ID!): Post postsByUser(userId: ID!): [Post!]! } type Mutation { createUser(name: String!): User createDraft(title: String!, userId: ID!): Post publish(postId: ID!): Post } type User { id: ID! email: String name: String! posts: [Post!]! } type Post { id: ID! title: String! published: Boolean! author: User }ファイルが出来たら、
gqlgenを実行してソースを自動生成します。$ cd gqlgen $ go run github.com/99designs/gqlgen以下のファイルが出来ました。
- gqlgen/
- generated.go
- resolver.go
自動生成されたGraphQLのリゾルバは枠しかないので、Prismaクライアントを使って実装していきます。
gqlgen/resolver.go//go:generate go run github.com/99designs/gqlgen package gqlgen import ( "context" "prisma-hello-world/generated/prisma-client" ) type Resolver struct { Prisma *prisma.Client } func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } func (r *Resolver) Post() PostResolver { return &postResolver{r} } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } func (r *Resolver) User() UserResolver { return &userResolver{r} } type mutationResolver struct{ *Resolver } func (r *mutationResolver) CreateUser(ctx context.Context, name string) (*prisma.User, error) { return r.Prisma.CreateUser(prisma.UserCreateInput{ Name: name, }).Exec(ctx) } func (r *mutationResolver) CreateDraft(ctx context.Context, title string, userId string) (*prisma.Post, error) { return r.Prisma.CreatePost(prisma.PostCreateInput{ Title: title, Author: &prisma.UserCreateOneWithoutPostsInput{ Connect: &prisma.UserWhereUniqueInput{ID: &userId}, }, }).Exec(ctx) } func (r *mutationResolver) Publish(ctx context.Context, postId string) (*prisma.Post, error) { published := true return r.Prisma.UpdatePost(prisma.PostUpdateParams{ Where: prisma.PostWhereUniqueInput{ID: &postId}, Data: prisma.PostUpdateInput{Published: &published}, }).Exec(ctx) } type postResolver struct{ *Resolver } func (r *postResolver) Author(ctx context.Context, obj *prisma.Post) (*prisma.User, error) { return r.Prisma.Post(prisma.PostWhereUniqueInput{ID: &obj.ID}).Author().Exec(ctx) } type queryResolver struct{ *Resolver } func (r *queryResolver) PublishedPosts(ctx context.Context) ([]prisma.Post, error) { published := true return r.Prisma.Posts(&prisma.PostsParams{ Where: &prisma.PostWhereInput{Published: &published}, }).Exec(ctx) } func (r *queryResolver) Post(ctx context.Context, postId string) (*prisma.Post, error) { return r.Prisma.Post(prisma.PostWhereUniqueInput{ID: &postId}).Exec(ctx) } func (r *queryResolver) PostsByUser(ctx context.Context, userId string) ([]prisma.Post, error) { return r.Prisma.Posts(&prisma.PostsParams{ Where: &prisma.PostWhereInput{ Author: &prisma.UserWhereInput{ ID: &userId, }}, }).Exec(ctx) } type userResolver struct{ *Resolver } func (r *userResolver) Posts(ctx context.Context, obj *prisma.User) ([]prisma.Post, error) { return r.Prisma.User(prisma.UserWhereUniqueInput{ID: &obj.ID}).Posts(nil).Exec(ctx) }先頭の
//go:generate go run github.com/99designs/gqlgenは、go generateコマンドでgqlgenを実行するためのコメントです。
スキーマを修正したら、以下のコマンドでコードを更新できます。例$ go generate gqlgen/resolver.go次に、アプリ起動部分を作成します。
server/server.gopackage main import ( "log" "net/http" "os" prisma "prisma-hello-world/generated/prisma-client" "prisma-hello-world/gqlgen" "github.com/99designs/gqlgen/handler" ) const defaultPort = "4000" func main() { port := os.Getenv("PORT") if len(port) == 0 { port = defaultPort } client := prisma.New(nil) resolver := gqlgen.Resolver{ Prisma: client, } http.Handle("/", handler.Playground("GraphQL Playground", "/query")) http.Handle("/query", handler.GraphQL(gqlgen.NewExecutableSchema( gqlgen.Config{Resolvers: &resolver}))) log.Printf("Server is running on http://localhost:%s", port) err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal(err) } }できたら、起動してみます。
$ go run server/server.go以下にアクセスすると、GraphQL Playgroundが開きます。
ためしに、ユーザや記事を追加してみます。
query-ユーザ追加mutation { createUser(name: "otanu") { id name } }結果{ "data": { "createUser": { "id": "cjuc3vysh000d0744f8n94vw4", "name": "otanu" } } }query-記事追加mutation { createDraft(title: "テスト", userId: "cjuc3vysh000d0744f8n94vw4") { id title published author { id name } } }結果{ "data": { "createDraft": { "id": "cjuc42km0000j07441ucjddnd", "title": "テスト", "published": false, "author": { "id": "cjuc3vysh000d0744f8n94vw4", "name": "otanu" } } } }query-公開mutation { publish(postId: "cjuc42km0000j07441ucjddnd") { id title published author { name } } }結果{ "data": { "publish": { "id": "cjuc42km0000j07441ucjddnd", "title": "テスト", "published": true, "author": { "name": "otanu" } } } }query-公開記事検索query { publishedPosts { id title } }結果{ "data": { "publishedPosts": [ { "id": "cjuc42km0000j07441ucjddnd", "title": "テスト" } ] } }アプリのDocker化
アプリもDockerComposeでまとめて起動できるように、Docker化していきます。
アプリをDocker化すると、prismaサーバへの接続がlocalhostでは繋がらななくなるので、環境変数
ENDPOINTを追加して、エンドポイントを変更できるようにします。server/server.gopackage main import ( "log" "net/http" "os" prisma "prisma-hello-world/generated/prisma-client" "prisma-hello-world/gqlgen" "github.com/99designs/gqlgen/handler" ) const defaultPort = "4000" func main() { port := os.Getenv("PORT") if len(port) == 0 { port = defaultPort } // 追加 var opt *prisma.Options endpoint := os.Getenv("ENDPOINT") if len(endpoint) != 0 { opt = &prisma.Options{ Endpoint: endpoint, } } client := prisma.New(opt) resolver := gqlgen.Resolver{ Prisma: client, } http.Handle("/", handler.Playground("GraphQL Playground", "/query")) http.Handle("/query", handler.GraphQL(gqlgen.NewExecutableSchema( gqlgen.Config{Resolvers: &resolver}))) log.Printf("Server is running on http://localhost:%s", port) err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal(err) } }次にDockerfileを準備します。
ついでにfreshでホットリロードも追加。DockerfileFROM golang:1.11-alpine AS build_base RUN apk add bash ca-certificates git gcc g++ libc-dev WORKDIR /app COPY go.mod . COPY go.sum . RUN go mod download RUN go get github.com/pilu/fresh COPY . . EXPOSE 4000 CMD cd server; fresh server.goDockerComposeにアプリの設定を追加。
これで、まとめて起動できるようになりました。docker-compose.ymlversion: '3' services: prisma: image: prismagraphql/prisma:1.30 restart: always ports: - "4466:4466" environment: PRISMA_CONFIG: | port: 4466 # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security # managementApiSecret: my-secret databases: default: connector: mysql host: mysql user: root password: prisma rawAccess: true port: 3306 migrations: true mysql: image: mysql:5.7 restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: prisma volumes: - mysql:/var/lib/mysql app: build: context: . dockerfile: ./Dockerfile ports: - "4000:4000" volumes: - .:/app depends_on: - prisma environment: ENDPOINT: http://prisma:4466 volumes: mysql:







