猫の魔法

主にruby系の技術メモを記載

is_a?についてのメモ

Highlineのソースを読んでいたら以下の部分でちょっと考えてしまった。

      if template_or_question.is_a? Question
        template_or_question
      else
        Question.new(template_or_question, answer_type, &details)
      end

悩んだのはtemplate_or_question.is_a? Questionの部分で、名前だけ見るとis_a?ってto_aと同じ意味(つまり配列かどうかを返す)のかと思って読んでいたが、そうすると後ろのQuestionが意味不明。調べてみたらis_a?はObjectのメソッドで、引数のクラスかどうかを返す物だった。

※リファレンスにもあるが、is_a?は対象が「引数 をインクルードしたクラスかそのサブクラス のインスタンスである場合」もtrueになるので、厳密に確認したいならinstanse_of?を使うのが正解。

名前から意味を推測して読むとこう言う落とし穴があるので注意せねば。。。

ちなみに上のソースと同じ文脈で配列かどうかを見るのは、

[1,2,3].is_a? Array

となるみたい。

deviseのメールアドレスバリデーション

最近deviseを使う事があったのだが、メールアドレスのバリデーションがどうなっているのか気になっったので調べてみた。

devise/devise_test.rb at master · plataformatec/devise · GitHub

メールアドレスのバリデーションテストは上記で行っている。

で、実際の処理はこっちで、

devise/devise.rb at master · plataformatec/devise · GitHub

モジュール変数としてemail_regexpを定義していて、ここにマッチしないメールアドレスを弾いている。

弾いているのだが、正規表現を見ると以下のように随分あっさりしたチェックになっている。

mattr_accessor :email_regexp
@@email_regexp = /\A[^@\s]+@[^@\s]+\z/

このチェックだと「ほげ@ほげ」でもチェックをすり抜ける。

どういう経緯なのか調べてみると前はもう少し厳しく見ていたようだ。

ただ、テストケースにexample@ttを追加したら通らないという話になり、

Devise::email_regexp rejects unusual but valid email · Issue #3997 · plataformatec/devise · GitHub

結果的に複雑な正規表現をするくらいなら、いっそ簡単にしようという話に纏まったみたい。

updated email_regexp and added test cases by kimgb · Pull Request #4001 · plataformatec/devise · GitHub

なのでメールアドレスを厳密に見たい各位は、deviseを使う際はバリデーションを変更する必要がある旨を留意する必要がある。

※ここからは検証が必要。 恐らくだが、email_regexpを書き換えればバリデーションを変えられるはず。何処で書き換えればいいかはちょっと時間見つけて調査してみようかな。

has_many throughの苦悩

railsのhas_many throughの使い方が分からなかったという話。

モデルの関連付けでhas_manyは分かりやすい。1対多のモデルは1側にhas_manyをつければいい。

問題はhas_many throughでこれが良く分からない。多対多の時につけるというが多対多の場合、リレーション用のテーブルがあるとは限らない。必ずリレーション用のテーブルが必要なのか?引く側も引かれる側も両方につけるの?そもそも多対多のモデル自体を一対多にした方がいいのでは?学生の時にrailsを勉強した時は恥ずかしながらそこがまったく腑に落ちず理解が全然進まなかった。

月日は流れ、今になってもう一度railsを学んでみると、そもそもDBを主軸に考えるから良く分からなくなることに気がついた。今のrailsガイドは非常に分かりやすい。

Active Record の関連付け (アソシエーション) | Rails ガイド

このガイドを読んで分かったのはhas_many throughは多対多を表すというよりは、「このモデルとの関連付けはこのモデルを通して行うよ」という宣言という事だ。

なので、別にモデル関の関連が多対多でなくてもthroughを使うことで直接キーがないモデルを操作することが出来る。 よって「has_many throughを使う時は必ずリレーション用のテーブルが必要なのか?」という問はYesという事になる。

また以下のような正規化されていない多対多のモデルについてはhas_manyで結び付けられるような気がするのだが、そこら辺はネットで情報を見つける事ができなかった。 これについてはどっかで実験してみようと思う(そもそも正規化しろという話だが、既存システムの載せ替え等の場合、こう言うまずい作りが多々ある気がする)

f:id:nekomaho:20170314003934p:plain

モデル≠DBのテーブルで無い事にもっと早く気がつけば良かったという話でした。

個人的メモ:Rails AntiPatterns(21):最後

CHAPTER10 Building for Failure

  • 発生した障害は素早く復旧される事と、黙って失敗しない事が大事

    AntiPattern: Continual Catastrophe

  • 前段の処理が失敗しているのにも関わらず、それをキャッチ出来ずに後続処理が動いてしまうのはよくない。

Solution: Fail Fast

  • 複数の要素の変更を途中まで処理したが何かしらのエラーが発生したので中途半端な状態で処理が止まってしまう形にしてはいけない
    • 処理前にすべての要素の変更が行えるかを確認する事。
    • 一貫したユーザエクスペリエンスを心懸ける事。

