ダメでしょ!

プログラミングとか怒られた話とか…

ActiveAdmin にCSV取込機能を追加する際に便利な gem active_admin_import

前提

ruby 2.1.10p492
Rails 4.2.8
active_admin_import

既存システムの修正依頼

前に他の担当者が作成した Rails アプリで、CSVインポートが上手くいかないという相談があり、アプリを確認したところ、管理サイト側が ActiveAdmin を利用していました。
そして、CSVインポート機能として採用されていたのは active_admin_importable という gem でした。
この gem なんですが、最新バージョンの更新日が2013年4月30日で、約4年更新がありません。
枯れているという意味ではいいのかもしれませんが、既存のソースを確認してみるとモンキーパッチを当てている箇所があったり、Shift-JIS の取込で苦労していそうな感じでした。
なので、別の gem を探してみたところ、今回利用した active_admin_import がいい感じでした。
調査時点での最新バージョンの更新日付は2017年4月26日です。

github.com

機能紹介

使い方はサンプル含め、以下のサイトに記載があります。

Active admin import by Fivell

ざっと機能を確認すると、

  1. 置換(Replacements)
  2. エンコード処理(Encoding handling)
  3. CSVオプション設定(CSV options)
  4. ヘッダー自動付加機能(Ability to prepend CSV headers automatically)
  5. activerecord-import を利用した一括インポート(Bulk import (activerecord-import))
  6. コールバック(Callbacks)
  7. Zipファイル取込(Zip files)

と、なかなか便利な感じです。

使ってみる

gem 'active_admin_import' # ActiveAdmin用CSVインポート機能

上記を Gemfile に記載し、bundle install します。

この gem は active_admin 用ですので、 app/admin/モデル名.rb ファイルに追記していくことになります。

以下は User モデルへの追加例です。

# app/admin/user.rb
ActiveAdmin.register User do
  active_admin_import validate: true, batch_transaction: true, template_object: ActiveAdminImport::Model.new(
    hint: "インポートするCSVファイルにヘッダー行は必要ありません。<br>
    文字コードは CP932(Windows-31J) を想定しています。(Excelを元にしたCSVファイルを想定)<br>
    <br>
    以下の順序で設定されているファイルを取り込みます:<br>
    'ID', 'パスワード', '姓', '名', '所属'<br>
    <br>
    取込に失敗した場合のエラーは5件分のみ表示しています。",
    csv_headers: ['user_id', 'password', 'last_name', 'first_name', 'dept'],
    force_encoding: :'CP932'
  )

上記のように記載するだけで、ヘッダー行なしのエクセルベースで作成したCSVファイルを取り込むことができるようになりました!

使ってみての利点

こちらの gem のいいところですが、

  1. エンコード対応
  2. バリデーション対応
  3. トランザクション対応

です。
日本語を扱う上で エンコードは常に問題になりますし、色々と考える必要に迫られます。しかし、特に英語圏で作成された gem はそのような課題にぶつからないのか、けっこう対応できていない gem は多いように感じます。
また、バリデーションに対応しており、バリデートエラーは画面にメッセージが表示されますので、利用者に優しいです。(active_admin_importable は取込がうまく言ったのかどうかがわかりにくかったんですよね。。)
さらに、トランザクション対応していますので、バリデートエラーが発生した場合はすべての取込をロールバックできます。(バリデートエラーが発生したレコード以外は取り込むように設定することもできます。)

日本語で検索するとactive_admin_importable を紹介しているサイトばかりで active_admin_import を紹介しているサイトがなかったので、今まで選択肢に上がってこなかったかもしれませんが、使ってみることをおすすめします。

sqale がサービス終了するので、Heroku に移行した

sqale からの移行推奨先は Heroku

info.sqale.jp

公式にそのような案内がありました。
ただ、正直な話、レイテンシが発生するということ、ドル払いになってしまうことから最初は他のサービスに移行したいと考えていました。

移行先調査した

