astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

そうだAsta4dでWebアプリケーションを作ろう(第4回)

Handlerの役割と使い方

Hanlerの話です。
Handlerの役割については色々ありますが、詳しくはJavaフレームワークAsta4Dの話に書いてますのでご覧ください。

今回はコードレベルでどのような形で書けば良いかという観点で書いてます。
動的URLに対して適したHTMLを表示させるというお題をもって説明いこうと思います。

今回のURLルール

package com.astamuse.blog_sample.rules;

import static com.astamuse.asta4d.web.dispatch.HttpMethod.GET;

import com.astamuse.asta4d.web.dispatch.mapping.UrlMappingRuleInitializer;
import com.astamuse.asta4d.web.dispatch.mapping.ext.UrlMappingRuleHelper;

import com.astamuse.blog_sample.Handler;

public class UrlRules implements UrlMappingRuleInitializer{
    public void initUrlMappingRules(UrlMappingRuleHelper rules) {
        rules.add(GET, "/").forward("/html/index.html");
        rules.add(GET, "/part4/{id:[0-9]+}").handler(Handler.class);
    }
}

そんな訳で動的なURLを書いてみました。
前回まで使っていたルールに1行、動的URLを追加してるだけですが。

asta4DのURLルールでは、「{}」内に変数を記載すると動的なURLとして認識され、HandlerやSnippet側でその変数が受け取ることが出来ます。
変数部分には正規表現で変数の内容を縛ることも可能です。
見て分かりますが、今回の場合は数字の場合だけ変数「id」に値が入りURLとして効力が発揮されます。

「/part4/1」「/part4/2」のように「id」の部分が数字の場合はこのURLとして認識されるが、「/part4/a」だと認識されないって感じですね。

で、このURLは今までと違ってforwardする処理が入っていません。
今回は「動的URLに対して適したHTMLを表示させる」ということでfoward先をURLに合わせて変更させなければならないので、Handler内でforward先を指定する実装という形で説明したいと思います。

なお、今回は描画部分のお話ではないのでHTMLやSnippet部分は省略します。

Handlerの実装

package com.astamuse.blog_sample.handler;

import com.astamuse.asta4d.web.annotation.QueryParam;
import com.astamuse.asta4d.web.dispatch.request.RequestHandler;

public class Handler {

    @RequestHandler
    public String handle(Integer id) {
        if(id == null) {
            new RuntimeException();
        }
        return "/html/" + id +".html";
    }
}

ということでHandlerを実装しました。
今回は変数で入ってきたidを元にforward先を決めるだけなので簡易な実装ですね。
実際はもっと複雑と言いたいところですが、複雑な実装になっているHandlerは実使用上でもあまりありません。
基本的にView Firstでは、View以外の実装については複雑にはしないのです。

それでは、1行ずつ見ていきたいと思います。

@RequestHandler
public String handle(Integer id)

@RequestHandlerのアノテーションが記載されたメソッドが最初にコールされます。

引数には、URLルール上に記載された変数を書いておくことで勝手にインジェクションしてくれます。
URLルール以外にも引数に含めておくと勝手にインジェクションしてくれるクラスがあるので、覚えておくと便利です。
HttpServletRequestHttpServletRequestが該当します(他にもあります)。
リクエストパラメータからなんかしたい場合やレスポンスヘッダを自力でいじりたい場合に使ったりします。

インジェクションされないような変数が引数にあってもエラーにはなりませんが、値には何も入りません。

        if(id == null) {
            new RuntimeException();
        }

ここはNullpo防止対策なので、ノーコメント。

return "/html/" + id +".html";

Handlerの結果を返却します。
Handlerではreturn値に様々な値を設定することが可能となります。
forwardメソッドにはHandlerから返却されたreturn値をもって振り分ける目的のメソッドがあるので、その値を持ってfoward先を決定することも可能です。

そして、今回のようにforward先のhtmlファイルを直接指定することも可能です。
ルール側だとforward先が多岐に渡る場合に困るので、Handler側でhtmlファイルを返却する方法を使ったりした方が良い場合もあります。
まぁ、使い分けですね。

