RoarとCarrierWaveを使ってみて躓いたこと

この4ヶ月間ほど余り更新出来ていいなかったので更新出来る間は更新していこうと思います。

今回は、RoarとCarrierwaveを使ったRailsアプリケーションでハマったことを書きます。

どんなアプリケーション?

実際のRailsアプリケーションは公開出来ないため、代替のRailsアプリケーションを作りました。
自分のGithubのリポジトリsurvey内にあるsystem_linkageに置いてあります。

概要

端的に言うとブログです!
ただ投稿用と表示用とでアプリケーションが分かれています。
「何故分かれているのだろう?」と、思うかもしれませんが、今回書きたいことの都合上分けています。

構成

system_linkageは非常にシンプルな構成になっています。

app1は記事投稿用のアプリケーション
app2はブログアプリケーション
simple_postsに共通のモデルとRepresenterを定義しているFull Engine
無駄な部分はありますが、シンプルな構成です。

アプリケーションの起動

アプリケーションは以下の手順で起動します。

redis-serverを起動
S3のアカウント情報をapp1,app2以下のconfig/environment.ymlに記載(config/environment.yml.exampleを参考)
app2以下のconfig/environment.ymlにapp1のURLを記載
app2以下でbundle exec sidekiq -C config/sidekiq.ymlを実行し、sidekiqを起動
app2を起動
app1を起動
これでアプリケーションの準備が整いました!

ブログ公開手順

すでにアプリケーションは起動できているはずなので、ブログアプリケーション側で表示する手順の概要だけ書きます。

app1でカテゴリを作成
app1で記事を作成
これだけです!後は、1分毎に定期実行しているsidekiqのジョブで取り込まれます。

作成した記事データが連携されない

「さぁいよいよ連携するぞ!」と思ってsidekiqを起動したのですが、いつまで経っても記事データが連携されません。

CarrierwaveのUploaderが原因?

まず、記事のモデルとRepresenterを見て下さい。



うまく連携されるように思えるのですが、CarrierwaveのUploaderをマウントしているカラムをRepresenterでproperty指定しているのが原因のようでした。
CarrierwaveのUploaderをマウントしているカラムは、Fileのインスタンスしかの代入しか受け付けません。(受け付けるかもしれないけど)
んで、取得したJSONがこれです。



見たら分かると思いますが、Hashを入れようとしますがUploaderは受け付けません。
そうしてthumbnailがnilとなり、バリデーションエラーとなり、データが連携されなかったのです...

CarrierwaveUploaderを使う時の対策

いくつか対策はあると思いますが、Representerでの対策を今回は書きます。
Representerのpropertyやcollectionには、setterやgetterなどのオプションを指定し、代入・取得時の処理を自身で定義することが出来ます。
これを利用してsimple_posts/app/representers/post_representer.rbを書き換えました。



Uploader経由で読み込み・書き換えを行うのではなく、モデルから直接読み込み・書き換えを行うようにしました。

とてつもなく、これでいいのか感はありますが...

作成・更新したデータを一括で取り込めない

作成した記事はうまく連携されるようになりました。
しかし、更新した記事はうまく連携されませんでした。

collection_representerのデフォルトの挙動

どうやらcollection_representerのデフォルトの挙動がアプリの挙動に合っていないようでした。
collection_representerのデフォルトのfrom_jsonの挙動は、渡されたjsonをAraryにしてそれぞれを新規作成するような挙動になっていたのです。
そうなると、同一IDのレコードを作成しようとして失敗し、連携されないようでした。
そこまで分かったのなら、collection_representerのparse_strategyに作成・更新のfind_or_instantiateを指定すれば解決すると思っていました...



SELECT * FROM posts WHERE ID IS NULL !?

parse_strategyにfind_or_instantiateを指定したのですが、これでもうまく連携されていません。
相変わらず作成だけはされていました。
何が起きているのか分からないのでアプリのログを見ていたのですが、取り込むときにこんなログが...




え、えぇ!


なんということでしょう。
既存のレコードを検索するときに、IDがNULLのレコードを検索しているようです。
もちろんそんなレコードは存在しないので、またまた新規作成しようと試みるわけです。
しかし、先ほどと同様に同一IDのレコードを作成しようとして失敗し、連携されないようでした。

そこでRepresentableのfind_or_instantiateのソースコードを見てみました。執筆時は、以下の様なコードになっています。



find_or_instantiateの振る舞いは結構問題※1があったので、また変な挙動をしているのかなー?と思ったのですが、この部分は変な挙動をしているわけではなさそうでした。
options[:instance]に入っている無名関数にはfragmentなどの引数が渡されるのですが、このfragmentにHash化したjsonが渡されてきます。
後はお察しの通り、fragmentのキーがidのバリューをとろうとするのですが、fragmentにidというキーが存在せず、nilを返し、object_class.find_by({id: nil})と同じ挙動をしているようでした。
それでは、この無名関数に渡されたfragmentがどのようなHashは以下の様なHashでした。



これを見れば分かると思いますが、そりゃidというキーはfragmentにはないですね。
これじゃnilになっちゃいますよね。
この現象、Representerにrepresentation_wrapを指定しなければ発生しないようで、representation_wrapを外せば大丈夫なようです。

collection_representerで一括取り込みを行う時の対策

結局、どうして解決したのかというと自分でparse_strategyを書きました。



怪しい匂いがプンプンしやがりますが根本的な対策は誰かがやってくれると信じて、表面的な対策だけしました。
fragmentのデータを元にwrapで指定した値をキーに指定して、それからidがキーのオブジェクトを取得するようにしました。
こうすればあれば見つかりますし、find_or_initialize_byにすれば検索失敗時にインスタンス作ってくれますしね!

とまぁ、Roarを使って複数システムの連携で躓いた事を書きました。
皆さんはどうでしょう、このような問題には直面しないでしょうか?
もしくは、そもそもRoarは使わない?
本格的に使うとなると根本的な対策を考えようと思いますが、出来れば使いたくないですね。



月曜日の健康診断が不安...



※1 アソシエーションもfrom_jsonで取り込むときにfind_or_instantiateを指定するとアソシエーションが先にsave呼ばれる問題など

コメント

このブログの人気の投稿

新生活始まります

ElasticIPを複数利用する時の注意

タスクの実行結果をwhenで指定するならcheck_modeつける