ということで、移行先を検討してみたんですが、国内サービスは(私の観測範囲では)見当たらなかったんですよね。
で、結局
AWS
GCP
・Azure
・Heroku
という形になってしまいました。
この内、
AWS
はIaaSしかなさそう、
GCP
はPaaSがあったが、rubyはベータ版?っぽい、
・Azure
はよくわからんかった。
となり、結局Herokuになってしまった。
もう少し調査に時間がかけられたら・移行に時間がかけられたら、というたらればはあるのですが、いかんせんあまり時間ばかりもかけていられなかったので。

Heroku に移行すると大半の人はコストアップになりそう

というのが、Heroku のデータベースに起因してきます。
デフォルトでは、DBの容量がたったの5MB。(ClearDB MySQLの場合)

Sqaleでは2GB使えていた(ハズ)なので、移行を検討している殆どの人が引っか

elements.heroku.com

かるのではないかと。
で、これを1GBにしようとすると、9.99ドル/月が必要になります。

Heroku サーバ自体が、24時間無停止にしようとすると7ドル/月 かかるため、併せて16.99ドル/月 が最低料金になるかと。

Heroku くらいしか同じように使えるところがないからしょうがない・・・

月額が約2倍となってしまい、更に東京リージョンではないためにレイテンシが発生するという全然嬉しくない結果なのですが、他が見当たらないのでしょうがないです。

sqale のサービス終了、痛いです。

移行してみて

で、実際 Heroku に移行してどうなのか、ですが、レイテンシはもちろんありますが、それ以外の部分は結構使いやすいです。
一つつまずいたのが、crontab です。
今までは whenever という gem で crontab の設定を管理していたのですが、Heroku ではcrontab が使えないようです。
代わりに Heroku scheduler というものでバッチ系の処理を管理するみたいです。

Heroku Scheduler | Heroku Dev Center

そのあたりがわかっていなかったので、対応に時間がかかりました。

Rails5 でバッチ処理を利用する際に知らないとハマるかもしれないポイント!

前提

Rails 5.0.2
Ruby 2.4.0

なににハマったのか?

Rails5 で新規アプリを作成していて、バッチ処理を作成しました。
処理は .\lib\tasks\ 配下に格納し、 .\config\application.rb に以下の設定を行いました。

class Application < Rails::Application
  ・・・
  config.autoload_paths += Dir["#{config.root}/lib"]
  ・・・
end

これで lib ディレクトリ配下は自動的に読み込まれる設定になっていると思っていました。
Rails4 時代はこれでよかったんです。。。
さらに、開発環境では問題なくバッチ処理を呼び出すことができます。
これによって問題の解消がますます困難になりました。。。

Rails5 の本番環境では autoload が無効化された

A Guide for Upgrading Ruby on Rails — Ruby on Rails Guides

上記に記載されていますが、Rails5から本番環境では、autoload が無効になっています。
これは、、、知らないと絶対ハマる。

どのように問題に対応するか

上記のURLには解決策として、.\config\application.rb

class Application < Rails::Application
  ・・・
  config.enable_dependency_loading = true
  ・・・
end

と記載することで、autoload するように変更することがあげられています。
しかし、autoload が無効化されたということは、今後使えなくなることが想定されます。
ですので、今回は eager_load を利用することにしました。 .\config\application.rb

class Application < Rails::Application
  ・・・
  config.paths.add 'lib', eager_load: true
  ・・・
end

と指定し、lib ディレクトリ配下を eager_load されるように指定することで問題を解消しました。
app ディレクトリ配下の読み込みはこの eager_load で行われているようなので、これで問題ないはずです。

Rails のルーティングに使用する id を別の内容に変更する方法

通常、edit 等に使用する URI に設定される id を、別のキー等で置き換えたい場合があります。

$ be rake routes
             Prefix Verb   URI Pattern                                        password_resets POST   /password_resets(.:format)              password_resets#create
 new_password_reset GET    /password_resets/new(.:format)          password_resets#new
edit_password_reset GET    /password_resets/:id/edit(.:format)     password_resets#edit
     password_reset PATCH  /password_resets/:id(.:format)          password_resets#update
                    PUT    /password_resets/:id(.:format)          password_resets#update

