20190323のGoに関する記事は5件です。

golangでUnionFindを作る

はじめに

Union-Findは素集合のデータ構造を表すのにとても便利です.
ここではそのUnion-FindをGo言語で作成していきます.
Union-Find自体には特に触れないのでご注意ください.

どんな機能が必要か

自分的に以下の機能が必要と考えました.

  • ある要素aの所属するグループを返す
  • ある要素aの所属するグループ内の要素数(木のサイズ)を返す
  • ある要素aの所属する[グループA] と ある要素bの所属する[グループB] を合体する
  • ある要素aとある要素bが同じグループに所属するかどうかを返す

実装

上の機能をもつように構造体を作成していきましょう.

type UnionFind struct {
    par []int
}

/* コンストラクタ */
func newUnionFind(N int) *UnionFind {
    u := new(UnionFind)
    u.par = make([]int, N)
    for i := range u.par {
        u.par[i] = -1
    }
    return u
}

/* xの所属するグループを返す */
func (u UnionFind) root(x int) int {
    if u.par[x] < 0 {
        return x
    }
    u.par[x] = u.root(u.par[x])
    return u.par[x]
}

/* xの所属するグループ と yの所属するグループ を合体する */
func (u UnionFind) unite(x, y int) {
    xr := u.root(x)
    yr := u.root(y)
    if xr == yr {
        return
    }
    u.par[yr] += u.par[xr]
    u.par[xr] = yr
}

/* xとyが同じグループに所属するかどうかを返す */
func (u UnionFind) same(x, y int) bool {
    if u.root(x) == u.root(y) {
        return true
    }
    return false
}

/* xの所属するグループの木の大きさを返す */
func (u UnionFind) size(x int) int {
    return -u.par[u.root(x)]
}

利用

func main() {
    // インスタンスの生成
    u := newUnionFind(N)

    // 各メソッドの利用
    u.unite(a, b)
    u.same(a, b)
    u.size(a)
    u.root(a)
}

内容について

親の要素には, その木の大きさをNとすると, $N \times (-1)$が与えられており,
子の要素には, それが属する木の親の値が与えられるようになっています.

そのため, 要素が負であれば親であることがわかり, それを正の数に戻すことでその木の大きさを得ることできます.

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

Goのmodulesを試してみたメモ

最近、Goを勉強しているのですが、一番最初にハマったのは実は$GOPATH内で作業しなければならない、という部分でした。

個人的にはいろいろな技術を試す場所をPC内で独自に決めていたので(例えば~/workみたいに)、この制約苦しいな、と思っていたら、今はmodulesというものがあることを知りました。

これなら$GOPATHでない所でもパッケージが見つからない問題が解消しそうだと思い、試してました。

誰かの参考になれば、と思いメモしておきます。

環境

go version 1.12.1

作るもの

以下のような構成で、main.goからtest.goを呼び出すだけの簡単なプログラムを作ります。

.
├── main.go
└── test
    └── test.go
main.go
package main

import (
  "fmt"
  "test"
)

func main() {
  fmt.Println("main")
  test.Test()
}
test.go
package test

import "fmt"

func Test() {
  fmt.Println("test")
}

そしてmain.goを実行してみます。
するとtestなんてpackageはない、と怒られます。

:!go run main.go
main.go:5:3: cannot find package "test" in any of:

modulesでpackage問題を解消する

ここでmodulesの出番です。

以下のようなコマンドを打つことで、ここが"modtest"というモジュールであることをどうやら指定する模様。

実行するとgo.modというファイルが出来ます。これがモジュールを宣言することになっているようです。

go mod init modtest

あとはimport部分を以下のように修正する。

main.go
package main

import (
  "fmt"
  "modtest/test"  // この部分をmodtest/testに修正する
)

func main() {
  fmt.Println("main")
  test.Test()
}

正しく実行できました。

:!go run main.go
main
test

modulesのことを知るまではdirenvとかで$GOPATHに現在のパスを指定する、など微妙に面倒なことをしていました。

参考

https://qiita.com/yoshinori_hisakawa/items/268ba201611401ca7935
https://qiita.com/spiegel-im-spiegel/items/5cb1587cb55d6f6a34d7

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

golang.tokyo #22+Okayama.go/Sendai.go 概要まとめ

はじめに

golang.tokyo #22+Okayama.go/Sendai.go - connpass
このイベントに行ってまいりました。
内容としては、このイベントの概要となります。
今回のテーマは、静的解析でした。

登壇者:
@tenntenn
資料:
A Tour of Static Analysis - Google スライド

このページについて | golang.tokyo コードラボ

そもそも静的解析ってなんの役に立つ

  • go vetとかgolintみたいなツールを作るのに向いている
  • コンパイラじゃ見つけてくれないバグを探すのに使える
  • Goの文法に詳しくなれる

静的解析の流れ

image.png

  • 字句解析
    文字列をトークンへ!! 字句解析で、文字列の塊だったソースコードが予約語のfuncなのか、識別子(変数名や関数名など)なのか、数値リテラルなのかなどを区別することができるトークンの塊に変換される。

字句解析についてはgo/parserパッケージ内でgo/scannerパッケージを用いて行われています。

