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

TruffleとVue.jsとTypeScriptでブロックチェーンアプリを作ってみる

自分の備忘録としてDappの作り方の手順をまとめてみました。 最近やり始めたので間違いもあるかもしれません。ご承知ください。

環境構築

Macでやっています。

solidityのインストール

$ brew install solidity

truffleのインストールとプロジェクトの作成

$ npm install -g truffle
$ mkdir project && cd project
$ truffle init
$ vue create front

vue create時にtypescriptを選択します。

*****現状の環境*****

$ node -v
v11.9.0
$ npm -v
6.5.0
$ vue -V
3.3.0
$ solc --version
solc, the solidity compiler commandline interface
Version: 0.5.3+commit.10d17f24.Darwin.appleclang
$ truffle version
Truffle v5.0.3 (core: 5.0.3)
Solidity v0.5.0 (solc-js)
Node v11.9.0

macOS Mojave version10.14

truffleプロジェクトの構造

上記に従ってプロジェクトの作成を行うと、以下のような構造を持つディレクトリが作られます。

-----contracts
 |---migrations
 |---test
 |---truffle-config.js
 |---front

ディレクトリの構造を理解するために、Dapp開発の流れを説明します。

Dapp開発の手順

  1. contractディレクトリにsolファイルを作成してコントラクトコードを書く。
  2. migrationディレクトリにデプロイのためのjsファイルを作成。
  3. testディレクトリにコントラクトコードをテストするファイルを作成。
  4. ganacheの導入
  5. terminalでtruffle migrateを行い、プライベートチェーン上にコントラクトをデプロイ
  6. フロント側でweb3.jsの設定を行う
  7. metamaskの導入
  8. 動作確認

contractディレクトリにsolファイルを作成してコントラクトコードを書く。

 solファイルはsolidityで書かれたファイルのことです。このファイルにコントラクトを書き込んでいきます。  コントラクトはブロックチェーンのネットワーク上で利用できるアカウントであり、このコントラクト上で実行されるコードをコントラクトコードと呼びます。  ここではブロックチェーンに関する解説は省きますが、ブロックチェーンのネットワークには送金などを行うアカウントコードとして実行されるアカウントが存在し、Dappでは後者を利用されています。

// Dogs.sol

pragma solidity ^0.5.0;

contract Dogs {
  // 犬のインスタンスを作っていきます
  struct Dog {
    uint id;
    string name;
  }

  // mappingはいわゆる連想配列のこと。ここではuint(id)をDogインスタンスに割り当てている。
  mapping(uint => Dog) public dogs; // Dogインスタンスを格納
  uint public dogsCount; // インスタンス作成時のidとして利用(毎インスタンス生成時に+1していく)

  constructor() public { // 初期インスタンス生成
    createProgress("pochi");
  }

  function createProgress(string memory _name) private {
    dogsCount++;
    dogs[dogsCount] = Dog(dogsCount, _name);
  }
}

migrationディレクトリにデプロイのためのjsファイルを作成。

migrationディレクトリには、deployのためのファイルを作成します。

// migration/2_deploy_contract.js

let Dogs = artifacts.require("./Dogs.sol");
module.exports = function(deployer) { deployer.deploy(Dogs); };

上記のようにファイルを作成し、以下のコマンドを実行すると、contractsディレクトリに作成されたコントラクトをブロックチェーン上にデプロイします。

$ truffle migrate

また、今後開発を行なっていく上で、一度デプロイしたコントラクトコードを修正したいと思うことがあると思います。 その時は以下のコマンドを実行して新しいコントラクトコードでデプロイし直します。

$ truffle migrate --reset

railsをやったことがある人であれば、db:migrate:resetと同じような感じで考えていただければ良いと思います。

testディレクトリにコントラクトコードをテストするファイルを作成。

migrate --resetでやり直せるにしても、ブロックチェーンにデプロイされたものは修正が効かず、それゆえに改竄不可能性を持つわけですが、そうなるとバグや予想しない動作を含むものをデプロイするわけにはいきません。 こういった不具合を未然に防ぐためにもコントラクトコードのテストを行う方が良いです。 テストはsolidityでもjavascriptでも書くことができます。

// 飽くまで例です
// jsの場合
const Dogs = artifacts.require("./Dogs.sol");
contract("Dogs", function(accounts) {
  it("initializes with a dog", function() {
    return Dogs.deployed().then(function(instance) {
      return instance.dogsCount();
    }).then(function(count) {
      assert.equal(count, 1); // solファイルのconstructorで作成したデフォルトのインスタンス
    });
  });
});

ganacheの導入

ganache ganacheはプライベートチェーン上に自動的に模擬アカウントを作成してくれるツールです。 以下のリンクからインストールできます。 https://truffleframework.com/ganache

ganacheを起動させてからコントラクトコードをプライベートチェーンにデプロイしてあげると、ganacheで生成されたアカウントがマイニングを行なってくれるので、すぐにコントラクトコードを実行できるようになります。

terminalでtruffle migrateを行い、プライベートチェーン上にコントラクトをデプロイ

ganacheを起動させた状態で、且つ、contractsディレクトリにsolidityファイル、migrationsディレクトリにdeploy用のjsファイル(testのファイルはここでの動作に影響はありませんが、soldityファイルにエラーがあればデプロイができなくなります)があることを確認してください。 その上で次のコマンドを実行してください。

$ truffle migrate

1_initial_migration.js
======================

   Replacing 'Migrations'
   ----------------------
   > transaction hash:    0xc3006c7464bb1e931d818226170e743976ca068ce15c92c0ad074f6a2685f71c
   > Blocks: 0            Seconds: 0
   > contract address:    0x8bBDf86104324ff2bA12d34F4D9D38eEe9e83748
   > account:             0x0f85Ddf6555a85e7bfb107faD34Fc13DF63475A4
   > balance:             99.92727632
   > gas used:            284908
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00569816 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00569816 ETH


2_deploy_contract.js
====================

   Replacing 'Dogs'
   ------------------
   > transaction hash:    0xb2e38fc03105dc5089418a24ede9f54559ca78264ce2ab9753f521e2338ee50f
   > Blocks: 0            Seconds: 0
   > contract address:    0x1EEB22378Ae20b4c18179E46F9C89D11B1f8Aa57
   > account:             0x0f85Ddf6555a85e7bfb107faD34Fc13DF63475A4
   > balance:             99.90698292
   > gas used:            972636
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.01945272 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.01945272 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0.02515088 ETH

デプロイができれば上記のようになります。

他の方法でデプロイができていることを確認するには、

$ truffle console