また、RedirectTargetProviderクラスを返却すると強制的に指定されたURLにリダイレクトしてくれます。

void型のHandlerを作成することも出来、その場合はExceptionが発生していなければ次の処理へ遷移します。
今回のケースでそうすると次の処理がないのでエラーになりますが。

Handlerの実使用例

実装の説明と共に、astamuse.comにて実際に使われているケースをざっくり語ることで理解を深めていただければと思います。

  • フォームから送信された内容をDBに保存する
  • ページを表示するために必要な最低限のデータが存在しているかのチェック
  • ログインが必要なページへのアクセス時のチェック
  • 外部サイトからのデータ取得(特許画像の取得や公報PDFの取得)
  • URL変更時のリダイレクト処理

起動

接続して思った通り表示されていれば大成功。
htmlファイルを用意してないURLにアクセスした場合はTmplateNotFoundExceptionが起きると思いますが、今回の場合はそれでも問題ありません。
(実際の運用上で起きたらまずいけど・・・)

終わりに

ここまでの回の内容が理解出来ればなんとなくWebアプリケーションを作ることは出来ます。
残り2回はプラスα的な内容をお送りしたいと思ってます。

次回予告

次回は、Handlerについてもう少しつっこんでみようと思います。

関連URL

第1回:環境構築して静的ページを表示するまで
第2回:Snippetを使って動的ページを作ろう
第3回:少し複雑なSnippet
第4回:Handlerの役割と使い方(今ここ)
第5回:Global Handlerとattribute
第6回:Form Flowを使おう

どこまでも迷走を続ける採用サイト【後編】

f:id:astamuse:20170201110635p:plain

採用サイト制作が始まってから久しいですが、先日(だいぶ前)ようやく公開の運びとなりました…!!!

recruit.astamuse.co.jp

取りあえずは一区切りついてよかったなぁ。
前2回のエントリでは「採用サイト作る作る詐欺」になっており、非常に心苦しくありました…
ということで、本エントリではそんな心苦しさを織り交ぜつつ、制作過程の振り返り及び今後の改修についてアレコレまとめました。

コンテンツ制作

1. どんなコンテンツを作るべきか?

まず始めに、サイトを構築する上で「アスタミューゼに入社することのメリットは何か?」という観点から最適なコンテンツの洗い出しを行いました。

前々回のエントリでも少し触れましたが、話し合いの結果

  • 無理な残業が少ない
  • 多用な働き方ができる
  • 勉強しやすい環境である

といった事が挙げられました。

また、上記を踏まえ「入社後の自分」をイメージしやすくするにはどうすれば良いか?という点について考えたところ

  • 自分の現状と比較できるコンテンツ
  • 社内がわかるようなコンテンツ

を作ることで入社後のイメージが具体的になり、より興味をもってもらうきっかけになるのではないかという結論に至りました。

次に「入社後の具体的なイメージ」を持って貰うためにはどのようなコンテンツをべきか? を検討したところ

  • 社員の1日
  • 前職との比較
  • 社内動画

の3つのコンテンツを作ることに決めました。


2. コンテンツ制作過程

2-1.動画

まず「社内がわかるようなコンテンツ」、といったらもう社内の様子を見て貰うのが一番手っ取り早いしウソがありません(演出されていたら別ですが…)。
社内の様子を見てもらえば出社時間から帰社時間、それぞれの仕事の仕方がわかりますし、イメージがしやすくなるでしょう。

しかしながら、ここで1つ問題が。

えー、まぁ何て言うか、正直な話、

デスク周りがお世辞にも綺麗とは言い難い(引越をしたばかりだったので)。

果たしてこれを世に送り出していいものか?

わたしたちは悩みました。

「もうちょっと片付いてからにしよう」

そう結論づけたために現在はちょっとフィルターを掛けてお送りしています。
今後、美しい感じで撮影できたら差し替えたい。
是非ともそうしたい。

もしイイ感じの動画がはまっていたら「ああ、ようやく片付いたんだなぁ…」と思って下さい。


2-2.社員の1日