go/parserで自動でやられるので点線表記みたいですね

  • 構文解析
    トークンをASTへ!! 構文解析を行うと、どの部分が関数定義で、どの部分がその引数の定義なのか、などを抽象構文木から取得することができるようになる。
  • 型チェック
    最後に型チェックを行うことで、抽象構文木から型情報を抽出
    型チェックは次の3つの工程から成る。
    1.識別子の解決
    2.型の推論
    3.定数の評価
    この3つの工程を行い、型情報を抽出することで、どの変数(識別子)がどういうデータ型でどこで定義され、どこで使用されているかなどを知ることができる。

Gopherを探せ!ハンズオン

資料:
Gopherを探せ!ハンズオン資料

概要

go@_gopher.go
package main

import (
        "fmt"
)

type Gopher struct {
        Gopher string `json:"gopher"`
}

func main() {
        const gopher = "GOPHER"
        gogopher := GOPHER()
        gogopher.Gopher = gopher
        fmt.Println(gogopher)
}

func GOPHER() (gopher *Gopher) {
        gopher = &Gopher{Gopher: "gopher"}
        return
}

上記のファイルから、名前付き型かつGopherという名前の識別子だけを
サーチするにはどうしたらいいかが

  • grepコマンドの限界
  • 式の構造解析
  • ファイルの構造解析
  • 型チェック

という流れで学べる、すごくわかりやすいハンズオンになっていました。

ハンズオンを書き換えて、理解を深める

ここからは個人で勝手にやった内容となります。
リテラルの"gopher"文字列をサーチするように書き換えてみます。

go@main.go
package main

import (
    "fmt"
    "go/ast"
    "go/importer"
    "go/parser"
    "go/token"
    "go/types"
    "log"
    "strconv"
)

func main() {
    // ファイルごとのトークンの位置を記録するFileSetを作成する
    fset := token.NewFileSet()

    // ファイル単位で構文解析を行う
    f, err := parser.ParseFile(fset, "_gopher.go", nil, 0)
    if err != nil {
        log.Fatal("Error:", err)
    }

    // 識別子が定義または利用されてる部分を記録する
    defsOrUses := map[*ast.Ident]types.Object{}
    info := &types.Info{
        Defs: defsOrUses,
        Uses: defsOrUses,
    }

    // 型チェックを行うための設定
    config := &types.Config{
        Importer: importer.Default(),
    }

    // 型チェックを行う
    _, err = config.Check("main", fset, []*ast.File{f}, info)
    if err != nil {
        log.Fatal("Error:", err)
    }

    // 抽象構文木を深さ優先で探索する
    ast.Inspect(f, func(n ast.Node) bool {

        // リテラルではない場合は無視
        basic, ok := n.(*ast.BasicLit)
        if !ok {
            return true
        }

        basicValue,err := strconv.Unquote(basic.Value)
        if err !=nil{
            panic(err)
        }

        //リテラルが"gopher"という値でなければ無視
        if basicValue  != "gopher" && basicValue != `json:"gopher"` {
            return true
        }

        fmt.Println(fset.Position(basic.Pos()))

        return true
    })
}

takafk9@narikawakiyoshinoMacBook-Pro [16時39分00秒] [~/go/src/github.com/golangtokyo/codelab/find-gophers/3_typecheck] [master *]
-> % go run main.go
_gopher.go:8:16
_gopher.go:19:27

きちんとリテラルを捕まえてますね

structタグのastが

Tag: *ast.BasicLit {
 ValuePos: 70
    Kind: STRING
    Value: "`json:\"gopher\"`"
}

なようなので、

if basicValue  != "gopher" && basicValue != `json:"gopher"`

という規制で、タグ内の"gopher"も捕まえられました
(slackで回答していただいた方々、ありがとうございました!)

感想

静的解析についてかなりディープで難しい領域に思っていましたが、今回のハンズオンで
少しイメージが変わりました。
(この流れをモジュール化したAnalyzerまで手を出せなかったので、時間あるときに、そちらもやっていきたい)
golang.tokyoのイベントにお邪魔するのは初めてなのですが、Slackでも疑問に即レスで答えていただけて、非常に素晴らしいイベントでした。
ちょっとastにおこして解析するだけで、かなり文法の知識を深められたので、
今度は自作で静的解析ツールを作りたいと思います。

ありがとうございました!

その他有益な資料まとめ

analysis.Analyzerを使っている今回のようなサンプルプログラム

Goの式の定義

静的解析のサンプルコード

tenntennさんが作ったAnalyzerを使った静的解析CLIを作り始めるのに便利奴

knsh14さんによるanalysisパッケージの解説記事

Goの標準パッケージではじめる静的解析入門①準備編 · mom0tomo

Goにおける静的解析のモジュール化について

analysis pkgh設計者(“プログラミング言語Go”執筆者)によるtypesの説明

motemenさんによるgo/astパッケージやgo/typesパッケージ

余談

Gopherオタクになりたて人間には最高のサイト、、、

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

Golang + MongoDBでGraphQLを使ってみる

GraphQLとは

