おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

rails-assets の調子が悪い時の回避策

皆さん、rails-assets は使っていますか?

rails-assets は、Gemfile で js や css のライブラリを指定して、バージョン管理や依存の解決などをしてくれるとても便利なサービスです。

しかし最近ではその役目を終えたとして、最大で今年末でサポートを終了するとしています。

Future of rails-assets · Issue #291 · rails-assets/rails-assets

そしてもう閉じることが決まったプロジェクトだからなのか、あまり活発にメンテナンスをしていないような印象を受けます。昨年からrails-assets を運営しているサーバが不安定になることが多く、rails-assets に依存しているプロジェクトを持つ会社さんは、bundle update やデプロイに失敗して辛い日々を過ごしたのではないでしょうか。

そんな折、明日 2016/1/19 にサーバのSSL証明書の期限が切れることがわかりました。証明書の期限延長対応がされるまで、困ったことになりますね…><

f:id:willnet:20160118191355p:plain

(追記) 証明書は lets'encrypt を利用しており、(おそらく)自動で更新されるようになっているようです。よかったですね。 SSL certificate for rails-assets.org will expire soon · Issue #306 · rails-assets/rails-assets

僕がお手伝いしているクリニカルプラットフォームさんでも rails-assets を利用しており、昨年 rails-assets が不安定になった際に、非常用として回避策を用意しておきました。

その回避策をシェアしておきますので、僕たち同様に困っている方がいらっしゃったらご利用下さい。以下は クリニカルプラットフォームさんの esa@masa_iwasaki がまとめてくれた記事を改変したものです。

rails-assets.org が不安定な場合

rails-assets.org へ接続ができない場合、 bundle install が終了せず最悪デプロイができない状況に陥ります。

急いでいる場合は、rails-assetsのリポジトリからソースコードを手に入れ、開発環境でgemパッケージをビルドし、外部に公開してそのURLをGemfileで参照するという手段があります。ただし、多少の時間はかかりますので、時間に余裕を持って取り組むのがベストです。

rails-assetsでgem生成

rails-assetsREADMEに従ってセットアップします。redis さえ動いていれば、最後の foreman start は必要ありません。

git clone git@github.com:rails-assets/rails-assets.git && cd rails-assets
bundle install && npm install
cp config/database{.sample,}.yml
cp config/application{.sample,}.yml
# edit config/database.yml and config/application.yml if necessary.
bin/rake db:setup
foreman start

手元でgemを以下のようにビルドします。各assetのバージョンは各プロジェクトのGemfile.lockを参照して取得してください。

bundle exec rake 'component:convert[jquery, 2.1.4]'
bundle exec rake 'component:convert[jquery-ui, 1.11.4]'

↑の作業を楽にするために、Gemfile.lock をパースして、実行すべき rake タスクを出力する簡単なスクリプトを書きましたのでご利用下さい。parse_gemfile_lock.rb のように保存して ruby parse_gemfile_lock.rb Gemfile.lock で実行できます。

files = ARGF.readlines.grep /rails-assets-/
files.map(&:strip!)
files.reject! { |file| file[-1] == '!' || file.match(/[>=<]/) }
files.each do |file|
  name, version = file.split(' ')
  name.gsub!(/rails-assets-/, '')
  version.gsub!(/[\(\)]/, '')
  puts "bundle exec rake 'component:convert[#{name}, #{version}]'"
end

外部への公開

rails-assetsのワーキングコピー直下にあるpublicディレクトリ以下をrsync等を使って外部に公開します(public以下のファイルさえあれば、gem serverとして機能します)。公開先はどこでもいいですが、最も簡単なのはAWS S3を利用することでしょう。

gem sourceの変更

プロジェクトのルートディレクトリに移動し、Gemfile 中の https://rails-assets.orghttp://公開URL に変更して、bundle install を走らせれば終了です。

ローカル環境で確認したい場合

localにgemがすでにインストールされている場合、公開URLのものは当然インストールしてくれません。もし、ちゃんとインストールが出来ることをローカル環境で確認したい場合は、インストール済みのrails-assets-*系gemを削除してください。ざっくりと全部抜くのであれば、以下のようなコマンドで削除できます。

$ gem list --no-version | grep rails-assets- | xargs gem uninstall

最後に

役に立った!と思ったら下記エントリの応援や応募などをお願いします (\( ⁰⊖⁰)/)

犬の手も借りたい!ヘルスケアITのスタートアップがRubyエンジニア募集! - クリニカル・プラットフォーム株式会社の求人 - Wantedly

合わせて読みたい

最近のお仕事について - おもしろwebサービス開発日記