弊社のデザイン・開発部はフレックス制ということもあって人によって出社・帰社の時間にかなり差があり、また自分に合ったペースで仕事ができる環境なので働き方も様々です。
なのでこちらのコンテンツを参考にしていただくことで「今後一緒に働く人かもしれない人がどんな働き方をしているのか?」「自分のペースでもやれそうかどうか?」など、イメージがし易くなると思います。

しかしながら、またここで1つ問題が。

数名にアンケートを採ってみたら

帰りがみんな揃って定時上がり。

こんな示しを合わせたように定時帰りだと、見ている人にウソだと思われるのでは…?

わたしたちは悩みました。

「でも本当だから仕方ない」

そう結論づけたために現在のコンテンツではみんなは仲良く定時帰りとなっています。
もうちょっとアクロバティックな出退勤の人にアンケートをお願いしてコンテンツを追加したい。
是非ともそうしたい。

コチラのコンテンツは今後もアンケートを採って少しずつ増やしていく予定ですので乞うご期待、です。


2-3.前職との比較

さらにアスタミューゼでの働き方をより具体的にイメージして頂くには、見ている方の現在と弊社を照らし合わせるコンテンツがあると良いのではないかと考えました。
デザイン・開発部の全員を対象にアンケートを採ったのですが、無記名で回答して貰ったので個人を特定されるプレッシャーもない結果だと思います。

しかしながら、またしてもここで1つ問題が、

特になかった。

よかったぁ…


全体の振り返り

今後のために制作を振り返って、良かった点/悪かった点についてまとめました。

良かった点/タメになった点

  • 意志決定が早い(方向性が決めやすい・共通認識が持ちやすい)
  • 自分たちの裁量で好きなように作れた(いつもと違うことができる)
  • 通常業務より広範囲に渡って考えることができる

弊社では通常、デザイン・フロントエンド・開発からそれぞれ1~2人が各プロジェクトに割り振られ、総勢5~7人のチーム体制でサービス運営をしています。
今回の採用サイトの制作・運営はいつもの半分、3人体制でした。
人数が少ない分、スピード感が生まれ、サクサク進められる印象を持ちました。
週に1度のMTGも少人数なので予定を確保しやすく、全体タスクの分量や割り振り、進捗確認もしやすいです。

また、裁量がコチラに任せられているということはモチベーションにも繋がりますし、どうやって流入を増やすか、CVさせるか、どう計測するか、その他諸々、コンテンツを含む全体の流れをより意識した制作ができたと思います。

良くなかった点/苦労した点

  • 期限を明確に決められなかった/想定が甘かった
  • プロジェクトに対する積極性

やはり期限を明確に決められなかったというのは個人的には痛かったと感じました。
期限もなんとなく、MTGの流れで「これくらいに出来たら(・∀・)イイネ!!」くらいの軽さだったのが良くなかったかなぁと今となっては反省です。
また、実際には想定していたよりも実装コストが高かったことと、タスクの兼ね合いでこちらの制作にまで手が回らないメンバーもいたため、途中でスケジュールを組み直して期限を切り直すなどの措置が必要だったと思います。

部長からは「きちんとしたコンテンツを目指すためであれば、多少の遅れはやぶさかではない」とのお達しがあったのですが、それでもあと少しリリースタイミングを意識できれば早めに手を打てたのかも知れない、と思いました。

また、上記と共通する話ではありますが、プロジェクトを進める上で3人のうち3人ともぐいぐいと引っ張るタイプではなかったため、今ひとつ積極性・コミットする力が足りなかったのかもしれないと思い至りました。
なので今後は(一部)ぐいぐいやって行きたいなぁ…となんとなく考えています。

今後の予定

今後の予定としましては、個人的にはPC版のデザインを綺麗にしたいのです。
元々、スマホ版からイイ感じに作っていきましょう、というところから始まったのでちょっとPC版のデザインが今ひとつ。
そこをまずキリッとさせたいな、というのが個人目標。

また、流入経路の確保がまだまだなので、そこら辺が当面の課題です。
ついでに申しますと、弊社Twitterアカウントなどがあるのですが、全然フォローされてない。

astamuse Lab (@astamuseLab) | Twitter

