GraphQLスキーマからCRUDを自動生成できるPrismaについて

Prismaは、様々なデータベースをバックエンドにGraphQLのスキーマからCRUDを行うためのエンドポイントを提供するプロキシとして動作するミドルウェアです。最近$4.5Mの資金調達をしてちょっとだけ話題になりました。

www.prisma.io

Prismaが提供するソフトウェアは現在オープンソースソフトウェアとしてGitHub上で公開されています。本体はScalaで書かれていますが、CLIはTypeScript(Node.js)で書かれているようです。Scalaのコードは関数型プログラミングを駆使したものではなく、比較的読みやすい部類だと思います。

github.com

触ってみる

GraphQLのエンドポイントを簡単に用意することができそうということで少し調べてみました。Webサイトにチュートリアルがあり、dockerを使って簡単に試すことができるようになっています。事前にnpmとdockerが利用できるようになっている必要があります。

まずはnpmでPrismaCLIをインストールします。

$ npm install -g prisma

インストールしたCLIを使ってサンプルを生成します。既存のデータベースを使うか、dockerで新たなデータベースを作成するかを聞かれます。いずれにしろPrisma自体はdockerで起動するので既存のデータベースを使用する場合はdockerコンテナからアクセスできるようになっている必要があります。

$  prisma init hello-world

Webサイトには様々なデータストアを利用できるようになると書かれていますが、今のところ対応しているのはMySQLPostgreSQLのみのようで、スケールさせる場合はデータベース側で頑張る(Prisma自体は横に並べればOK)ということのようです。現在はまだベータのようですが、既存のテーブルからGraphQLスキーマを生成するという機能も実装されているようです。

生成されたサンプルを見てみましょう。

$ cd hello-world
$ cat datamodel.graphql
type User {
  id: ID! @unique
  name: String!
}

ディレクトリ内にはこの他にもPrismaやデータベースをdockerで起動するためのdocker-compose.ymlや、Prismaの設定ファイルであるprisma.ymlが生成されています。

Prismaにデプロイして動作確認してみます。まずはdockerコンテナを起動します。

$ docker-compose up -d 

スキーマをデプロイします。するとデータベースに自動的に対応するテーブルが作成され、エンドポイントが利用可能になります。

$ prisma deploy

以下のコマンドでGraphQL PlaygroundというWebブラウザでスキーマを確認したりクエリを投げるためのコンソールを表示することができますので、ここで色々試してみるとよいと思います。クエリが間違ってると赤線が表示されたり、スキーマを確認できたりするのでなかなか便利です。

$ prisma playgroud 

f:id:takezoe:20180523121831p:plain

Prismaスキーマのオートマイグレーションもサポートしています。先ほどのスキーマに以下のようにemailプロパティを追加してみます。

type User {
  id: ID! @unique
  name: String!
  email: String
}

これをprisma deployでデプロイすると以下のようにemailプロパティが追加されていることがわかります。プロパティの削除も可能ですが、すでにデータが入っている場合は警告が表示され、--forceオプションをつけてデプロイを実行する必要がありました.

f:id:takezoe:20180523122625p:plain

Prismaにはサーバーサイドサブスクリプションという機能があり、条件を指定してWebフックを登録しておくことができます。たとえば以下のような設定をprisma.ymlに入れておきます(host.docker.internalというのはdockerコンテナ内からホストにアクセスするためのホスト名ですので実際は呼び出したいホストに置き換えてください)。

subscriptions:
  userChangedName:
    webhook:
      url: http://host.docker.internal:8080/test
    query: |
      subscription {
        user(where: {
          mutation_in: [UPDATED]
        }) {
          node {
            id
            name
          }
        }
      }

Userが変更されると指定したURLに以下のボディを持ったPOSTリクエストが送信されます。

{"data":{"user":{"node":{"id":"cjhij3xjs000h09748tmsaku4","name":"Alice"}}}}

設定ファイルに1つずつ記述しないといけないので数が増えるとちょっと大変そうですが、この機能を使えば簡単な外部連携などはサーバサイドで完結させることができそうです。

感想的なもの

Prismaを使うとシンプルなCRUD用のGraphQL APIを簡単に作成することができるのですが、実際のシステムではそれだけでは済まないケースがほとんどなのではないかと思います。このような場合、Prismaとクライアントの間にもう1層BFF的なGraphQLサーバを作成することが推奨されているようです。ただ、PrismaをラップするGraphQLサーバを自前で作るのであればバックエンドをPrismaでGraphQL化する意味があまりないような気がするんですよね…。SQLを直接書くよりBFFを書きやすいというのはあるのかもしれませんが…。