AntiPattern: Inaudible Failure

  • あるエラーを補足する為に他のエラーを丸め込んでしまっている状態はよくない。

Solution: Never Fail Quietly

  • 明らかにシステムエラーにするべき物については静かに失敗しないように明示的に例外が出るようにする。
  • システムエラーが発生した場合は、ユーザにそれを通知するとともに、内部的な監視システムへもそれを通知するようにする。
    • ログ監視ツールとしてはHoptoadがメジャーだが、exception_notification,Exceptional,NewRelicRPMと言った物もある。
      • ★あとで詳しく調べる。
  • 例外と負の戻り値は無視しては行けない。エンドユーザと監視システムの両方に通知すること。

おしまい

感想

Rails Anti意外と時間かかったが、夜(たまに朝)の通勤時間のみで2ヶ月位で読みきれた。Rails3の頃の情報なので色々と情報が古い面もあるが、参考になる面も多かった。

ブログにメモ書き残したのは一部だったけど、今度何か読むときはちゃんと記録したい。

あとで調べるになっている所はなるべくキャッチアップして置こうと思う。

関連

個人的メモ:Rails AntiPatterns(一覧) - 猫の魔法

個人的メモ:Rails AntiPatterns(20)

AntiPattern: Wet Validations

  • DBによってはvalidationで指定された制約をサポートしていない場合がある。その場合、DB上で発生する不整合データを検知出来ない可能性がある。

Solution: Eschew Constraints in the Database

  • ★適切なgemを使うことで上記を回避出来ると記載があるが、ここで上げられているforeignerはrails4.2でrailsに機能が組み込まれた為廃止されている。

  • カラムの定義する際はデフォルト値を入れた方がいい。

CHAPTER 10 Building for Failureの手前まで読了

関連

個人的メモ:Rails AntiPatterns(19)

Solution: Never Use External Code in a Migration

  • migrationをする際にDB上のデータを一緒に変更する事はよくある事だが、upメソッド内にrubyでその手順を直接書くべきではない
  • 理由はパフォーマンスと、DBから見た外部依存性がrails側にできてしまう為で、このような場合はupメッソド内に生のSQLを書けないかを検討する。

  • もしrubyのコードにmigrationの内容を書く必要があるなら、モデル間の依存性をすべて移行用クラスの中に記載する。

    • 例えばUserがhas_manyでJobを持っているのであれば、下記のように依存関係自体をモデルではなくmigrationスクリプトの中に内包する(以下コード抜粋)
class AddJobsCountToUser < ActiveRecord::Migration
  class Job < ActiveRecord::Base 
  end 
  class User < ActiveRecord::Base 
     has_many :jobs 
  end
  def self.up 
    add_column :users, :jobs_count, :integer, :default => 0
    User.reset_column_information 
    Users.all.each do |user| 
       user.jobs_count = user.jobs.size user.save 
    end
  end 
  def self.down 
    remove_column :users, :jobs_count 
  end 
end
  • ★つまりmodelの最新版とmigration時点でのDBの状態は必ずしも一致しないので、マイグレーション時に想定されている依存性を記載した方がいいということ。

  • User.reset_column_informationはこれが呼ばれた時点のDBの内容を読み込み、それをActiveRecodeに反映させる。

Solution: Always Provide a down Method in Migrations

  • 不可逆なmigrationを行う場合はActiveRecord::IrreversibleMigration例外をdownメソッドのなかで発生させることで、開発者に手動でのDB修正が必要な事を知らせるようにする。

AntiPattern: Wet Validationsの手前まで読了

関連

個人的メモ:Rails AntiPatterns(一覧) - 猫の魔法

個人的メモ:Rails AntiPatterns(18)

Chapter 9 Databases

  • railsはORMマッパーによって、DBの事を意識せずに済むように設計されているが、それでも気にしなければいけない点はある

AntiPattern: Messy Migrations

  • railsはmigrationによってDBへ直接変更を加えなくてもよくなっているが、時間が立つとこのmigration機能自体が厄介な問題を引き起こす可能性がある。

Solution: Never Modify the up Method on a Committed Migration

  • migrationはリモート環境で実行したら絶対に変更してはならない。
  • すでに配布されているmigrationスクリプトを変更すると、他の開発者とのバッティングを起こすだけでなく、DB自体の破壊につながる。
  • そのような自体にならないように、ローカルでよくmigrationを検証する必要がある。
  • その為にはdb:migrationだけでなく、db:migration:redoも実施し、問題が無いかを確認したほうが良い。
  • db:migration:redoはdownメソッドを実行した後にもう一度migrationを適用する
    • ★ネットで調べると、rollbackの代わりに使ってるみたいだが、本当にmigrationを再適用してるのか不明。あとで試す。
    • ★これの意図は一度downしてからもう一度upさせることで、双方のスクリプト正しく動くかをローカルdr確認できて安心という意図っぽい。