悲しみに暮れています。
これもどうにかした方が良いかなぁって、考えたり、考えなかったり…

もし良い案をお持ちの方、いらっしゃいましたらどうぞコチラからご入社ください。
お待ち申し上げております。

Spark 2.0を使ってみた

山縣です。

新年あけましておめでとうございます。

本年もよろしくお願いいたします。

 去年弊社の福田が CDH のアップグレードをしてくれてSpark が1.6系になるとともに、並行してSparkの2.0が使えるようになりました。(Spark2.0の導入については福田の記事をどうぞ→ もう待てない、Spark2.0の導入と実践 - astamuse Lab)

 現在弊社の環境でインストールされている Spark2.0 はまだベータということもあり、当初は少し触ってみる程度に留めようと考えていました。しかし1系のSparkが1.5→1.6に上がったことで問題に遭遇したこともあり、いくつかのバッチは2.0で動かしています。

 今回は1.6や2.0を触っていて遭遇した問題や気になった点などについて書いていきたいと思います。

Spark 1.6 と Tungsten

 社内のSparkが1.6系にバージョンアップされ、自分のジョブの一つが途中で abort してしまう現象が起きてしまいました。ログを見ると複数のDataFrameをJOINする処理で OutOfMemory が出ています。

 原因が分からずいろいろ調査をしていたのですが、改めてログを見直すとTungsten が有効になっていることに気が付きました。

 実は自分の実行している Job では今まで以下のパラメータ指定で Tungsten を無効にしているはずでした。

--conf spark.sql.tungsten.enabled=false

 Tungsten は Spark 1.4 から導入されたSpark の性能を上げるための仕組みです。社内のSpark が 1.3系から1.5系に上がったときに、この Tungsten が原因と思われるエラーが起きたため、上記パラメータで Tungsten を無効にすることで回避していました。

 もしやと思ってSpark 1.6 のリリースノートを見てみると以下のような記述がありました。

The flag (spark.sql.tungsten.enabled) that turns off Tungsten mode and code generation has been removed. Tungsten mode and code generation are always enabled (SPARK-11644).

 

Tungstenモードとコード生成を無効にするフラグ spark.sql.tungsten.enabled は削除されました。Tungstenモードとコード生成は常に有効となります。(SPARK-11644)

 つまり Tungsten は1.6から常に有効で無効化することができなくなってしまったようです。

 その後エラーを回避するためパラメータの調整などを試みましたがうまくいかず、最終手段として Executor のメモリ量を多くすることで何とか回避はできました。

 とりあえず回避はできましたが、このやり方ではデータ量が増えたり、処理がより複雑になったりしたときに、問題が再発する恐れがあります。そこで試しにこのバッチを2.0へ移行してみました。

Spark 2.0 への書き換え

 1系から2系へのメジャーバージョンアップということで当初は結構大変かと身構えましたが、やってみると意外と問題は少なく小規模な修正で済みました。具体的には以下のような修正をしました。

  • Scala のバージョン変更…Scala が 2.10系から2.11系に変わりました。
  • Spark ライブラリの変更…build.sbt で指定するSpark ライブラリのバージョンをSpark2.0のものに変更しました。
  • SparkSession 対応…Sparkのプログラミングをする上でのエントリポイントとして新しくSparkSession が導入されました。spark-shellにおいてsqlContext:SqlContext が無くなったので spark:SparkSession からSqlContext を取るようにしました。
  • spark-csv の置き換え … CSVファイルの入出力をするためのライブラリspark-csv の機能が2.0から標準で含まれるようになったので spark-csv を依存ライブラリから外しました。合わせて csv の入出力周りの処理を修正しました。
  • registerTempTable … Dataframe を SparkSQLから使うときにDataFrameのregisterTempTable メソッドでテーブルとして登録します。2.0からはこのメソッドがDeprecatedになったので createOrReplaceTempView に変えました。 (注 2.0 からDataFrameクラスは無くなりDataset になったので正確には Dataset のメソッドになります)

 つらつらと書いてみましたが、どれも大した変更ではなく、予想していたよりも楽に移行ができました。実際にビルドして実行してみると1.6で落ちていたジョブは無事に完了することができました。

 ただジョブそのものはtaskのエラーもなく進んでいるのに、driverのコンソールにエラーログが大量に表示されたり、途中で止まった処理の情報がコンソールに残ってしまったり、ベータだからか、まだ挙動が少し安定していない雰囲気もありました。そういうこともあり2.0への移行は様子を見ながら必要に応じてと考えています。

 と、これを書いている時点で、Cloudera社から Spark 2.0 Release1 が出ていることに気が付きました。いずれ社内にも導入されると期待しています。