この id を code に置き換えたい場合、.\config\routes.rbに以下のようにparam: codeを設定することで、置き換えることができます。

  resources :password_resets, param: :employee_code, only: [:new, :create, :edit, :update]

上記のようにすることで、ルーティング情報は以下のようになります。

$ be rake routes
             Prefix Verb   URI Pattern                                    Controller#Action        password_resets POST   /password_resets(.:format)                     password_resets#create
 new_password_reset GET    /password_resets/new(.:format)                 password_resets#new
edit_password_reset GET    /password_resets/:code/edit(.:format) password_resets#edit
     password_reset PATCH  /password_resets/:code(.:format)      password_resets#update
                    PUT    /password_resets/:code(.:format)      password_resets#update

id とは別のキーを主キーのような形で使う際に重宝します。

Perl でパスワードZipを求められた際の対応方法

前提

Perl は初めて触りました。
対応を求められたサーバの環境は古いです。

$ perl -v
This is perl, v5.10.1 (*) built for x86_64-linux-thread-multi

QBK (急にボールが来たので)

他にヘルプできそうな人間がいなかったということで、触ったことのない Perlソースコード修正を求められました。

内容は、Webフォームに添付されたデータをメールで送信するシステムの添付ファイルをパスワードZipで圧縮すること。

Perl 触ったこともないので、極力他に影響を与えたくない一心で対応しました。
調べてみると、Perl の標準モジュールに「Archive::Zip」というものがあるのですが、これはパスワードZipに対応していませんでした。。。
どうしようかなー、と悩んだのですが、Perl からコマンドが呼び出せるらしいので、コマンドでパスワードZipを作成し、それを添付することにしました。
ということで対応したコードが以下。

#-----------------------------------------------------------
#  パスワードzip生成
#  引数: ファイル名
#  備考: $cf{upldir} には、ファイルの保管場所ディレクトリパスが入っている
#-----------------------------------------------------------
sub zip {
    my $ZIP_CMD = "/usr/bin/zip";
    my $ZIP_PASSWD = "************";
    
    my ($origin_name) = @_;
    my $zip_name = "$origin_name.zip";

    my $cmd = "$ZIP_CMD -jP $ZIP_PASSWD $cf{upldir}/$zip_name $cf{upldir}/$origin_name"; # パスワードzipの生成コマンド
    system($cmd); # コマンドの実行
}

で、これをメール送信処理中の添付ファイルをセットする前に

&zip($fname); # $fname には対象となるファイル名が格納されている

と呼び出して、生成したファイルを添付することで対応しました。

今回はパスワード自体をランダム化する必要が無いという要件だったので、ソースコード内にパスワードを埋め込んでいますが、あまりよろしくないという認識はあります。
また、system関数は便利な半面、セキュリティリスクになりそうなので、使い所を見極めて使う必要がありますね。

初めて Perl に触りましたが、デバッグ方法もよくわからないし、辛かった。。。

ActiveRecord に対する scope は条件に一致するレコードが存在しない場合 .all の結果を返却する

scope を利用していたところ、思わぬところでハマってしまいました。。 指定の条件で検索を行い、返却された結果が nil かどうかで条件分岐させようと思っていたのですが、scope を利用すると常に nil ではなく、.all の結果が返却されてきます。

scope :search_with_user, -> (user_id) { where('user_id = ?', user_id) }

よくよく調べて見たところ、 scope はメソッドチェーンを実現するために、nil を返すことはなく、.all を返すんだとか。

要は scope の利用方法に関する認識不足だったのですが、他の人も同じような勘違いをする可能性もあるかも、と思ったのでブログに残しておきます。

Sqaleがサービス終了するとのことです

PaaSとして柔軟に利用していたGMOペパボのサービスであるSqaleですが、終了するとのこと。

【サービス終了のお知らせ】 | Sqale Information

安価であったため、結構使い勝手がよく、ちょこちょこ利用させてもらっていたんですが、rubyのバージョンアップに追従してなかったんで心配していたのですが。。。 代替サービスとしてはHerokuが推奨されています。

www.heroku.com

Herokuはドル建ての請求になるだろうから、それが経理からいやがられるんだよな。。