でtruffleのコンソールに入って、以下のようにコントラクトコードを実行してみることができます。

$ truffle console
truffle(ganache)> Dogs.deployed().then(function(dogs) { app = dogs })
undefined
truffle(ganache)> app.dogs(1).then(function(d) { dog = d })
undefined
truggle(ganache)> dog[1]
'pochi'

Dogs.deployed().then(function(dogs) { app = dogs })の部分で、appにdogsコントラクトを渡し、そのappを使って、app.dogs(1)のように、コントラクトコード上に定義したmappingにidを渡して該当のインスタンスを抜き出します。 最後にdog = dの部分で抜き出したインスタンスを変数に格納し、最後にそれの[1]番目の要素、つまりnameを参照しています。

このように名前を参照することができればデプロイできている証拠です。 次にフロント側の設定を行なっていきます。

フロント側でweb3.jsの設定を行う

それでは今度はフロント側を実装していきます。 ここではvue.jsとtypescriptを使っていきます。

まず、vue createでプロジェクトを作成してください。 作成時にtypescriptを利用する選択を行なってください。 csscssプリプロセッサを使うことで楽に記述できるようになるのでおすすめです。

$ vue create frontend

プロジェクトが作成されれば、中に入ってweb3とtruffle-contractのモジュールをインストールします。

$ npm install --save-dev web3@1.0.0-beta.37 truffle-contract

ここではweb3のバージョンを指定していますが、私の環境ではweb3だけでnpm installすると、このあと紹介するmetamaskというツールを利用した時にエラーが発生したので、これはその対処として行なっています。

vueとtypescriptの説明は行わないので、コンポーネントのファイルに書くブロックチェーンとやりとりするためのコードを解説します。 el-系のinputタグやbuttonタグはelementUIをインストールすると利用できるようになります。非常に使いやすいのでお勧めです。 https://element.eleme.io/#/en-US

*templateにはpugを利用しています。npm install --save-dev pug pug-plain-loaderで使えます。

<template lang="pug">
  #Contract
    .list(v-for="dog in dogs")
      p {{ dog.name }}
    .form
      el-input(v-model="name")
      el-button(@click="createDog") 作成
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Web3 from 'web3'
import TruffleContract from 'truffle-contract'
import artifacts from '../../../build/contracts/Dogs.json' // デプロイするとbuild/contracts配下に作成されます。
import $ from 'jquery'
const Dogs = TruffleContract(artifacts)

@Component({ components: { Dogs } })
export default class ProgressChainIndex extends Vue {
  private ownAddress: string = ""
  private name: string = ""
  private dogs: Array<object> = []

  created() {
    if (typeof web3 !== 'undefined') {
      web3 = new Web3(web3.currentProvider)
    } else {
      console.warn('No web3 detected.')
      web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:7545'))
    }

    Dogs.setProvider(web3.currentProvider)
    web3.eth.getCoinbase().then((account) => {
      this.ownerAddress = account
      Dogs.defaults({ from: account })
    });
  }

  beforeMount() {
    // ここでブロックチェーン上に記録されたデータを取得して画面上に表示する
    let dogsInstance
    let self = this
    Dogs.deployed().then(function(instance) {
      dogsInstance = instance
      return dogsInstance.dogsCount()
    }).then(function(dogsCount) {
      for (let i = 1; i <= progressId; i++) {
        // ブロックチェーン上のdogインスタンスを一つ一つ拾ってきて、そのidとnameをまとめたオブジェクトを作成、配列dogsに格納する
        dogsInstance.progresses(i).then(function(dog) {
          self.dogs.push({
            id: dog[0],
            name: dog[1]
          })
        })
      }
    }).catch(function(err) {
      console.warn(err)
    })
  }

  createDog() {
    // ブロックチェーンのコントラクトコードにインスタンスを記録
    let self = this
    Dogs.deployed().then(function(instance) {
      return instance.createProgress(
        self.name,
        { from: self.ownerAddress })
    }).catch(function(err) {
      console.error(err)
    })
  }
}
</script>

このコンポーネントを好きなように親コンポーネントに組み込んであげてください。

metamaskの導入

metamask metamaskはブラウザとブロックチェーンを繋げるツールです。 このツールが実行されていれば、先ほどのコード上でweb3を認識することができます。 https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=ja

アプリケーションの実行

上記のようにコードを作成し、vueのディレクトリでnpm run serveを行います。 localhost:8080を開き、コンポーネントで配置したinputにdogの名前を入力して、作成ボタンを押します。 すると、ブロックチェーンへのアクセスを承認するか拒否するかの選択が表示されるので、承認ボタンを押しましょう。 承認後、作成したインスタンスをブラウザ上に反映させるためには一度リロードを行う必要があります。

また、ここでエラーが発生するようであれば、コードに何らかの原因があるか、ganacheやmetamaskの問題が考えられます。 ganacheの再起動やmetamaskでアカウントのリセットを行うと改善されることがあるので、その辺を確認してみましょう。

以下の動画は非常に参考になります。実際に動かしながら説明しているのでこのページを読むより楽かもしれません。 https://www.youtube.com/watch?v=3681ZYbDSSk

Vue.jsとExpress.jsでSPAを作ってみる

フロントエンドにvue.jsを利用し、バックエンドにexpress.jsを利用しました。

前提

私の環境ではそれぞれのバージョンは以下のようになっています。 一応OSはmacOS mojave バージョン10.14です(2019年1月の時点での状態です)。

$ node -v
v10.1.0
$ npm -v
5.6.0
$ vue -V
2.9.3

話す順番

大まかにここでの解説を順序立てします。

  1. フロント側の作業ディレクトリの作成
  2. サーバー側の作業ディレクトリを作成
  3. フロント側からサーバー側へリクエストを送りレスポンスを受け取る

まず、フロント側とサーバー側でディレクトリを分けます。 フロント側でvue initし、サーバー側ではnpm initでサーバー処理を書いていきます。 つまり、フロント側で作成したページからサーバー側へ通信するようにするわけです。

ですが、この作業を何も考えずに行うと、CORSポリシーに抵触することになり、ブラウザでエラーが発生します。 そのため、サーバー側のjsファイルでcorsというモジュールを利用します。 https://www.npmjs.com/package/cors

ということで作っていく

フロント側の作業ディレクトリの作成

フロント側とサーバー側でディレクトリを作成し、それぞれが互いにやり取りを行うようにします。 そのため、それらをまとめるディレクトリを作成すると、整理されて作業がしやすいと思います。

$ mkdir vue_application

次に、vueプロジェクトを作成します。 Install vue-router?はyesにします。ESlintやtest等はここではnoにしました。