Lotus についての雑感

今回の ginza.rb ミートアップで、Lotus と呼ばれる、Ruby製フレームワークについて学びました。

ginzarb.doorkeeper.jp

@y_yagi さんが概要を資料にまとめてくれたので、それに沿って特徴や機能について理解を深めつつ、気になることについてワイワイ話しました。

感想

Lotus は、Rails とは異なる明確なポリシーで設計されており好感が持てます。Rails は少人数、短期間で素早くサービスを作るのに向いているフレームワークだと思いますが、最近はある程度大規模な開発にも使われているはずです。その時に開発メンバーの習熟度にばらつきがあると、例えばコントローラに不必要なインスタンス変数が定義されたり、ActiveRecord を継承したモデルにビジネスロジックが書かれすぎてモデルが肥大化したりします。こうしたことは少しなら問題ないのですが、開発が進むにつれ徐々にビジネスロジックの実装を難しくし、バグが入り込みやすくなり、さらにコードを読み書きする楽しさを奪っていきます。Lotus は、そのようなことが(比較的)起こりにくい設計になっています。

機能的に足りていない部分(関連やi18n)があるので、どんどん使いましょう!とは言えないのですが、Rails で消耗した人向けのもう一つの選択肢になればいいなあ。

最近のお仕事について

ここ半年ほど、Clinical Platform さんのお手伝いをしています。最近エンジニア募集を開始したので、どんな感じでお仕事を進めているかを簡単に紹介しようと思います。

作っているもの

クリニック向けのクラウド電子カルテです。

Ruby や Rails のバージョン

Railsエンジニアが会社を選ぶときは使っているRubyとRailsのバージョンで決めるらしいので一応。

  • Rails 4.2.4
  • Ruby 2.2.2
  • PostgreSQL 9.3.6

Ruby と PostgreSQL のバージョンが少し古いのは、本番環境である Engine Yard のバージョンに合わせているためです。gem 類は、週に一回自動で bundle update のプルリクエストが生成される仕組みが作られているためほぼ全て最新バージョンです。

(2015/11/09追記)Engin Yard が Ruby 2.2.3 に対応したので、現時点では 2.2.3 になっています。

フロントエンドは vue.js を使っています。特にカルテ内では多くの情報を素早く編集、閲覧する必要があるので多用しています。 今のエンジニアは僕含めサーバサイドが得意なメンバーが多いので、苦手なフロントエンド成分を補充するために週1で社内勉強会を開催しています。現在のテーマは React です(今の所 React に書き換える予定はありません)。

開発の進め方

普通のことを普通に進めています。Railsでウェブサービスを開発する、といったときにみなさんが想像するようなことは大体やってるはずです。これ不便だな、こうやったら便利になるのでは?と思ったら10分後にはpivotalでチケット化されていて、誰かがカッとなったときに導入されます。

  • Pivotal Tracker でチケット駆動開発
  • esa でドキュメント管理&日報管理
  • GitHub でソースコードレビュー
  • 通知類とチャットは Idobata に集約
  • CircleCI で CI まわす
    • 自動で rubocop, simplecov, brakeman などを動かして GitHub にコメントをつける
  • 朝会
  • 週一の全体ミーティング&KPT

ただ、技術的にチャレンジングなことをしている!エッジな技術をどんどん採用している!…というわけではなくて、どうやったらユーザである医療従事者の方々にとって便利なサービスになるか、を一番重視しています。

クリニックによってカルテの使い方は全然異なるので、それらを置き換える電子カルテの仕様を決めることは難しく、トライアンドエラーの連続になります。最初から決まったものを作るのではなくて、小さく作って様子を見てから大きく育てていく。当然、仕様も降って湧いてくるというより、みんなで考えて決めていく感じです。

エッジな技術に囲まれて仕事をしたい!という人よりは、チームで楽しく開発したいとか、ユーザに価値を届けるために知恵を絞るのが好きとかいう人が向いてると思います。

もちろん、新技術を採用する明確なメリットがあるなら、躊躇なく採用していくつもりです!

雰囲気

楽しくやっています。個人的にはもともとの知り合いばかり周りにいるのでやりやすいという面もありますが、それを差し引いても良い雰囲気で開発できていると思います。

チームがどんな雰囲気で開発をしているか、チームメイトである @tatsuoSakurai のエントリも参考になると思います。

カッとなるチーム - 開発チームのモチベーションをチョットあげる工夫のお話 - tatsuo.sakurai's blog

最後に