すでに紹介されている記事があるので、その辺を参考にしてみてください。
GraphQL入門 - 使いたくなるGraphQL
アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた
GraphQLはRESTの置き換えではない
GraphQLは何に向いているか

GraphQLは概念的に難しくはないですが、実際にどうやって動かすのかがわかりにくいかも知れません。かく言う私も。。。

日本語のテキストも多くはないと思うので参考にしていただけたら嬉しいです:pencil:

GolangでGraphQL

今回はGolangでGraphQLを使います。
Goでなんかしてみたいなと思ってGraphQLの使い方を調べてみましたが、あまり日本語で解説されているところ(graphql-goを使っているページ)が多くなかったので、英語記事等も参考にして勉強しました。

GolangもGraphQLも新参者なので、正確ではない部分や改善すべき点もあると思いますが、その時は忌憚なく指摘いただければと思います:sweat_smile:

使うもの

Golang
  go version go1.11.5 darwin/amd64
GraphQL
  v0.7.7
MongoDB
  MongoDB shell version v4.0.3
macOS
  Mojave version 10.14.3

インストール方法とかは特筆する必要もないので省きます。

実装手順

  1. MongoDBに接続してデモ用にデータを作成
  2. GraphQLの導入
  3. GraphQLを利用してデータを取得

GraphQLを使うので、RDBMSは使わずに、NoSQLを利用します。
GraphQLは決してRDBMSとともに利用できないと言うわけではないようですが、利用例が少ないみたいで、基本的にはNoSQLと利用することが多いと言うような話を耳にしました。
ですのでここではドキュメント指向型のNoSQLであるMongoDBを使っています。

ちなみに、HasuraというサービスではpostgresqlでGraphQLを利用できるらしいです。
:point_right_tone2::point_right_tone2::point_right_tone2: Hasura

MongoDBに接続してデモ用にデータを作成

すでにMongoDBがインストールされていると言うことを前提として、作業でディレクトリで$ sudo mongod --dbpath ./を実行します。
するとディレクトリ内に様々なファイルやフォルダが作成され、MongoDBが起動します。

この状態で$ mongoを実行すると、MongoDBとのコネクションを始めることができます。

MongoDBで使えるコマンドは以下を参考にしてみてください。
MongoDB の 基本的 な 使い方

ここでは基本的に、

> db ・・・ データベース参照
> show collections ・・・ コレクションのリストを表示
> db.collection_name.find() ・・・ 指定したコレクションの全件取得
> db.collection_name.drop() ・・・ 指定したコレクションの削除

これらのコマンドを使うことになります。
*MongoDBのようなドキュメント指向型のNoSQLではコレクション=テーブル、ドキュメント=レコードみたいに考えればいいと思います。

GraphQLの導入

GolangでGraphQLを使う場合、graphql-goやGQLgenなどいくつかパッケージの選択肢がありますが、ここではgraphql-goを利用します。

$ go get github.com/graphql-go/graphql
import (
  "github.com/graphql-go/graphql"
)

graphql-go/graphql
GoでGraphQLを使う場合の選択肢

// localのデータベースtestに接続
func main() {
  session, _ := mgo.Dial("mongodb://localhost/test")
  defer session.Close()
  db := session.DB("test")
}

MongoDBでのInsertとFind

// insert into users
newUser := &User{
  Id: bson.NewObjectId(),
  Name: "名前",
  Email: "アドレス",
  Password: "パス"
}
col := db.C("users")
if err := col.Insert(newUser); err != nil {
  log.Fatalln(err)
}

// fetch all users
var AllUsers []User
query := db.C("users").Find(bson.M{})
query.All(&AllUsers)
// AllUsersは構造体なので、JSONにする場合はjson.Marshal()を。

好きなようにデータを作成しておいてください。
updateとdeleteはここでは説明を省きます。
公式ドキュメントが一番手っ取り早いです。
mongoDB Documents

GraphQLを利用してデータを取得

今回はサンプルとしてUser構造体を作成し、これをGraphQLの定義に利用します。

type User struct {
    Id bson.ObjectId `bson:"_id"` // MongoDBで自動生成される_idとUser.Idを同一のものとする
    Name string `bson:"name"`
    Email string `bson:"email"`
    Password string `bson:"password"`
}

bsonって何? って感じですが、jsonと同じものと考えれば問題ないです。
MongoDB超入門

次に、スキーマとリクエストを用意します(この2つの正式な名称ではありませんが、わかりやすいと思ったのでこう呼ばせていただきます)。
GraphQLでデータを取って来るためには、これら2つの要素が必要になります。

最終的には以下のようなコードにスキーマとリクエストを突っ込みます。

params := graphql.Params{
  Schema: schema, // スキーマ
  RequestString: request, // リクエスト
}
r := graphql.Do(params) // GraphQLの実行し、rに結果を格納
if len(r.Errors) > 0 { // エラーがあれば
  log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r) // 構造体をJSONに変換して、
fmt.Printf("%s \n", rJSON) // プリントする。

スキーマ

以下コードの下から4行目にあるのがスキーマとなります。
スキーマとは、GraphQLが要求を投げかける対象のデータ群やそのデータ群の設定を含めた仕様書のようなものです。
データベースからデータを取り出したり、データ一つ一つの型やリクエストで渡されてくることになるidなどの引数の設定を行います。