$ cd vue_application
$ vue init webpack frontend
$ cd frontend
$ npm run dev

サーバー側の作業ディレクトリの作成

expressを利用しますが、express-generator(下記リンク参照)でプロジェクトを作成すると、ごちゃごちゃしてしまうのでここでは簡便に行います。 https://expressjs.com/ja/starter/generator.html

$ cd vue_application
$ mkdir backend
$ cd backend
$ npm init
$ npm install --save nodemon express body-parser cors

npm init時に作成されたpackage.jsonのscriptsの部分を編集します。 nodemonを利用することで、変更が加えられたファイルが検知されると自動的にプロセスを再起動させることができるようになります。 https://qiita.com/twipg/items/cb969b335d66c4aee690

# package.json

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "./node_modules/nodemon/bin/nodemon.js index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.18.3",
    "cors": "^2.8.5",
    "express": "^4.16.4",
    "nodemon": "^1.18.9"
  }
}

index.jsを作成してここに処理を書いていきます。 package.jsonのmainのファイル名と同じファイルを作成していますが、異なる名前のファイルを作成しても良い様子です。 package.jsonについては以下を参考にしてみてください。 https://qiita.com/dondoko-susumu/items/cf252bd6494412ed7847

$ touch index.js
// backend/index.js

const express = require('express')
const bodyParser = require('body-parser')
// corsポリシーに抵触するため、その対策としてcorsを利用する
const cors = require('cors')

const app = express()
app.use(bodyParser.json())
app.use(cors())

app.get('/test', function(req, res) {
  res.send({
    message: 'Hello world!'
  })
})

app.listen(process.env.PORT || 3000)

この時点でnpm startを行い、http://localhost:3000/test にアクセスすると、以下の画面が表示されると思います。 スクリーンショット 0031-01-13 午前5.28.05.png

サーバー側でのapiの準備は基本的に上記のように行います。

フロント側からサーバー側へリクエストを送りレスポンスを受け取る

$ cd vue_application/frontend
$ npm install --save axios

axiosはnode.jsのHTTPクライアントです。 https://github.com/axios/axios これを用いてapi通信を行います。

次に、frontendの同じレベルにあるsrcディレクトリ内にapi通信を行うためのファイルを作成します(わかりやすくapiフォルダ内に作成します)。

$ cd src
$ mkdir api
$ touch api/index.js

まず、index.jsでapi通信を行うためのaxiosの設定をインスタンス化します。

// api/index.js

import axios from 'axios'

export default () => {
  return axios.create({
    baseURL: `http://localhost:3000/`
  })
}

そして、index.jsで作成したインスタンスを利用してpostを行うメソッドを作成します。 ここではテストとして定数itemにハッシュでtextを与えます。

// api/methods.js

import Api from './index'

export default {
  testPosting () {
    const item = { text: 'Success!' }
    return Api().post('/test', item)
  }
  // 他の処理も追加可能
}

これでhttp://localhost:3000/test  にpostを行うための準備が整いました。 次はmethods.jsで定義したpostメソッドをUIのボタンと連動させ、ボタンが押されるとhttp://localhost:3000 にtextの値が送信されるようにします。

既存のHelloWorld.vueを以下のように書き換えます。

// frontend/src/components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <!-- ごちゃごちゃしていたのを全て消して、ボタンを配置 -->
    <button @click='post'>click me</button>
  </div>
</template>

<script>
import Methods from '@/api/methods'

export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  methods: {
    // サーバーから返ってくる値をログに出力したいのでasyncとawaitを行う
    async post() {
      let response = await Methods.testPosting()
      console.log(response)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

また、backend/index.jsに作成したapiはgetとなっていたので、これをpostに書き換える必要があります。 送られてきたtextはreq.body.textで取得できます。

// backend/index.js

app.get('/test', function(req, res) {
  res.send({
    message: 'Hello world!'
  })
})

// 以下に書き換え

app.post('/test', function(req, res) {
  res.send({
    message: req.body.text
  })
})

これでようやくフロント側とサーバー側の通信を行うことができます。 フロント側でnpm run dev、サーバー側でnpm startを行い、http://localhost:8080 (vue側のポート)へアクセスしましょう。

以下のような画面が表示されるので、ボタンを押します。 すると、consoleに事前にセットしておいた、'Success!'の文字がdata.messageとして表示されるはずです。 スクリーンショット 0031-01-13 午後3.48.21.png

今回はボタンを押すことでpostを行うように実装しましたが、基本的にボタンでpostを行うなどということは普通しないと思います。 vueファイルにinputタグを設置し、その配下に@click='submit'のような要素を持つbuttonタグを配置することで、UI上で入力した任意の値を渡すこともできますので試してみてください。

簡単に書くと以下のような感じになると思いますが。

// frontent/src/components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <input type='text' name='text' v-model='text'>
    <br>
    <button @click='post'>post</button>
  </div>
</template>

<script>
import Methods from '@/api/methods'

export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      text: ''
    }
  },
  methods: {
    async post() {
      let element = { text: this.text }
      let response = await Methods.testPosting(element)
      console.log(response.data.message)
    }
  }
}
</script>
// frontend/src/api/methods.js

import Api from './index'

export default {
  testPosting(item) {
    return Api().post('/test', item)
  }
}

ここで作ったものをgithubにもあげましたのでご参考までに。 https://github.com/yutaro1204/vue_client_and_express_server

以下の動画も参考になります。 https://www.youtube.com/watch?v=Fa4cRMaTDUI

Azure FaceAPIでアイドルの顔を見わけさせる

Azure FaceAPIの利用

AzureのFaceAPIを利用するには、まずAzureにアカウントを作成しなければなりません。 アカウント作成は無料で行えます。 https://azure.microsoft.com/ja-jp/

登録後、ポータルへ入り、リソースの作成を行ます。

スクリーンショット 0030-08-14 午後10.27.18.png

AI + Machine LearningからFace APIを選択し、 適当に項目を埋めて、作成します。

すこし時間を待っていると、作成したリソースがデプロイされますので、そこでようやくダッシュボードでリソースを選択することができるようになります。

リソースを選択し、Keysの項目をクリックすると、KEY1とKEY2が出てきます。 これらはあとでsubscription keyとして利用するので、どこかに控えておいても良いかもしれません(利用するのはKEY1で良いです)。

スクリーンショット 0030-08-14 午後10.31.43.png

faceAPIの仕組み

faceAPIにおいて人物の顔を登録し、それを学習させる過程としては、まず、人物の集合であるグループを定義する必要があります。

FACE APIについて

Person Groupの作成