興味ある方は、Wantedly 経由、もしくは @netwillnet、その他関係者っぽい人に声かけてください!採用決まったら一緒にペアプロしましょう (\( ⁰⊖⁰)/)

医療界にインパクトを与えるクラウドサービスを一緒に開発しませんか? - クリニカル・プラットフォーム株式会社の求人 - Wantedly

Rails 4.2 で postgresql を使う場合の注意事項

Rails 4.2 では、DB の string 型が255文字制限ではなくなりました。もともとあまり意味がなかったらしいです(詳しい人補足求む)。対象は postgresql と sqlite で、mysql は対象外です。

PostgreSQL, remove varchar limit. by senny · Pull Request #14579 · rails/rails

Rails 4.2 以降は、string は文字数制限未指定 の character varying 型に変換されます。ただそれって、text 型と何が違うんでしょうね。

と、調べた限りでは違いはなさそうでした。

string - PostgreSQL: Difference between text and varchar (character varying) - Stack Overflow

ちなみに Rails 4.1 で string 型のカラムに256文字以上の文字列を入れて保存するとこのようになります(PostgreSQLは 9.4.1 でためしてます)。Rails 4.2 だとエラーにならず普通に保存されます。

ActiveRecord::StatementInvalid: PG::StringDataRightTruncation: ERROR:  value too long for type character varying(255)

この挙動を頼りにして、これまで string 型に validation をかけてこなかったみなさん、ちゃんと validation かけていきましょうね(\( ⁰⊖⁰)/)

補足 character varying(n) の挙動について

上記では制限している文字数以上の文字を入れようとするとエラーと書きましたが、例外があります。超過分の文字列が空白の場合はそれが切り詰められて格納され、エラーにはならないようです。SQL標準の仕様らしいけど変な挙動ですね…。

SQL defines two primary character types: character varying(n) and character(n), where n is a positive integer. Both of these types can store strings up to n characters (not bytes) in length. An attempt to store a longer string into a column of these types will result in an error, unless the excess characters are all spaces, in which case the string will be truncated to the maximum length. (This somewhat bizarre exception is required by the SQL standard.)

PostgreSQL: Documentation: 9.4: Character Types

mysql の場合

mysql は特に挙動は変わっていません。mysql 5.6 のデフォルトである STRICT_ALL_TABLES が有効になっていると次のようなエラーになります。無効だとエラーにはならず、256文字以降が自動でカットされて格納されます

ActiveRecord::StatementInvalid: Mysql::Error: Data too long for column 'name' at row 1: INSERT INTO `users` (`created_at`, `name`, `updated_at`) VALUES (?, ?, ?)

Rails で fat model を避けるための、あまり知られていない方法について

このエントリで書いた内容は、ほぼ Growing Rails Applications in Practice の内容が元になっています。英語ですが、ここで挙げた内容以外にもコードを綺麗に保つテクニックが書かれており、かつページ数も少なく読みやすいです。コードを綺麗に保つのが好きな方は一読してみることをおすすめします。

はじめに

Rails で fat model を避けるための方法は、7 Patterns to Refactor Fat ActiveRecord Models を始めとして、多くのやり方が存在します*1

validation や callback は ActiveRecord(以下AR) を継承せずとも利用することができます。7 Patterns to Refactor Fat ActiveRecord Models の 「3. Extract Form Objects」がそのいい例ですね。ただ、(ARの)モデルに関する validation や callback をかけたい場合は、モデルに直接書いてしまう人が多いのではないでしょうか。

しかしそれは悲劇の始まりでもあります。例を挙げてみましょう。

友達と好きな趣味を共有しあうSNSのようなサービスを考えてみます。さらに、

  • 自分自身の変更は友達に通知される
  • 趣味は必ず1つ以上持っていなければならない

とします。すると User モデルは次のようになるでしょう

class User < ActiveRecord::Base
  has_many :friends, through: :friendships
  has_many :friendships
  has_many :hobbies, through: :likes
  has_many :likes

  validate :should_have_hobbies

  before_save :notify_friends

  private

  def notify_friends
    # 友達に自分自身の変更を通知するコード
  end

  def should_have_hobbies
    errors[:base] << '好きな趣味を登録してください。' if hobbies.count == 0
  end
end

「自分自身の変更は友達に通知される」を callback で、「趣味は必ず1つ以上持っていなければならない」を validation で実装しています。一見、何の問題もないように思えます。

それぞれ、具体的に何が辛いのか見ていきましょう。

callback の辛さ

このサービスは、毎日の運勢を占ってくれる機能があります。毎日バッチ処理でランダムに 0..100 の点数を出し、User の luck カラムに格納することにします。するとどうなるでしょうか。

