冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

Unfiltered + HerokuでTwitter ToDoサービスを作る(heroku Add-onと設定ファイルとScalate使ってみる編)

Twitter ToDoサービス(TwoDo)を作る連載5回目。
前回までで認証を永続化して、2回目以降の認証省略(Cookieの有効期限がある限り)までできるようにしました。今回は、ようやくheroku Add-onの追加や、UIや設定ファイルのあたりを整理してみようと思います。

herokuアプリケーションにAdd-onを追加する

herokuにはサードパーティのサービスを含めた様々なAdd-onがあり、アプリケーションの要求に応じて追加することができます。
今回は、ユーザデータやタスクの状況の永続化のために、データベースを使う必要があります。前回のローカル環境で設定したように、データベースにはMongoDBを使うことにします。heroku側のアドオンはMongoLabアドオンです。無料枠で240MBという太っ腹。Add-onのページのStarterプランに書いてある以下のコマンドを叩いてみましょう。

$ heroku addons:add mongolab:starter
-----> Adding mongolab:starter to furious-stream-2974... failed
 !    Please verify your account to install this add-on
 !    For more information, see http://devcenter.heroku.com/categories/billing
 !    Confirm now at https://heroku.com/confirm

……こけましたね。
アカウントの認証をしろと言っています。その後のURLあたりを見ると、支払い方法とかですね。無料枠でもAdd-onを使うにはクレジットカードの登録が必要なようです。登録をすませた後で、もう一度叩いてみます。

$ heroku addons:add mongolab:starter
-----> Adding mongolab:starter to furious-stream-2974... done, v22 (free)
       Welcome to MongoLab.

今度は上手く行きました。
Add-onの説明ドキュメント(MongoLab | Heroku Dev Center)を見ると、接続文字列はheroku上に"MONGOLAB_URI"という環境変数で登録されるようです。

設定ファイル(.properties)を使う

今まで、ソースの全貌をGithubにpushしたり全部公開したりしていませんでした。それには一つ理由があります。
設定がらみをハードコーディングしてて、かっこわるいからです。
決して、イケてないコードをさらしてマサカリ投げられるのが怖かった訳ではありません。まあ、さすがにTwitterのconsumerKeyとconsumerSecretあたりまでハードコーディングしてる所まで公開してしまうのはまずかったので……。
というわけで、設定ファイル(.properties)に設定を固めていこうと思います。環境ごとに同じコードで設定値を切り分けるのは後ほどやりますが、まずは設定ファイルを使っていくところまで。
設定の内容によって設定ファイルのグルーピングをしたいので、抽象クラスを作ります。

abstract class AbstractSimpleTwoDoProperties {
  val prop = new Properties()
  val basedir = "target/scala-2.9.1/classes/"

  def propertiesFileStream: FileInputStream

  def get(key: String) = prop.getProperty(key)
}

basedirは実行時のクラスがおかれるパスです。どうも、sbt runでのルートパスがプロジェクトルートっぽいので設定しています*1
getメソッドをあえて作っているのは、継承先のオブジェクトからアクセスする際に"オブジェクト名.get(キー)"という形でアクセスするためです。設定ファイルを使う上での可読性の問題です。
この抽象クラスを実装して設定ファイルを扱うオブジェクトを実装します。幾つか実装していますが、基本はファイル名が変わるくらいです。

object MessageProperties extends AbstractSimpleTwoDoProperties {
  override def propertiesFileStream = new FileInputStream(basedir + "message.properties")

  prop.load(propertiesFileStream)
}

あまり説明する必要はないですね。シングルトンオブジェクトにしたのは、読み込むたびにロードさせたくなかったからです。
このオブジェクトのgetメソッドを介して、設定値にアクセスします。ハードコーディングしている箇所をひたすら.propertiesファイルに置き換えましょう。

ローカル開発環境とheroku環境で設定を切り分ける

設定ファイルを介して設定値にアクセスすることができるようになりましたが、今のままではローカル環境とheroku環境で異なる設定値を使い分けるのには以下のように少し手間がかかります*2

  1. ローカル環境でテストが終わったら、設定ファイルの内容をheroku環境の値に変更する
  2. git push heroku masterでソースをアップしてビルドする
  3. ビルドがちゃんと通ったら、ローカル用の設定に戻す

いちいち、gitのコミット対象となってしまいますし、毎回デプロイするたびにこの手順は面倒です。というわけで、以下のような方針で設定の使い分けができるようにしたいと思います。

  • heroku側では環境変数としてセットする
  • ローカル環境では設定ファイルを使う
  • 設定値は環境変数を優先する
  • ローカルの設定ファイルはgitの管理下におかない

上記方法で設定値を使い分けるのは以下です。

  • クッキーのセッションID(これは使い分けるというより隠したい方です)
  • TwitterOAuth認証後のコールバックURL
  • MongoDBのアクセス文字列
  • TwitterのCunsumer Keyとconsumer Secretの値(これも使い分けというより隠したい方)

オブジェクトの実装は以下のようになります。

object ServerEnvSettings extends AbstractSimpleTwoDoProperties {
  override def propertiesFileStream = new FileInputStream(basedir + "localsetting.properties")

  val propertiesFile = catching(classOf[FileNotFoundException]) opt propertiesFileStream
  if (propertiesFile.isDefined) prop.load(propertiesFile.get)