PersonGroupにはこれから登録する顔が所属するグループを作成します。 たとえば、自分の好きなアイドルグループとか。

ここではpythonのHTTPライブラリであるrequestsを利用してPersonGroupを作成します。

# face_api.py

import requests

def createPersonGroup():
    result = requests.put(
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/persongroups/欅坂46',
        headers = {
            'Ocp-Apim-Subscription-Key': (subscription key for Azure FaceAPI)
        },
        json = {
            'name' = '欅坂46'
        }
    )

このようにfaceAPIのPersonGroup作成用のURLにputリクエストを出してあげると、resultにその結果が入ります。 その後print(result.text)をしてみると、作成に成功した場合、レスポンスのボディは空になって返ってきます。 エラーが起きた場合には、エラーの種類と原因が返ってきますので、それを参考に問題を解決すれば良いです。

PersonGroupを作成した後、今度はそのグループに属するPersonを登録していきます。

Document How to create PersonGroup

Personの登録

それでは作成したPersonGroupにPersonを登録していきます。 「欅坂46」グループに「長濱ねる」を作成する場合、 Person登録用のURLは https://[location].api.cognitive.microsoft.com/face/v1.0/persongroups/{personGroupId}/persons なので、

# face_api.py

import requests
import json

def createPerson(personName):
    result = requests.post{
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/persongroups/欅坂46/persons',
        headers = {
            'Ocp-Apim-Subscription-Key': (subscription key for Azure FaceAPI)
        },
        json = {
            'name': personName
        }
    }
    personId = json.loads(result.text)['personId'] # personのidを抽出できる
    return personId

createPerson('長濱ねる')

この次の工程では登録したpersonにその顔画像を、personその人として登録していくので、返ってくるpersonIdが必要になってきます。

Document How to create Person

Add Faceの工程

登録したpersonの顔写真を登録していきます。 おそらく、以下のような実装になるかと思います。

# face_api.py

import requests

def addFace(personId, imageUrl):
    requests.post(
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/persongroups/欅坂46/persons/' + personId + '/persistedFaces',
        headers = {
            'Ocp-Apim-Subscription-Key': (subscription key for Azure FaceAPI)
        },
        json = {
            'url': imageUrl
        }
    )

personId # createPerson()で得られたpersonId
imageUrl # personの画像url personその人しか写っていないものでないと、エラーになる
pictures = [imageUrl, imageUrl, imageUrl......]
for i in pictures:
    addFace(personId, i)

事前に取得しておいたpersonIdに、顔写真一枚一枚をそれぞれを登録していきます。

Document How to add face to a person

さて、ここまでの処理で学習させるための素材の準備が完了しました。 PersonGroupを作成し、そこに所属する人間と、その人間の写真を登録しましたので、これらを学習させてみましょう。 そうすることで、こちら側で用意した画像のurlを読み込ませて、そこに写る顔から、該当する人間の情報を返させるようにします。

Trainの実施

さきほどのaddFaceが終わった後、以下のメソッドを実行します。

# face_api.py

import requests

def trainGroup():
    requests.post(
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/persongroups/欅坂46/train',
        headers = {
            'Ocp-Apim-Subscription-Key': (subscription key for Azure FaceAPI)
        },
        json = {
            'personGroupId': '欅坂46'
        }
    )

これで学習が終了しました。 あっけなくて逆に物足りなさを感じるかもしれません。

Document How to train PersonGroup

それでは、今度はFaceAPIに、無作為で選んだ画像urlを渡し、それが「長濱ねる」かどうかを判定させましょう。 現状では「長濱ねる」だけを学習させていますので、他のメンバーも学習させて、その中から「長濱ねる」を当てられるようにしておきます。

# face_api.py

import requests

# 鈴本美愉の登録
personId = createPerson('鈴本美愉')
pictures = [imageUrl, imageUrl, imageUrl......]
for i in pictures:
    addFace(personId, i)

# 渡邉理佐の登録
personId = createPerson('渡邉理佐')
pictures = [imageUrl, imageUrl, imageUrl......]
for i in pictures:
    addFace(personId, i)

# グループの学習
trainGroup()

画像から顔を判別してPersonGroupの中から同一の顔の持ち主を探す

判別させたい画像のurlを用意します。

# face_api.py

import requests
import json

imageUrl = 'http://cdn.keyakizaka46.com/images/14/752/02396f50b62224f8b6b686b1cffcb/400_320_102400.jpg'

def detectFace(imageUrl):
    result = requests.post(
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/detect',
        headers = {
            'Ocp-Apim-Subscription-Key': (subscription key for Azure FaceAPI)
        },
        json = {
            'url': imageUrl
        }
    )
    detectedFaceId = json.loads(result.text)[0]['faceId']
    return detectedFaceId # 画像から取得された顔のid

def identifyPerson(detectedFaceId):
    result = requests.post(
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/identify',
        headers = {
            'Ocp-Apim-Subscription-Key': (subscription key for Azure FaceAPI)
        },
        json = {
            'faceIds': [detectedFaceId],
            'personGroupId': '欅坂46'
        }
    )
    identifiedPerson = json.loads(result.text)[0]['candidates']
    return identifiedPerson # detectedFaceIdをから抽出されたcandidatesを格納

detectedFaceId = detectFace(imageUrl)
identifiedPerson = identifyPerson(detectedFaceId)

print(identifiedPerson)
# [{'personId': '*********************************', 'confidence': 0.68672}] というような形で画像の顔と一致した人物のデータを返す

まず、detectFace()に画像のurlを渡し、その画像から読み取られた顔のidを手に入れます。 次にそのidをidentifyPerson()に渡し、PersonGroup内に登録した人物の顔とdetect時に読み込ませた画像の顔を比較します。 最後に、その結果得られた候補者(candidates)のidを抽出します。

この後、このcandidateのidはそのpersonの名前を抽出するために利用します。

Document How to detect the registered person's face How to identify the person

特定されたpersonIdからpersonの情報を取得する

最後の工程です。 identifyPerson()から取得したpersonIdを利用して、そのidの持ち主の名前を抽出します。

# face_api.py
import requests
import json

def getPersonNameByPersonId(personId):
    result = requests.get(
        'https://japaneast.api.cognitive.microsoft.com/face/v1.0/persongroups/欅坂46/persons',
        headers = {
        "Ocp-Apim-Subscription-Key": (subscription key for Azure FaceAPI)
        },
        json = {
            'personGroupId': '欅坂46'
        }
    )
    # personsには登録された全てのpersonが入っている
    persons = json.loads(result.text)
    for person in persons:
        # 渡されたpersonIdと合致するidを持つpersonを抽出して、その名前を返す
        if person['personId'] == personId:
            return person['name']