GraphQLではまず対象となるデータをすべて、前もってデータベースから引っ張り出しておきます。
ですのでGraphQLが直接的にデータベースとやりとりするわけではありません。
GraphQLが要求するのはすでに取り出されたデータ群であり、いわばそのひと塊をふるいにかけて、必要なデータだけを取り出すのがGraphQLだということです。

// MongoDBと接続してAllUsers(構造体)を用意する
session, _ := mgo.Dial("mongodb://localhost/test")
defer session.Close()
db := session.DB("test")
var AllUsers []User // フィールド定義時にこれを使う
query := db.C("users").Find(bson.M{})
query.All(&AllUsers)

var userType = graphql.NewObject( // GraphQL用の型定義
  graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
      "id": &graphql.Field{
        Type: graphql.String,
      },
      "name": &graphql.Field{
        Type: graphql.String,
      },
      "email": &graphql.Field{
        Type: graphql.String,
      },
      "password": &graphql.Field{
        Type: graphql.String,
      },
    },
  },
)
fields := graphql.Fields{ // フィールド(リクエストで使われるデータの扱い方に関する設定)
  "user": &graphql.Field{
    Type: userType,
    Description: "Fetch user by Id",
    Args: graphql.FieldConfigArgument{ // リクエストに渡す引数についての設定
      "id": &graphql.ArgumentConfig{
        Type: graphql.String,
      },
    },
    Resolve: func(param graphql.ResolveParams) (interface{}, error) { // 帰って来るデータの設定
      id, ok := param.Args["id"].(string)
      if ok {
        for _, user := range AllUsers { // AllUsersはmonngoDBから取ってきた全てUserのデータ
          if user.Id.Hex() == id { // Hex()でMongoDBのobjectIdをstringに変換する
            return user, nil
          }
        }
      }
      return nil, nil
    },
  },
  "list": &graphql.Field{
    Type: graphql.NewList(userType),
    Description: "Fetch users list",
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
      return AllUsers, nil // AllUsersはmonngoDBから取ってきた全てUserのデータ
    },
  },
}
rootQuery := graphql.ObjectConfig{
  Name: "RootQuery",
  Fields: fields,
}
schemaConfig := graphql.SchemaConfig{
  Query: graphql.NewObject(rootQuery),
}
schema, err := graphql.NewSchema(schemaConfig) // スキーマ
if err != nil {
  log.Fatalf("failed to create new schema, error: %v", err)
}

流れとしては、

  1. GraphQL用の型を定義する
  2. フィールドを定義(リクエストを投げた時に帰ってくるデータの設定。ここでは"user"と"list"を定義している)
  3. Schemaの設定としてまとめ(ObjectConfigとSchemaConfigのあたり)、NewSchemaでスキーマを作成する

かなりざっくりとしていますが、だいたいこんな感じです:information_desk_person_tone2:

フィールドについては、
"user"が特定のユーザーを取得する場合に使われる要求
"list"が全てのユーザーを取得する要求
となっています。

だから、"user"のResolveの項目では、リクエストで渡されることになる(あとで説明します)引数の"id"に合致するuser.Idの持ち主をreturnで返しています。

Resolve: func(param graphql.ResolveParams) (interface{}, error) {
  id, ok := param.Args["id"].(string) // リクエストで渡されることになる引数の"id"
  if ok {
    for _, user := range AllUsers { // MongoDBから引っ張って来る全てのuserのデータ
      if user.Id.Hex() == id { // 引数の"id"と合致すれば通る
        return user, nil
      }
    }
  }
  return nil, nil
},

"list"ではAllUser全てをreturnしていることも確認しておいてください。
また、"list"でTypeの定義時にgraphql.NewList(userType)としているのは、この"list"が配列であるためです。
単体であれば"user"と同様にuserTypeのみを設定すれば良いですが、あくまでリストなのでこのように設定しています。

リクエスト

リクエストとは、GraphQLによる問い合わせの表現です。

request := `
  {
    user(id: "5c94f4d7e803694b2d09da75") {
      id
      name
      email
      password
    }
  }
`

userの後に()で指定している"id"こそ、先ほどスキーマで説明した「リクエストで渡されることになる引数」そのものです。このように設定された引数が、先ほどのResolve内の関数の中で処理されます。

上記のリクエストでは、idに合致するドキュメント(レコード)のid, name, email, passwordを返します。
なので、nameだけを指定して書くと、もちろん該当のドキュメントのnameだけを返します。

また、このリクエストでは、"user"の情報(user1人の情報)しか引き出せません。
全てのユーザーの情報を取得するためには、先ほどスキーマで作成した"list"を指定する必要があります。

request := `
  {
    list {
      id
      name
      email
      password
    }
  }
`

このリクエストによって全てのユーザーが配列で返って来るようになります。

では、これらスキーマとリクエストを組み合わせてGraphQLを実行します。

GraphQLの実行

先ほどのコードに戻ります。

params := graphql.Params{
  Schema: schema, // スキーマ
  RequestString: request, // リクエスト
}
r := graphql.Do(params) // GraphQLの実行
if len(r.Errors) > 0 { // エラーがあれば
  log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r) // 構造体をJSONに変換して、