Solution: Never Use External Code in a Migrationの手前まで読了

関連

個人的メモ:Rails AntiPatterns(17)

Solution: Move Processing into Background Jobs

  • 処理時間がwebでの応答に不適切なくらい掛かる場合はcronやキューイングといった。バックグラウンドJobの仕組みを検討した方がいい

  • cronの場合スクリプトを別に作ってそれを動かす。

    • 例えば、テーブルのカウントをするのに毎回3分かかるような処理があるなら、事情が許すならcronで30分置きにカウントした結果を別のテーブルに格納する方がパフォーマンスの向上に繋がる。

    • cronは簡単だが、対象をcronスクリプト側で特定しなければならない。可能性であればキューイングを検討する方がいい

  • キューイングは画面からcsvをダウンロードする機能のような、バックグラウンドで纏まった処理をしなければならない物に向いている。

  • キューイングの機能としてはdelayed_jobとResqueが有名。

    • Redisを使っているのでなければdelayed_jobを使った方がいい。
      • Rails 4.2からはActive Jobがあるので、普通はそっちを使う。
  • キューイングやcronはは便利な機能だが、使い過ぎるとシステムが複雑になる。節度を持って使う事

    • ★ 関係ないが「exercise restraint」で「抑止力を働かす」というのが面白い

Chapter9 Databasesの手間まで読了

関連

個人的メモ:Rails AntiPatterns(一覧) - 猫の魔法

個人的メモ:Rails AntiPatterns(16)

忙し過ぎて時間が空いてしまった。

CHAPTER 8 Scaling and Deploying

AntiPattern: Painful Performance

  • 単純なパフォーマンスの問題を防ぐ為の事例を紹介する。

Solution: Don’t Do in Ruby What You Can Do in SQL

  • RubySQLで出来る事をやらせないようにする
  • レコードの総件数を求める方法は3つ考えられるが、どれも裏側の動きが異なる
@article.comments.count 
@article.comments.length
@article.comments.size
  • 一つ目の例は、DBのcount関数を実行する。
  • 二つ目の例は、lengthがActiveRecordの関連で無い為、すべてのレコードをDBから取得する
  • 三つ目の例はすでにレコードがロードされていればそれを使い、そうでない場合はDBのcountを使う。
  • ケースに応じて使い分ける事が重要。

  • SQLを上手く使えていないパターンとして以下の2パターンがある。

    • SQLで実現する方法を知っているがRubyで書いてしまう。
      • 例えば下記のような例(コードは抜粋)
@account = Account.find(3) 
@users = @account.users.sort { |a,b| a.name.downcase <=> b.name.downcase }.first(5)
  • 上記のコードよりも下のコードの方が、処理をよりDBに移譲できる為パフォーマンスを高められる
@users = @account.users.order('LCASE(name)').limit(5)
  • SQLで実現する方法を知らない為Rubyで書いてしまう。
    • 例えば下記のような例(コードは抜粋)
class User < ActiveRecord::Base
 has_many :comments has_many :articles, :through => :comments

 def collaborators 
   articles.collect { |a| a.users }.flatten.uniq.reject {|u| u == self } 
 end 
end
  • 上記よりも、下記の方がより処理をDBに任せる事が出来る。
    • このパターンはflattenを使うような処理になってしまっている場合に置き換えが出来る事が多い
    • ★?の部分は、順番に配列の第2,第3要素が入る。
class User < ActiveRecord::Base
 has_many :comments
 has_many :articles, :through => :comments
 
 def collaborators User.select("DISTINCT users.*").
        joins(:comments => [:user, {:article => :comments}]).
           where(["articles.id in ? AND users.id != ?", self.article_ids, self.id]) 
 end 
end

Solution: Move Processing into Background jobsの前まで読了。

関連

個人的メモ:Rails AntiPatterns(一覧) - 猫の魔法

個人的メモ:Rails AntiPatterns(15)

CHAPTER 8 Scaling and Deploying

AntiPattern: Sluggish SQL

Solution: Reassess Your Domain Model

  • 高度に正規化されたテーブルはテーブル間の結合によってパフォーマンスの問題を起こしやすい。
  • 単純に結合させるのではなく必要な要素をrails側で取得するようにするとパフォーマンスを改善する事が出来る。
    • ★UserがPrefとCityと関連がある場合、下記のように取る事で負荷が減らせる場合がある。
search_pref = Pref.find_by_name('Kanagawa') 
search_city = City.find_by_name('Yokohama')
User.where(pref: search_pref,city: search_city)
  • 大きなサイズのテーブルになる事が分かっているなら、テーブル自体非正規化した方がパフォーマンスを向上出来る。

AntiPattern:Painful Performanceの前まで読了。

関連

個人的メモ:Rails AntiPatterns(一覧) - 猫の魔法