identifiedPersonName = getPersonNameByPersonId(personId)
print(identifiedPersonName) # => 渡邉理佐

getPersonNameByPersonId()でPersonGroupに登録した人物の情報を全て抜き出してから、 あらかじめ渡しておいたpersonIdと同じidを持つ人物だけを抜き出します。 returnしたperson['name']の結果が正しい結果かどうかを確認しましょう。 FaceAPIも完璧ではないでしょうから、時には間違った答えを返すこともあると思いますが、基本的には正しい答えを返してくれると思います。 実装が間違っていなければ、基本的には。。。

ここで行ったこと

だいたいの流れをまとめます。 とにかくこの流れさえわかっておけば、間違えることはないと思うので。

#####学習工程#####
・PersonGroupの作成
・Personの登録
・PersonにFaceを登録
・PersonGroupを学習
#####判別工程#####
・特定の画像からFaceをdetect
・detectした人物の画像から、それに該当する人物のidを抽出
・idを基にその人物の名前を抽出

あと、一貫したソースコードも載せておきます。

# face_api.py

#-*-coding:utf8-*-
import json
import logging
import requests
import time

BASE_URL = 'https://japaneast.api.cognitive.microsoft.com/face/v1.0/'
SUBSCRIPTION_KEY = 'your subscription key'
GROUP_NAME = '欅坂46'

def createPersonGroup():
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME
    requests.put(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'name': GROUP_NAME
        }
    )

def createPerson(personName):
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME + '/persons'
    result = requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'name': personName
        }
    )
    personId = json.loads(result.text)['personId']
    return personId

def addFace(personId, imageUrl):
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME + '/persons/' + personId  + '/persistedFaces'
    requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'url': imageUrl
        }
    )

def trainGroup():
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME + '/train'
    requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'personGroupId': GROUP_NAME
        }
    )

def detectFace(imageUrl):
    end_point = BASE_URL + "detect"
    result = requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'url': imageUrl
        }
    )
    detectedFaceId = json.loads(result.text)[0]['faceId']
    return detectedFaceId

def identifyPerson(faceId):
    end_point = BASE_URL + 'identify'
    faceIds = [faceId]
    result = requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'faceIds': faceIds,
            'personGroupId': GROUP_NAME
        }
    )
    candidates = json.loads(result.text)[0]['candidates']
    return candidates

def getPersonNameByPersonId(personId):
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME + '/persons'
    result = requests.get(
        end_point,
        headers = {
        "Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY
        },
        json = {
            'personGroupId': GROUP_NAME
        }
    )
    persons = json.loads(result.text)
    for person in persons:
        if person['personId'] == personId:
            return person['name']

if __name__ == '__main__':
    # learning face
    createPersonGroup()
    # neru.txtに書き込まれた画像urlを配列にしてsourcesに格納
    sources = open('neru.txt').read().split('\n')
    personId = createPerson('長濱ねる')
    for i in sources:
        addFace(personId, i)
    # addFace()処理を待ってからtrainGroup()をしたいので、応急処置的にsleepしてます。。。
    # promise的なの使って改善すべき?? ちょっとこの辺まだよくわかってないです m(__)m
    time.sleep(10)
    trainGroup()

    # identify person
    imageUrl = 'https://scontent-nrt1-1.cdninstagram.com/vp/e714b56e8acc9001bd6e9c938cd738b5/5BDED0D0/t51.2885-15/e35/26180878_1782452602056444_9161322227717636096_n.jpg'
    detectedFaceId = detectFace(imageUrl)
    identifiedPerson = identifyPerson(detectedFaceId)
    if identifiedPerson[0]['personId']:
        personId = identifiedPerson[0]['personId']
        personName = getPersonNameByPersonId(personId)
        print(personName)

neru.txtには画像urlを1行ずつ書き込んでおく

http://48pedia.org/images/e/ee/2018%E5%B9%B4%E6%AC%85%E5%9D%8246%E3%83%97%E3%83%AD%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB_%E9%95%B7%E6%BF%B1%E3%81%AD%E3%82%8B_2.jpg
https://i0.wp.com/free-style-info.com/wp-content/uploads/2017/12/neru008-20171216-174300.jpg?resize=400%2C600&ssl=1
https://dreaming-baby.com/wp-content/uploads/2017/12/ac2eea4df1b0b02ac7bcdbf1a9f0479b.jpg
http://livedoor.blogimg.jp/fumichen2/imgs/1/1/1192251d.jpg
https://rr.img.naver.jp/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20160918%2F79%2F7753109%2F205%2F853x1280xd8fa341ed662577f5d29ba9.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r

参考にしたもの

  1. Getting Started with Face API in Python Tutorial
  2. Azure face APIで遊んでみよう その2~顔の認識
  3. AIで似ているAV女優を紹介しているスケベAI「スケベ博士」を作りました。

特にdaiさんのnoteは参考になりました。 qiitaに書いておきたいなと思い始めたのも、daiさんのnoteを読んで、自分で画像認識を実装してからでした。 正直なところ、daiさんのだけではわからなかったり、抜けていると感じたところがあったので、そういった点も含めてFaceAPIの使い方を残しておこうと思いました。 参考になれば嬉しいです。

間違った点や改善すべき点などがあればご指摘ください。

Express.jsサーバでSequelize.jsを使ってMySQLデータベースとやりとりさせる

Nodeにはrailsのようにデータベースを管理できるモジュールであるsequelizeがあります。 expressで開発をやっていると、railsがどれだけ便利だったのかを思い知らされますね笑

expressでアプリケーション開発

https://gist.github.com/mitsuruog/fc48397a8e80f051a145

ざっと説明すると、

$ npm install -g express-generator
$ express --view=pug your-app-name
$ cd your-app-name
$ npm install
$ npm start

云々、ということです。

mysqlの導入

この前expressでpassport認証する際にmysqlを導入する内容を書いたのでそっちをみてみてください。 https://qiita.com/y4u0t2a1r0/items/db6c39e9dcea21f8e994 ググればすぐです。

sequelize

cliで利用するためにsequelize-cliをグローバルでインストールします。

$ npm install -g sequelize-cli
$ npm install --save sequelize

仕事場の環境上、windowsで仮想環境(vagrant)を立ち上げて開発を行なっていたのですが、 windowsの場合、シンボリックリンクが貼れずにnpm installがうまくいかないことがあるので、 もしnpm installでエラーが発生したら、

$ npm install -g sequelize-cli --no-bin-links
$ npm install --save sequelize --no-bin-links