ユーザは、友達全員の今日の運勢を通知として毎日受け取ることになってしまいますね。

もちろん、 skip_callback メソッドを利用することで、 callback 処理をスキップさせることは可能です。しかし、通知をさせずに User を変更したいときに、毎回それを念頭に置いてコードを書くのは大変ですね。そしてそれを忘れてしまうともっと大変です。

また、before_save などの callback 用メソッドに if や unless などのオプションを渡して、skip_callback とは逆に、「callbackを実行したい時だけなんらかの処理をする」という仕組みにすることもできます。例えば

class User < ActiveRecord::Base
  before_save :notify_friends, if: :enable_notification

  attr_reader :enable_notification

  def use_notification
    @enable_notification = true
  end
end

のようにして、

user.use_noticication
user.save

のようにしたときだけ通知を飛ばすような仕組みです。これは一見うまいやり方に見えます。実際、アプリケーションコードが少ない時はこれでもうまく回ると思います。しかしアプリケーションコードが肥大してくると、大量の callback とその条件分岐のためのコードによりモデルが太ってきてしまいます。

さらに、上記の例だと条件が単純なため特に問題はないですが、複雑な条件を満たした時のみ callback を実行したいようなケースもありますね。その条件に対して、さらに callback 実行をオンオフするための条件式を追加するのはなるべくやりたくない感じです。

validation の辛さ

次に、User モデルのユニットテストを書いてみましょう。User を save したときに、ちゃんと User#notify_friends が実行されるかをテストしてみます。素直に書くとこんな感じでしょうか。

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#notify_friends' do
    it 'save したときに、User#notify_friends が実行されていること' do
      user = User.new
      expect(user).to receive(:notify_friends)
      user.save
    end
  end
end

しかしこれだとうまくテストが通りません。なぜでしょうか。

validate :should_have_hobbies があるので、関連する hobbies がないと validation が通らず、 callback が実行されないからですね。次のように Hobby を作り、User と関連付けさせれば通ります。

hobby = Hobby.create!(name: 'ジョギング')
user = User.new
user.likes.create!(hobby: hobby)
user.save

しかし、User を保存するテストコード全てに、このようなコードを追加していくのは大変です。factory_girl などの fixture replacement を使うことで大変さをいくらか減らせますが、そうすると今度は factory_girl の定義自体の複雑さが増して辛いことになったりします。

ではどうするとよい?

ここまで、 callback や validation をモデルにそのまま書くことにより、それらを必要としない場合に複雑さをもたらす例を見てきました。この複雑さを解消するにはどのようにするべきでしょうか?

それには継承を使います。具体的なコード例として、先述の User モデルを書きなおしてみます。

class User < ActiveRecord::Base
  has_many :friends, through: :friendships
  has_many :friendships
  has_many :hobbies, through: :likes
  has_many :likes
end
class User::AttributeUpdator < User
  before_save :notify_friends
  validate :should_have_hobbies

  private

  def notify_friends
    # 友達に自分自身の変更を通知するコード
  end

  def should_have_hobbies
    errors[:base] << '好きな趣味を登録してください。' if hobbies.count == 0
  end
end

通常の User と、何らかの属性を変更するとき用に使う User::AttributeUpdator というクラスに分け、User::AttributeUpdator は User を継承するようにしました。User::AttributeUpdator は User のサブクラスなので、当然 User 側で定義している has_many の関連も使えます。

これにより、他の友だちに通知させたいときは User::AttributeUpdator を利用し、そうでないときは User を使うという形で使い分けをすることができます。

必要なときに必要な分だけの validation や callback を設定することができ、さらにそれぞれの処理をサブクラスに移すことで User モデルがスリムになりました。

やりましたね!…と言いたいところですが、いくつか注意事項があります。

継承を利用する場合の注意点

form_forurl_for に ActiveRecord のオブジェクトを引数として渡した時、URLの組み立てに model_name が使われます。そして User::AttributeUpdator.new.model_name.to_s #=> 'User::AttributeUpdator' なので、User::AttributeUpdator オブジェクトを渡した時は意図した URL が作られないことになります。

また、User モデルが STI を利用していたとしましょう。その場合 User::AttributeUpdator は STI とは違うので、 type カラムに入れたくないですね。

これらを解決するために、active_type という gem を使います。

普通に Gemfile に入れて bundle install して、User::AttributeUpdator の定義箇所を次のようにするとうまく動きます。

class User::AttributeUpdator < ActiveType::Record[User]
 #...
end

ちょっと見慣れない書き方ですね。やっていることはそれほど複雑ではないので、気になる人は独自の gem を作ってしまってもいいかもしれません*2