Dataset について

 Dataset はSpark 1.6から導入されたSparkの新しいデータ形式です。Dataset は従来のDataFrame を拡張し、型パラメータを持ちます。

scala> import org.apache.spark.sql.{Dataset,Row}

scala> case class Msg(id:Int, msg:String)

scala> val ds:Dataset[Msg] = Seq(Msg(1, "I have a pen."),Msg(2, "I have an apple."),Msg(3, "Do you have a pen?")).toDS()
ds: org.apache.spark.sql.Dataset[Msg] = [id: int, msg: string]

 型パラメータを持つことでタイプセーフなプログラミングが可能となりました。従来 DataFrame を RDDに変換したりmap()foreach()などで処理する場合、DataFrame の各レコードは Row というクラスで表され、カラムからデータを取り出すときはRowクラスの getAs[T]()getString(), getInt() などのメソッドでデータを変換して取り出す必要がありました。

 Dataset では型パラメータとしてcase class などを指定することが可能で、各行を取り出すとき case class のインスタンスとして取り出すことが可能です。

scala> ds.map(x => x.msg).collect //x の型は Msg
res11: Array[String] = Array(I have a pen., I have an apple., Do you have a pen?)

 DataFrame では、例えばテーブルAのDataframe (dfA)もテーブルBのDataframe(dfB) も同じDataFrame のインスタンスなので、dfAに対して処理する関数を間違えてdfBに適用したとしてもコンパイルエラーになりません。

 しかしDataset を使えば型パラメータが違うのでコンパイル時にバグを発見することができます。下記のように Dataset[Msg] に対して処理する関数procDSMsg()に Dataset[Person] を渡すと引数の型が違うのでエラーとなります。

scala> case class Person(id:Int, name:String)

scala> val ds2 = Seq(Person(1, "Taro"), Person(2, "Jiro"), Person(3, "Subro")).toDS
ds2: org.apache.spark.sql.Dataset[Person] = [id: int, name: string] 

scala> def procDSMsg(ds:Dataset[Msg]):Unit = println("Hello")

scala> procDSMsg(ds)
Hello

scala> procDSMsg(ds2) // 引数の型が違うのでエラーとなる

<console>:35: error: type mismatch;
found : org.apache.spark.sql.Dataset[Person]
required: org.apache.spark.sql.Dataset[Msg]
procDSMsg(ds2)

 なお 2.0 からは 従来までのDataFrame はクラスとしては無くなり、Dataset に統合されました。DataFrame は以下の通り定義されています。

type DataFrame = Dataset[Row]

 つまりDataFrame は Rowを型パラメータとしてもつDatasetということになります。

Dataset用のCase classを半自動生成する

 Dataset によりタイプセーフなプログラミングが可能になりましたが、一方でこの case class 誰が作るの?テーブルごとに作るのめんどくさいので自動生成したい、と思うのは自然なことかと思います。

 ということでどうしようかなと思って考えたのですが、はじめに思い付いたのはDBライブラリのクラスの自動生成機能を使うことです。

 普段利用させていただいている ScalikeJDBC にも sbt のプラグインとしてclassを生成する scalikejdbcGen があります。試しに一つのテーブル用のclassを生成し、必要なところだけを抜き出して試してみました。しかし結果はうまくいきませんでした。理由はscalaikejdbcGen ではDBのカラム名の記法が sneak(例 abc_def_ghi) の場合に、対応するメンバ変数の名前を camel (例 abcDefGhi) にしてくれるのですが、Dataset 側でそういう変換には対応してくれないからでした。また、よくよく考えるとこの方法だと JDBCに対応したRDBMS以外のデータソースに対応できないという問題がありました。

 そこで次に思い付いたのが DataFrame/Datasetが保有しているスキーマ情報から生成することです。

 DataFrame/Dataset は schema:StructType に、DataFrame/Dataset を構成するカラム情報を保有しています。 StructType は各カラムのカラム名やデータ型などの情報を表すStructField の配列(Array)をデータとして持っていますのでこのデータを使えば case class のメンバーを定義できるはずです。