という感じで--no-bin-linksをつけてあげてください。 http://eiua-memo.tumblr.com/post/117361529158/npmvagrantvagrant%E3%81%AE%E5%85%B1%E6%9C%89%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E4%B8%8A%E3%81%A7npm

で、うまくインストールできたら、railsチックにデータベースを作成していきましょう!

$ sequelize init

initしてあげると、アプリケーションのディレクトリに、 config, migration, model の3つのディレクトリが作成されます。

migration, modelのディレクトリにはまだ何も入っていませんが、 configディレクトリ内にはconfig.jsonなるものがあります。

# config.json

{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

こんな感じになっているので、利用する用途に合わせて、 mysqlのパスワードや利用するデータベースの名前(任意)、ホスト(ローカルホストならlocalhost)を書いてあげます。

 まずはデータベースを作成しましょう。

$ sequelize db:create

これで先ほどconfig.jsonに書き込んだdatabaseを作成します。 次のコマンドたちもよく使いますのでメモ。

$ sequelize db:drop # dbを落とす
$ sequelize db:create # dbを作成
$ sequelize db:migrate # migrate実行
$ sequelize db:migrate:undo:all # 実行されたmigrateを全て取り消し
$ sequelize db:seed:all # 設定されていたseedファイルをmigrate
$ seqeulize db:seed:undo:all # seedファイルのmigrateを全て取り消し

次に、例としてusername(type=string), age(type=integer)のカラムを持つuserテーブルを作成します。

$ sequelize model:create --underscored --name user --attributes "user_name:string,age:integer"

railsみたいにmodelを作成できて、最初感動しました笑 ですが、sequelizeではrailsのように外部キーなどをcli上で指定することはないっぽいです。 基本的にmodel:createで作成されるmigrationファイルに外部制約などを設定してdb:migrateする感じですね。

// app/migrations/migration_file.js

user_id: {
  allowNull: false,
  references: {
    model: 'users',
    key: 'id'
  },
  type: Sequelize.INTEGER
}

上のコードはuserの子テーブルに定義されたuser_idカラムの設定を想定しています。 references:{}の中に親のmodelとその参照するkeyを設定します。

associationについて、公式で色々読めるので細かいところはそっちを参考にしてみてください。 http://docs.sequelizejs.com/manual/tutorial/associations.html

多対多のテーブルを作成する場合も、

$ sequelize model:create --underscored --name communities_users --attributes "community_id:integer, user_id:integer"

このように外部キーであるカラムを用意しておいて、migrationファイル内にreferencesを設定していきます。 (例としてusersを複数要するcommunitiesとcommunitiesを複数要するusersの中間テーブルを考えています) modelにも、hasManyやbelongsToなどを定義してあげれば良いです。 中間テーブルの場合throughで関係を持たせてあげればオッケーです。

この点も公式のassociationの項目を参考にしてみてください。 http://docs.sequelizejs.com/manual/tutorial/associations.html#belongsto

sequelizeでCRUD

mysqlをexpressで使うと、

// app.js

let mysql = require('mysql2');
let connection = mysql.createConnection(
  host: 'localhost',
  user: 'root',
  password: '********',
  database: 'testdb'
});

connection.query('select * from users;', function(err, users) {
  console.log(users); // queryの結果が返ってくる
}

みたいな感じのを書いて、 select, insert, update, deleteを行うことになりますが、 sequelizeではこのようになります。↓

// app.js

let db = require('./models/index'); // cliでinitした時に作成されるmodels配下のindex.js

// findAll
db.users.findAll({}).then((instances) => { // usersのところが自分で作成したモデル
  console.log(instances); // usersの中身を全て取得した結果
});

// create
db.users.create({
  username: '#####',
  email: '#####@#####'
}).then((createdUser) => {
  console.log(createdUser); // 作成されたuserインスタンスの詳細
});

こんな感じでデータベース内の要素を全取得したり、作成保存したり、更新削除することができます。

ajaxでフロントからデータを飛ばして、

// front.js

$.ajax({
  url: 'users/new',
  type: 'post'
}).done((data) => { // data = サーバーから返ってきたデータ
  console.log(data);
}).fail((err) => {
  console.log(err);
});
// app.js

router.post('/new', function(req, res, next) {
  db.users.create({
    username: '#####',
    email: '#####@#####'
  }).then((createdUser) => {
    res.send(createdUser); // ajaxのdoneにdataを渡す
  }).catch((err) => {
    res.status(500).send(err);
  });
});

こんな感じにサーバーでデータベースを更新するっていうのが基本的な使い方でしょうか。

Express.jsとMySQLを使って認証機能を実装する

expressでは、passportの認証機能を利用するのが便利です。 twitterfacebookなどとも連携できます。 ここではローカルでの認証について、アウトプットとしてまとめました。

expressアプリの作成

# terminal

$ npm install -g express-generator
$ express --view=pug your-app-name
$ cd your-app-name
$ npm install

以降、

# terminal

$ npm start

でアプリケーションを起動させます。 http://localhost:3000

必要項目のインストール

# terminal

$ npm install --save passport
$ npm install --save passport-local
$ npm install --save express-session

ログイン実装

// app.js

// 既に色々入っているので、そこへ追加
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var session = require('express-session');

// signinページの追加
var signinRouter = require('.routes/signin');
app.use('/signin');

// session, passport.initialize, passport.sessionは以下の順番で追加
app.use(session({
  secret: "testing",
  resave: false,
  saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());

// authentication
passport.serializeUser(function(username, done) {
  console.log('serializeUser');
  done(null, username);
});

passport.deserializeUser(function(username, done) {
  console.log('deserializeUser');
  done(null, {name:username});
});

passport.use(new LocalStrategy(
  {
    // signinのformで定義したnameの要素をセット
    usernameField: "username",
    passwordField: "password"
  },
  function(username, password, done){
    // ここでは、データベースを使わずに、仮にusernameとpasswordを固定で入れています。
    if(username == "test" && password == 123456789){
      return done(null, username);
    }
    return done(null, false, {message: "invalid"});
  }
));

app.post('/signin',
  passport.authenticate('local',
    {
      failureRedirect: "/signin"
    }
  ),
  function(req, res, next){
    // res.redirect("/")でreq.userが渡せなかったので、ここでfetchを使っています。
    // https://github.com/jaredhanson/passport/issues/244
    // fetchは以下のようにインストール
    // npm install --save isomorphic-fetch
    // var fetch = require('isomorphic-fetch');
    fetch("http://localhost:3000/signin",
      {
        credentials: "include"
      }
    ).then(function(){
      res.redirect("/");
    }).catch(function(e){
      console.log(e);
    });
  }
);

で、index.pugへリダイレクトした時に、そのページ上にログインしたユーザーの名前を表示します。

// index.js

var express = require('express');
var router = express.Router();

// index.pugをgetした時にコールバックを実行
router.get('/', function(req, res, next) {
  console.log(req.user); // {name:'test'}
  if (req.user) { // ログインしているユーザーが存在する場合のみ有効
    res.render("index", { username: req.user.name});
  } else { // ユーザーが存在しなければ、サインインページへ飛ばされる
    res.redirect('/signin');
  }
});

module.exports = router;
// index.pug

extends layout

block content
  h1= username
  p Welcome to Express

こんな感じでtestの文字が出てきます。 Screen Shot 2018-07-18 at 23.15.22.png

signin.jsはindex.jsをコピペしてsignin用に書き換えるだけで良いです。

// signin.js

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
  res.render("signin", {});
});

module.exports = router;

一応signin.pugも

// signin.pug

extends layout

block content
  form(action="/signin" method="post")
    .username
      label username
      input(type="text" name="username") // nameはnew LocalStrategyのところで使います。
    .password
      label password
      input(type="password" name="password") // nameはnew LocalStrategyのところで使います。
    .signin
      input(type="submit")

ログイン時にconsole.log('serializeUser')とconsole.log('deserializeUser')が実行されていることを確認しましょう。 serializeUserだけがコンソールで出力されていてもdeserializeUserが出力されていなければ、ログインは成功していません。 その時は、app.use時のsession、passport.initilizer、passport.sessionを書く順番がおかしくなっていないかとか、誤字脱字等がないかどうかとか、チェックしていってください。

MySQLのユーザー情報を基にログイン

mysqlで適当にデータベースを作ります。

# terminal

mysql> create table users (id int not null primary key, username varchar(20), password int);

とかにして、

# terminal

mysql> insert into users values (1, 'test', 123456789);

みたいにデータを入れておきます。 次にapp.jsでmysqlを使い、データを取り出してログイン時に入力された値と照合させるようにします。

// app.js

// npm install --save mysql2
var mysql = require('mysql2');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'your-password', // mysqlの自分のパスワード
  database: 'testdb' // db名は自分で自由に作った名前を当てはめる
});