  override def get(key: String) = {
    val envVal = System.getenv(key)
    if (envVal != null) envVal
    else prop.getProperty(key)
  }
}

上記のオブジェクトが他の設定ファイル用のオブジェクトと異なるのはプロパティファイルのロードと、getメソッドのオーバーライドの箇所です。
ローカルの設定ファイルはgitの管理下におかず、heroku環境やGithubリポジトリにアップしません。そのため、heroku環境でファイルが存在しないケースに対処しています。
また、getメソッドをオーバーライドして環境変数から優先して取得するようにしています。

mongoDBの接続URLの対応

前回で、mongoDBのデータベースにアクセスする部分のコードは以下のように記載していました。

  val conn = MongoConnection()
  val db = conn("simple_twodo")
  val usersDataCollection = db("users_data")

データベースへのコネクションインスタンスを生成して、データベースへのアクセスインスタンスの生成、コレクションへのアクセスインスタンスの生成という流れです。
MongoConnectionに引数を渡さない場合、"localhost:27017"への接続を省略したことと同じになります。これだと当然mongoLABへの接続はできない訳です。
実際にmongoLABの接続文字列がどういう風に設定されているか確認してみましょう(アプリ特有の設定はマスキングしています)。

$ heroku config
--省略--
MONGOLAB_URI            => mongodb://[username]:[password]@ds031407.mongolab.com:31407/[database name]
--省略--

データベースまで含めた接続文字列になっているようです。上記の接続箇所のコードを変えなければなりません。余計なバグを埋め込まない用にするため、できればこの形式の文字列をパースせずにそのまま使いたいです。ってか、面倒なんですもの。
casbahのscaladocを漁って見るとMongoURIというコンパニオンオブジェクトがあり、これは上記接続文字列の形式に対応しているようです。さらに、このコンパニオンオブジェクトが提供するMongoURIクラスにはconnectDBというメソッドがあります。これを利用する形に変更してみましょう。

  val db = MongoURI(ServerEnvSettings.get("MONGOLAB_URI")).connectDB
  val usersDataCollection = db("users_data")

URIは先のheroku環境とローカル環境を切り分ける設定オブジェクトから取得するようにしています。そのため、localsetting.propertiesに以下の設定を追加します。

MONGOLAB_URI = mongodb://localhost:27017/simple_twodo

これで、ローカルで実行したときはローカルのデータベースを参照し、heroku環境ではmongoLABのデータベースを参照することができるようになりました。

Scalateでユーザインタフェースを作る

皆さん、初回の環境構築編であれだけ苦労して結局未だに使っていないライブラリがあるのを覚えていますか?そうです、Scalateです。サーバエンドの処理ばかり書いていたのでしょうがないと言えばしょうがないのですが、完全においてかれております。認証やデータの永続化もできるようになってきたので、そろそろUIを作っていける感じになっています。
今回利用するScalateはScalaのテンプレートエンジンです(http://scalate.fusesource.org/index.html)。対応しているテンプレートフォーマットは、下記4種類のようです。

  • Mustache
  • Scaml
  • Jade
  • SSP

どれを利用しても良いようです。それぞれのテンプレートフォーマットのマニュアルとサンプルにざっくりと目を通した上で、今回はScamlを採用しました。ソースがシンプルでわかりやすいのが最大の理由です。リファレンス(http://scalate.fusesource.org/documentation/scaml-reference.html)を読みながら基本的なUI部分を実装してみた結果が以下になります。

!!! 5
%html
    %title Simple Two Do
    %body
        %h1 Simple Two Do
        -@ val userData: com.simpletwodo.mongodbutil.SimpleTwoDoUserData

        #welcommsg
            ようこそ、#{userData.screenName}さん。

        #description
            Tweetを利用してToDo管理をするためのサービスです。
            %br
            自分に対して、以下のようにリプライを飛ばすことで、ToDoリストに追加されます。
            %br
            .twodousage
                \@ユーザアカウント タスクの内容 #SimpleTwoDo
            %br
            リプライで検索しているため、ユーザアカウントは最初につけてください。

        #listspace
            - for(tweet:com.simpletwodo.mongodbutil.SimpleTwoDoTask <- userData.userTaskList)
                - import tweet._
                .listdetail
                    %input.chkbox{:type => "checkbox", "twid" => {tweetId}} #{tweetStatus}

基本的にリファレンス以上のことはしていません。なお、userDataという変数名で、バインド属性を持っています。
認証後のトップ画面(/twodolist)に上記のテンプレートとユーザデータをテンプレートエンジンに渡すため、フィルタの結果返却部分を以下のように変更しましょう。

              Ok ~> HtmlContent ~> Scalate(req, "twodolist.scaml", ("userData", userData))

これで、結果返却時にユーザデータとテンプレートを利用してHTMLを生成し、クライアントに返却することができるようになりました。非常にベーシックではありますが、ようやくユーザインタフェースを持った訳です。


これで、基本的なUIを持ったToDoリストを表示する状態まで行きました。まだ、ToDoタスクの状態を変更管理することができません。次回は、チェックボックスのイベントを拾って、サーバに状態変更リクエストを飛ばして処理する部分と、CSSjQueryを使って見た目を整えていきたいと思います。

*1:動きとしてはこれでとりあえずOKなのですが、実際問題これで大丈夫なのかどうかがドキュメントベースで探し当てられませんでしたorz

*2:sbtやgitのオプションで行けるようになるのかどうかは未検討です