scala> ds.schema.fields.foreach(println)

StructField(id,IntegerType,false)
StructField(msg,StringType,true)

 そこで、case class を生成する Schema2CaseClass というクラスを作ってみました。

 試すにはリンク先のソースコードをコピーして spark-shell の paste で実行します。

scala> :paste
// Entering paste mode (ctrl-D to finish)
import org.apache.spark.sql.types._

class Schema2CaseClass {
...
...
// Exiting paste mode, now interpreting.

import org.apache.spark.sql.types._
defined class Schema2CaseClass

使い方は以下のようになります。

scala> val df = Seq(Msg(1, "I have a pen."),Msg(2, "I have an apple."),Msg(3, "Do you have a pen?")).toDF()

scala> val s2cc = new Schema2CaseClass
scala> import s2cc.implicits._

scala> println(s2cc.schemaToCaseClass(df.schema, "Msg2"))
case class Msg2 (
    id:Int,
    msg:Option[String]
)

 上記のようにDataFrameのスキーマ情報から Msg2 という case class を生成したので実際に試してみます。

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class Msg2 (
    id:Int,
    msg:Option[String]
)

// Exiting paste mode, now interpreting.

defined class Msg2

scala> val ds = df.as[Msg2]
ds: org.apache.spark.sql.Dataset[Msg2] = [id: int, msg: string]

scala> ds.collect
res14: Array[Msg2] = Array(Msg2(1,Some(I have a pen.)), Msg2(2,Some(I have an apple.)), Msg2(3,Some(Do you have a pen?)))

 上記のように Msg2 を 型パラメータとして指定してDataset を作ることができました。

 生成されたMsg2 は msg がOption[String]型になっています。これは元のDataFrameのスキーマ定義で nullable が設定されているからです。

 Optionが適切に処理されるのか確認してみます。

scala> val df = Seq(Msg(1, "I have a pen."),Msg(2, "I have an apple."),Msg(3, null)).toDF()
df: org.apache.spark.sql.DataFrame = [id: int, msg: string]

scala> val ds = df.as[Msg2]
ds: org.apache.spark.sql.Dataset[Msg2] = [id: int, msg: string]

scala> ds.collect
res16: Array[Msg2] = Array(Msg2(1,Some(I have a pen.)), Msg2(2,Some(I have an apple.)), Msg2(3,None))

scala> ds.filter(_.msg.isDefined).collect
res17: Array[Msg2] = Array(Msg2(1,Some(I have a pen.)), Msg2(2,Some(I have an apple.)))

 上記のように元データが nullの場合は None と変換され、filterなどで処理ができます。

DatasetとDataFrame の関係

 Datasetが出たので今後は DataFrame から Dataset に移行が進んでDataFrameは使われなくなっていくのかなと思っていました。しかし実際に使ってみると DataFrame も今後も使われていくという印象を受けました。 Dataset は確かにタイプセーフで良いのですが、例えばJoin した結果などで、それ自体が処理を分割するための中間的なデータであったり、一時的にしか使われないようなデータに対して、わざわざ case class を定義する必要は無いのではと思います。 また SparkSession.sql は DataFrame を返します。 2.0からDataFrameをDataset[Row] としたことでDataFrame/Dataset間がシームレスに使えるようになったことも合わせると今後ともDataFrameとDataset をうまく使い分けていくのが良いのかなと考えています。

終わりに

 2016年の Spark は2.0のメジャーバージョンアップも含めて非常に高速に進化していった印象を受けました。年末にはすでに2.1.0も出るなど相変わらずバージョンアップが激しいです。

 今年もどのように進化していくのか楽しみなプロダクトです。

Copyright © astamuse company, ltd. all rights reserved.