// 上で書き込んだpassport.useの部分を書き換えます。
passport.use(new LocalStrategy(
  {
    usernameField: "username",
    passwordField: "password"
  },
  function(username, password, done){
    connection.query("select * from users;", function(err, users) {
      // usernameもpasswordもユニーク前提
      var usernames = [];
      var passwords = [];
      for (i = 0; i < users.length; i++) {
        usernames.push(users[i].username);
        // input(type="password")で渡される値はstringのようなので、
        // データベースから取り出した値もstringにしています。
        var pw = users[i].password.toString();
        passwords.push(pw);
      }
      if (usernames.includes(username) && passwords.includes(password)) {
        return done(null, username);
      }
      return done(null, false, {message: "invalid"});
    });
  }
));

これで、ログイン時に ユーザー名: test パスワード: 123456789 を入れてあげると、ログインできるようになります。

サインアップは、サインアップのフォームを作って、post時に/signupへやってきた時のコールバックにロジックを書いてあげれば良いです。

// app.js

app.post('/signup', function(req, res, next){
  connection.query('select * from users;', function(err, users){
    connection.query('insert into users set ? ;', {
        username: req.body.username,
        email: req.body.email,
        password: req.body.password,
        created_at: new Date(),
        updated_at: new Date()
      },
      function(err){
        console.log("サインアップに関するエラー: " + err);
        res.redirect("/signin");
      }
    );
  });
});

まあ、こんな感じで。

サインアウトは、

// app.js

app.get('/signout', function(req, res, next) {
  req.logout();
  console.log('ログアウトしました');
  res.redirect('/');
});

これだけでOKです。 いい感じのところにサインアウトボタンを配置してあげてください。

Nuxt.jsで簡単なログイン機能をつくってみる

nuxt.jsでアプリを開発する案件があり、勉強することになったのですが、結局はnode.jsのexpressで開発することになったので、nuxtの知識を応用することがあまりできませんでした。 でもせっかく勉強したのでここら辺で一旦、気楽にアウトプットしておきたいと思います。 このエントリーはそんなノリで書かれています。

ログイン機能の実装

nuxtの使い方とかは省きます。 nodeの入れ方とか、vue-cliとかで調べるとすぐできます。

まずログインする画面です。

// pages/signin.vue

<template>
  <div class="login-form">
    <form @submit.prevent="login">
      <p class="error" v-if="error">{{ error }}</p>
      <p><input type="text" v-model="email" placeholder="email" name="email"/></p>
      <p><input type="text" v-model="password" placeholder="password" name="password"/></p>
      <div class="login-btn">
        <button type="submit">ログイン</button>
      </div>
    </form>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        error: null,
        email: "",
        password: "",
      }
    },
    methods: {
      async login() {
        try {
          await this.$store.dispatch("login", {
            email: this.email,
            password: this.password
          })
          this.$router.push("/")
        } catch(e) {
          this.error = e.message
        }
      }
    }
  }
</script>

一般的なログインフォームをhtmlで作成し、 script内で定義したデータにフォーム内で入力された内容が格納されます。

v-modelで指定しているのは、scriptでexport defaultをしている中の、 data(){return: ~}の中の要素です。

また、htmlのformタグに@submit.prevent="login"と設定しているので、 script内のmethodsに定義したメソッドlogin()がそのフォームの送信時(ログインのボタンを押した時)に起動します。

login()メソッドの中身の仕組みは以下の通り。 1. formに入力されたdataの要素をstoreへ送ります。 (this.$store.dispatchの部分) 2. awaitをしているので、1.が完了してから次の処理("/"のページへリダイレクト)へ移ります。エラーならcatch(e)の方の処理(エラーメッセージの表示)を行います。

要素の送り先であるstoreの構造は以下の画像を参考にしてみてください。

vuex.png

流れとしては、 1. 緑のVue Componentsというのが先に載せたsignin.vueの中身となります。 2. そこからdispatchして、storeにあるactions.js(にあるloginメソッド)に要素を渡します。 3. その後commitを行なってmutations.jsに同じ要素、または整形した要素を渡し、 4. 最終的にその内容がstateに反映されるようになります(mutate)。 5. 最後のrenderでstateをhtmlに適用することができるようになります。