fmt.Printf("%s \n", rJSON) // プリントする。

schemaをSchemaに、requestをRequestStringに設定して、graphql.DoでGraphQLを実行します。
すると、返ってきた結果が構造体として変数に格納されるので、ここではjsonに変換してからプリントしています。

ファイルを実行すると、

$ go run main.go
{"data":{"user":{"email":"ai@kizuna","id":"ObjectIdHex(\"5c94f4d7e803694b2d09da75\")","name":"キズナアイ","password":"kizunAI"}}} 

こんな感じで帰って来ると思います(ここではクエリーにuser(id: "id_string")で指定して一つだけ取ってきています。listなら配列になります)。

POSTして使う

GraphQLの利点としては、RESTfulAPIのようにエンドポイントを何個も用意する必要がない点が挙げられます。

APIを一つ作っておいて、POSTしてクエリーを渡すようにすれば、都度そのクエリーに応じた結果を得ることができるようになります。
そうすることで、ユーザーを取って来るにしても、GET:usersやらGET:users/:idやらとエンドポイントを分ける手間が省けるようになります。

まあ、サーバー構築をして利用するところまでは気が向いたら書き継ごうかなと思っています:ramen:
とりあえずこの辺で:robot:

あれ、データのfetchしかしてなくね?

ここで紹介したGraphQLの活用方法では、データの取得方法しか解説していません。
ですが、データの取得以外にもGraphQLではInsertやUpdateなども行うことができます(ただ、前述したようにGraphQLが直接的にInsertなどを行うわけではないので厳密にはこの表現は間違っているかもしれません)。もちろんエンドポイントを増やすことなしに。

ではどうするのかという話ですが、これ以上ページを冗長にするのはあまりよろしくないと思われますので、別にページを設けようと思っています。完成次第こちらにリンクを載せるのでそちらも参考にしてみてください。

そんなに長くないですし、コードの方が分量的に文章よりも多いかもしれません。

ただ、一つだけここで説明しておくと、先ほどからリクエストやスキーマで出てきた「要求」という語句には、クエリーとミューテーションという概念を含まれており、

クエリー・・・データの取得要求
ミューテーション・・・データの更新要求

というようにGraphQLでは処理の振り分けを行なっています。

ここで説明してきたリクエストはこのクエリーに該当し、Insertなどの処理を行うときにはこのミューテーションを利用することになります。

英語ですが下記公式ページでも解説していますので、参考にしてみてください。
Queries and Mutations

サンプルコード

このサンプルコードではMongoDBは使っていません。
コピペして実行するだけでGraphQLを実行することができます。

UserのIdをintに変更している点、フィールドの定義も少し異なるので中止してください。

package main

import (
  "encoding/json"
  "fmt"
  "log"
  "github.com/graphql-go/graphql"
)

type User struct {
  Id int
  Name string
  Email string
  Password string
}

var userType = graphql.NewObject(
  graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
      "id": &graphql.Field{
        Type: graphql.Int,
      },
      "name": &graphql.Field{
        Type: graphql.String,
      },
      "email": &graphql.Field{
        Type: graphql.String,
      },
      "password": &graphql.Field{
        Type: graphql.String,
      },
    },
  },
)

func generateUsers() []User {
  user := User{
    Id: 1,
    Name: "キズナアイ",
    Email: "ai@kizuna",
    Password: "kizunAI",
  }
  var users []User
  users = append(users, user)
  return users
}

func main() {
  users := generateUsers()

  fields := graphql.Fields{
    "user": &graphql.Field{
      Type: userType,
      Description: "Fetch user by Id",
      Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
          Type: graphql.Int,
        },
      },
      Resolve: func(param graphql.ResolveParams) (interface{}, error) {
        id, ok := param.Args["id"].(int)
        if ok {
          for _, user := range users {
            if int(user.Id) == id {
              return user, nil
            }
          }
        }
        return nil, nil
      },
    },
    "list": &graphql.Field{
      Type: graphql.NewList(userType),
      Description: "Fetch users list",
      Resolve: func(params graphql.ResolveParams) (interface{}, error) {
        return users, nil
      },
    },
  }
  rootQuery := graphql.ObjectConfig{
    Name: "RootQuery",
    Fields: fields,
  }
  schemaConfig := graphql.SchemaConfig{
    Query: graphql.NewObject(rootQuery),
  }
  schema, err := graphql.NewSchema(schemaConfig)
  if err != nil {
    log.Fatalf("failed to create new schema, error: %v", err)
  }

  request := `
    {
      user(id: 1) {
        id
        name
        email
        password
      }
    }
  `

  params := graphql.Params{
    Schema: schema,
    RequestString: request,
  }
  r := graphql.Do(params)
  if len(r.Errors) > 0 {
    log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
  }
  rJSON, _ := json.Marshal(r)
  fmt.Printf("%s \n", rJSON)
}

これ参考になった
Go GraphQL Beginners Tutorial

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

Go + MongoDBでGraphQLを使ってみる

GraphQLとは

すでに紹介されている記事があるので、その辺を参考にしてみてください。
GraphQL入門 - 使いたくなるGraphQL
アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた
GraphQLはRESTの置き換えではない
GraphQLは何に向いているか

GraphQLは概念的に難しくはないですが、実際にどうやって動かすのかがわかりにくいかも知れません。かく言う私も。。。

日本語のテキストも多くはないと思うので参考にしていただけたら嬉しいです:pencil:

GolangでGraphQL

今回はGolangでGraphQLを使います。
Goでなんかしてみたいなと思ってGraphQLの使い方を調べてみましたが、あまり日本語で解説されているところ(graphql-goを使っているページ)が多くなかったので、英語記事等も参考にして勉強しました。

GolangもGraphQLも新参者なので、正確ではない部分や改善すべき点もあると思いますが、その時は忌憚なく指摘いただければと思います:sweat_smile:

使うもの

Golang
  go version go1.11.5 darwin/amd64
GraphQL
  v0.7.7
MongoDB
  MongoDB shell version v4.0.3
macOS
  Mojave version 10.14.3

インストール方法とかは特筆する必要もないので省きます。

実装手順

  1. MongoDBに接続してデモ用にデータを作成
  2. GraphQLの導入
  3. GraphQLを利用してデータを取得

GraphQLを使うので、RDBMSは使わずに、NoSQLを利用します。
GraphQLは決してRDBMSとともに利用できないと言うわけではないようですが、利用例が少ないみたいで、基本的にはNoSQLと利用することが多いと言うような話を耳にしました。
ですのでここではドキュメント指向型のNoSQLであるMongoDBを使っています。

ちなみに、HasuraというサービスではpostgresqlでGraphQLを利用できるらしいです。
:point_right_tone2::point_right_tone2::point_right_tone2: Hasura

MongoDBに接続してデモ用にデータを作成

すでにMongoDBがインストールされていると言うことを前提として、作業でディレクトリで$ sudo mongod --dbpath ./を実行します。
するとディレクトリ内に様々なファイルやフォルダが作成され、MongoDBが起動します。

この状態で$ mongoを実行すると、MongoDBとのコネクションを始めることができます。

MongoDBで使えるコマンドは以下を参考にしてみてください。
MongoDB の 基本的 な 使い方

ここでは基本的に、

> db ・・・ データベース参照
> show collections ・・・ コレクションのリストを表示
> db.collection_name.find() ・・・ 指定したコレクションの全件取得
> db.collection_name.drop() ・・・ 指定したコレクションの削除

これらのコマンドを使うことになります。
*MongoDBのようなドキュメント指向型のNoSQLではコレクション=テーブル、ドキュメント=レコードみたいに考えればいいと思います。

GraphQLの導入

GolangでGraphQLを使う場合、graphql-goやGQLgenなどいくつかパッケージの選択肢がありますが、ここではgraphql-goを利用します。

$ go get github.com/graphql-go/graphql
import (
  "github.com/graphql-go/graphql"
)

graphql-go/graphql
GoでGraphQLを使う場合の選択肢

// localのデータベースtestに接続
func main() {
  session, _ := mgo.Dial("mongodb://localhost/test")
  defer session.Close()
  db := session.DB("test")
}

MongoDBでのInsertとFind

// insert into users
newUser := &User{
  Id: bson.NewObjectId(),
  Name: "名前",
  Email: "アドレス",
  Password: "パス"
}
col := db.C("users")
if err := col.Insert(newUser); err != nil {
  log.Fatalln(err)
}

// fetch all users
var AllUsers []User
query := db.C("users").Find(bson.M{})
query.All(&AllUsers)
// AllUsersは構造体なので、JSONにする場合はjson.Marshal()を。

好きなようにデータを作成しておいてください。
updateとdeleteはここでは説明を省きます。
公式ドキュメントが一番手っ取り早いです。
mongoDB Documents

GraphQLを利用してデータを取得

今回はサンプルとしてUser構造体を作成し、これをGraphQLの定義に利用します。

type User struct {
    Id bson.ObjectId `bson:"_id"` // MongoDBで自動生成される_idとUser.Idを同一のものとする
    Name string `bson:"name"`
    Email string `bson:"email"`
    Password string `bson:"password"`
}

bsonって何? って感じですが、jsonと同じものと考えれば問題ないです。
MongoDB超入門

次に、スキーマとリクエストを用意します(この2つの正式な名称ではありませんが、わかりやすいと思ったのでこう呼ばせていただきます)。
GraphQLでデータを取って来るためには、これら2つの要素が必要になります。

最終的には以下のようなコードにスキーマとリクエストを突っ込みます。

params := graphql.Params{
  Schema: schema, // スキーマ
  RequestString: request, // リクエスト
}
r := graphql.Do(params) // GraphQLの実行し、rに結果を格納
if len(r.Errors) > 0 { // エラーがあれば
  log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r) // 構造体をJSONに変換して、
fmt.Printf("%s \n", rJSON) // プリントする。

スキーマ

以下コードの下から4行目にあるのがスキーマとなります。
スキーマとは、GraphQLが要求を投げかける対象のデータ群やそのデータ群の設定を含めた仕様書のようなものです。
データベースからデータを取り出したり、データ一つ一つの型やリクエストで渡されてくることになるidなどの引数の設定を行います。

GraphQLではまず対象となるデータをすべて、前もってデータベースから引っ張り出しておきます。
ですのでGraphQLが直接的にデータベースとやりとりするわけではありません。
GraphQLが要求するのはすでに取り出されたデータ群であり、いわばそのひと塊をふるいにかけて、必要なデータだけを取り出すのがGraphQLだということです。

// MongoDBと接続してAllUsers(構造体)を用意する
session, _ := mgo.Dial("mongodb://localhost/test")
defer session.Close()
db := session.DB("test")
var AllUsers []User // フィールド定義時にこれを使う
query := db.C("users").Find(bson.M{})
query.All(&AllUsers)

var userType = graphql.NewObject( // GraphQL用の型定義
  graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
      "id": &graphql.Field{
        Type: graphql.String,
      },
      "name": &graphql.Field{
        Type: graphql.String,
      },
      "email": &graphql.Field{
        Type: graphql.String,
      },
      "password": &graphql.Field{
        Type: graphql.String,
      },
    },
  },
)
fields := graphql.Fields{ // フィールド(リクエストで使われるデータの扱い方に関する設定)
  "user": &graphql.Field{
    Type: userType,
    Description: "Fetch user by Id",
    Args: graphql.FieldConfigArgument{ // リクエストに渡す引数についての設定
      "id": &graphql.ArgumentConfig{
        Type: graphql.String,
      },
    },
    Resolve: func(param graphql.ResolveParams) (interface{}, error) { // 帰って来るデータの設定
      id, ok := param.Args["id"].(string)
      if ok {
        for _, user := range AllUsers { // AllUsersはmonngoDBから取ってきた全てUserのデータ
          if user.Id.Hex() == id { // Hex()でMongoDBのobjectIdをstringに変換する
            return user, nil
          }
        }
      }
      return nil, nil
    },
  },
  "list": &graphql.Field{
    Type: graphql.NewList(userType),
    Description: "Fetch users list",
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
      return AllUsers, nil // AllUsersはmonngoDBから取ってきた全てUserのデータ
    },
  },
}
rootQuery := graphql.ObjectConfig{
  Name: "RootQuery",
  Fields: fields,
}
schemaConfig := graphql.SchemaConfig{
  Query: graphql.NewObject(rootQuery),
}
schema, err := graphql.NewSchema(schemaConfig) // スキーマ
if err != nil {
  log.Fatalf("failed to create new schema, error: %v", err)
}

流れとしては、

  1. GraphQL用の型を定義する
  2. フィールドを定義(リクエストを投げた時に帰ってくるデータの設定。ここでは"user"と"list"を定義している)
  3. Schemaの設定としてまとめ(ObjectConfigとSchemaConfigのあたり)、NewSchemaでスキーマを作成する

かなりざっくりとしていますが、だいたいこんな感じです:information_desk_person_tone2:

フィールドについては、
"user"が特定のユーザーを取得する場合に使われる要求
"list"が全てのユーザーを取得する要求
となっています。

だから、"user"のResolveの項目では、リクエストで渡されることになる(あとで説明します)引数の"id"に合致するuser.Idの持ち主をreturnで返しています。

Resolve: func(param graphql.ResolveParams) (interface{}, error) {
  id, ok := param.Args["id"].(string) // リクエストで渡されることになる引数の"id"
  if ok {
    for _, user := range AllUsers { // MongoDBから引っ張って来る全てのuserのデータ
      if user.Id.Hex() == id { // 引数の"id"と合致すれば通る
        return user, nil
      }
    }
  }
  return nil, nil
},

"list"ではAllUser全てをreturnしていることも確認しておいてください。
また、"list"でTypeの定義時にgraphql.NewList(userType)としているのは、この"list"が配列であるためです。
単体であれば"user"と同様にuserTypeのみを設定すれば良いですが、あくまでリストなのでこのように設定しています。

リクエスト

リクエストとは、GraphQLによる問い合わせの表現です。

request := `
  {
    user(id: "5c94f4d7e803694b2d09da75") {
      id
      name
      email
      password
    }
  }
`

userの後に()で指定している"id"こそ、先ほどスキーマで説明した「リクエストで渡されることになる引数」そのものです。このように設定された引数が、先ほどのResolve内の関数の中で処理されます。

上記のリクエストでは、idに合致するドキュメント(レコード)のid, name, email, passwordを返します。
なので、nameだけを指定して書くと、もちろん該当のドキュメントのnameだけを返します。

また、このリクエストでは、"user"の情報(user1人の情報)しか引き出せません。
全てのユーザーの情報を取得するためには、先ほどスキーマで作成した"list"を指定する必要があります。

request := `
  {
    list {
      id
      name
      email
      password
    }
  }
`

このリクエストによって全てのユーザーが配列で返って来るようになります。

では、これらスキーマとリクエストを組み合わせてGraphQLを実行します。

GraphQLの実行

先ほどのコードに戻ります。