また、Prismaクラウドサービスとして利用可能なPrisma Cloudというサービスも提供しています。が、こちらはデータベースは自前で用意する必要があり、Prisma Cloudからインターネット経由でデータベースに接続することが想定されているようです。前述のようにPrismaの手前に別途GraphQLサーバを立てるとすると、手元にあるデータベースにアクセスするためにインターネット経由で一往復しないといけないという状況が発生します。さすがにこれはちょっと厳しいのではという気がします。

機能面にしろ運用面にしろ面倒な部分を外部に丸投げしてる感があり、個人的にはクライアントアプリケーション向けのGraphQL API開発に使ってもあまり楽にならなそうな印象を受けました。いまのところPrismaはGraphQL DB的なものを志向しているように見えるので、そういうものだと割り切って使う必要がありそうです。

GitBucket 4.24.0をリリースしました

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.24.0をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.24.0

プルリクエストのレビューコメントにDiffを表示

プルリクエストのConversationタブではレビューコメントに該当部分のDiffも表示されるようになりました(この機能はバージョンアップ後に登録されたコメントのみ有効になります)。また、古いコミットに対するコメントは折り畳み表示されます。

f:id:takezoe:20180429231608p:plain

複数メールアドレスをサポート

1アカウントに複数のメールアドレスを登録できるようになりました。

f:id:takezoe:20180429232938p:plain

コミット一覧画面にタグを表示

コミット一覧画面に表示される各コミットにタグも表示されるようになりました。

f:id:takezoe:20180429233042p:plain

オンラインエディタでの改行設定の保存

オンラインエディタでの改行設定を変更するとWebブラウザのlocal storageを使用して設定が保存されるようになりました。この設定はGistプラグインにも共有されます。

f:id:takezoe:20180429233235p:plain

Gistプラグインのレイアウトを修正

Gistプラグインの画面レイアウトを大幅に見直しました。

f:id:takezoe:20180429233318p:plain

CIプラグインにWeb APIを追加

CIプラグイン(非公式プラグイン)にビルドをトリガーしたり、ビルド情報を取得するためのWeb APIを追加しました。これらのAPICircleCI API v1.1と互換性があり、現時点では以下のAPIをサポートしています。

  • User
  • Recent Builds For a Single Project
  • Recent Builds For a Project Branch
  • Single Build
  • Retry a Build
  • Cancel a Build
  • Trigger a new Build
  • Trigger a new Build with a Branch

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

GoogleのMachine Learning Crash Courseをやってみた

少し前にGoogleが社内教育用のコンテンツとして使用しているというMachine Learning Crash Courseが公開されていました。PredictionIOのコミッタをやっていながら機械学習はほぼ素人というのもどうかと思っていたこともあり、社内で毎日1時間ずつこの講座を進めてみることにしました。

developers.google.com

15時間で終わるということになっていますが、英語の動画やテキストの理解に時間がかかってしまい、最終的には40時間くらいかかってしまったものの、なんとか完走することができました。機械学習ディープラーニングの基礎について25のレッスンがあり、それぞれのレッスンは以下のコンテンツから構成されています。

  • 動画による概要の解説
  • より詳細な内容を説明したテキスト
  • ブラウザ上でビジュアルな実験が可能なプレイグラウンド
  • ノートブックを使用したプログラミングエクササイズ
  • 理解度チェック

動画を見たりテキストを読んだりするだけでなく、プレイグラウンドやエクササイズで手を動かすコンテンツがあり、特にプレイグラウンドでの実験はモデルの収束や過学習の様子などをビジュアルに確認することができるので直感的に理解ができます。

エクササイズではTensorFlowを使うのですが、丁寧なガイドやコードサンプルがありますし、コピペしてパラメータを変えるくらいの課題が多いので予備知識なしでも問題ないです。わからなければ解答にコード例がありますし、きちんと説明もあるので時間がなければすぐに解答を読んでしまってもよいと思います。

動画はメイン講師の方の英語が自分には結構聞き取りづらかったのですが、字幕もありますし、内容的にはテキストのサマリなので、テキストだけ読んでおけば問題ないです。自分たちはレッスン毎にテキストを読んでから復習を兼ねて動画を見るという流れで進めました。

レッスンの内容は機械学習の基本的な用語やアルゴリズム、学習やモデル精度の検証のプロセス、ニューラルネットワークについて基礎的な理解を得ることができるものになっています。内容そのものは初歩的なものだと思うのですが、各所に実践的なアドバイスが散りばめられています。記憶に残っているものをいくつかあげておきます。

  • まずはできるだけ簡単な方法から始めよう
  • 適切な学習データを用意するためにデータの内容を把握しよう
  • 精度が出ない場合、後付けの対応ではなく根本的な原因を見つけよう
  • 精度改善とモデルの複雑化とのトレードオフを意識しよう
  • モデルの設定はコードで管理し、必ずレビューをしよう
  • 実験結果やドキュメントをきちんと書こう

