20190411のGoに関する記事は6件です。

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へ値を渡す。)

channel.png

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
World

Directions

チャネルを関数の引数として使うと送信か受信のどちらを意図しているか指定(わかりやすく)することができる。
directions.png

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)
}

出力

Hello

Select

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 two

Timeouts

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 1

Closing 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 jobs

Timers

将来のある時点や一定間隔で繰り返し、ある部分を実行したい際に利用する
タイマーは待ち時間を指定すると、その時間にチャネルが処理を実施します。
例では、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 stopped

Worker 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秒に!!

図とかのイメージはあっているのか...?

参考

GoのChannelを使いこなせるようになるための手引 - Qiita

並行処理、並列処理のあれこれ - Qiita

Go by Example: Goroutines

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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から制御することもできる。

deploy
https://japan.zdnet.com/article/35135525/

デプロイしてみた

https://cloud.google.com/run/docs/quickstarts/build-and-deploy?_ga=2.161504639.-2138276196.1534942258
を参考に進めていく。

ちなみに、動作環境は下記コンテナ内に行う。
https://hub.docker.com/r/google/cloud-sdk

step1. 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 Dockerfile
helloworld.go
package 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


result

感想

普段私は、個人開発をしているときによくつかっている now.shというServerless Deploymentsを使っている。こちらは、v1のときはdockerコンテナを使えていたのだが、v2になると使えなくなってしまった。ただ、無料で簡単にデプロイできるものを選んでいると、こちらのサービスが最善だと感じていた。

しかし、今回のGoogleCloudNext19の発表で、CloudRunというものをBeta版でリリースされたことを知り、早速使ってみた。
何事もなく、今回の手順を進めて一切失敗することなく、3分以内にデプロイまで進めることができた。
これは、なんて楽で便利なんだと感心してしまった。
また、価格テーブルを見ると、CloudFunctionsのようなリクエストによる従量課金制で、月2百万リクエストまで無料だ。個人開発においては、AppEngineのようなインスタンス起動時間による料金設定よりも、こちらの方が断然オトク。
これはもうnow.shをやめて、こっちに乗り換えるっきゃない!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ルーチンにするだけです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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"`
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prisma

Prisma.ioとは?

prisma.png

  • 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とか)

SUH6AqW.png

  • Database
    MySQL。Dockerで構築。prisma initで自動的に作ってくれる。

  • Data Access Layer(Prisma)
    Dockerで構築。prisma initで自動的に作ってくれる。

  • Application / API Service
    今回はGoで作成。 GraphQLサーバのフレームワークはgqlgenを使用。
    最初はホストで直接起動。後からDocker化。

  • Client(ブラウザ)
    gqlgenGraphQL 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.1

Prisma 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.yml
version: '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.yml
endpoint: http://localhost:4466
datamodel: datamodel.prisma

generate:
  - generator: go-client
    output: ./generated/prisma-client/

データモデル定義。

datamodel.prisma
type User {
  id: ID! @unique
  name: String!
}

起動・デプロイ

まずは初期状態で起動してみます。

$ docker-compose up -d
$ prisma deploy

以下にアクセスすると、PrismaサーバのGraphQL Playgroundが開きます。

http://localhost:4466
prisma_playground.png

以下のアドレスにアクセスすると、データ管理が出来るPrisma Adminが開きます。

http://localhost:4466/_admin
prisma_admin.png

Goクライアントを作成

データが空っぽなので、Goクライアントを実装してデータを追加してみます。

まず、GO MODULESを初期化します。

$ export GO111MODLUE=on
$ go init

Prismaクライアントを使ってデータを登録するソースを作成します。

index.go
package 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.prisma
type 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.go
package 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.go
package 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.yml
schema: 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.graphql
type 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.go
package 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が開きます。

http://localhost:4000/
gql-app.png

ためしに、ユーザや記事を追加してみます。

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.go
package 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でホットリロードも追加。

Dockerfile
FROM 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.go

DockerComposeにアプリの設定を追加。
これで、まとめて起動できるようになりました。

docker-compose.yml
version: '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:
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prisma.io + Go のチュートリアル

Prisma.ioとは?

prisma.png

  • 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とか)

SUH6AqW.png

  • Database
    MySQL。Dockerで構築。prisma initで自動的に作ってくれる。

  • Data Access Layer(Prisma)
    Dockerで構築。prisma initで自動的に作ってくれる。

  • Application / API Service
    今回はGoで作成。 GraphQLサーバのフレームワークはgqlgenを使用。
    最初はホストで直接起動。後からDocker化。

  • Client(ブラウザ)
    gqlgenGraphQL 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.1

Prisma 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.yml
version: '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.yml
endpoint: http://localhost:4466
datamodel: datamodel.prisma

generate:
  - generator: go-client
    output: ./generated/prisma-client/

データモデル定義。

datamodel.prisma
type User {
  id: ID! @unique
  name: String!
}

起動・デプロイ

まずは初期状態で起動してみます。

$ docker-compose up -d
$ prisma deploy

以下にアクセスすると、PrismaサーバのGraphQL Playgroundが開きます。

http://localhost:4466
prisma_playground.png

以下のアドレスにアクセスすると、データ管理が出来るPrisma Adminが開きます。

http://localhost:4466/_admin
prisma_admin.png

Goクライアントを作成

データが空っぽなので、Goクライアントを実装してデータを追加してみます。

まず、GO MODULESを初期化します。

$ export GO111MODLUE=on
$ go init

Prismaクライアントを使ってデータを登録するソースを作成します。

index.go
package 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.prisma
type 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.go
package 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.go
package 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.yml
schema: 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.graphql
type 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.go
package 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が開きます。

http://localhost:4000/
gql-app.png

ためしに、ユーザや記事を追加してみます。

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.go
package 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でホットリロードも追加。

Dockerfile
FROM 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.go

DockerComposeにアプリの設定を追加。
これで、まとめて起動できるようになりました。

docker-compose.yml
version: '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:
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む