scala

2014年4月22日 (火)

WebSocketアプリのモデルを考える

ども。  
Salesforce Hackチャレンジに参加していたQuizarは安定の選外でした。。。 
ま、Salesforce1まったく関係なかったから無理もない。(^^;  

さて、それはさておきQuizarの製作を通してWebSocketを使えば今まで世の中に存在しなかったようなアプリを作れる可能性があるということを実感したわけですが、現実にはほとんどWebSocketアプリは開発されていません。

こうした状況の最大の原因は世の中にWebSocketアプリ開発のためのモデルやフレームワークがほとんど存在しないためだと思っています。

現状では低レベルのAPIだけはあるものの、それしかないからその上のモノは全部自分で作らないといけないわけです。
Quizarの開発過程でやったことだけでも、メッセージのルーティング、リクエスト/レスポンスの対応付けなど通常のWeb開発ではやらないようなプログラミングをかなり行っています。

状況としてはhttp/1.0の頃にServlet APIだけで開発していた極初期のWebアプリ開発に近いかな?
あるいは、ひと固まりのメッセージがあるだけでそこに何の付帯情報もないということを考えると生ソケットで直接メッセージを読み書きしている状況に近いという気も。(^^;

いずれにせよこの状態から一本のアプリを作り上げるのは結構な労力なわけで、逆に言えば、になんらかのモデルとフレームワークがあればWebSocketアプリ開発のハードルは一気に下がる気がします。

いくつかのアプリ開発を通してなんとなくその形が見えてきはじめているので、今回はそれをざっくりまとめてみます。


★ モデル

多分WebSocketアプリのモデルはほとんどすべてルームモデルに収まります。

「ルームモデル」という言葉は小西の造語ですが、とりあえず以下のように定義します

  • アプリ空間に複数のルームがあり、
  • ルームには複数の人が入室でき、
  • ルーム内にいる人同士がリアルタイムにコラボレーションできるアプリケーション

シンプルな定義ですが、それだけにほとんどのWebSocketアプリはこのモデルにあてはまるはずです。

Quizarは典型的なルームモデルアプリケーションですが、ルームの中にいるのはクイズの出題者と回答者という役割の違う2種類の人達です。
このようにルームに入室した各人の役割は複数あり得るので

  • ルーム内にはロールの異なる複数の人が存在して良い


というのも定義に加えて良いかもしれません。


★ フレームワークに必要な機能

次にルームモデルアプリケーションを作成するために必要な機能を考えてみます。
サーバー側にルームを管理する機能が必要なのはもちろんですが、最低限メッセージのフォーマットを決めておかないとリクエストのハンドリングもままなりません。
また、クライアント側も必然的にSPA(Single Page Application)となるのでWebSocket以外にも多くの機能が欲しくなります。

ここではとりあえずメッセージ、サーバー、クライアントの3つにわけて必要と思われる機能を列挙してみます。(細かく説明し始めるとあまりにも長くなりそうなので。。。)

ちなみにこのリストもQuizarの開発経験からの逆引きです。

● メッセージ

  • JSON形式のエンベロープとする
  • リクエスト毎にIDを持つ
  • リクエストのコマンドが識別できる
  • 任意複数のパラメータが渡せる
  • レスポンスのエラーが識別できる
  • JSON、HTML、Textなど複数の形式のレスポンスを返すことができる


● サーバー

  • 複数のWebSocket接続を束ねるルームを持つ
  • ルームの永続化(RDB等へのルーム情報の保存)機能は範囲外
  • スケールアウトするための外部のPub/Sub機能を利用できる
  • ルーム内ユーザーへのメッセージブロードキャスト
  • ブロードキャストメッセージのフィルタリング
  • メッセージのルーティング
  • メッセージのロギング


外部のPub/Subは今のところRedis以外に知りませんが、差し替えられるように抽象化はした方が良さそうです。

● クライアント
クライアント側ではルームモデルに限らずSPA全般で必要となる機能も含みますが、それぞれの機能は独立して使えるモノとします。

  • WebSocketをAjaxライクに使用できる(リクエストに対応するレスポンスを取得できる)
  • メッセージのエラーハンドリング
  • リクエストにひもづかないメッセージのルーティング
  • サーバーサイドからの切断時の再接続
  • アイドル状態の識別とイベントハンドリング
  • プラグイン可能なデバッグログ
  • ポーリング
  • WebStorageでのテンプレート管理
  • PushState対応


サーバーとの通信はAjax併用でも良いんですが、多分全部WebSocketでやる方がシンプルだし速度面やセキュリティ面でも優位があります。(これはそのうち別に書きます。)

逆に面倒な部分としては切断時の対応とデバッグ(ブラウザのWebコンソールでメッセージを見ることができない)があるんですが、これらをうまくフレームワーク側で吸収できればメリットだけを享受することもできるはずです。

□□□□
とりあえず、こんなところでしょうか。
なんか忘れてる気もしますが、まぁそれはおいおい(^^;

実際にはQuizar開発当初からフレームワーク化を意識していたので、上に書いた内容は概ね実装イメージがあります。(もちろんブラッシュアップや再考が必要な部分も多々ありますが。。。)

サーバーサイドはWebSocketとRedisのPub/Subが使えれば何でも良いんですが、とりあえずPlayで。(他でも作るとしたら多分次はNode.js)

というわけで、6月末を目途にフレームワークとなんか適当なルームモデルアプリを作ります。(^^v



2014年1月14日 (火)

HerokuでスケーラブルWebSocket

前回の続きです。
前回Play+Redisでスケーラブルに動くWebSocketアプリケーションが完成したので今回はそれを実際にHeroku上で動かしてみます。

といってもCLIで「heroku labs:enable websockets」を叩く以外は特に変わったことをする必要はありません。
この辺は以前にも書いたのでそちらも参照してください。

http://blog.flect.co.jp/labo/2013/12/herokuwebsocket-aad6.html

実際のところはデプロイすれば特に何の問題もなく動きます


★HerokuでWebSocketアプリを動かす場合の留意点

Herokuの特性と照らしてWebSocketアプリを動かす時に気をつけなければならない点がいくつかあります。
ざっと思いつくところは以下です。

  1. 通信が無い状態が30秒続くと接続が切れる
  2. マルチDyno対応
  3. デイリー再起動がある


1番目の30秒ルールへの対応はクライアントまたはサーバから定期的にPing的なメッセージを送信することで回避できます。

2番目のマルチDyno対応は通常のスケールアウトと何ら変わるところがないのでRedisを使うことでクリアです。

やっかいなのは最後、再起動対応です。
HerokuのDynoManagerはWebSocket接続がある場合でも容赦なく再起動をかけてくると思います。(試してませんが。。。)
これはHerokuの内部仕様なのでユーザ側では制御不能です。

以下、これに対してどういう対策が可能かを考えてみます。


★接続遮断時のクライアントの動作

Dynoの再起動をローカルでシュミレートする方法は単純に起動中のPlayframeworkをCTRL+Dで止めるだけです。

この時にクライアント側のJavaScriptがどのような動作になるかをまずは検証します。検証の方法は単純にWebSocketの onopen/onerror/oncloseイベントにconsole.logを仕込むだけです。

検証に使用したブラウザはChrome, Firefox, IE10です。
サーバ強制終了時に発生するイベントとその順序は以下です。

  • Chrome: onclose
  • Firefox: onclose
  • IE10: onerror, onclose


IEのみonerrorが発生していますが、これは多分IEの問題はまたPlayのWebSocket実装の問題です。強制終了に限らずサーバ側からWebSocketを切断(EnumeratorでInput.EOFを送信)した場合、常に発生するので。

ざっと検索した感じ(Playの話ではないですが)この辺りが関連トピックかなと思いますが詳しくは調査していません。

onerrorイベントは発生していますが、例外でスクリプトが停止する等の実害はないのでここでは気にせず話を進めることにします。

重要なのは強制終了による切断であってもクライアント側からは単にサーバからWebSocketを切断されただけに見えるという点です。(PlayはShutdownHookでクリーンアップを行っているのでそれも関係あるかもしれませんが)


★oncloseで再接続してみる

安易ですがoncloseイベントが発生した場合に再接続してみることにします。

ローカルのテストでは手動で再起動を行っている関係で、切断直後はまだサーバが起動していません(※1)。当然接続はエラーになりますが、その場合以下の順序でイベントが発生しました。

  • Chrome: onerror, onclose
  • Firefox: onerror, onclose
  • IE10: onerror, onclose


すべてのブラウザでonerrorとoncloseが発生しています。
onopenとoncloseって必ず対で発生するとは限らないんですね。。。イマイチこれが実装依存なのか正規の仕様なのかがW3Cの仕様見てもわからないんですが、とりあえずoncloseを拾って時間をおいて数回再接続を試みるような実装はできそうです。(※2)


※1 HerokuでマルチDynoで動かしている場合は再接続は別のDynoにつながるのでエラーなしで再接続できます。

※2 console.logは見てませんがiOS、Android(のChrome)でもリトライでの再接続ができているので、やはり接続失敗時にoncloseは発生しています。


★Window#onunloadでのoncloseイベントのクリア

ここまでの内容を実装して動かしてみるとローカル、Herokuともにいい感じに再接続ができています。(Herokuでの再接続のテストはCLIで「heroku ps:restart web.2」のようにDyno単位での再起動を行います。)

あともう一つ注意が必要なのはWebSocket#oncloseイベントはブラウザでページを閉じた場合などにも発生するのでその場合には再接続を行ってはいけないという点です。

ここではWindow#onunloadイベントでoncloseをクリアしていますが、ページ遷移以外のWebSocket切断があるアプリでは他にも制御が必要になるでしょう。

最終的なクライアント側のコードはこうなりました。(タグをつけようか迷いましたがmasterの最新です。)

https://github.com/shunjikonishi/websocketchat-redis/blob/master/app/views/chatRoom.scala.html

★まとめ

Dyno再起動に対応した再接続はがんばればできそうです。
しかし、ここをがんばる位なら素直にHeroku以外のプラットフォームを選んだ方が良いように思います。(^^;;;

しかししかし、EC2を使うとしてその場合にサーバの異常終了による予期しない切断の可能性を考慮しなくて良いかどうかは疑問です。
個人的な感覚としてはWebアプリはエラーが発生したとしてもリロードで回復するなら許せるんですが、かなり高い確率でそれもなんとかせぇと言われそうな気がしますね。。。(--

□□□□


まぁ必要に迫られたら考えますが、ふとこんな言い回しを思いつきました。

パフォーマンスもアベイラビリティも金で買ってください。
パフォーマンスは性能正比例で性能2倍になれば価格も2倍ですが、アベイラビリティは1%向上する毎に価格は10倍です!


昔友人が言っていた言葉の焼き直しですが、あながち間違ってないと思いますね。(^^;

2014年1月10日 (金)

PlayとRedisでスケーラブルWebSocket(実装編)

前回の続きです。

実際にPlayとRedisでWebSocketアプリケーションを作成したコードサンプルを示します。


★題材

今回の調査の途中でドンピシャのサンプルを見つけています。

http://www.ryantanner.org/2013/03/using-play-iteratees-and-enumerators.html

PlayにバンドルされているサンプルのチャットをRedis対応したサンプルアプリです。
接続情報を環境変数から取ってるふりして実はlocalhost固定だったとか、チャットルームのメンバリストをローカル管理してるから厳密にはスケーラブルになってないとか、手を入れ始めるといろいろ気にはなったんですが(^^;、WebSocket/Redisのコードサンプルとしては非常に有益で必要十分な内容でした。
記してここに感謝します。


今回の作業のゴールはこれを改良して汎用的に使えるWebSocket/Redisのベースクラスを作成することです。

https://github.com/shunjikonishi/websocketchat-redis

ただし現時点ではそれを切り出して単体のjar/pluginにすることは考えていません。現実にはWebSocket/Redisを使って何かを作る予定は今のところないしPlay、Redis共に開発サイクルが早いので、実際に必要になった時にはAPIが変わっている可能性も高いからです。

ここでは実際に必要になった時のプロトタイプとなり、その考え方や注意点を示せれば十分と思っています。


そんなわけでRedisService.scalaとChatRoom.scalaはかなり丁寧に書きましたが、それ以外(JavaScriptとか)は割と適当です。(^^;

JavaScript版のWebSocketラッパーもそのうち作りたいとは思うんですけどね。。。


★PlayでのWebSocketアプリの作り方

http://www.playframework.com/documentation/2.2.x/ScalaWebSockets

Controllerでリクエストハンドラを作る時にActionの代わりにWebSocketを使って受信データをハンドルするIterateeと送信を実行するEnumeratorを返すようにすればOKです。

以上、終わり(^^;

。。。なんですがちょっとだけIterateeとEnumeratorについても触れておきます。
これらを解説するサイトは日本語のものだけでも、かなり多数ありますが抽象度の高い概念なのでなかなか理解するのは難しいです。というか抽象度の高い議論は途中からついていけません。。。(^^;;;

そうした概念的な話はさておき、WebSocketの文脈に限定して話をするなら、

- Iteratee
クライアントからデータを受信した時と接続が切れた時に何をするかを定義するもの

- Enumerator
クライアントに発信するデータを供給するもの


とだけ理解しておけば十分です。


これ、よく考えるとアプリで必要な機能以外は何ひとつ作らなくて良いインターフェースになってるんですよね。

今までPlay2を触ってきた中でこのWebSocketのAPIが一番いけてると思いました。(^^;


★RedisService

https://github.com/shunjikonishi/websocketchat-redis/blob/master/app/models/RedisService.scala

プロトなので関連するクラスを全部1ファイルにまとめています。

RedisServiceクラスの機能は

- コネクションプール
- WebSocketで使用するPubSubChannelの作成

の二つだけです。

- コネクションプール
基本的にRedisを使う場合はコネクションプールを併用した方が良いです。RedisClientは基本的にソケット繋ぎっぱなので、接続のコストをカットできます。

ただし、RedisClientをSubscriberとして使用する場合は注意が必要です。Subscribe時のRedisClientは別スレッドでSocketの入力を監視しているのでプールに戻すタイミングが難しくなります。

今回はやや強引な方法でSubscriberもプールに戻してますが、シビアにスレッドセーフに気を使う必要があるので、素直にSubscriberとして使用する場合は新しいインスタンスを作成して使い捨てた方が良いと思います。(使い捨てる場合もunsubscribe時にdisconnectすることを忘れてはいけません。)

このプロトでは試行錯誤の過程を残す意味でプールに戻すコードを残していますが、もしこれを本当にライブラリ化するのであればSubscriberでは常に新しいインスタンスを使用するようにします。(そうするとborrowClient/returnClientメソッドが不要になるのでAPI的にもすっきりします。)


- PubSubChannel
WebSocketにひもづけるIteratee/Enumeratorをラップするクラスです。
コンストラクタの必須引数は購読するチャネル名のみです。
インスタンスを作成してメンバ変数のinとoutをWebSocketに返せばそれだけで単純なEchoアプリが作れます

オプションとして

  • send: クライアントから受信した文字列をRedisに送信する前に加工する関数
  • receive: Redisから受信した文字列をクライアントに送信する前に加工する関数
  • disconnect: クライアント切断時にRedisに送信する文字列を返す関数


などを指定できます。
exception/subscribe/unsubscribeなどのSubscriber関連のイベントハンドリング関数も一応指定できるようになってますが多分使うことはないです。

実装に関して言うと

  • Publisher: Actorになっていて都度プールから取ってきたクライアントで送信
  • Subscriber: ひとつのクライアントでredis-scalaの内部スレッドにおまかせ


となっています。
WebSocketではinとoutは一対ですが、RedisのPub/SubはWebSocketだけで使うものとは限らないので本来的にはは多対多であり、Publish自体はどこから行っても構わない訳です。

こう考えるとPublisherのActorはシングルトンでも構わない気がしますが、Publishのコストは購読者数に比例する(らしい)のでチャネル毎にわけています。

あとこのクラスではsubscribeするチャネルを一つに限定しています。Redisの機能を考えるとマルチチャネル購読を使いたいケースというのは十分に考えられるわけですが、そうすると

  • -> in/outにチャネル名も入れる必要がでてくる
  • -> 入出力の型をStringからTuple(String, String)に変更
  • -> in/outを直接WebSocketに接続できるというメリットが失われる


ので止めました。
まぁマルチチャネル対応版をサブクラスなりTraitに切りだすなりして対応するのはそんなに難しくないので必要に迫られた時に考えたいと思います。


★ChatRoom

https://github.com/shunjikonishi/websocketchat-redis/blob/master/app/models/ChatRoom.scala

ChatRoomに関して具体的なアプリの内容に関する説明はここではしません。
ここではWebSocket/Redisアプリを作る際のポイントとなる部分についてのみ説明します。


- クラスで状態管理をしてはならない
元々のサンプルではチャットルームのメンバ一覧をクラスのvar変数で管理していますが、複数サーバある場合に状態を共有できないのでアウトです。
状態管理の変数は常にRedis上に置く必要があります。

RedisにはListやSetを扱う機能があるのでとても重宝します。(^^;

- WebSocketとPubSubChannelは1対1とは限らない
WebSocket/Redisアプリを作る一番簡単な方法は先に作成したPubSubChannel(のin/out)を直接WebSocketにひもづけることです。

状態管理をすべてクライアント側で行うアプリであれば、サーバ側には一切ロジックを記述する必要がありません。(このサンプルはそうなっていませんが、チャットはそういうやり方でも作成できます。)

ですが、それだと接続毎にRedisのコネクションを消費することになるので無駄が多いわけです。

チャットの場合同じ部屋にいるユーザが購読するRedisチャネルは皆同じなのでホストで一つチャネルを開いてそれを共有できれば大幅にコネクションを節約できます。

実際、PubSubChannelのoutは複数のWebSocketで共有することができます

ですがinはダメです。何故ならこのIterateeではクライアント切断時にチャネルをクローズしているからです。つまり1人のユーザが退出したら全ユーザのWebSocketが切れます

しかし、前述の通りPublishはどこから行っても構わないのでPubSubChannelのinをそのまま使う必要はなく自前のIterateeを使うことができます

この時問題となるのはRedisの接続管理です。

- いつunsubscribeするか?
今でしょ!なわけないですよ。(^^;

論理的にはWebSocket接続数を数えておいて、0になったらunsubscribeすれば良いのですが、どこでcountUp/Downするかも悩ましかったりするわけです。考慮漏れがあると即リソースリークになるので。。。(--

で。。。結論なんですがこれは変に考えすぎず、クライアントからの切断にのみ反応するようにするべきと思います。具体的にはIteratee#Doneの中。

WebSocketの切断が発生するのは多くの場合ブラウザの画面遷移やクライアントの電源断(タブレット等ののスリープ含む)です。

これらはいつ発生するかはわからないので変に状態管理するよりも確実にそれだけを捕まえるのが良いと思います。

直観ですが、WebSocketアプリではサーバーサイドからの切断もあんまりやらない方が良い気がします。

- どこで接続管理を行うか?
これは絶対にActorを建ててそこでのみ行う必要があります。
IterateeはWebリクエストのワーカースレッド内で動くので、スレッドセーフにするためにはcountUp/Downを伴う操作は同じActorの中からしか実行してはいけません。

なのでIteratee#Done内で実行するDisconnet操作もActorにDisconnectメッセージを投げるだけとなっています。

□□□□
ここまでたどり着くのに相当試行錯誤しており、AkkaのActorにも慣れてないのでイマイチ実装に確信を持ててないんですが、概ね抑えるべきところは抑えたつもりです。

何かおかしなところがあれば是非ご連絡を!



★Playの残念なお知らせ

さて最後に今回気がついたPlayframeworkの衝撃の事実を。。。。(--

Playで修正/リコンパイルを繰り返している時には、修正前に使用されていたインスタンスは回収されません。。。マジでかーーーー!!


前回書いた通り、Redisの最大接続数=10の状態で作業しているとほんの数回修正を行っただけで接続エラー。。。。

その度にPlayの再起動を繰り返すのはなかなか苦痛でした。(最初はPlayのせいとは思わず自分かredis-scalaのリソースリークを疑っていたのでなおさら。。。(--)

Global#onStopでcloseすれば大丈夫かと思ったんですが、ここのロジックが実行されるのはリコンパイルが行われた後っぽい。。。意味ねーーーー!!!

シングルトン(object)とか、どうなってるのかと思うけどこれも単なるクラスのインスタンスなのでポインタ変わって新しいインスタンスが作られるだけなのかなと思ったり。

唯一有効だったのはActorのpostStopにcloseを仕込んで置くことだけど、これも常に有効かどうかは疑わしい。。。(上の推測通りシングルトンのアドレスが変わるのであればアウトだと思う。)

修正前のインスタンスが残るのは仕方ない気がするけど、せめてonStopはリコンパイル前に走って欲しい。。。(--

□□□□

以上、こんだけ書いておけば将来ホントにライブラリ化する日が来たとしても、ちょっとこれ作った奴出てこい!とか思わずに済むでしょう。。。多分。。。(^^;


次回、「HerokuでスケーラブルWebSocket」(かもしれない)

2013年7月 1日 (月)

FLECT OSS Libraryが公開になりました。

こんにちは

なんと、今日から7月です。つまり2013年ももう半分終わってしまいました。。。(--

そして、小西がFLECTに入社してからちょうど1年半たったということでもあります。
はやっ!年々時が経つのが速くなっているようでかないませんな。(--

ちなみにこのブログを書き始めたのは2012年の5月末です。1年1カ月で記事数31件。
さぼっている時期もありますがだいたい月に2回位書いている計算ですね。もうちょっと頑張ります。

 

★FLECT OSS Libraryとは

これです。

http://oss.flect.co.jp/

この1年半で僕が作ったものがあらかた入っている感じですね。内容はかなり雑多な感じです。なんでこういうものを作ろうと思ったかは以前にも書いたので割愛します。

個人的に汎用性の高いモノ、開発者にとって有益なモノを作りたいという志向性が強いのでこういう形での開発はテンションがあがります。(^^;

 

★Salesforce関連

思い起こせば1年半前、Salesforceを外部からAPI連携するのをやりやすくするライブラリを整備してほしいというお題に対して、いきなりSOAPClientから作り始めたのがFLECTでのキャリアの始まりでした。

それをベースにして作成したSalesforce APIのラッパーがSalesforceClientで、これでどれ位のことができるかを示すために作成したアプリが、Salesforce Explorerです。

完全に上司の想像のナナメ上をいっていたと思います。(^^;

Salesforceを使う開発者ならそれなりに便利に使えると思うので興味ある人は触ってみると良いかもしれません。(多分)

 

★Excel関連

excel2canvasはHTML5Canvasの調査をしている時に「絵心無いし、Canvasにテスト描画する適当なものが思いつかねー(--」と困ったあげくなんとなくExcelをそのまま描画してみようと思い立ったのが最初です。凝り性なのでやり始めると最初のお題からまったく関係ないシロモノになっていきましたがそこはそれ。(^^;R&Dとはそういうものです。(キッパリ)

これ、何気にSlideも結構なViewを稼いでいるし少なくとも日本では絶対うける思うんですけどね。どの位Excelを再現できるかはサービス化を目論んで開発したExcelReportで確認できるので、なんか適当な帳票フォーマットをExcelで持っている人はアップロードしてみてください。

 うまくブラウザで描画されないという場合は連絡くれれば可能な限り直します。(多分)

 

★その他

あとはHeroku上で動かすことを前提として作成したアプリをいくつか公開しています。

ちなみに最近作っているアプリは全部Play2で開発しています。社内のHerokuチームが使用している言語はPlay1なんですが、最近はメンテもされてないようなので次期主力言語として使えるかどうかを評価するという目的も兼ねています。

ちなみにPlay2の評価は。。。うーん、どうなんだろう?(--
まだまだベストプラクティスを模索している状態なので、書いていてももっと良い書き方があるんじゃないの?とか、本当にこんなめんどくさいことを毎回書かないといけないのか?とか思うこともしばしばです。

(↑もっとも、Scalaのおかげで圧倒的に楽になっている部分もあるので、このように不満点だけをあげるのは正当な評価ではありません。Play2については近日資料にまとめるつもりです。)

とにかく最近はHerokuが面白くてしょうがないので、Herokuを便利に使うための小物ツールをもうちょっと作っていこうかなと思っています。まだOSS Libraryにはリストされてませんが現在つくっているものもHerokuユーザーにとってはかなり便利です。(多分)

 

会社としても絶賛Herokuエンジニア募集中なので興味ある人は覗いてみてください。

2012年7月 2日 (月)

Play1とPlay2の比較 - まとめ編

ども。小西です。
ちょっと間が空いてしまいました。

その間に世の中ではplay2.0.2がリリースされました。

https://groups.google.com/forum/#!msg/play-framework/Z97GQ2VnR5M/T-STGaeuN68J

リリースメールを見る限り大きな変更点はなく、バグ修正といけてないコードのリファクタリングが主なんではないかと思います。
手元のアプリをバージョンアップしてみても特に差は感じませんでした。

さて、数回にわたって書いてきたPlay1とPlay2の比較記事もそろそろたたんでしまいたいと思います。
今回は使いながら気がついた細かい点をつらつらと書いてみます。




★heroku編
Play2は普通にherokuで動作しますが、新規アプリケーションを作成して1行もコードを書かない状態でpushしてもいきなりSlugサイズが40MBを超えます。
Apache POIなど大き目のjarファイルをいくつかインクルードするとそれだけで60MB超。困ったことにpushにかかる時間は約2分です。

herokuのドキュメントには「Slugサイズが50MBを超えたら構成を見直せ」みたいなことが書かれているんですが、フレームワークだけでそのサイズを超過するのでもはやユーザーにできることはありません。。。(--

ちなみにPlay1でもイニシャルで20数MBになるので、こちらも重いフレームワークではあるんですけどね。

いずれにせよherokuで使うことを考えたらPlay2はちょっとお勧めできません。

 

 

 

★WS編
前にも書いたと思いますがPlay2のWebServiceクライアントであるWSクラスでは同期APIが削除され非同期APIのみとなっています。
非同期APIの方がスレッドを効率的に使えるという理屈はわかりますし、今後は非同期APIが主流になっていくというのもそうなのかもしれません。
でも

http://www.playframework.org/documentation/2.0.2/ScalaWS

のサンプルを見ると結局同期でレスポンスを返しているので意味ないんでは。。。と思ってしまいます。(--

実際JDBCをはじめGoogleやAWSのSDKもほとんど同期APIなので、ここだけ非同期にすることにどの程度の効果があるのかは疑問です。
結局のところWebサーバーの仕事はクライアントに対して何らかのレスポンスを同期で返すことなんで、複数の外部サービスにすべて非同期でアクセスして待ち合わせするようなケース以外では非同期のメリットは享受できない気がします。

であれば、WSから同期APIを削除したメリットはほとんどなくて先のサンプルのようにユーザーに非同期プログラミングを意識させなければいけないというデメリットだけが残っているように見えます。

個人的にはWSの同期APIは復活してほしいです。

 

 

 

 

★JSON編
JSONを扱うためのライブラリがGoogleのgsonからjacksonに替わっています。
ざっとドキュメントを読んだ感じだと、使い勝手はそれほど変わらない様だしパフォーマンスに関してはjacksonの方が大分速いようです。

これだけであればjacksonへの変更はむしろ歓迎なわけですが、フレクトでは社内のライブラリでgsonをメチャメチャ多用しているのでPlay2にあげた場合でもgsonを使い続けるという選択になります。(--

パフォーマンスという観点ではPlay1.2.4ではgsonのバージョンが1.7.1だったのが、Play1.2.5では2.2にあがるようなのでそこはちょっと期待しています。(2012/07/02時点の最新版は2.2.1です)

 

 

 

 

★その他のライブラリ編
これを書くに当たってBuild.scalaを見直していて思い出しましたが、log4jも標準では入っていません。
デフォルトのLoggerはslf4j+logback。
log4jの開発は事実上終了しているので新たに起こしたプロジェクトとしてはこの選択は妥当だと思います。

そうはいっても、なんかライブラリ入れるとすぐにlog4jも一緒に入っちゃいますけどね。

 

 

 

 

★まとめ
大体持ちネタは以上です。
今回Play2のソースはまぁまぁ読みましたが、実際にはそれ程多くのものを作ってはいないので、色々と勘違い等もあるかもしれませんがこうした技術系のブログを読む場合はそのすべてを鵜呑みにしてはいけないというのは基本だと思うので、そのつもりで軽く読み流していただければ幸いです。(^^;;;

読んでてわかると思いますが、現状では僕のPlay2の評価はそれほど高くはありません。良いところもたくさんあるんですが、それらはほとんどPlay2の長所というよりもScalaの長所なんですよね。
Scalaは好きな言語なんでそれを使って開発したいという気持ちはあるんですが、当面はPlay1を使うことになりそうです。

2012年6月14日 (木)

Play1とPlay2の比較 - コントローラ編

こんにちは。小西です。
前回の予告どおり今回はPlay1とPlay2のコントローラの比較をやってみたいと思います。

予想ですがPlay1を使い慣れた人がPlay2にやってきた場合最初にこのコントローラの違いにとまどい「Play1の方が便利じゃん。。。」と思うような気がします。
Play1の頃のやり方からの推測で「多分こうだろ」と勘で使おうとすると確実にはまります



<Play1のコントローラ>

まず僕がPlay1でコントローラを書く場合の典型的なコードを示します。

pubilc static void process(String s1, String s2, String s3, int n1, boolean b1) {
    if (s1 == null || s2 == null || s3 == null) {
        badRequest();
    }
    ResultObject obj = app.process(s1, s2, s3, n1, b1);
    render(obj);
}

 

特に説明の必要はないかと思いますがコントローラを作る際のパターンとしては以下になります。

  1. ガード条件として最低限の引数のエラーチェックをしてエラーの場合は問答無用でbadRequest
  2. 独立したApplicationオブジェクトに処理を委譲して結果を取得
  3. ビューに結果を渡してレンダリング(またはrenderJSONでJSONで結果を返す)

 

アプリケーションロジック自体はフレームワークに依存したくないのでほとんどの場合ApplicationオブジェクトはPlay非依存で別に作成します。(なのでPlayのモデルクラス群は実はほとんど使ったことがなく、この比較記事にもモデル編はありません。)

そのため、コントローラのコードはほぼ100%ワンパターンに上のようなコードになります。

Play1では引数の追加などの際にただ単純にコントローラの引数を追加するだけで良く、intやbooleanへの型変換も自動でやってくれるあたりが素晴らしいです。

また、routesで

*       /{controller}/{action}                  {controller}.{action}

を定義しておけばメソッドの追加の際にも他の設定ファイルを変更する必要がなくいきなりコントローラにメソッドを追加できまるのも楽で良いですね。

 

 

<Play2のコントローラ - 駄目な例>

さて、先のサンプルコードを次はPlay2で書き直してみます。
ろくにドキュメントも読まずに僕は最初こう書きました

 

def process(s1: String, s2: String, s3: String, n1: Int, b1: Boolean) = Action {
    if (s1 == null || s2 == null || s3 == null) {
        BadRequest
    } else {
        ResultObject obj = app.process(s1, s2, s3, n1, b1)
        Ok(view.html.process(obj))
    }
}

ほぼPlay1で書いたバージョンの逐語訳です。
これ、動くには動きますがかなり問題があります。

 

routesの問題
まず最初に単純にコントローラにメソッドを追加しただけではそのメソッドをHttpリクエストで実行できるようにはなりません。
Play2のroutesにはワイルドカード構文がないのでメソッド追加時には必ずroutesにもメソッド定義を追加する必要があります。

routesの構文はPlay1の時と同じく空白区切りで先頭からHTTPメソッド、URIパターン、実行するActionの順で指定しますが、HTTPメソッドでもワイルドカードは使えないのでGETとPOSTは区別して定義する必要があります。

GET /test/process controllers.Test.process

書式としては上のような感じでPlay1とほとんど変わりませんが、先のメソッド定義の場合このroutes定義ではまだエラーになります。
Play2ではroutesのメソッド定義は引数まで正確に定義する必要があるからです。

GET /test/process controllers.Test.process(s1: String, s2: String, s3: String, n1: Int, b1: Boolean)

これで一応先のメソッドはブラウザから実行できるようになります。
ブラウザのURLで「http:.../test/process?s1=aaa...」のようにパラメータをつけて実行すると正常に実行できていることが確認できます。

次のテストとして「/test/process」のようにパラメータを外して実行すると「400 BadRequest」が返ってきますが、これは実は自前のnullチェックが機能したからではありません。

Play1ではコントローラの引数に指定したパラメータが見つからない場合はnullまたは0,falseなどのデフォルト値でメソッドが実行されましたが、Play2ではメソッド実行前にBadRequestが返ります

また、routesのHTTPメソッド定義をPOSTに変更して、パラメータを設定したPOSTリクエストを発行してもやはりBadRequestが返ってきます。
何故かと言うとroutesのメソッド定義で指定した引数にマップされるのはURLパラメータまたはURIパターン内で定義した変数だけでPOSTパラメータは対象外だからです。

 

 

ここまでに書いた内容ではPlay2に良いところはありません。
Play1に慣れた人ならば「いくらなんでもそれはないんじゃないの?」と思うことでしょう。
その通りです。

 

実際にはPlay2ではURIパターンを使用する場合以外はパラメータをこのように引数で指定することはありません。
ただ、routesのドキュメントにも例つきでこのやり方が載っているしPlay1を使ってた人ならば、ほとんどこの道を通ると思うんですよね。。。(--

 

余談ですが何故引数まで正確に必要かと言うとPlay2のroutingはreflectionではなく静的にメソッドをバインドしたクラスをジェネっているからだと思います。(つまりroutingのコストはPlay1よりもはるかに小さいと思われます)

<Play2のコントローラ - 正しい例>

Play2のコントローラでパラメータを処理する書き方は実際には以下のようになります。


def process = Action { implicit request =>
    val form = Form(
        tuple(
            "s1" -> text,
            "s2" -> nonEmptyText,
            "s3" -> nonEmptyText,
            "n1" -> number(min = 0, max = 100),
            "b1" -> boolean
        )
    )
    form.bindFromRequest.fold(
        e => BadRequest(e.errors.head.message),
        p => {
            val obj = app.process(p._1, p._2, p._3, p._4, p._5)
            Ok(views.html.process(obj))
        }
    )
}

細かい文法は置いておいて雰囲気はわかるでしょうか?

Formとしてパラメータを定義してそれをリクエストからバインドしています。

サンプル的にFormの中でnumberのminやmaxを定義していますがFormには簡単なガード条件を記述するだけのスペックがあります。(僕はあまり使いませんでしたがPlay1でもAnnotationやValidationで同じようなことができます。)

Form#foldメソッドは第1引数にエラーがあった場合に実行する関数、第2引数にエラーがない場合に実行する関数を指定できるのでここでガード条件の分岐を行うことができます。

routesでのパラメータ定義も不要になるので、これならば十分にシンプルでメソッドの仕様変更などの場合にもあまりストレスはなさそうです。

 

ここではパラメータのマッピングにTupleを使用していますが、それ以外にもcase classを使用してオブジェクトにマッピングできるので、自由度という点ではPlay2の方がPlay1よりも優れていると思います。

ただ、慣れの問題もあるにしても、どちらがわかりやすいかと言われればPlay1の方じゃないかとは思いますね。

 

 

<ちなみにFileアップロードを扱う場合は?>

FormではFileを扱うことはできないようです。

ファイルとその他のパラメータを両方扱う場合のコードがいまいち綺麗に書けないなーと思ってるんですが、今のところアップロードを扱う場合のコードは以下のように書いています。

def upload = Action(parse.multipartFormData) { implicit request =>
    val form = Form (
        tuple(
            "sheet" -> text,
            "download" -> boolean
        )
     ).bindFromRequest
    val file = request.body.file("file")
    if (file.isEmpty || form.hasErrors) {
        BadRequest
    } else {
        doUpload(file.get, form.get._1, form.get._2)
    }
}

Actionに謎の引数が増えてますがMultipartFormDataを扱う場合は使用するパーサーをそれ用に変更しなければなりません。

このあたりをちゃんと理解しようとするとソースのかなり深いところまで読む羽目になるのである程度の所で挫折しました。(--

 

いずれにせよPlay1のメソッドの引数をFileクラスで宣言すれば良かっただけというお手軽さとは比べるべくもない感じです。

ざっと見た感じだとFileもFormで扱えるようにすることはおそらく可能だと思うので、ここはPlay2の開発チームにもうちょっとがんばってほしいところです。

2012年6月 5日 (火)

Play1とPlay2の比較 - テンプレート編

こんにちは。小西です。

今回はPlay1とPlay2のテンプレートについて考察してみようと思います。
有志の方が翻訳してくれているのでPlay2についても日本語ドキュメントが徐々に揃いつつあり、最近では情報収集も大文楽になりました。

Play1のドキュメント - テンプレートエンジン
Play2のドキュメント - テンプレートエンジン

<とりあえず比較>
私的に両者の違いをまとめると以下のようになります。

Play1

  • Groovyベース
  • 宣言なしで任意の数のパラメータを渡すことができる
    • その代わりパラメータは一度ローカル変数に代入しなければならない
    • 不正なパラメータ名はNullオブジェクトとして扱われエラーにならない
  • マジックワードは複数を使い分け(変数は「&」、タグは「#」、スクリプトは「%」など)
  • カスタムタグをJavaまたはテンプレートで作成可能
  • テンプレート内で使用するJavaオブジェクトを拡張できる(Stringにrawメソッドを追加など)

Play2

  • Scalaベース
  • パラメータの宣言が必要
    • デフォルト引数やパラメータの名前渡しが可能(Scala標準機能)
    • リテラルや関数を直接パラメータに渡せる(どちらかというとできないPlay1の方がトリッキー)
    • 不正なパラメータ名はコンパイルエラー
  • マジックワードは「@」のみ
    • Play1との対比で言えば「%(スクリプト)」相当
    • 「@param」のような使い方ができるのは、ブロックの中の最後の行が戻り値となるScalaの言語仕様から
  • カスタムタグ相当のものはない
    • なぜなら「@」だけで同じことができるから
  • Javaオブジェクト拡張はない
    • なぜならScalaには「implicit関数」があるので最初から同じことができるから

いかがでしょう?
僕はこの二つのリストから機能よりもむしろむしろPlay開発者の意図の違いのようなものを感じます。
どういうことかというと、

  • Play1では、利用者の利便性を図るために多くの機能が作りこまれている
  • Play2では、Scalaのパワーを活用するために意図的に独自機能を盛り込んでいない

というような違いがあるように感じるのです。
あるいは、Javaの頃は色々と作りこまないと不便だったのがScalaにするとそんなことする必要がなくなったということかもしれません。

<Play2テンプレート>

Play2のテンプレートはほとんどScalaそのものであり、実際にscalaファイルに変換されてコンパイルされます。

ちなみに変換されたscalaファイルは「target/scala-x.x.x/src_managed」の下にあります。
(今回はとりあげませんがroutesファイルも変換されてここに置かれます。つまりroutesファイルもScalaです。)

生成されたメソッドの中身がどういうものであるかはこの際どうでも良いんですが、以下のことをチェックしておくとテンプレートの動作が理解しやすいと思います。

  • ファイル名に対応する名前でobjectが生成されている
  • そのapplyメソッドとして自分の宣言したパラメータをとり、HTMLを返すメソッドが生成されている
  • 「views.html._」がimportされている

要するにテンプレートの実行やその中で行っている処理は通常のScalaコード以外の何者でもないということです。

<Play2テンプレートの注意点>

さて、上に書いたようなことは別に知らなくても良いことではあるんですが、何故それを書いたかというとPlay2のテンプレートにはちょっとした問題点があるからです。

それが何かというと。。。。

実はPlay2ではこんなテンプレートは書けません。

{

はい。開き中括弧がひとつあるだけです。
Play2のテンプレートでは中に中括弧が含まれているとそれがどのような形であれ整合していない場合はエラーになります。

これはおそらく外部からパラメータとして渡す以外に回避する方法はありません。試した限り「""」で文字列として括っても一度変数に代入してもどうやってもエラーになりました。

まぁ整合しない中括弧が書けなくても実用上は特に問題はないですけれど、個人的にはこれはバグだと思っています。

<まとめ>
以上、ちょっと偏った視点からですがPlay1とPlay2のテンプレートについて考察してみました。

こうしてみるとPlay2のテンプレートは良く考えられていると思うし非常に面白いです。
しかし実際に使うとなるとPlay1のテンプレートの方が使いやすいと感じています。

何故そう思うんだろう?と考えてみたところどうも意識の差が大きいようです。
Play1の場合は「テキストを書いている」という気分で書けるのに対して、Play2の場合はどこまでも「Scalaを書いている」という気がします。まぁ僕だけかもしれませんけど。

中括弧の問題もJavaScriptを書く際にはちょっと気になります。(実際にはむしろバグの検出タイミングは早くなるはずなんですけどね。。。)

ここまで書いておいて何ですが、どれだけテンプレートエンジンが高機能でもテンプレートには可能な限りロジックは持ち込まない、ということが一番重要だよなと改めて感じました。
テンプレートは開発者以外の人がメンテすることもありますし。

まとめると

  • Play2のテンプレートは思わず語りたくなっちゃう程面白い
  • しかし実際に使いたいかどうかは話は別  
    • 個人的にはテキスト気分で書けるPlay1の方が楽
  • それだけ見て何やってるかわからないようなテンプレートは書いちゃ駄目

という感じですかね。

それでは、また。
次回はコントローラの比較をやります。

2012年6月 1日 (金)

Playframework 2.0 Javaのこと

こんにちは。小西です。
今回Play1とPlay2の比較記事を書くにあたり、Play2をJavaで使用するという選択肢はアリなのか?という点についても調べてみました。
なにしろJavaの方がScalaよりも圧倒的に技術者の数が多いので、Play2がScalaだけでなくJavaからも(まともに)使用できるならばそれはとても価値のあることだと思うからです。

で、とりあえずExcel2CanvasのサイトをJavaに移植してみたのですが。。。。

う~ん、ないですね。これは。。。

Play2をJavaで使おうなどとは未来永劫考えない方が良いでしょう。(--

一応できるはできるんですが、Scalaをまったく知らないJava技術者がこれを使うのは正直かなり苦しいと思います。
そして当たり前ですが、Scalaを知っているなら絶対にScalaを使った方が素直にコードを書けるわけです。

以下、ScalaコードをJavaに移植してみて感じたことをいくつかのポイントに分けて書いてみます。

1、Play2のベースコードはScala
Play2自体のコードにはJavaで書かれたコードとScalaで書かれたコードの両方が含まれています。
パッケージで言うと

・play.api.xxxがScalaコード
・play.xxx(「api」パッケージ以外)がJavaコード

となっており、自前のコントローラを書く際にもそれぞれのクラスをimportして使用することになります。

Java、Scalaのどちらを使う場合でもコントローラは「Controller」クラスを継承したクラスであり、コード中では「Request」とか「Response」などのオブジェクトを扱いますが、それらはパッケージも書かれた言語さえも異なる全く別のクラスです。

そして、Play2のベースとなるコードはおそらく全てScalaで書かれておりJavaAPIでは内部的にScalaAPIをコールしたり、それが不可能な場合は新たに書き直されたりしています。

Play2のドキュメントには「JavaをベースとしながらScalaのパワーを解放するのは難しい」というようなことが書かれていますが、「ScalaをベースにJavaでも使用できるライブラリを作成する」ということはもっと難しいだろうと感じましたね。。

ソースを見ていたのは1日2日ですが、それでもそこかしこにいびつさを感じます。(主たる原因はやはりJavaにはない関数リテラルがScalaでは多用されるためかと思います。)

2、テンプレートはどちらもScala
Scalaを使用する場合でもJavaを使用する場合でもテンプレートの仕様は共通でどちらもScalaになります。(厳密に言えばScalaベースの独自仕様で、テンプレート内でScalaコードを自由に記述することができます)

実際、今回ScalaコードをJavaに移植してみたわけですが、テンプレートのコードは1行たりとも修正していません。

Scalaベースといっても単純に文字列内に埋め込まれたパラメータを置き換えたいだけのようなケースではScalaの知識は特に必要なく、 ここまでならJavaから使う分にも特に問題はないんですが、Listなどのコレクションを使用してテンプレート内で繰り返し処理を行いたいような場合はちょっと困ったことになります。

そう。ベースがScalaなのでコレクションもScalaのコレクションにしないとJavaのままでは使いにくいのです。
もちろんテンプレートの内側でも外側でもScalaのJavaConversionsを使用して変換することはできるんですが、かなり「なんだかな。。。」という気分になります。(--

また、細かいところですがScalaではデフォルト引数とパラメータの名前渡しが使えるのでテンプレートのパラメータで下記のようにデフォルト値を宣言しておけば選択的にパラメータを渡せますがJavaから使う場合は全てのパラメータを明示的に指定する必要があります。

@(name: String, data: String, download: Boolean = false, version: String = "", 
strs: Seq[String] = Nil, pictures: Seq[(String, String)] = Nil)

3、バグ多し
これは1とも関係することですが、ScalaのコードとJavaのコードは別に書かれているので、Scala側に問題がなくてもJava側でバグが混入する可能性はあります。というより、現状ではかなり多そうな印象です。

わずか1日の移植作業で遭遇しただけでも、

・WS(HttpClientラッパー)のレスポンスからバイナリデータを取得する方法がない  
  (Scala版のWSにあるメソッドがJava版にない)
・日本語を使っているわけでもないのに何故かエラーメッセージがもれなく文字化け
・割と頻繁にエラーメッセージに表示される行番号が正しくない。

などがあります。
さらっと書いてますがわりと致命的です。(--

Scalaでコードを書いている時にはそれほど困った記憶がないので、これらはおそらくJavaだけにある問題です。
開発者の力のかけ方の差を感じます。

本筋とは関係ありませんがPlay2のWSではPlay1の時にあった同期APIがなくなり非同期APIのみになっています。
今回バグ回避のためにWSがラップしているningのHttpClientを直接使用しましたが、同期処理の場合は多分そうした方が素直に作れます。

4、Scalaはやっぱり便利
今回ScalaのコードをJavaに書き換えるにあたって面倒を感じたのは以下の点です。

・Tupleが使えないのが少し。。。
・List処理を全部forに修正するのがかなり。。。
・XMLリテラルでやってたことをStringベースにするのはとても。。。

これらは色々なところでScalaの利点として語られているので、ここでは多くを述べませんが一度Scalaに慣れてしまった技術者がJavaをおっくうに感じる気持ちはとてもよくわかります。

以上が、Play2のScalaコードをJavaに移植して感じたことです。
こうして改めて書いてみるとPlay2はまだまだこなれていない印象を強くします。

バグは直せば良いだけですけど、1や2は根源的な問題なのでそう簡単には解決しないでしょう。
また、現在Play2を開発している人たちの意識もそこには向いていないと思います。
個人的にはもうJavaのサポートは止めちゃっても良いぐらいだと思いますがこれからどうなっていくでしょうね。

いずれにせよPlay2を使うのであればがんばってScalaを覚えましょう!(^^)/

2012年5月30日 (水)

Playframework 1.2.4と2.0.1の比較をやってみるよ!

2日続けてこんにちは。
小西です。

さて、このブログなんですが「R&Dブログ」とか名乗っちゃってるので、それなりに気の利いたことを書かないとならんのじゃないかというプレッシャーがあるわけなんですが、当面はPlayframeworkの1系と2系(以下Play1、Play2と称します。)の違いについて書いていけば、それなりに興味深い内容になるんじゃないかと思っています。拙い文章ですがお付き合いいただければ幸いです。

 

 

 

 

<とりあえず経験値など>

まず筆者はJavaに関しえいえば10年以上の開発経験があります。アプリケーションサーバーの開発などにも従事していたのでJava屋としての実力はまぁそこそこです。

そんな筆者がPlay1を使い始めたのは今年の1月から。だいたい今で5ヶ月くらいですが、現在もメインの仕事はPlay1でのアプリケーションの開発で、ほぼ毎日コードを書いています。

ある程度Javaをわかっている人からするとPlay1はそんなに迷うことなくすんなりと使えるようになると思います。むしろServletでの開発経験の長い人なら色んなことがあまりにも簡単にできてしまうので笑っちゃうかも。(^^;

##もっともPlay1のControllerはJavaではないのでそこだけは最初とまどうかもしれません。
##これについてはいずれどこかで取り上げたいと思います。

一方Play2の方は先日Excel2Canvasの公開サイトを作る際にはじめて使いました。今で1週間くらいですね。

Scala自体は一時期はまって立て続けに5冊くらい本を読んだりもしたんですが、仕事で使ったことがないのと約半年ぶりだったこともあって大分忘れてましたねー。

何が言いたいかと言うとPlay1についてはまだしも、Play2についてはひょっとしたら外したことを言うかもしれませんがそういう時は優しく突っ込んでください。m(_ _)m

 

 

 

<ちなみにPlay1とPlay2のどちらを使うべきか?(先に結論)>

色々な考え方があると思いますが、僕の回答は当面Play1です。

(この回答には回りにScalaを使える人がほとんどいないという一番現実的な理由は含まれていません。)

理由はいろいろあって今後書いていきますが、自分がScala半人前という点を割り引いても、僕の評価は現時点ではPlay1の方が大分上です。
もちろん、これはPlay2が「2.0.1」と正規リリースされたばかりのバージョンであることと無関係だとは思いませんし、Play2の方が優れていると思う点もたくさんあるんですけどね。。。

あるいはこれを書いていく過程で自分の考え方も変わるかもしれないしその事が少し楽しみでもあります。
Play2(Scala)推進派の皆様のご意見をお待ちしております。m(_ _)m

採用情報

株式会社フレクトでは、事業拡大のため、
Salesforce/Force.comのアプリケーション
開発
HerokuやAWSなどのクラウドプラッ
トフォーム上でのWebアプリケーション開発

エンジニア、マネージャーを募集中です。

未経験でも、これからクラウドをやってみた
い方、是非ご応募下さい。

フレクト採用ページへ

会社紹介

株式会社フレクトは、
認定コンサルタント
認定上級デベロッパー
認定デベロッパー
が在籍している、
セールスフォースパートナーです。
heroku partnersにも登録されています。
herokuパートナー
株式会社フレクトのSalesforce/Force.com
導入支援サービス
弊社の認定プロフェッショナルが支援致します。
・Visualforce/Apexによるアプリ開発
・Salesforceと連携するWebアプリ開発
も承っております。
セールスフォースご検討の際は、
お気軽にお問合せください。
Powered by Six Apart