// store/actions.js
export default {
  async login({ commit }, { email, password }) {
    console.log(commit)
    try {
      // ここでは仮に
      // メールアドレス: test@email
      // パスワード: 123456789
      // が登録されてある状態を想定しています。
      if (email != "test@email" || password != 123456789) {
        throw new Error("error!!!")
      }
      // 入力したメールアドレスとパスワードが
      // すでに登録されているメールアドレスとパスワードと一致した場合、変数dataに入力値が渡されます。
      let data = { email: email, password: password }
      // 変数dataのを次のmutations.jsにあるAUTHED_USERメソッドに渡します。
      commit("AUTHED_USER", data)
    } catch (e) {
      throw e
    }
  }
}
// store/mutations.js

export default {
  AUTHED_USER: function (state, data) {
    state.authUser = data // 入力したemailとpasswordがここに入る
  }
}

このactions.jsから渡ってきた値が格納される、state.authUserというのは、 store/index.jsのstateに定義されたauthUserを表しています。 この時のstore/index.jsは以下のようになります。

// store/index.js

import Vuex from "vuex"
import mutations from "./mutations"
import actions from "./actions"

const store = () => {
  return new Vuex.Store({
    state: {
      // ユーザーのログイン状況フラグ
      authUser: null
    },
      mutations,
      actions
    })
}
export default store

これでログイン機能はひとまず落ち着きました。 ですが、そもそも登録をしないでログインができるサービスなんてものはありませんから、今度は新規登録ページを実装していきましょう。

新規登録機能の実装

// pages/signup.vue

<template>
  <div class="signup-form">
    <form @submit.prevent="registration">
      <p class="error" v-if="error">{{ error }}</p>
      <p><input type="text" v-model="email" placeholder="email"/></p>
      <p><input type="text" v-model="password" placeholder="password"/></p>
      <div class="signup-btn">
        <button type="submit">Sign up</button>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      error: null,
      email: "",
      password: ""
    }
  },
  methods: {
    async registration() {
      try {
        await this.$store.dispatch("registration", {
          email: this.email,
          password: this.password
        })
        this.$router.push("/")
      } catch(e) {
        this.formError = e.message
      }
    }
  }
}
</script>

vueファイルは簡単にフォームを設け、 そこで入力された値がactions.jsに送られることになります。

# store/actions.js

async registration({ commit }, { email, password }) {
  try {
    // サインアップ時のvalidationチェック
    // @がなければ無効且つパスワードは5字以上10字以下の数字を必要とする
    if (email.includes("@") === false) {
      throw new Error("メールアドレスには@が含まれている必要があります。")
    } else if (password.length >= 10 || password.length <= 5) {
      throw new Error("パスワードは5文字以上10文字以下で作成してください。")
    }
    let data = { email: email, password: password }
    commit("REGISTER_USER", data)
  } catch(e) {
    throw e
  }
},
// 上で作成したlogin処理
async login({ commit }, { email, password }) {
  console.log(commit)
  try {
    if (email != "test@email" || password != 123456789) {
      throw new Error("error!!!")
    }
    let data = { email: email, password: password }
    // 変数dataのを次のmutations.jsにあるAUTHED_USERメソッドに渡します。
    commit("AUTHED_USER", data)
  } catch (e) {
    throw e
  }
}

registration(新規登録)では、mutations.jsのREGISTER_USERへ整形した値(ここではメールアドレスとパスワードのバリデーション処理)を渡しています。

// store/mutations.js

export default {
  REGISTER_USER: function (state, data) {
    state.registeredUser["email"] = data["email"]
    state.registeredUser["password"] = data["password"]
    state.authUser = data
  },
  AUTHED_USER: function (state, data) {
    state.authUser = data // 入力したemailとpasswordがここに入る
  }
}

mutations.jsのREGISTER_USERでstore/index.jsのstateに定義されたregistered_user(emailとpasswordのプロパティを持っている)に、 新登録フォーム入力された値が格納されるようになります。 故にstore/index.jsは以下のようになります。

// store/index.js

import Vuex from "vuex"
import mutations from "./mutations"
import actions from "./actions"

const store = () => {
  return new Vuex.Store({
    state: {
      registered_user: {},
      authUser: null
    },
      mutations,
      actions
    })
}
export default store

そして、このstore/index.jsでregistered_userを定義したあと、 signin.vueのscriptを以下のように変更します。

// pages/signin.vue
<script>
  export default {
    data() {
      return {
        error: null,
        email: "",
        password: "",
      }
    },
    methods: {
      async login() {
        console.log(this.registeredUser)
        try {
          await this.$store.dispatch("login", {
            email: this.email,
            password: this.password,
            // ここに登録した情報と、ログイン時に入力した情報を照らし合わせるために、登録された値を置きます。
            authData: this.$store.state.registeredUser
          })
          this.$router.push("/")
        } catch(e) {
          this.error = e.message
        }
      }
    }
  }
</script>

または、

// pages/signin.vue

import { mapState } from "Vuex"

export default {
  (省略)......
  computed: mapState([
    "registeredUser"
  ])
  (省略)......
}

とすることで this.$store.state.registeredUser を this.registeredUser に置き換えることができます。

そして、store/actions.jsで登録時の情報とログイン時に入力した情報を照らし合わせる処理を書きます。

// store/actions.js

async registration({ commit }, { email, password }) {
  try {
    if (email.includes("@") === false) {
      throw new Error("メールアドレスには@が含まれている必要があります。")
    } else if (password.length >= 10 || password.length <= 5) {
      throw new Error("パスワードは5文字以上10文字以下で作成してください。")
    }
    let data = { email: email, password: password }
    commit("REGISTER_USER", data)
  } catch(e) {
    throw e
  }
},
async login({ commit }, { email, password, authUser }) {
  console.log(commit)
  try {
    // authUserには渡された登録ユーザーのメールアドレスとパスワードが格納されています。
    // それらの値と入力された値が異なればエラー、同じなら次の処理に進みます。
    if (email != authUser.email || password != authUser.password) {
      throw new Error("error!!!")
    }
    let data = { email: email, password: password }
    commit("AUTHED_USER", data)
  } catch (e) {
    throw e
  }
}

これで新規登録ができるようになり、その登録情報を基にログインすることができる仕組みを作ることができました!

あとはログアウトについてですが、これはstoreのauthUserにnullを渡してあげれば良いだけなので、 actions.jsにログアウトボタンを押した時に発火する処理を作成して、mutations.jsのAUTHED_USERにnullを渡してあげれば問題ないです。

まあでも、もっとちゃんとしたログイン機能を作るんならまだまだ色んなことしないといけないのでしょうけど、、、、、、

気楽といったものの、割と長くなりました。 おつかれさまです。