機械学習ではコードやロジックではなくデータにフォーカスする」という違いはあるものの、上記のアドバイスは一般的なソフトウェアエンジニアリングでも重要なことばかりですね。機械学習の知識だけでなく、こういったエンジニアリングに対する基本的な姿勢がきちんと押さえられているのは素晴らしいと思いました。

コースの冒頭でディレクターのPeter Norvingさんは、プログラマ機械学習を学ぶことのメリットを述べていますが、その中でも「ロジックに落とせない未知の事象に対処する方法を得ることができる」という点が印象的でした。初歩的な内容とはいえ非エンジニアだとちょっと厳しいかなという気はしますが、ソフトウェアエンジニアであれば機械学習に携わる予定のない方でも受講してみる価値のあるコースだと思います。

PlayのRedisプラグインあれこれ

PlayのCache APIのバックエンドにRedisを使えるようにするプラグインがあり、以前はTypesafe社によってメンテされていたのですが、現在はメンテナンスが停止してしまっています。

github.com

このプラグインはバックエンドにJedisとJedisのScalaラッパーであるSedisを使っているのですが、どうやらSedis側に問題があり、壊れたコネクションがプールから除去されないという問題があるようです。

github.com

あまり使ってる人がいなかったのかもしれませんが、割とクリティカルな問題がある状態でメンテナンスが止まってしまっており、上記のイシューで紹介されているフォーク版でもこの問題は解消されていないように見えたので仕方なくフォークして修正することにしました。

ちなみにSedisにこの問題を修正するためのプルリクが出ていますが、Sedisもメンテが止まってしまっているようです。

github.com

SedisはJedisの薄いラッパーでしかなくあまり有難味がないこと、play-redisでもSedisの機能を使っているのはローンパターン的な部分だけだったのでSedisを外して直接Jedisを使うように修正してみました。で、作ってみたのがこちら。

github.com

すでにMaven Centralにも反映されているのでそのまま使えます。いまのところPlay 2.5用のものしかないのですが、Play 2.6にも対応させたいと思っています。(追記:その後Play 2.6にも対応したバージョンもリリースしました。JedisをFutureでラップしただけのものですが非同期APIも利用可能です。)

ちなみに他の選択肢としてrediscalaを使ったPlayプラグインも存在するようです。

github.com

Play 2.6ではCache APIにも非同期処理用のインターフェースが追加されているのですが、Jedisと違ってrediscalaはノンブロッキングなライブラリなのでPlay 2.6でFutureベースのコードを書くならこちらを使うのがいいのかもしれません。

GitBucket 4.23.0をリリースしました

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.23.0をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.23.0

URL末尾のスラッシュを許容するように

URLの末尾にスラッシュがついていてもスラッシュがついていない場合と同じように振る舞うようになりました。これによってJenkinsなどの外部システムとの連携が改善されます。

GitBucketは内部的には末尾がスラッシュで終わるリクエストをスラッシュを取り除いたURLとして扱うようになっています。そのためプラグインでパスの末尾がスラッシュで終わるアクションを定義している場合、そのアクションが呼び出されることはなくなります。プラグイン開発者の方はご注意ください。

リリース一覧にタグのコミットメッセージを表示

リリース一覧に、各リリースに対応するタグのコミットメッセージが表示されるようになりました。このコミットメッセージは新規リリースを作成する際に説明文の初期値としても使用されます。

f:id:takezoe:20180330162754p:plain

イシューとプルリクエスAPIのレスポンスにlabelsプロパティを追加

イシューやプルリクエストの情報を取得するAPIのレスポンスにlabelsプロパティが追加されました。

 [
  {
    "number": 1,
    "state": "open",
    ...
    "labels": [
      {
        "name": "enhancement",
        "color": "84b6eb",
        "url": "http://localhost:8080/api/v3/repos/root/gitbucket/labels/enhancement"
      }
    ],
    ...
  }
]

プラグイン一覧API

インストール済みのプラグインの一覧情報を取得するためのAPI/api/v3/gitbucket/pluginsとして追加されました。このAPIは将来的にプラグインビルドファームで利用されることを想定したものです。