params := graphql.Params{
  Schema: schema, // スキーマ
  RequestString: request, // リクエスト
}
r := graphql.Do(params) // GraphQLの実行
if len(r.Errors) > 0 { // エラーがあれば
  log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r) // 構造体をJSONに変換して、
fmt.Printf("%s \n", rJSON) // プリントする。

schemaをSchemaに、requestをRequestStringに設定して、graphql.DoでGraphQLを実行します。
すると、返ってきた結果が構造体として変数に格納されるので、ここではjsonに変換してからプリントしています。

ファイルを実行すると、

$ go run main.go
{"data":{"user":{"email":"ai@kizuna","id":"ObjectIdHex(\"5c94f4d7e803694b2d09da75\")","name":"キズナアイ","password":"kizunAI"}}} 

こんな感じで帰って来ると思います(ここではクエリーにuser(id: "id_string")で指定して一つだけ取ってきています。listなら配列になります)。

POSTして使う

GraphQLの利点としては、RESTfulAPIのようにエンドポイントを何個も用意する必要がない点が挙げられます。

APIを一つ作っておいて、POSTしてクエリーを渡すようにすれば、都度そのクエリーに応じた結果を得ることができるようになります。
そうすることで、ユーザーを取って来るにしても、GET:usersやらGET:users/:idやらとエンドポイントを分ける手間が省けるようになります。

まあ、サーバー構築をして利用するところまでは気が向いたら書き継ごうかなと思っています:ramen:
とりあえずこの辺で:robot:

あれ、データのfetchしかしてなくね?

ここで紹介したGraphQLの活用方法では、データの取得方法しか解説していません。
ですが、データの取得以外にもGraphQLではInsertやUpdateなども行うことができます(ただ、前述したようにGraphQLが直接的にInsertなどを行うわけではないので厳密にはこの表現は間違っているかもしれません)。もちろんエンドポイントを増やすことなしに。

ではどうするのかという話ですが、これ以上ページを冗長にするのはあまりよろしくないと思われますので、別にページを設けようと思っています。完成次第こちらにリンクを載せるのでそちらも参考にしてみてください。

そんなに長くないですし、コードの方が分量的に文章よりも多いかもしれません。

ただ、一つだけここで説明しておくと、先ほどからリクエストやスキーマで出てきた「要求」という語句には、クエリーとミューテーションという概念を含まれており、

クエリー・・・データの取得要求
ミューテーション・・・データの更新要求

というようにGraphQLでは処理の振り分けを行なっています。

ここで説明してきたリクエストはこのクエリーに該当し、Insertなどの処理を行うときにはこのミューテーションを利用することになります。

ミューテーション実装の記事を書きました!
GoでGraphQLのMutationを実装する

英語ですが下記公式ページでも解説していますので、参考にしてみてください。
Queries and Mutations

サンプルコード

このサンプルコードではMongoDBは使っていません。
コピペして実行するだけでGraphQLを実行することができます。

UserのIdをintに変更している点、フィールドの定義も少し異なるので中止してください。

package main

import (
  "encoding/json"
  "fmt"
  "log"
  "github.com/graphql-go/graphql"
)

type User struct {
  Id int
  Name string
  Email string
  Password string
}

var userType = graphql.NewObject(
  graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
      "id": &graphql.Field{
        Type: graphql.Int,
      },
      "name": &graphql.Field{
        Type: graphql.String,
      },
      "email": &graphql.Field{
        Type: graphql.String,
      },
      "password": &graphql.Field{
        Type: graphql.String,
      },
    },
  },
)

func generateUsers() []User {
  user := User{
    Id: 1,
    Name: "キズナアイ",
    Email: "ai@kizuna",
    Password: "kizunAI",
  }
  var users []User
  users = append(users, user)
  return users
}

func main() {
  users := generateUsers()

  fields := graphql.Fields{
    "user": &graphql.Field{
      Type: userType,
      Description: "Fetch user by Id",
      Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
          Type: graphql.Int,
        },
      },
      Resolve: func(param graphql.ResolveParams) (interface{}, error) {
        id, ok := param.Args["id"].(int)
        if ok {
          for _, user := range users {
            if int(user.Id) == id {
              return user, nil
            }
          }
        }
        return nil, nil
      },
    },
    "list": &graphql.Field{
      Type: graphql.NewList(userType),
      Description: "Fetch users list",
      Resolve: func(params graphql.ResolveParams) (interface{}, error) {
        return users, nil
      },
    },
  }
  rootQuery := graphql.ObjectConfig{
    Name: "RootQuery",
    Fields: fields,
  }
  schemaConfig := graphql.SchemaConfig{
    Query: graphql.NewObject(rootQuery),
  }
  schema, err := graphql.NewSchema(schemaConfig)
  if err != nil {
    log.Fatalf("failed to create new schema, error: %v", err)
  }

  request := `
    {
      user(id: 1) {
        id
        name
        email
        password
      }
    }
  `

  params := graphql.Params{
    Schema: schema,
    RequestString: request,
  }
  r := graphql.Do(params)
  if len(r.Errors) > 0 {
    log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
  }
  rJSON, _ := json.Marshal(r)
  fmt.Printf("%s \n", rJSON)
}

これ参考になった
Go GraphQL Beginners Tutorial

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