クラスの変更

ログインが必要なアプリーションを作る時、大抵次のように current_user のようなメソッドを定義することになると思います。

def current_user
  @current_user ||= User.find(session[:user_id])
end

ログインユーザが自分の情報を変更するケースを考えてみましょう。この場合は User::AttributeUpdator を利用するのでしたね。しかし current_user は User クラスのオブジェクトを返してきます。困りました。

そこで、User オブジェクトを User::AttributeUpdator オブジェクトに変換します。

先ほど紹介した active_type が変換用のメソッドを用意してくれています。

user = ActiveType.cast(current_user, User::AttributeUpdator)
user.update(params[:user])

おわりに

継承を使うことにより、callback や validation を分割し、必要なときのみ使えるようにすることができました。このテクニックを実際に仕事のコードとして使ってみましたが、想定通りに整理されたコードを作ることが出来てなかなかよい感じです。

しかし、このテクニック自体はあまり認知されておらず、知らない人が見ると面食らう可能性があるのでもうちょっと普及して欲しいところです。

また、ここで紹介したよりももっと良い方法があるのではないか?という気持ちもあります。もし、もっといい方法知ってるぜ!という方がいたらブログのコメントなりTwitterでメンションなりいただけると嬉しいです (\( ⁰⊖⁰)/)

*1:パーフェクト Ruby on Rails でも一部触れていますね

*2:作ろうと思ったのですがなかなか時間取れないのでした

iphone のカメラを修理した

iphone6 plus で写真を撮るときに、フォーカスが合わずにぼやけた写真しか撮れなくなってしまいました。

ググるとどうやらハードウェアの不具合の模様。

iPhone 6 Plusのカメラのピントが合わない不具合が発生 - 非天マザー by B-CHAN

ジーニアスバーに持って行こうと思って調べるも、予約可能な日時を考えると一週間ほど待つことになりそうなので、宅配便を使った修理サービスを使用してみることにしました。apple のことだから代替機とかも用意してくれるのではと勝手に思い込んでいたのですが、普通に iphone を持って行かれてしまい、しばらく電話なし生活を余儀なくされました。

普段ほとんど電話を使うことはないので基本的には問題ないのですが、一点困ることがありました。僕の家はインターフォンがないので、宅配のものを受け取るのに電話が必須だったのです…><

つまり iphone を受け取るのに iphone が必要という詰んだ状況に。困った。

部室にあった検証用のSIMフリーのAndroid端末をお借りして、iphone の SIM を入れればなんとかなる…と思いきや、SIMのサイズが合わない。

そこで電気屋に行ってSIMサイズを変更するアダプタ購入した*1ら電話を使うことができるようになりました。

ググったらどうもiphoneのSIMはアダプタを使っても、端末によって使えたり使えなかったりするようなので、今回電話が使えたのは運が良かったようです*2

というわけでなんとかiphoneを修理することができました。どうやらググると代替機を用意してくれるプランもあったようです。次回はこれを使うか、おとなしくジーニアスバーを利用したいと思います…。

Apple - サポート - 修理サービス Q&A センター

*1:新宿の大きい電気屋を二軒まわりました。SIMアダプタの在庫は少ないようですね…

*2:ただ、インターネットは使えませんでした。

nested attributes なレコードを、特定の属性が空の時に削除する

nested attributes なレコードを削除したい場合、accepts_nested_attributes_forallow_destroy: true オプションを渡すと削除可能になります。削除するには、対象となる対象に { _destroy: 1 } のようなパラメータを渡します。

これを踏まえて素直にフォームを作ろうとすると、削除用のチェックボックスをつける事になるでしょう。しかし次のようなフォームにチェックボックスをつけると、ユーザにとってわかりづらいUIになってしまいます。単純にテキストフィールドを空にして更新したらレコードが削除されて欲しい。

f:id:willnet:20150725173208p:plain

そこで次のようにします。

class User < ActiveRecord::Base
  accepts_nested_attributes_for :family_members,
                                reject_if: :reject_family_member,
                                allow_destroy: true

  def reject_family_member(attributes)
    exists = attributes[:id].present?
    empty = attributes[:email].blank?
    attributes.merge!(_destroy: 1) if exists && empty
    !exists && empty
  end
end

reject_if の手続きの中で、レコードとして保存済みでかつメールアドレスが空のものに { _destroy: 1 } を追加しています。これでチェックボックスなしで nested attributes なレコードを削除することが出来ました。

参考

ruby on rails - Destroy on blank nested attribute - Stack Overflow