[
  {
    "id": "maven-repository",
    "name": "Maven Repository Plugin",
    "version": "1.3.0",
    "description": "Host Maven repository on GitBucket.",
    "jarFileName": "gitbucket-maven-repository-plugin-assembly-1.3.0.jar"
  },
  {
    "id": "ci",
    "name": "CI Plugin",
    "version": "1.4.0",
    "description": "This plugin adds simple CI functionality to GitBucket.",
    "jarFileName": "gitbucket-ci-plugin-assembly-1.4.0.jar"
  }
]

プライベートアクセストークンによるGit認証

Gitリポジトリの認証時にアカウントのパスワードの代わりにプライベートアクセストークンが使用できるようになりました。特に自動化ツールなどからGitリポジトリにアクセスする場合、セキュリティ上の観点からユーザのパスワードの代わりにアクセストークンを使用するとよいでしょう。

CIプラグインの設定

CIプラグインで並列実行可能なビルド数および保存する履歴の最大数を設定できるようになりました。

f:id:takezoe:20180330162811p:plain

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

なお、今回のバージョンではRELEASEテーブルがRELEASE_TAGテーブルにリネームされ、これに伴ってDBアクセス用のモデルクラスも変更されているため、DBアクセスを行う多くのプラグインでバイナリ互換性が失われています。プラグイン作者の方は必要に応じてGitBucket 4.23.0でビルドし直したバージョンをリリースしていただければと思います。また、プラグイン利用者の方はGitBucket 4.23.0で動作しないプラグインを発見したら各プラグインにフィードバックをお願いできればと思います。

Apache PredictionIOとApache Zeppelinを組み合わせてみる

Apache PredictionIOとApache ZeppelinはどちらもApache Software Foundationのプロジェクトです。PredictionIOはSparkML / MLlibベースの機械学習アプリケーションを開発・運用するためのプラットフォームを提供するもので、ZeppelinはSpark / SparkSQLを使用可能なノートブックを提供するものです。

PredictionIOは学習に使用するイベントデータをRDBMSやHBase、Elasticsearchに保存するのですが、これをZeppelin上でSQLを使って分析することができたら便利ではないかと思いやってみました。本当はPredictionIOが提供しているAPIをそのままライブラリとして使えたらよかったのですが、初期化処理などがPredictionIOのライフサイクルにべったり依存していて厳しそうだったので無理やりラップして簡単に使えるライブラリを作ってみました。

github.com

このライブラリはMavenセントラルにpublishしてあるのでZeppelinのノートブック上で以下のようにロードするだけで使用することができます。

%dep
z.load("com.github.takezoe:predictionio-toolbox_2.11:0.0.1")

以下のようにPIOToolboxクラスを使用してイベントデータをRDDとして取得し、SparkSQLで利用するためにテーブルとして登録します。

import com.github.takezoe.predictionio.toolbox._

// Create toolbox with PIO_HOME
val toolbox = PIOToolbox("/Users/naoki.takezoe/PredictionIO-0.12.1")
val eventsRDD = toolbox.find("MyApp1")(sc)

case class Rating(
  user: String,
  item: String,
  rating: Double
)

val ratingRDD = eventsRDD.map { event => 
  val ratingValue: Double = event.event match {
    case "rate" => event.properties.get[Double]("rating")
    case "buy" => 4.0 // map buy event to rating value of 4
    case _ => throw new Exception(s"Unexpected event ${event} is read.")
  }
  // entityId and targetEntityId is String
  Rating(event.entityId,
    event.targetEntityId.get,
    ratingValue)
}

val df = spark.createDataFrame(ratingRDD)
df.registerTempTable("rating")

するとSQLで検索することが可能になります。

%sql
select rating, count(*) from rating group by rating

f:id:takezoe:20180330140251p:plain

PIOToolboxクラスにはこの他にもRDDをイベントデータとして登録するメソッドなどがあります。利用可能なメソッドについてはソースコードを参照していただければと思います。

ノートブックというとJupyterの利用者が多いのではないかと思いますが、ZeppelinはデフォルトでScala / Spark / SparkSQLを利用することができるので、同じくScala / Sparkベースの機械学習プラットフォームであるPredictionIOとは相性が良さそうです。PredictionIOで機械学習アプリケーションを開発する場合、学習データのインポートや前処理などにZeppelinを使うのはありかもしれないと感じました。

Twilio社がOSS化したScala用SwaggerコードジェネレータGuardrailを試してみる

先日のScalaMatsuri 2018でTwilioの方が発表されていた、SwaggerからAkka HTTP用(http4sにも対応している)のコードを生成するコードジェネレータがGitHubで公開されていました。

github.com

発表を聞いていて良さげな感じだったのでちょっと試してみました。

まずはGitHubからリポジトリをクローンしてきます。

$ git clone https://github.com/twilio/guardrail.git
$ cd guardrail

コード生成にはルートディレクトリのcli.shというシェルスクリプトを使います。ソースツリーにサンプルのSwaggerファイルが含まれているのでこれを指定してAkka HTTP用のコードを生成してみます。クライアント側とサーバ側のコードをそれぞれ生成できるようです。

$ ./cli.sh --client --specPath modules/codegen/src/main/resources/petstore.json --packageName petstore --outputPath /tmp/petstore-client/src/main/scala
$ ./cli.sh --server --specPath modules/codegen/src/main/resources/petstore.json --packageName petstore --outputPath /tmp/petstore-server/src/main/scala

このとき--framework http4sというオプションを付けるとhttp4s用のコードを生成することもできるようです。

生成されるのはコードのみなので実行するにはbuild.sbtなどを別途作成する必要があります。それぞれのプロジェクトに以下のような感じのbuild.sbtを作成しました。

name := "petstore-server"
version := "1.0"
scalaVersion := "2.12.5"
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
libraryDependencies ++= {
  val akkaHttpV   = "10.0.10"
  val catsVersion = "0.9.0"
  val circeVersion = "0.8.0"
  Seq(
    "com.typesafe.akka" %% "akka-http" % akkaHttpV,
    "io.circe" %% "circe-core" % circeVersion,
    "io.circe" %% "circe-generic" % circeVersion,
    "io.circe" %% "circe-java8" % circeVersion,
    "io.circe" %% "circe-parser" % circeVersion,
    "org.typelevel" %% "cats" % catsVersion
  )
}

ではサーバから見ていきましょう。実際に処理を行うハンドラ部分がトレイトとして生成されているのでこれを実装する必要があります。

object Main extends App {
  implicit val system: ActorSystem = ActorSystem("helloAkkaHttpServer")
  implicit val materializer: ActorMaterializer = ActorMaterializer()
  implicit val executionContext: ExecutionContext = system.dispatcher
  
  val userHandler = new UserHandler {
    override def getUserByName(respond: UserResource.getUserByNameResponse.type)(username: String): Future[UserResource.getUserByNameResponse] = {
      Future(respond.OK(User(username = Some("takezoe"))))
    }
    override def createUser(respond: UserResource.createUserResponse.type)(body: User): Future[UserResource.createUserResponse] = {
      Future(respond.OK)
    }
    // TODO: Implement other methods
    ...
  }

  val routes = UserResource.routes(userHandler)
  val serverBindingFuture: Future[ServerBinding] = Http().bindAndHandle(routes, "0.0.0.0", 8081)
  ...
}

ハンドラの各メソッドの最初の引数リストにはレスポンスのためのオブジェクトの型、2つめの引数リストにはSwaggerで定義したパラメータ群が渡されてきます。レスポンスのステータスもSwaggerで定義したものしか返せないようタイプセーフになっているのが嬉しいところです。

続いてクライアントサイドです。こちらはHttpClientを定義する必要があります。これはAkka HTTPのHttpRequestを受け取りFuture[HttpResponse]を返す関数のエイリアスなのですが、トレーシングなどのために処理を挟めるよう関数で抽象化されているようです。

object Main extends App {
  implicit val system: ActorSystem = ActorSystem("helloAkkaHttpClient")
  implicit val materializer: ActorMaterializer = ActorMaterializer()
  implicit val executionContext: ExecutionContext = system.dispatcher
  implicit val httpClient: HttpClient = (request: HttpRequest) => Http().singleRequest(request)

  val client = UserClient("http://localhost:8081")

  val result = for {
    _    <- client.createUser(User(username = Some("takezoe")))
    user <- client.getUserByName("takezoe")
  } yield user

  val user = Await.result(result.value, Duration.Inf)
  println(user)
}

クライアントの各メソッドの戻り値はCatsのEitherTになっているのでfor式で簡単に合成することができます。また、クライアント、サーバ共に生成されたコードを直接編集せずに処理を実装することができるようになっているので、Swaggerファイルを変更した場合は単純にもう一度コードを生成しなおせばよさそうです。

ScalaのWebフレームワークにおけるSwaggerとの連携機能は、コードからSwaggerのAPI定義を生成することを目的としたものが多く、API定義をDSLソースコードに埋め込むものが多いのですが、これだと二度手間ですし、実装と定義の整合性をプログラマが担保しなくてはなりません。GuardrailはSwaggerを正としてコード生成を行うので二度手間になりませんし、スペックに従った実装であることをツールで担保できる有用なアプローチなのではないかと思います。

ちなみに試しに生成してみたAkka HTTPサーバとクライアントのソースコードは以下のリポジトリに置いてあります。

github.com