Help us understand the problem. What is going on with this article?

LGTM画像を驚くほど簡単に作れるWebサービスをScalaで作る

はじめに

Scala, Play Framework, Akka Actor, Slick を利用し、LGTM画像を驚くほど簡単につくれるWebサービスを作った話です。

LGTM とは

Looks Good To Me の略です。
GitHubのプルリクエストをレビューした時、問題なかった場合には「私は問題ないと思います」くらいの感じで「LGTM」と書きこみます。

LGTMコメント例

LGTM画像とは

ただLGTMとコメントするだけではそっけないので、Looks Goodな画像を付けてコメントする場合もあります。

LGTM画像の例

こういった画像を探すのに便利な http://lgtm.in/ というサービスがあります。(現在は閉鎖?されてしまったようです)

しかし、LGTM.in はあくまでも単なる画像共有サービスです。
LGTM.inに自分の気に入った画像がないので作りたい!もうワンランクの上のLGTM画像が欲しい!
でも画像を用意して文字を重ねて...とかするのは面倒だ!というのが僕の思いでした。

そこで、「なんかいい感じのLGTM画像を驚くほど簡単につくれる」Webサービスを作りました。

デモ

herokuでアプリを公開しています。

https://lgtmoon.herokuapp.com/

使い方

  1. 一番上の検索窓に、キーワードを入力します。
  2. 画像検索が走るので、LGTM画像のベースにしたい画像をクリックします。
  3. しばらく待つか、画面をリロードするとLGTM画像ができあがります。
  4. 出来たLGTM画像をクリックすると画像のURLやGitHubのMarkdownが出て来るので、プルリクのコメントなどに貼り付けます。

より高度な使い方

  1. 一番上の検索窓に、LGTM画像のベースにしたい画像のURLを入力します。
  2. しばらく待つか、画面をリロードするとLGTM画像ができあがります。
  3. 以下同様です。

注意点

  • herokuの無料プランですので、動作が重かったり、容量がいっぱいになってLGTM画像が生成できなくなったりします。優しく扱ってください。 有料プランにしました。
  • どこかから怒られそうな画像は使わないようにしてください。
  • ドキュメントは雑ですが、ソースコードは公開しています。

技術的な話

アプリケーション構成

アプリケーション構成は以下の通りです。
フロントでの画像表示はvue.jsを使っています。採用理由は、コードがシンプルになるからです。画像生成にImageMagickを、DBアクセスにはSlickを使っています。herokuはPostgreSQL推しなので、DBにはPostgreSQLを利用しています。

lgtmoon.png

フロー図

フロー図は以下の通りです。ベース画像のダウンロードと、LGTM画像の生成には多少の時間がかかりますので、Actorを使って非同期に処理しています。APIは、LGTM画像生成リクエストを受け付けたら、Actorにメッセージを送るだけ送って、フロントには即座にレスポンスを返します。

image.png

vue.js(フロント)

フロントの画像表示部分にはvue.js ( https://jp.vuejs.org/ ) を使っています。vue.jsは、慣れるまでの学習コストは若干高いですが、シンプルな事をやる際にはコードが短くスッキリします。ただし、今回はAPIリクエストはjQueryでAjaxを使ってやっちゃってます。手抜きです。

Scala/Play(バックエンド)

バックエンドの言語にはScalaです。ScalaのWebフレームワークであるPlay Frameworkを使っています。Scalaの他のWebフレームワークを使っていないのですが、Play Frameworkは無難で(最近はわりと)使いやすく、スタンダードなフレームワークなのではないかなぁと思います。

Akka Actor

画像のダウンロードや加工といった操作は時間のかかるものです。画像生成APIが叩かれた際に、レスポンスはすぐに返し、時間のかかる処理は裏で進めたいものです。その際に役に立つのが Akka Actor です。今回のアプリケーションでは、「画像を生成しろ」というメッセージを送ると、Actorが裏で画像のダウンロードと加工を行ってくれます。

まず、「画像を生成しろ」メッセージを定義します。

case class ImageGenerateMessage(id: Long, url: String)

idは、DBの情報を更新するために必要な情報、urlはダウンロードすべきベース画像のURLです。

Actorは以下のようになっています。

class ImageActor extends Actor {
  val downloadDir = "/tmp"
  val convertedDir = "/tmp"
  val imageStorage = ImageStorage
  val imageRepository = ImageRepository
  val imageMagickService = ImageMagickService

  /** メッセージを受け取った時の処理 */
  override def receive: Receive = {
    case ImageGenerateMessage(id, url) => {
      val downloadPath = downloadDir + "/" + id
      val convertedPath = convertedDir + "/" + id
      // 画像のダウンロード
      imageStorage.download(url, downloadPath)
      // 画像の変換
      imageMagickService.convert(downloadPath, convertedPath)
      // convertされた画像をバイナリで取得してDBに入れる
      val bin = imageStorage.binary(convertedPath)
      imageRepository.updateStatus(id, imageRepository.AVAILABLE, bin)
    }
  }
}

receiveImageGenerateMessage を受け取ったら画像のダウンロードと加工をします。
Actorはメッセージを受け取ったら処理、受け取ったら処理、ということをひたすらするだけです。
(画像のバイナリをDBに直接入れている件はまた後ほど説明します。)

Controllerでは、Messageを投げた後レスポンスを返します。( https://github.com/yoshikyoto/lgtmoon/blob/master/app/controllers/ImageGenerateController.scala )

/** 画像生成を行うコントローラ */
class ImageGenerateController extends BaseControllerTrait {
  /** 非同期で画像生成をするためのActor */
  val imageActor = Akka.system.actorOf(Props(new ImageActor()))

  /** postされたurlから画像生成をする */
  def withUrl = Action.async { request =>
    val jsonOpt = request.body.asJson
    jsonOpt match {
      case None => Future(NOT_JSON_RESPONSE)
      case Some(json) => {
          (json \ "url").asOpt[String] match {
            case None => Future(PARAMETER_KEYWORD_NOT_FOUND_RESPONSE)
            case Some(url)  => {
              // とりあえずURLだけ先に払い出して返す
              ImageRepository.create() map {
                case None => DATABASE_CONNECTION_ERROR_RESPONSE
                case Some(id) => {
                  // 加工後の画像のURLになる予定のやつ
                  val lgtmUrl = UrlBuilder.imageUrl(id.toString)
                  // Actorに投げる
                  imageActor ! ImageGenerateMessage(id, url)
                  // レスポンスを返す
                  Ok(JsonBuilder.imageUrl(lgtmUrl))
                }
              }
            }
          }
      }
    }
  }
}

Slick

Slickは非同期にDBIOを行うことのできるScalaのライブラリです。
http://slick.lightbend.com/

Slickの特徴といえば以下の点だと思います。

  • 非同期処理
  • 関数型チックなクエリ生成
  • コード自動生成

非同期処理

Scalaの特徴が非同期処理といえ、非同期処理に対応したDBのORマッパ的なものは意外となかったりします。しかし、Slickは非同期処理となっており、結果はFutureで帰ってくるため、重いDBIOも効率的に処理できたりします。(ただし今回くらいの規模のアプリケーションではあまり効果がない気もしますが)

関数型チックなクエリ生成

Slickは、List操作のような形でDBのクエリを書くことができます。
https://github.com/yoshikyoto/lgtmoon/blob/master/app/domain/image/ImageRepository.scala

例えば、Selectのクエリを見てみましょう。

  /**
   * 画像を最新20件取得する
   * 
   * @param limit: Int 何件取得するか。デフォルト20
   * @return Future[Option[Seq[ImageRow]]]
   */
  def images(limit: Int = 20): Future[Option[Seq[ImageRow]]] = {
    val action = Image.filter(_.status === AVAILABLE)
      .sortBy(_.createdAt.desc)
      .take(limit)
      .result
    db.run(action).map {
      case images: Seq[ImageRow] => Some(images)
      case _ => None
    }.recover {
      case e => None
    }
  }

トップページに表示する画像20件を取得するクエリです。sortBy.take と、関数型の特徴であるList操作に近い形でクエリを書くことができて非常によいです。

コード自動生成

ここがなかなかクセのあるところかもしれませんが。Slickは、DBにテーブルを作成しておき、そこからコードを自動生成してやる必要があります。

今回はこのようなテーブルスキーマになっています。

create table Image (
       id            bigserial          primary key,
       content_type  varchar(32)        not null,
       created_at    timestamp          not null default current_timestamp,
       status        smallint           not null,
       bin           bytea              default null
);

create index image_created_at on image (created_at);

これをDBに流した状態で以下のようなコードを書き

object Codegen extends {
  def main(args: Array[String]): Unit = {
    val slickDriver = "slick.driver.PostgresDriver"
    val jdbcDriver = "org.postgresql.Driver"
    val url = "jdbc:postgresql://localhost/lgtmoon"
    val outputDir = "../app"
    val pkg = "repositories"
    val user = "postgres"
    val password = "postgres"
    slick.codegen.SourceCodeGenerator.main(
      Array(slickDriver, jdbcDriver, url, outputDir, pkg, user, password)
    )
  }
}

https://github.com/yoshikyoto/lgtmoon/tree/master/sql

このようにbuild.sbtを用意して sbt run すると

https://github.com/yoshikyoto/lgtmoon/blob/master/app/repositories/Tables.scala

このコードが自動生成されます。ここでは、Imageテーブルに対応するImageクラスと、テーブルの一つの要素に対応するImageRowクラスができます。これをもとに先ほどのようなクエリを書くことができます。

さいごに

今回はLGTMを簡単に生成するアプリケーションを作成する話でした。

今後つけたい機能

  • 手元の画像をアップロードできるようにする
  • 背景が白い画像や明るい画像、キャラクターなどの顔にLGTM画像がかぶってしまう時などへの対策
  • 画像の検索やお気に入り的な機能
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした