Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1381 articles
Browse latest View live

Rails tips: RSpecでコード実行回数をスマートに数える(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecでコード実行回数をスマートに数える(翻訳)

RSpecを使って、指定のメソッドが呼び出される回数をexpectで指定することができます。その方法を説明する前に、呼び出し回数を知ることの重要性についてお話します。これはコードの振る舞いを制御するためのものです。説明のため、わざと誤ったコードを書いてみます。

class SomeClass
  def something
    @something = service.call
  end
end

これのどこが誤りかおわかりでしょうか。ここではメモ化によって、SomeClass#somethingが呼ばれるたびにservice.callが2回以上呼ばれることのないようにしたいと考えています。しかしメモ化の||=演算子ではなく=演算子が使われているため、期待どおりメモ化されていません。

このコードに対して次のようなテストを書くとします。

expect(some_class_instance).to have_received(:something)

このテストでは、メモ化が行われていないという誤りを検出できません。上のようなシンプルな例ならともかく、たとえばservice.callが外部APIを呼び出していて、限りあるリソースを食いつぶしているとしたら非常に残念ことになります。これを回避するには、メソッド呼び出しが何回行われるべきかを指定します。

expect(some_class_instance).to have_received(:something).once

このexpectationを用いることで誤りが検出されます。しかしTDD(テスト駆動開発)アプローチではこのテストは決して作られることはないでしょう。このことから、TDDを用いるとともにexpectationを詳細に書くことの重要性がわかります。

2回以上のメッセージ呼び出しのexpectationをRSpec文法で書くには

コードが2回以上呼び出されたかどうかをテストしたい場合や、少なくとも2回または3回以上呼び出されたかどうかをテストしたい場合はどのように書けばよいでしょうか?RSpecの文法でこれらを指定することもできます。

  • メッセージがn回呼び出されるexpectation:
expect(some_class_instance).to have_received(:something).once
expect(some_class_instance).to have_received(:something).twice
expect(some_class_instance).to have_received(:something).exactly(3).times
  • メッセージがn回以上呼び出されるexpectation:
expect(some_class_instance).to have_received(:something).at_least(:once)
expect(some_class_instance).to have_received(:something).at_least(:twice)
expect(some_class_instance).to have_received(:something).at_last(3).times
  • メッセージの呼び出しが n回以下のexpectation:
expect(some_class_instance).to have_received(:something).at_most(:once)
expect(some_class_instance).to have_received(:something).at_most(:twice)
expect(some_class_instance).to have_received(:something).at_most(3).times

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: テスティングアンチパターン –前編(翻訳)

ソフトウェアテストでstubを使うコストを考える(翻訳)


Railsで学ぶSOLID(2)オープン/クローズの原則(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

訳注: 「オープン/クローズド」とされることもありますが、本シリーズでは「オープン/クローズ」で統一しました。

Railsで学ぶSOLID(2)オープン/クローズの原則(翻訳)

「SOLIDの原則シリーズ」へようこそ。このシリーズ記事では、SOLIDの原則をひとつずつ詳しく説明し、分析します。シリーズの最後にはいくつかのヒントや考察を含む総括記事をお送りしますのでどうぞご期待ください。

それでは始めましょう。「SOLIDの原則」とはそもそも何なのでしょうか?SOLIDとは、オブジェクト指向プログラミング設計における一般的な原則であり、ソフトウェアをより理解しやすくし、拡張性やメンテナンス性やテストのしやすさを高めることを目的としています。

  1. 単一責任の原則SRP: Single responsibility principle)
  2. オープン/クローズの原則OCP: Open/closed prinsiple)(本記事)
  3. リスコフの置換原則LSP: Liskov Substitution Principle)
  4. インターフェイス分離の原則ISP: Interface Segregation Principle)
  5. 依存関係逆転の原則DIP: Dependency Inversion Principle)

今回お送りするのは、第2の「オープン/クローズの原則」です。

オープン/クローズの原則(OCP)

クラスは、変更に対しては門戸を閉ざし、拡張に対しては門戸を開くべきである。

言い換えれば、クラスの機能の拡張は、「クラスのコアの振る舞いを変更せずに」行うべきだということです。この原則を満たすために以下の手法が使えます。

  • 継承メカニズムを用いる
  • コンポジションを用いる
  • 依存性の注入パターンを適用する
  • Decoratorパターンを適用する
  • Storategyパターンを適用する

もちろん、解決法はこれだけではありません。

メリットとしては、元のクラスのコードを安心して使えるようになることです。元クラスのコードが改変されていないので振る舞いは変わらないはずだからです。

注意点としては、常識に従うべきであるということです。実際には、クラスを少々変更しても普通なら害がない状況なのに、(訳注: この法則を守ろうとするあまり)派生クラスを山ほど作ったりしないよう注意しなければなりません。そして、コードが望ましくない挙動を示したらそれに気づけるよう、私たちがコードに対して必ずテストを書いている主な理由がこれです。

では次の例で考えてみましょう。ユーザーデータのバリデーションと更新を担当するService Objectを1つ書きたいとしましょう。残念なことに、バリデーションは一部の条件に応じて変わる可能性があります。このクラスはおそらく次のような感じになるでしょう(Gist)。

class UserCreateService
  def initialize(params)
    @params = params
  end

  def call
    return false unless valid?
    process_user_data
  end

  def valid?
    validator = assign_validator
    validator.new(params).validate
  end

  def assign_validator
    if some_condition
      AdvancedUserValidator
    else
      SimpleUserValidator
    end
  end

  def process_user_data
    ...
  end
end

バリデーションロジックを別クラスに追い出したにもかかわらず、このコードにはまだ問題が残っています。

  • バリデーションロジックを新しく追加するのがつらくなる: そのたびにif条件、下手をするとswitch文まで増やさなければならない。
  • バリデーションロジックを変更するのに、バリデーションに責任を持たないクラスを改造しなければならない。つらさが倍増する。
  • テストがやりにくくなる: 処理とバリデーションの両方のロジックをさまざまな場合についてカバーしなければならなくなる。

解決法のひとつを以下に示します(Gist)。

class UserCreateService
  def initialize(params, validator: UserValidator)  # バリデータを外から渡す
    @params = params
    @validator = validator
  end

  def call
    return false unless validator.new(params).validate
    process_user_data
  end

  attr_reader :params, :validator

  def process_user_data
    ...
  end
end

バリデータオブジェクトをコンストラクタ(訳注: #initialize)経由でService Objectに渡しているだけです。ここで使った手法は依存性の注入(DI: dependency injection)と呼ばれるものです。一部のユーザーの属性に応じたバリデータを選びたい場合は、どのバリデータクラスを選ぶべきかを決定するクラスを別に作ることもできます。(どのコントローラにいるかなどの)コンテキストに応じてバリデータを選択するのであれば、Service Objectに渡したいバリデータを選んで渡すだけで済みます。

この方法によって、単一責任の原則も同時に満たされていることにご注目ください(余分な責務を他のクラスに逃してあります)。これによって、データをバリデーションするクラスを増やしたくなったときにも元のクラスを改変する必要がなくなりました。必要に応じてバリデータクラスを作り、更新したいクラスに渡すだけで作業は完了します。

そして前回の単一責任の原則のときと同様に、コードがクリーンになってメンテしやすくなり、テストもずっとやりやすくなるというメリットを得られます。更新されるService Objectと、別の環境に分離されたバリデータクラスをテストすればよいのです(Service Objectのテストは、バリデータオブジェクトのレスポンスをモック化するだけでできます)。

常に実用を優先しよう

驚くほど柔軟性の高いソリューションになりました。今や新しいバリデーションルールのセットを楽々追加できます。時間と手間はちょっぴり余分にかかりますが、そのおかげで鼻高々です。では新しいバリデーションを追加する必要がその後まったく生じなかった場合はどうでしょうか?答えはもう明らかです。そのためにかけた時間は(そしておそらく費用も)無駄になったのです。このソリューションを使うかどうかという決定は、それが必要になったときに、開発者が経験と(おそらく)今後の変更を考慮しながら、その都度行わなければなりません。

「こうやっておきさえすればいい」という銀の弾丸はないのです。個人的には、無条件に良いソリューションも、無条件に悪いソリューションもないと思っています。あるとすれば、(その状況において)よく合うソリューションと、うまく合わないソリューションでしょう。

私の哲学は「常に実用を優先しよう」(訳注: 原則を振りかざさないようにしようということ)「今必要でないことまでやらないようにしよう」です。

このトピックについては、シリーズ最終回でもう一度扱いたいと思います。

関連記事

YAGNIを実践する(翻訳)

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

Rails tips: コードとテストを同じファイルに書けるRSpec autorun(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: コードとテストを同じファイルに書けるRSpec autorun(翻訳)

もう皆さまもアプリではテストを書くべきであることは納得いただいているかと思います。これまでにスパイスタブモックなどのさまざまなスタブをご紹介しましたが、今回はもう少し一般的なお題にしたいと思います。

ほとんどの場合、コードとテストはそれぞれ別ファイルに保存するのが自然ですが、たとえば教育現場やデモなどでコードとテストを同じファイルに書く必要が生じることがあります。こんなときにはRSpecのautorun機能が役に立ちます。rspec gemがインストール済みであれば、ファイルを1つ作成してたとえばtest.rbという名前にし、ファイルの冒頭にrspec/autorunを書いておけば、コードとテストをひとつのファイルに書けるようになります。

require 'rspec/autorun'

class Person
  def initialize(first_name:, last_name:)
    @first_name = first_name
    @last_name = last_name
  end

  def name
    [first_name, last_name].join(" ")
  end

  private
  attr_reader :first_name, :last_name
end

describe Person do
  describe "#name" do
    it "returns full name" do
      person = Person.new(first_name: "Tom", last_name: "Black")

      expect(person.name).to eq("Tom Black")
    end
  end
end

後はtest.rbを実行すればテストの結果を表示できます。この種のテストは、TDDの学習中に基本的な部分を動かしてみたいときに非常に便利です。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: テスティングアンチパターン –前編(翻訳)

Rails tips: RSpecでコード実行回数をスマートに数える(翻訳)

Railsで学ぶSOLID(3)リスコフの置換原則(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

訳注: 原文のparent classは原則として「基底クラス」と訳出しました。

Railsで学ぶSOLID(3)リスコフの置換原則(翻訳)

「SOLIDの原則シリーズ」へようこそ。このシリーズ記事では、SOLIDの原則をひとつずつ詳しく説明し、分析します。シリーズの最後にはいくつかのヒントや考察を含む総括記事をお送りしますのでどうぞご期待ください。

それでは始めましょう。「SOLIDの原則」とはそもそも何なのでしょうか?SOLIDとは、オブジェクト指向プログラミング設計における一般的な原則であり、ソフトウェアをより理解しやすくし、拡張性やメンテナンス性やテストのしやすさを向上させることを目的としています。

  1. 単一責任の原則SRP: Single responsibility principle)
  2. オープン/クローズの原則OCP: Open/closed principle)
  3. リスコフの置換原則LSP: Liskov Substitution Principle)(本記事)
  4. インターフェイス分離の原則ISP: Interface Segregation Principle)
  5. 依存関係逆転の原則DIP: Dependency Inversion Principle)

今回は、3番目の「リスコフの置換原則」を見ていきましょう。

3: リスコフの置換原則(LSP)

基底クラスから派生したクラスは、望ましくない挙動を一切伴わずに、常に基底クラスと置き換え可能でなければならない。

次のように表すこともできます。

STの派生型であるとすると、プログラム内における型Tのオブジェクトは、プログラム内の望ましいプロパティを一切変更することなく、型Sのオブジェクトに置き換え可能である。

実践的に書くと、派生クラスは基底クラスを継承するときに、常に基底クラスの挙動を一切変えないようにすべきであるということです。

最も古典的なLSP違反例を以下のスニペットで示します(Gist)。

class Rectangle
  attr_accessor :height, :width

  def calculate_area
    width * height
  end
end

class Square < Rectangle
  def width=(width)
    super(width)
    @height = width
  end

  def height=(height)
    super(height)
    @width = height
  end
end

rectangle = Rectangle.new
rectangle.height = 10
rectangle.width = 5
rectangle.calculate_area # => 50

square = Square.new
square.height = 10
square.width = 5
square.calculate_area # => 25

数学的にはどこもおかしくはありません。ここで扱っているのは正方形なので、高さと幅が同じでなければなりません。高さ(と幅)を10に設定し、続いて幅(つまり高さも)を5に設定して面積を算出します。

基底クラスと派生クラスで同じ手順を踏んでいますが、両者の振る舞いが異なっていることが見て取れます。インターフェイスが基底クラスと派生クラスで一貫していないのです。

つまり、publicなインターフェイス(そしてもちろんそれらの振る舞いも)は、基底クラスと派生クラスで同じになってなければならないということが言えます。

LSPとポリモーフィズム

もうひとつ極めて重要な点があります。「得られる結果が異なるからLSPに違反する」のではありません。「期待しない振る舞いが生じるからLSPに違反する」のです。

次の例で考えてみましょう(Gist)。

class Shape
  def draw
    raise NotImplementedError
  end
end

class Rectangle < Shape
  def draw
    # 四角形を描画
  end
end

class Circle < Shape
  def draw
    # 円を描画
  end
end

drawメソッドは、派生クラスに応じて異なる図形を描画しますが、描画はまったく期待を裏切っていません。

この点をもう少し考えてみましょう。「得られる結果が異なれば即LSP違反」ということになってしまえば、OOPの極めて強力なツールであるポリモーフィズムは全部LSP違反になってしまいます。

基底クラスのメソッドを派生クラスでオーバーライドするとき、クラスの振る舞いは変更すべきではありませんが、派生クラスの特定の側面によって振る舞いを拡張(extend)できます。

前提条件を派生型で増強してはならず、事後条件を派生型で弱めてはならない。

または次のようにも言えます。

サブクラスは、要求事項を(基底クラスよりも)増やすべきではなく、できることを(基底クラスよりも)減らすべきではない。

LSPに従うことで、自信を持ってポリモーフィズムを使えるようになり、期待しない結果が生じる心配なしに、基底クラスを参照している派生クラスを呼び出せるようになります。

問題はいったいどこに潜んでいるのか

この問題は、抽象化の中に潜んでいます。数学的には、正方形は四角形の一種ですが、プログラミングにおいては(少なくともこの場合は)違います。つまり、抽象化のモデリングに誤りがあったというだけのことです。

私がこの例を愛する理由

上の例には、OOPに関する重要なポイントが1つ示されているからです。OOPは、現実世界を単純にオブジェクトにマッピングしただけのものではありません

OOPとは「抽象を作り出す」ことであり、「概念を作り出す」ことではありません!

もう少し改良を加える

正直に申し上げると、完全無欠の解決方法というものはありません(いつものことですが)。

  1. 上の例に登場するクラスたちに共通の振る舞いが存在しないことを認識します。こういう2つのクラスを結合してはいけません。振る舞いの異なる2つのクラスを作るだけにしましょう。
  2. インターフェイスの型を「シミュレート」する抽象レイヤーを1つ追加します(解決は継承によっても行われますが、方法は異なります)。

第2の解決法の例を示します(Gist)。

class Shape
  def calculate_area
    raise NotImplementedError
  end
end

def Rectangle < Shape
  attr_accessor :height, :width

  def calculate_area
    height * width
  end
end

def Square < Shape
  attr_accessor :side_length

  def calculate_area
    side_length * side_length
  end
end

この方法の最大のデメリットは何だかおわかりでしょうか。クラスの派生は(実際のインターフェイスとは逆に)1つの基底クラスからだけ行えます(もちろん、インターフェイスを継承するのではなくインターフェイスを実装します)。つまり、これらのクラスを介してさらに別の振る舞いを共有する理由があるとしても、この手法によって阻止されます。

いずれにしろ、本シリーズで扱っているのはRubyという動的型付け言語なのですから、私たちはそのようなことを強制するつもりはありません。ここでもっとも重要なのは「常識を働かせる」ことです。静的型付け言語の解決法を強引に持ち込むことが最善とは限りません。

LSPは「よい継承」を決定づける因子なのか?

継承よりコンポジション」(composition over inheritance)という言葉を目にしたことがあるかと思います。しかし、時には継承が必要になることもあれば継承が欲しいときもありますし、継承するしかないこともあります。そのことには何も問題はありません。継承はOOPの部品、それも極めて強力な部品なのです。LSPを満たすことは、「正しく作成された継承関係」の兆しにはなるでしょう

訳注: 「継承よりコンポジション」は、『Effective Java』の有名な言葉です。

以下の2つを自分自身に問いかけるべきです。

  1. Bは、Aの完全なインターフェイス(すべてpublicメソッドとして)を「Aに期待するのと同じようにBを使える形で」公開したいのか?
    この場合(おそらく)継承が必要でしょう
  2. Bは、Aが公開している振る舞いの一部だけが欲しいのか?
    この場合(おそらく)コンポジションが必要でしょう

その後で、LSPを使って以下の問いかけに答えます。

この型を継承すべきか?

重要: もちろん、LSPだけが決定因子ではありません。「A is Bなのかどうか(ABなのか、そうでないのか)」という問いかけの方が中心にあることを忘れてはなりません。上の2つの問いかけは決定に役立ちますが、それらの問いかけは解決方法そのものではありません。

「よい継承関係の作成」は、これだけで別記事が一本書ける(下手すると本が一冊書ける)ほどの重たい話題です。今ここで申し上げたいのは、LSPを満たすことは必要だが、LSPは「よい継承を作り出す条件のひとつに過ぎない」ということです。

LSP違反の兆し

LSP違反の兆候を示す典型的なサインをいくつか目にすることがあります。

  • 派生クラスで、基底クラスのメソッドをオーバーライドしてまったく新しい振る舞いを追加している
  • 派生クラスで、スーパークラスのメソッドを空メソッドでオーバーライドしている
  • 派生クラスで、スーパークラスから継承したメソッドの一部について「クライアント側で呼んではならない」と書かれている
  • 派生クラスで、(チェックが行われていない)追加の例外をスローしている

まとめ

もうお気づきかと思いますが、私たちはこれらの原則を、動的型付け言語であるRubyのコンテキストで解釈しています。そのため、この特定の原則の意味がさして重要ではないように思えるかもしれません。しかしいずれにしろ、私はこの原則を甘く見ないようにしています。

Rubyではインターフェイスの一貫性を保つことを強制されませんし、その気になれば基底クラスと異なる型を派生クラスから返すことだってできます。しかしそんなことをすべきでしょうか?私はそうは思いません。それは非常に、非常にまずいやり方です。空の配列、論理値、文字列のどれが返されるかわからないメソッドを書いたとしたらどうでしょうか?

結論はシンプルです。オブジェクト指向プログラミングのよい手法の実践は、どんなときでも賞賛に値します。どんな言語を使っているかは関係ありません。繰り返しますが、ここでは常識を働かせることが肝心です。動的型付け言語のメリットを十分享受しつつ、そのことに責任を持って使いましょう。

動的型付け言語では柔軟性が高まることは間違いありません。しかし私は個人的に、その柔軟性のおかげであらゆる作業が楽になるとは限らないと考えています。動的型付け言語の柔軟性はもっと注意深く扱い、乱用せぬよう自らを律しなければなりません。

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

Rails tips: ランダムにコケるRSpecテストの修正に便利なbisect(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: ランダムにコケるRSpecテストの修正に便利なbisect(翻訳)

テストを書いていて一番ムカつくことのひとつは、テストが失敗したりしなかったりすることです。えてしてこういうspecは、development環境ではコケないくせにCIでコケたりします。CIがよくわからない方はWikipediaの継続的インテグレーションか、Travis CICircle CIといったサービスをチェックしてください。

ランダムに失敗するspecのデバッグには時間がかかります。最初にspecの問題を再現できる実行手順を特定してから、次にその問題を修正します。RSpecは問題の修正そのものについては無力ですが、失敗するシナリオを再現する順序を見つけることについては支援が可能です。これを行うには、RSpecを実行するときに--bisectフラグを付けます。

bundle exec rspec spec/controllers --bisect

--bisectフラグはRSpecバージョン3.3から利用できます。

これで次のような出力を得られます。

Bisect started using options: "spec/controllers"
Running suite to find failures... (3.37 seconds)
Starting bisect with 2 failing examples and 5 non-failing examples.
Checking that failure(s) are order-dependent... failure(s) do not require any non-failures to run first

Bisect complete! Reduced necessary non-failing examples from 5 to 0 in 3.25 seconds.

The minimal reproduction command is:
  rspec ./spec/controllers/contacts_controller_spec.rb[1:2:2,1:2:3]

この出力には、ランダムに失敗するspecを再現するときに行うべきことが示されています。その後の問題の修正は自分でやることになります。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Jenkinsで特定のプロジェクトだけ閲覧できるユーザを作成するには

[Rails] RSpecをやる前に知っておきたかったこと

Ruby製Webアプリのcookie-onlyセッション破損でセキュリティリスクの可能性(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby製Webアプリのcookie-onlyセッション破損でセキュリティリスクの可能性(翻訳)

RackやRailsにはクッキーモンスターがいます。

ブラウザは、ドメインやレスポンス内でcookieの数やサイズに制限を設けています。この制限を超えると、まずいことが起こる可能性があります。明らかなケースについてはRackやRailsが防止を試みますが、本記事では現在の実装で生じる問題について説明します。また、RubyのWebアプリで発生する問題の潜在的なインパクトや、問題の緩和方法についても検討します。

この情報は、(セッションIDに限らない)セッションハッシュ全体をエンコードした内容を含んでいるセッションcookieを転送するWebアプリと強く関連します。つまりcookie-onlyセッションのことです。MarshalやJSONを用いるRack::Session::Cookieや、RailsのデフォルトのActionDispatch::Session::CookieStoreや、(専用のレスポンスヘッダの代わりに)cookieを転送に用いるJWT(JSON Web Tokens)の実装などがそうです。cookie-onlyセッションが、cookieを切り詰める(truncate)古いブラウザの挙動に遭遇すると、セキュリティリスクが最大になります(このときFlashメッセージなどの任意の巨大データもセッションに含まれている可能性があります)。

手短に言うと、Rubyアプリでcookie-onlyセッションを使う場合は、Rack::Protection::MaximumCookieをミドルウェアスタックに追加することを検討しましょう。

ブラウザのcookie制限

制限の正確な内容はブラウザごとに異なっています。ブラウザにはcookieの個数やサイズの制限がありますが、本記事では後者の「サイズ」に着目します。Chrome、Edge、Firefox、Safariといった定番ブラウザでは、「cookie単位」の比較的寛容なサイズ制限が設けられています(かつ、制限を超えた場合の挙動はおかしくありません)。しかし古いブラウザではこれと対照的に、(「cookie単位」のオーバーヘッドの有無にかかわらず)サイズ制限が「ドメイン単位」になっており、サイズを文字数またはバイト数でカウントしていることがあります(制限を超えるとcookieは切り詰められます)。

定番ブラウザの場合

最新(記事執筆当時)のデスクトップ向け定番ブラウザをテストした結果、以下の制限が明らかになりました。

  • Chrome 64: cookie単位: 4,095バイト、ドメイン単位: 180バイト
  • Edge 38: cookie単位: 5,117バイト、ドメイン単位: 10,234バイトおよびcookie 50個(Ianに感謝!)
  • Firefox 56: cookie単位: 4,096バイト、ドメイン単位: cookie 150個
  • Safari 11: cookie単位: 4,096バイト

ブラウザが制限値を超えると、単にcookieの破棄を開始します。これによって、たとえばログインしていたはずのユーザーが知らないうちにログイン画面に戻されたりする可能性が生じます。何らかの防御対策が行われていなければこの現象の発生に気づけないかもしれません(ユーザーから苦情が寄せられれば別ですが)。

ここで強調しておきたいのは、サイズ制限の対象がcookieのキーや値のみならず、あらゆるディレクティブにも適用されるという点です。たとえば、key=value文字列のcookieがUTF-8で4,096バイトだとすると、Set-Cookieに; path=/を追加するだけで制限値に達し、上述のあらゆるブラウザで警告なしにcookieが破棄されることがあります。実際、; path=/shop;domain=example.org; expires=Sat, 04 Nov 2017 00:02:20 -0000;Secure; HttpOnly; SameSite=Strictのようなディレクティブはかなりのスペースを消費することがあります。

古いブラウザの場合

注意: 簡単に言うと、「古いブラウザ」にはIE11のように現代的だが定番ではないものや、iOS向けSafariのようなモバイルブラウザも含まれる可能性があります。

古いブラウザにおける制限はバラバラです。この資料はかなり古ぼけていますが、ブラウザの種類やバージョンによってどれだけ制限値がばらついているかをざっくり知るにはよいでしょう。注目したいのは次の2点です。

  1. 典型的なサイズ制限は「ドメイン単位」であり、「cookie単位」ではない
  2. 一部のブラウザでは「cookie単位」のオーバーヘッドが生じる

おそらく例で説明するのが一番わかりやすいでしょう。あるブラウザでは、cookieのサイズがドメイン単位で4,096バイトに制限され、cookie1つあたり3バイトのオーバーヘッドが生じているとしましょう。(example.orgなどの)どんなドメインを指定しても、4,093バイトのcookie 1個か、4,000バイトのcookie 1個と90バイトのcookie 1個か、1,000バイトのcookie 4個と81バイトのcookie 1個か、などのようになります。cookieサイズの合計とcookieごとのオーバーヘッドは、この制限値を超えてはなりません。

定番ブラウザとのもうひとつ重要な違いは、古いブラウザは制限値を超えたときにcookieを切り詰めてしまう可能性があるという点です。これは次の2つの理由によって深刻な問題です。

  1. データやダイジェストが破損し、セッションが不正になる可能性が生じる
  2. SecureHttpOnlySameSiteなどのディレクティブがcookieから脱落する可能性がある

Secureディレクティブが脱落すると、ブラウザが誤ってこのcookieをセキュリティで保護されていない接続で送信され、悪意のある第三者がcookieを盗んだり悪用したりする可能性が生じます。HttpOnlyディレクティブが脱落すると、cookieはXSS(クロスサイトスクリプティング)攻撃(JavaScriptからアクセス可能になるなど)に対して脆弱になります。SameSiteディレクティブが脱落すると、cookieはCSRF(クロスサイトリクエストフォージェリ)攻撃に対して脆弱になります1

RackとRailsの場合

明らかにcookieのサイズ制限は今に始まった話ではありませんし、RackやRailsのメンテナーはそれなりにこの点を認識しています。Rack::Session::CookieActionDispatch::Session::CookieStoreでは、key=value文字列のサイズが4,096バイト(または文字)を超えていないかどうかをチェックすることでこの問題の防止を試みます。しかしこの戦略は、以下の理由から有効ではありません(ここまでお読みいただいた方であればきっとおわかりいただけるはずです)。

  1. 戦略にディレクティブが含まれていない
  2. cookie単位のオーバーヘッドが考慮されていない
  3. ドメイン単位のcookieサイズが加えられていない

さらに、key=value文字列は初期段階のサイズチェックをパスすることがあり、このときにRack::Utilsescapeメソッドを巧妙に用いたある種のエンコーディングは、文字列を肥大化させて制限値を超えさせてしまう可能性があります。Rack::Session::SmartCookieのREADMEでサンプルをご覧いただけます。

JWTについて

cookie転送を用いるJWTの実装は、独自の制限チェックを実行しなければなりません。

解決方法

RackやRailsでこの潜在的な問題が修正されるまで新規や既存のWebアプリを保護する最善の方法は、正しいcookie制限チェックを実装した小さなミドルウェアを追加することです。このミドルウェアは、制限値を超えたときにエラーをraiseし、アプリレベルで修正対応が取れるようにします。

私の公開したRack::Protection::MaximumCookie gemは、この必要を短期または中期間満たすためのものであり、JWTやその他のユースケースについても同様に修正します。記事執筆時点では作業中ですが、デフォルト設定はほとんどのWebアプリで適切なものになっているはずです。設定で対象を定番ブラウザに限定したり、慎重さのレベル(levels of conservativeness)を上げたりすることもできます。注意書きをよくお読みください。皆様からのフィードバック、ご意見ご感想、プルリクを歓迎いたします。


  • この問題は10/28にRackコアチームに、11/3にRailsセキュリティチームにそれぞれ最初に報告されましたが、著しいセキュリティリスクとは見なされていません(そう見えます)。私のgemと本記事は、これらのチームが応答する時間を十分確保できるよう11/20まで保持しました。

関連記事

Unicodeで絶対知っておくべきセキュリティ5つの注意(翻訳)

RailsのCSRF保護を詳しく調べてみた(翻訳)


  1. まあ確かに、複数のブラウザの共通部分がSameSiteを実装し、かつサイズ制限を超えたcookieを切り詰める可能性は、ゼロではないにしてもおそらくきわめて小さいと思われます。 

Rails tips: スコープを用いてif条件をシンプルにする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: スコープを用いてif条件をシンプルにする(翻訳)

シンプルなスコープを使ってif条件を簡単にできる状況はとてもよく見かけます。サンプルのクラスでやり方をご覧にいれます。

class User < ActiveRecord::Base
  has_many :posts
end
class NotificationService
  def initialize(company:)
    @company = company
  end

  def notify_user(email:)
    user = User.find_by(email: email)

    if user.present? && user.posts.any?
      Mailer.new_posts_reminder(user.id).deliver
    end
  end

  private
  attr_reader :company
end

Userモデルがあって、has_many :postsによって各ユーザーが複数のpostsを持つことができます。NotificationServiceは、ユーザーが新しく投稿を作成したときに簡単なメールメッセージでユーザーにリマインダーを送信するのに用いられます。

ここではusers.posts.any?をチェックする必要はありません。というのも、これはデータベースレベルで行われるからです。postsを持つユーザー向けのスコープを作成してみましょう。

class User < ActiveRecord::Base
  has_many :posts

  scope :with_posts, -> { joins(:posts) }
end

スコープはクラスメソッドとして定義することも可能であり、どちらにするかはあなた次第です。どちらにすべきかよくわからない方は、モデルのクエリをカプセル化する2つの方法をご覧ください。ともあれ、これで更新されたデータベースクエリが使えるようになり、条件をリファクタリングできるようになりました。

class NotificationService
  def initialize(company:)
    @company = company
  end

  def notify_user(email:)
    user = User.with_posts.find_by(email: email)

    user && Mailer.new_posts_reminder(user.id).deliver
  end

  private
  attr_reader :company
end

コードがぐっとシンプルかつ短くなりました。しかもクエリが自らの挙動を語るようになりました。ほんのささやかなリファクタリングですが、この違いは大きいものです。この方法は、今後ユーザーがpostsを持つかどうかを決定する責務を持つコードを変更するときにも役立つのが嬉しい点です。変更前の方法では、ユーザーがpostsを持つかどうかをあちこちでチェックしなければなりませんでしたが、変更後の新しい方法ならUserモデルのスコープを変更するだけで済みます。

上の変更は、belongs_to関連付けのクエリのリファクタリングにも関連します。


Railsでお困りの方にお知らせ

知りたいことがありましたら、twitter または連絡用フォームにてお気軽にお問い合わせください。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

週刊Railsウォッチ(20180406)ruby-sass gemが非推奨に、Roda gem、paiza.ioは便利、Linuxha/procで遊ぼうほか

$
0
0

こんにちは、hachi8833です。記事数抑えたはずが遅くなってしまいました。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

⚓Rails: 今週の改修

全体にrafaelfrancaさんのコミットが目立ちます。まずはまだ動いている5-2-stableから。

⚓Rack::BodyProxyでbodyが改変されていたのを修正

@app.callが返すオブジェクトが(定数などで)保存済みだと、Rack::BodyProxyでbodyをラップする継続サイクルでbodyが改変され、最終的にSystemStackErrorになる。以下は再現コード。
同PRより

# 同PRより
class HealthcheckApp
  SUCCESS_RESPONSE = [200, { 'Content-Type' => 'text/plain' }, ['success']].freeze

  def initialize(app)
    @app = app
  end

  def call(env)
    return SUCCESS_RESPONSE if env['PATH_INFO'] == '/heartbeat'
    @app.call(env)
  end
end

app = HealthcheckApp.new(-> (_x) { [200, {}, nil] })
logger = Rails::Rack::Logger.new(app)

logger.call('REQUEST_METHOD' => 'GET')

つっつきボイス: 「ほあっSystemStackErrorって割りとキツイやつ?」「修正前のはresp[2] =みたいに配列を直接書き換えてますね↓」「ははー、なるほど!修正前はresp = @app.call(env)と配列が参照渡しになってた」「修正後のは変数展開で渡してるから書き換わらないと」
「直接書き換えてても普通に使われてる分にはほとんどの場合問題は起きなかったんでしょうけど」「使いまわしているうちにcontent lengthとかMD5あたりで不整合が生じそう」

# railties/lib/rails/rack/logger.rb
-          resp = @app.call(env)
-          resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
-          resp
+          status, headers, body = @app.call(env)
+          body = ::Rack::BodyProxy.new(body) { finish(request) }
+          [status, headers, body]

「しかしSystemStackErrorって心にインパクトあるというか、見ると絶望感に襲われそうw」「これだからconstできない言語は(爆)」

⚓ActiveRecord::QueryCacheミドルウェア内を最適化

# activerecord/lib/active_record/query_cache.rb#L28
     def self.run
-      ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool|
-        caching_was_enabled = pool.query_cache_enabled
-
-        pool.enable_query_cache!
-
-        [pool, caching_was_enabled]
-      end
+      ActiveRecord::Base.connection_handler.connection_pool_list.
+        reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
     end

-    def self.complete(caching_pools)
-      caching_pools.each do |pool, caching_was_enabled|
-        pool.disable_query_cache! unless caching_was_enabled
-      end
+    def self.complete(pools)
+      pools.each { |pool| pool.disable_query_cache! }

       ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
         pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?

つっつきボイス:mapで回していたのをrejecteachに変えたと」「呼び出し多いだろうからmapで全回しじゃなくてrejectでフィルタしてからeachする方が効率いいぜ、ってことか」「ベンチマーク見たいなー: ないのかな?」

⚓params#digの修正

params = ActionController::Parameters.new(a: { b: { c: 1 } })
params.dig(:a, :b)[:c] = 2
params 
# 修正前=> <ActionController::Parameters {"a"=>{"b"=>{"c"=>1}}} permitted: false>
# 修正後=> <ActionController::Parameters {"a"=><ActionController::Parameters {"b"=><ActionController::Parameters {"c"=>2} permitted: false>} permitted: false>} permitted: false>
# actionpack/lib/action_controller/metal/strong_parameters.rb#L592
     def dig(*keys)
-      convert_value_to_parameters(@parameters.dig(*keys))
+      convert_hashes_to_parameters(keys.first, @parameters[keys.first])
+      @parameters.dig(*keys)
     end

つっつきボイス: 「確かにdigは修正前みたいな挙動を期待しないですよね普通: 上と似た感じのミューテーション問題」「参照渡しから値渡しへの変更」

「それにしてもこういう書き方↓されるとイヤだよねって思うw: 推奨したくない」「ものすごく追いにくいコード」「どう呼ばれても安全にしておくのがいい書き方デス!」「ほんとその通り!」

params.dig(:a, :b)[:c] = 2

⚓メソッド呼び出しのダブルsplat**を普通の引数に変更

続いてRails 6向けmasterから。

# actionview/lib/action_view/helpers/form_helper.rb#L1973
-        fields_for(scope || model, model, **options, &block)
+        fields_for(scope || model, model, options, &block)

つっつきボイス: 「Ruby 2.6ではダブルsplat **の使用が警告されるみたいですね」「上だけ見ると、メソッド定義側ではなくてメソッド呼び出し側のダブルsplatですね」「Rubyのその変更ってどこで知らされたんだろう?(記号探しにくい…)」「ruby 2.6.0dev (2018-04-04 trunk 63085)で出る警告だということはわかったけど」

「うーん**とか普段使わないからなー、pryでちょい試しているけどうまくダブルsplatのbad caseを出せない…: おー、呼び出し側では**があってもなくてもハッシュをキーワード引数として渡せる↓ことはわかった」

h = {a:1, b:2, c:3}
def hoge(arg=99, a:, b:, c:)
   puts "arg:#{arg}, a:#{a}, b:#{b}, c:#{c}"
end

hoge h   #=> z:99, a:1, b:2, c:3
hoge **h #=> z:99, a:1, b:2, c:3

「つ#12106: 2年前にオープンしたまま閉じてませんが、何かそれっぽいissue見つけたので誰か頑張って解読してください」「works fineってどの辺がfineなんだか」

Rubyのパラメータと引数の対応付けを理解する(後編)

⚓不要なActionController::を削除

# actionpack/lib/action_controller/metal/exceptions.rb#L25
-  class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
+  class UrlGenerationError < ActionControllerError #:nodoc:
   end

修正そのものはシンプルですが、 rafaelfrancaさんのEiNSTeiN-:patch-1というブランチ名が何だか気になりました。


つっつきボイス: 「superfluous: 余分な、無駄な」「すごーく堅苦しい言葉ですね」

確かこの言葉を最初に見たのはスピノザの「エチカ」の英語版でした(原文はラテン語)。

⚓finalize_compiled_template_methodsをデフォルトでオンに

# actionview/lib/action_view/template.rb#L9
   class Template
     extend ActiveSupport::Autoload

-    mattr_accessor :finalize_compiled_template_methods
-    self.finalize_compiled_template_methods = true
+    mattr_accessor :finalize_compiled_template_methods, default: true

action_view.finalize_compiled_template_methodsはconfigなんですね。


つっつきボイス: 「んなオプション(゚⊿゚)シラネ」「この設定ってどこだろ?」「つ9facd9a

「ところで、このPRはどうやら単なる書式修正ですね」「お」「その前のeede8d8 Add action_view.finalize_compiled_template_methods config optionでの機能追加したときの書式が古かったからちょっと直したということみたい」「しまった、そっちが本編でしたか」

⚓Rails

⚓RubyのSassが非推奨に


sass/dart-sassより


つっつきボイス: 「今日Slackに流してもらった情報ですね」「しばらくちょい面倒なことになりそうな気がする: sass-railssassc-railsの両方に依存してたらどうなっちゃうんだろうとかw」「LibSassはC/C++で速そうではある」「しかしRuby版がなくなるのはちょっと悲しいなー: マイナーなアーキテクチャ上でちょっとした処理するときとか便利だったんで」

⚓Webpacker 4 pre版リリース(Hacklinesより)


つっつきボイス:Webpack 4はこないだ出てる(ウォッチで既報↓)から、Webpacker gemも追随してるということですね」「割と紛らわしい…WebpackとWebPacker」「もうpre2になってるし↓」「まあまだしばらくかかるかな」


github.com/rails/webpackerより

週刊Railsウォッチ(20180302)Ruby 2.6.0-preview1とWebpack 4.0リリース、爆速検索APIサービスAlgolia、Clowneでモデルをクローンほか


webpack.js.orgより

⚓Rails 5.2のcredentialをKubernetesで使う(Ruby Weeklyより)

Kubernetesは最近「クバ」とか「クーバァ」みたいにじわじわ略されつつあるような雰囲気?


つっつきボイス: 「確かにそろそろ呼び方統一してもいいんじゃね?」「記事の方は、Railsのcredentialsはそもそもマイクロサービス的なコンテナを意識したソリューションだから当然できますね」「確かに: 記事短いし、『やってみたらできたうれしい!』という感じ」

⚓マイグレーション支援gem 4種

handcuffs(手錠)ってすごい名前。


つっつきボイス: 「strong_migrationsはTechRachoにも何度か登場してますね: 有名」「何というか微妙によくやってくれるヤツ」「この手のマイグレーションに機能を追加するのって怖くてしょうがないんですけどw」「マイグレーションファイルを1つだけずうっと書き換えるってのこっそりやってますが何か」「何とw」「あ、でもそれってクックパッドさんでおなじみのridgepoleがまさにそれですよね: ridgepoleは結構いい」「ridgepoleを使うときは、マイグレーションの履歴を知りたければGitのログ見れということになる」「言われてみれば履歴を二重管理することもないですね」「本格的に使ったわけではないけれど、ridgepoleがつらくなることがあるとすれば、ridgepoleが対応していないDBMSの独自機能をいじり始めたときかな」

参考: クックパッドにおける最近のActiveRecord運用事情 - クックパッド開発者ブログ

「zero_downtime_migrationsはおそらくPostgreSQLの機能を使ってやるんじゃないかな: PostgreSQLに確かそういうスキーマ変更時のロック回避関連の機能があった」

「outriggerはマイグレーションにタグを打てるみたいですね↓」「いやぁ、マイグレーションでタグが必要になる運用って何か間違ってる気がする…」

# instructure/outriggerより
class PreDeployMigration < ActiveRecord::Migration
  tag :predeploy
end
class PostDeployMigration < ActiveRecord::Migration
  tag :super_fun
end

Switchman::Rake.shardify_task('db:migrate:tagged')

「handcaffsもマイグレーションにphaseを設定できますが、じゃこれも?」「同じくその運用自体がどうかと思うし、マイグレーションを複雑にするのは何かが間違ってる気がする…そう思わない?」「フレームワークが整備される前は機能としてのマイグレーションはなかったし、本番リリースすれば捨てていいぐらいのものだったはずだし」「そうそう、マイグレーションがソースコードとして永続化するのは方向性として何か違う気がする」「欲しいのは本来スキーマ」「昔は皆てんでばらばらにマイグレーションのバッチを書き捨てて追えなくなっちゃったりしただろうから、Railのマイグレーションファイルはそうした書式と置き場所を統一するためのものだったのかもですね」

「そういえば以前Excelにテーブルを書くとSQLを生成するマクロを作りましたよ: 今もこっそり使われてますが」「そうそうSIerがよくやってるヤツ」「新規はともかく変更し始めるとすごく面倒くさかったけどw」「そもそもRailsはスキーマ変更頻繁すぎ」「まあそれはRailsの思想ということで」

⚓Roda: プラグイン方式の柔軟で強力なルーティングエンジン


roda.jeremyevans.neより

y-yagiさんが最近好きなフレームワークだそうです↓。近々READMEの翻訳をTechRachoで公開しようと思います。


techplay.jpより

なお、以前翻訳したRodauthもRodaを全面的に使っています。「RodauthのREADMEは名ドキュメント」とy-yagiさんとうなずきあいました。

Ruby: 認証gem ‘Rodauth’ README(翻訳)


つっつきボイス: 「Sinatraのルーティングがベースになってるらしいです」「あー、ネストで書けるのかなるほど!」「ソースを見ると、本体はわずかで、ほとんどの機能をプラグインとして装備してました」

# http://roda.jeremyevans.net/より
# cat config.ru
require "roda"

class App < Roda
  route do |r|
    # GET / request
    r.root do
      r.redirect "/hello"
    end

    # /hello branch
    r.on "hello" do
      # Set variable for all routes in /hello branch
      @greeting = 'Hello'

      # GET /hello/world request
      r.get "world" do
        "#{@greeting} world!"
      end

      # /hello request
      r.is do
        # GET /hello request
        r.get do
          "#{@greeting}!"
        end

        # POST /hello request
        r.post do
          puts "Someone said #{@greeting}!"
          r.redirect
        end
      end
    end
  end
end

run App.freeze.app

「まあわかるなー: 何しろRailsのルーティングは最近になるほどよくわからなくなってきてるから(爆)」「この間もそんな話になりましたね」「Railsのルーティングの完全なドキュメントが欲しいし、ルーティングまじで難しい: ルーティングをネストしても必ずしもURLがネストされるとは限らないからかなり不可思議な感じになるとか」

「結局Railsガイドのルーティングが頼りってことですかね?日本語まだ追いついてないですが(やらなきゃ…)」「つguides/source/routing.md、とよく見たらRailsガイドのソースか」「ルーティングにconcernあるんですけど?」「そう、あるんですよ」「defaults: { format: 'jpg' }あたりからだんだん不思議感あふれてくる」「constraints、これ割りと便利だけど気をつけないと死ぬ: ルーティングが重複しているときに記述の順序で評価順序が変わってハマったことあった」「to: MyRackAppでRackアプリを指定できるんでlambda書けるし」

「やっぱり最新のガイドかー」「だいたいは見覚えあるけど、知りたいのはこれが組み合わさるとどうなるのかってところなんですけどねw」

「話逸れますが、Railsのルーティング機能って何か名前が付いてましたよね: Developers Meetup2日目オーラスの対談で耳にしたときに何のことだかわからなくて」「journey」「それだっ」

参考: rails/journey

「線路」と「旅」としゃれたんでしょうね。

Hanami 2.0でRodaを採用しようという提案も出されてるんですよ: breaking changeを承知で」「Roda、きれいに書けるしHanamiに入るのはいいんじゃないかな」

⚓GoCD: デプロイをビジュアル管理するサービス(Ruby Weeklyより)


gocd.orgより

サイトの図のデザインが個人的に好きです。図がどうもカッコよく作れない人は、描線を思い切って太くするだけで相当印象変わるとこっそり思っています。


gocd.orgより


つっつきボイス: 「最近こういう感じのをよく見かけますね: ↓これGitLabにも同じようなのが入ってる」「ほんとだ」「AWSにもこういうのあるし、多くのCIに取り入れられてますね」

「AWSはCIとCD両方できる」「前にも話したけど、このCIとCDとCDって略称どうにかならない?w」「ですねー: 短くてかぶってて扱いづらい」

  • CI: Continuous Integration
  • CD: Continuous Deployment
  • CD: Continuous Delivery

⚓昔のRailsのdynamic finderって何がまずかったの?

ツイートで見かけて気になったので、ラジオのお葉書感覚で拾ってみました。


つっつきボイス: 「dynamic finderは昔のRailsにあったfind_なんちゃらみたいなやつということで」「やっぱりメソッドが大量に生成されたのと、method_missingで取るのが嫌がられたとかでしょうね: method_missingだとデバッグも拡張もしづらいし」「かといって前もって生成するという方向にもならなかったし」

参考: Rubyリファレンスマニュアル method_missing
参考: activerecord-deprecated_finders — gemに切り出されたんですね

Rails3のfind系メソッドと注意のまとめ

まったく個人的にですが、method_missingというと排水口に仕掛ける網みたいなのを連想してしまいます。

⚓ActiveRecordを非同期でマイグレーションする(Hacklinesより)

# 同記事より
class MigrationProcessingJob < ApplicationJob
  def perform(params)
    async_migration = AsyncMigration.find(params.fetch(:async_migration_id))

    all_migrations = migration_context.migrations
    migration = all_migrations.find { |m| m.version == async_migration.version }

    # actual work!
    ActiveRecord::Migrator.new(:up, [migration]).migrate

    async_migration.update!(state: "finished")
  end

  def migration_context
    ActiveRecord::Base.connection.migration_context
  end
end

つっつきボイス: 「ははー、ジョブでマイグレーションやる感じか」「Rails Developers Meetupでからあげエンジニアことささたつさんのプレゼンで、彼がやっているClassidb:migrateがすごく時間かかるという話をしてて、そういう状況ではこういう形のasyncマイグレーションというのは検討の余地あるかも」「Classiさんはこの分野でけっこうシェアを伸ばしているそうでスゴイ」

その後、学校とインターネット接続の話題でしばし盛り上がりました。


classi.jpより

その他記事など

⚓Ruby trunkより

⚓Stack consistency error

/bundle/gems/sprockets-3.7.1/lib/sprockets/mime.rb:122: [BUG] Stack consistency error (sp: 960, bp: 959)
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
(snip)

つっつきボイス: 「あまり見かけないエラーな気がして」「Stack consistency error見たことない…」

⚓ISeq#to_binary -> load_from_binary -> evalすると落ちる

$ ./miniruby -ve 'str = "class P; def p; end; def q; end; E = \"\"; N = \"\#{E}\"; attr_reader :i; end"; iseq = RubyVM::InstructionSequence.compile(str); bin = iseq.to_binary; RubyVM::InstructionSequence.load_from_binary(bin).eval'
ruby 2.6.0dev (2018-04-02 trunk 63063) [x86_64-linux]
Segmentation fault (core dumped)

⚓prependされたモジュールからシングルトンメソッドを取れない

module Empty; end

class MyClass
  singleton_class.prepend(Empty)

  def self.foo; end
end

MyClass.singleton_methods(false) # => [:foo]
MyClass.singleton_method(:foo) # => NameError (undefined singleton method `foo' for `MyClass')

これと関係ありそうな記事がちょうどRubyFlowに流れてきました。


つっつきボイス:singleton_methods?普段使わないけど」「私もtakanekoさんに教えてもらって知りました: シングルトンメソッドはクラスメソッドだということです」

参考: Rubyリファレンスマニュアル Object#singleton_methods

⚓Ruby

⚓Rubyで時間を「正確に」測る方法(Ruby Weeklyより)

# 同記事より
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# time consuming operation
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed = ending - starting
elapsed # => 9.183449000120163 seconds

つっつきボイス:RTCで時間取れとかそういう話?」「『時間は一方向に進むとは限らない』みたいな見出しがちらっと見えた」「まあ実際そう: monotonicという概念とか」「monotonicって、数学の『単調に増加する』の単調ですね」
「Androidアプリとか、バックグラウンドに回るとクロックが嘘ついたりするんで」「ええぇ!」「時間を頻繁に測りすぎたらいけないとか何とかあってですね」

「この辺の時間がらみの知識はパフォーマンスとかベンチマークをやるときには必須で、ちゃんとしたコンピュータサイエンスの本だと最初の方で解説することが多いですね」「測定そのものにも時間がかかると」

参考: 時間情報の取得 clock_gettime() - 時間の扱い - 碧色工房 — RTCについて詳しく書かれています

⚓searchkick: ユーザーの検索パターンを学ぶ検索gem(Awesome Rubyより)

Elasticsearchを使っているそうです。chewy gemと比較されていました

# 同リポジトリより

class Product < ApplicationRecord
  searchkick language: "german"
end

Product.search_index.tokens("Dish Washer Soap", analyzer: "searchkick_index")
# ["dish", "dishwash", "washer", "washersoap", "soap"]

Product.search_index.tokens("dishwasher soap", analyzer: "searchkick_search")
# ["dishwashersoap"] - no match

Product.search_index.tokens("dishwasher soap", analyzer: "searchkick_search2")
# ["dishwash", "soap"] - match!!

つっつきボイス: 「表記ゆれやスペルミスとか部分一致に対応する感じですね: クックパッドさんとかが自力で頑張ってる分野」「見た感じ英語だけなのかな?」

Elasticの日本語プラグイン: kuromojiが使えるようです。

「タマネギ、たまねぎ、玉ねぎ、玉葱のどれで検索してもヒットさせるとか、単にネギって書いたときに長ネギとタマネギと万能ネギのどれにヒットさせるか、みたいな処理とか」「そういうのをちょっとだけお気楽にできるようにするんでしょうね」

⚓Kernel#at_exit、初級編、中級編、魔境編(Awesome Rubyより)

# 同記事より
puts "start"

at_exit do
  puts "start of first at_exit"
  at_exit { puts "nested inside first at_exit" }
  at_exit { puts "another one nested inside first at_exit" }
  puts "end of first at_exit"
end

at_exit do
  puts "start of second at_exit"
  at_exit { puts "nested inside second at_exit" }
  at_exit { puts "another one nested inside second at_exit" }
  puts "end of second at_exit"
end

puts "end"

つっつきボイス:at_exit?フック系か」「これって2回書いたら後のやつが上書きしちゃいます?」「書いたのと逆順に実行しますね」「お、ちゃんとスタックに積むのか: それはスバラシイ」「Ruby技術者認定試験には何年か前に出ましたヨ」「やべーw」「あの試験にはこういう知る人ぞ知るみたいなものが出ますねー」

at_exit、何かで見たことありますね: スレッドが分かれた後の後始末みたいなところとか」「プログラム自体が強制終了しない限りは終了時に実行してもらえる感じなんでしょうね」「/tmpの下に作ったゴミを終了時に片付けるとか」

参考: RubyリファレンスマニュアルKernel.#at_exit

⚓継続的開発のつらさ(それはそうとStruct.newを継承すんな)(Awesome Rubyより)

class Country < Struct.new(:request, :ip, :country_code, :country_code2, :country_code3, :country_name, :continent_code)

つっつきボイス:Rubyスタイルガイドで『Struct#newで初期化したインスタンスを継承しないこと』ってあったのを思い出したので」「それはやっちゃダメなやつ: さすがにこういうのは見たことないけど」「…何かで見た気がする」「…やったことある: 単にattr_accessorとコンストラクタを書く手間を省きたかった、以上」「ま気持ちはわかりますw」「この書き方するとRubyMineとかで捕捉できなくなっちゃうんですよね」

⚓RejectKaigi 2018も開催

現時点では参加フォーム準備中だそうです。登壇受け付けは行われています。


つっつきボイス: 「RejectKaigiっていう名前がイイですね: Rで始まってるとことか」「韻を踏んでるw」「本編のRubyKaigiはあまりRails寄りの話が出なかったりするんで、むしろこっちの方がRails寄りとか業務運用寄りの話聞けるチャンスあるかも」「凄いのは、これRubyKaigiの公式イベントなんですよね」「ホントだ」

⚓Ruby 3×3のゴールを目指して

RubyKaig 2017のラス前↓を務めた、大御所Vladimir Makarovさんの記事です。


つっつきボイス: 「出たマカロフさん」「やっぱりJITは大きな話題だよね: まJITを入れるとベンチマークをいくらでも偽装できるみたいなところがないわけではないけどw: 長いんでこれは後で読もう」

⚓cron的なRubyプログラム対決


つっつきボイス: 「ところでcronの何がいいって、他が死んでもめったに死なないことだよね」「godなんてすぐ死にますよ」「godなのに(´・ω・`)」「で結局安定してる監視ソフトとなると昔ながらのmonitになる: config書きにくいけどなっ」「monitは大量にエントリ書くと全部グローバルになって、あれがつらい: 前にも話したsupervisorは一応{}で括れるから本当はsupervisorの方がいいんだけどなー」

⚓🌟paiza.ioは便利🌟


paiza.ioより


つっつきボイス: 「Paizaこういうのを始めたのか」「そうそう、paiza.ioはいいですよー、面接相手に使ってもらうときなんかにとっても便利!: ログインいらないし対応言語豊富だしIDEっぽいことたいていできるし」「Chromebookとかで使ってもらえば後始末もいらなくてさらに安心」

久々に🌟を進呈いたします。おめでとうございます。

⚓その他記事など

⚓SQL

⚓PostgreSQLでLLVM JITサポートに向けて取り組み開始(Postgres Weeklyより)


同記事より


つっつきボイス: 「ぽすぐれまでJIT!」「SQLクエリをJITコンパイルするのか: むしろとっくにやってたのかと思ってた」「前にウォッチでも話したウィンドウ関数とかJITが効きそうなのに」

⚓PostgreSQL 10向けにみっちりコメントのついたconfigファイル(Postgres Weeklyより)

# 同設定ファイルより

...
# max_connections
# ------------------------
# An integer setting a limit on the number of new connection processes which
# PostgreSQL will create.  Should be set to the maximum number of connections
# which you expect to need at peak load.  Note that each connection uses
# shared_buffer memory, as well as additional non-shared memory, so be careful
# not to run the system out of memory.  In general, if you need more than 200
# connections, you should probably be making more use of connection pooling.
#
# Note that by default 3 connections are reserved for autovacuum and
# administration, and more may be used by replication.

  max_connections = 100  # small server
# max_connections = 500  # web application database
# max_connections = 40   # data warehousing database
...

つっつきボイス: 「MySQLだと、configはmy-huge.cnfあたりから引っ張ってきますね」「MySQLはソースからインストールすると/sharedの下にこの手の設定済みファイルがどっさり入ってくるんでそれを使うと: こういうのは自力で設定するもんじゃないw」

⚓pgDash: PostgreSQLを詳しく監視する


pgdash.ioより

⚓JavaScript

⚓consola: ちょっとカッコいいコンソールロガー(GitHub Trendingより)


同リポジトリより

数日で★1400超えです。


つっつきボイス: 「やけに★が多いなと思って」「JavaScriptユーザーはめちゃ多いからw」

⚓puppeteer: ChromeのヘッドレスNode API


つっつきボイス: 「これたしか社内で使ってる人いますよね」「…使ってます」「karmaにしたかったけど画面をiframeにはめたりしてネイティブで動かしていない制約があったり云々(聞き取れず🙇)」


karma-runner.github.ioより

⚓V8エンジン6.6がリリース

Array#reduceが速くなったというのがとりあえず目につきました。


同記事より

⚓CSS/HTML/フロントエンド

⚓WebSocketsって、要る?(RubyFlowより)


同記事より

⚓JavaScriptとRustをWebAssemblyで(Awesome Rubyより)

# 同記事より
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

Mozilla Hackブログの記事です。

⚓iOSでPWAする


同記事より


つっつきボイス: 「PWA知見貯めたいです」「そういえば前にPWA記事翻訳したら結構読んでもらえましたね」「PWA記事は炎上しがち」

WebサイトをPWA(Progressive Web App)に変える簡単な手順(翻訳)

⚓ブランディングのコツ(Hacklinesより)


同記事より


つっつきボイス: 「非エンジニア向けにもいいかと思って」「うん、こういうのはいいですね: サービスの開発をするんだったらこういうブランドづくりの話も理解できるようにしておきたい」

⚓その他

⚓「最近のGo」コーナー

参考: WebAssemblyとは何であり、何でないのか - yhara.jp

つながりありませんがここに貼りたくなったので↓。

⚓仮想通貨を勝手にマイニングするChrome拡張をGoogleが全面禁止

参考: Google、仮想通貨を採掘するChrome拡張機能を全面禁止に - ねとらぼ

⚓Linuxで楽しいのは/procディレクトリ


つっつきボイス:/proc探検はLinux初心者ならぜひやるべきですね」「そうそう: とりあえず雑にcatしたりrmしてみたり、それで何が起こっているのか見るのがオモシロイ」

Linuxの面白みを手っ取り早く知りたい方におすすめです、/proc探検。

⚓envison: ネットワーク可視化ツール(Ruby Weeklyより)


つっつきボイス: 「内容はともかく、リポジトリのトップに書かれている”red / blue team”って何だったかなと思って」「つRed team
「セキュリティ関連の書籍なんかでは、攻撃側をred team、防衛側をblue teamって呼ぶのが普通」「あ、AliceとBobとCharlieみたいな」「そうそう、セキュリティ界隈ではそう呼ぶことに決まってるので、単なる固有名詞以上の意味がある」「ちなみにこの3人は、頭文字がそれぞれABCなのがポイントですね」「カタカナに翻訳されるとわかんなくなるw」「悪いヤツはEで始まることになってるとか」「evilに通じるからですかね」

参考: レッドチーム(Red Team)とブルーチーム(Blue Team) 図解サイバーセキュリティ用語 – 図解サイバー攻撃 [Cyber Attack dot Net]

参考: Wikipedia-en Alice and Bob

⚓番外

⚓英語の5-7-5

haml.infoのトップにある俳句のことみたいです。


同サイトより

英語はシラブルの概念が日本語とまったく違うから、5-7-5よりむしろラップをベースに小節数で考えたらいいのかな。

⚓ナンプレ

ナンプレ本ってどんな田舎のスーパーの雑誌コーナーにも必ずあるのがちょっと不思議。

⚓「安定な結婚の問題」

参考: Wikipedia-ja 安定結婚問題

もしかして記憶違いかもしれませんが、だいぶ前にシャプレーのこの手法の紹介を読んだときに「4人目の異性と付き合った時点で結婚するのが最適化が最も進む」と説明されていてなるほどと思いました。付き合う人数をそれ以上増やしてもベストマッチングには貢献しないと。

考えてみたら、一発目で最良の縁結びをしようなんてたいそう虫のいい考えかもしれませんね。大学入試や結婚もそうですが、転職などもそうなのかも。クラウドサービスなんかも4種類ぐらい試してから決める方がマッチング進むかも。

⚓放送開始、もう20年前か


つっつきボイス: 「ざわ…」「ざわ…」

⚓そろそろ脳に埋められるようになったっぽい

参考: 脳に埋め込み神経細胞と接続する電子プローブ――脳医学研究や生体信号によるロボット制御が狙い | fabcross for エンジニア

⚓デスティニー

仏滅にデプロイしてコケたときには「ああやっぱり」と自分を責めずに済み、大安にデプロイしてコケたときには「大安でも勝てぬか…」と自らを慰める効果があります、きっと。

画面では今まで四捨五入してた(´・ω・`)


pc.watch.impress.co.jpより

遠い遠い昔にニュートン・ラフソン法を(確かRubyだったと思いたい)お遊び実装してみたら、手動で数回ループを回すだけでびっくりするぐらい高精度な結果が出て、確かにこれなら大昔の原始的な電卓の機械語でもすぐできるなと思ったのを思い出しました。


今週は以上です。

おたよりコーナー

バックナンバー(2018年度)

週刊Railsウォッチ(20180330)春のリリースラッシュ: Rails 5.1.6/5.0.7とRuby 2.5.1など、Ruby 2.2は3月でメンテ終了ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

Ruby on Rails Security Project

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Frontend Focus

frontendfocus_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

JavaScript Live

jslive_logo_captured

JSer.info

jser.info_logo_captured

Hacker News

160928_1654_q6srdR

Github Trending

160928_1701_Q9dJIU


Rails tips: rescue_fromでコントローラのエラーをrescueする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: rescue_fromでコントローラのエラーをrescueする(翻訳)

Ruby on Railsのコントローラで発生したエラーをrescueしなければならなくなることがときどきあります。多くの場合、クエリ実行後データベースにレコードが存在していなければActiveRecord::RecordNotFound例外からのrescueが必要になるでしょう。

サンプルのコントローラを書いてみましょう。

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    redirect_to :root, alert: 'User not found'
  end

  def edit
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    redirect_to :root, alert: 'User not found'
  end
end

このコントローラには2つのメソッドがあり、どちらも同じ例外を同じ方法で扱っています。このコードをもっと再利用するために、ActiveSupportモジュールのrescue_fromメソッドを使う手があります。

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound do |exception|
    redirect_to :root, alert: 'User not found'
  end

  def show
    @user = User.find(params[:id])
  end

  def edit
    @user = User.find(params[:id])
  end
end

before_actionフィルタを用いてshowメソッドとeditメソッドをリファクタリングすることもできますが、今回は解説しません。さて、上のようにすることでコントローラが読みやすくなり、同じコードを2回も書かなくてよいようになりました。気持ちいいですね!ブロックの代わりにメソッド名を渡すこともできます。

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_homepage

  def show
    @user = User.find(params[:id])
  end

  def edit
    @user = User.find(params[:id])
  end

  protected

  def redirect_to_homepage
    redirect_to :root, alert: 'User not found'
  end
end

concernsを使って、1つのメソッドを複数のコントローラで使いまわすことだったできます。この方法が使えるかどうかは、渡された例外をrescueするのに用いるロジック次第です。rescue_fromは、エラー発生時にユーザーに見栄えのよいエページを表示したいときにとても便利です。このメソッドがなければ、しょぼいデフォルトエラーページが表示されることになるでしょう。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails tips: スコープを用いてif条件をシンプルにする(翻訳)

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

後半で紹介されているgemについては先週のRailsウォッチもどうぞ。

Rails: データベーススキーマをダウンタイムなしで変更する(翻訳)

Discourseのメンバーはいつも継続的開発の大ファンであり、コミットのたびにCIのテストスイートと対決しています。すべてのテスト(UI、単体、結合、スモーク)にパスすれば、自動的にコードの最新バージョンがhttps://meta.discourse.orgにデプロイされるようになっています。

私たちが継続的開発というパターンに沿って実践していることで、ソフトウェアを自分でインストールした数千人のユーザーがいつどんなときでもtests-passedバージョンに安全にアップグレードできるようになっています。

私たちは頻繁にデプロイを行っているので、デプロイ中に止まったりしないようひときわ注意を払う必要があります。アプリのデプロイ中に止まってしまうよくある原因のひとつが、データベーススキーマの変更です。

スキーマ変更に伴う問題

現在のデプロイのしくみを雑に説明すると次のような感じになります。

  • データベースを新しいスキーマに移行する
  • アプリを単一のDockerイメージにバンドルする
  • レジストリにプッシュする
  • 古いインスタンスを止め、新しいインスタンスをpullして立ち上げる(これを繰り返す)

万が一互換性のないデータベーススキーマを作成してしまうと、古いバージョンのコードを実行している古いアプリのインスタンスが全部止まってしまうリスクが生じます。実際には数十分の停止💥という形で表面化します。

これはActiveRecordにとって特に悲惨な状況です。production環境のデータベーススキーマはキャッシュされており、スキーマでカラムのリネームや削除を行う何らかの変更が生じると、たちまち影響範囲にあるモデルであらゆるクエリが効かなくなってしまい、無効なスキーマの例外がraiseされるリスクが生じます。

この問題を克服し、停止時間を最小限にとどめてスキーマ変更を安全にデプロイできるようにするために、私たちは数年がかりでさまざまなパターンを導入してきました。

マイグレーションの情報を詳しくトラッキングする

ActiveRecordにはschema_migrationsという名前のテーブルが1つあります。ここには、実行されたマイグレーションに関する情報が保存されます。

残念なことに、このテーブルに保存されている情報の量は極端に制限されています。実際、次の情報ぐらいしかありません。

connection.create_table(table_name, id: false) do |t|
  t.string :version, version_options
end

このテーブルには、実行したマイグレーションの「バージョン」を保存する孤立したカラムが1つあります。

  1. マイグレーションが実行された時刻はこのカラムに保存されない
  2. マイグレーションの実行にかかった時間はこのカラムに保存されない
  3. マイグレーションの実行時に動かしていたRailsのバージョンに関する情報が何もない

この情報の少なさ(特に「いつ実行されたのか」という情報が取れないこと)のせいで、スキーマ変更をきれいに扱えるシステムの構築が困難になっています。さらに、情報が貧弱なためにマイグレーション中に発生する奇妙でわけの分からない問題のデバッグがものすごくつらくなります。

Discourseでは、Railsにモンキーパッチを適用することでマイグレーションの情報を詳しくログ出力しています(github.com: 記事では一部省略)。

module FreedomPatches
  module SchemaMigrationDetails
    def exec_migration(conn, direction)
      rval = nil

      time = Benchmark.measure do
        rval = super
      end

      sql = <<SQL
      INSERT INTO schema_migration_details(
        version,
        hostname,
        name,
        git_version,
        duration,
        direction,
        rails_version,
        created_at
      ) values (

パッチのおかげで、マイグレーションがらみの情報を非常に詳しく取れるようになりました。これはマジでRailsに取り入れるべきです。

カラム削除の延期実行

マイグレーションログを詳しく取れるようになり、これまでのマイグレーションが「いつ行われた」かがすっかりわかるようになったので、カラムの削除を延期実行(defer)できるようになりました。

つまり、危険なスキーマ変更を実行する前に、新しいコードがスキーマ変更を行おうとしていることを私たちが認識することを保証できるようになったのです。

実際には、私たちはカラムの削除にはマイグレーションを使いません。その代わり、カラム削除の延期実行についてはdb/seedで面倒を見ます(github.com: 記事では一部省略)。

Migration::ColumnDropper.drop(
  table: 'users',
  after_migration: 'DropEmailFromUsers',
  columns: %w[
    email
    email_always
    mailing_list_mode
    email_digests
    email_direct
    email_private_messages
    external_links_in_new_tab
    enable_quoting
    dynamic_favicon
    disable_jump_reply
    edit_history_public
    automatically_unpin_topics
    digest_after_days
    auto_track_topics_after_msecs
    new_topic_duration_minutes
    last_redirected_to_top_at

これらの延期削除は、(次回のマイグレーションサイクルの)実行中に参照された特定のマイグレーションの少なくとも30分後に実行されます。新しいアプリのコードが確実に存在することで安心感を得られます。

カラム名を変更する場合は、新しくカラムを1つ作って値をそこに複製し、古いカラムをリードオンリーにしてからトリガーで延期削除をかけます。

テーブル名を変更したりテーブルを削除する場合も同じような要領で進めます。

延期削除のロジックはColumnDropperTableDropperで見られます。

自分たち自身を信用しない

アプリごとに行うさまざまな作業において、「徹底」はひとつの大きな問題です。

安全を担保するためのパターンはいろいろありますが、それでも、カラムやテーブルの削除を絶対にActiveRecordマイグレーション方式で行うべきではないことを忘れてしまったりします。

マイグレーション中に危険なスキーマ変更を絶対にやらかさないようにするために、PG gemにパッチを当てて、マイグレーションのコンテキストで特定のステートメントの利用を禁止しました。

DROP TABLEしようとしたりカラムをDROPしようとしても、例外がraiseされます。

これで、社内のベストプラクティスを無視してリスクのあるスキーマ変更をかけることが困難になりました。

== 20180321015226 DropRandomColumnFromUser: migrating =========================
-- remove_column(:categories, :name)

WARNING
-------------------------------------------------------------------------------------
An attempt was made to drop or rename a column in a migration
SQL used was: 'ALTER TABLE "categories" DROP "name"'
Please use the deferred pattrn using Migration::ColumnDropper in db/seeds to drop
or rename columns.

Note, to minimize disruption use self.ignored_columns = ["column name"] on your
ActiveRecord model, this can be removed 6 months or so later.

This protection is in place to protect us against dropping columns that are currently
in use by live applications.
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

Attempt was made to rename or delete column
/home/sam/Source/discourse/db/migrate/20180321015226_drop_random_column_from_user.rb:3:in `up'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

このロジックはsafe_migrate.rbで見られます。これは最近のパターンなので、強制を行ったのはマイグレーション後まだ数日間というところです。

その他の方法

gemでできることもあれば、gemではできないこともあります。

  • Strong Migrations gemは、強制機能を提供します。このgemでは、PostgreSQLでインデックスをコンカレントに作成するかどうかなどの興味深い条件を多数設定することもできます。強制は、ActiveRecordのマイグレータにパッチを当てる形で行います。つまり、誰かが生SQLでやらかしてしまえば強制をすり抜けてしまいます。
  • Zero downtime migrationsもStrong Migrationsと非常によく似たgemです。

  • Outrigger gemは、マイグレーションにタグを付与できるようにします。これを用いてデプロイ前マイグレーションやデプロイ後マイグレーションを行うことで、デプロイを改善できます。デプロイ中の停止を避けられるようにマイグレーションを管理する手法としては、これが最もシンプルです。

  • HandcuffsはOutriggerととてもよく似たgemで、マイグレーションに複数のフェーズを定義できます。

訳注: handcuffs: 手錠

さて皆さんはどうすべきでしょうか?

弊社で使っているカラムやテーブルの延期削除パターンは、社内ではうまくいっていますが、まだ理想的とは言えません。データの「seed」を担当するコードはスキーマの補正やカラム削除のタイミングも行っていて、本来あるべき姿まで十分制御されてはいません。

理想は、どんなときでもrake db:migrateを実行するだけで何もかも魔法のようにやってくれることです。ホスティング先がどこであろうと、スキーマのバージョンが何であろうと関係なくやって欲しいのです。

とはいうものの、さしあたって本記事で私からおすすめしたいベストプラクティスは、さまざまなアイデアを合わせ技にすることです。どの方法も、Railsにふさわしい方法だからです。

ベストプラクティスの強制はRailsがやるべき

私としては、ActiveRecordに安全なスキーマ変更を強制するしくみを導入すべきだと思います。Railsを使っていれば誰しも多かれ少なかれ同じことに気づくでしょう。スキーマ変更のデプロイにおけるダウンタイムをゼロにできれば実用的です。

class RemoveColumn < ActiveRecord::Migration[7.0]
  def up
     # こういうのはエラーをraiseすべき
     remove_column :posts, :name
  end
end

さらに確実にするには、マイグレーションファイルにafter_deployフラグを追加することを例外なく強制すべきです。

class RemoveColumn < ActiveRecord::Migration[7.0]
  after_deploy! # こう指定するか、オプションをグローバルにオフするかのどちらかだけにする
  def up
     # Postクラスにignored_columns: [:name]がなければraiseすべき
     remove_column :posts, :name
  end
end
class RemoveColumn < ActiveRecord::Migration[7.0]
  after_deploy!(force: true)
  def up
     # この場合はignored_columnsにかかわらずマイグレーションできるべき
     remove_column :posts, :name
  end
end

もうひとつ、SQL解析に基づく強制が最も理想的だと思います。しかしこの方法はRailsの規模で問題をややこしくする可能性があります。サポートするデータベースが1つに限られている現実的な理由はこれです。

rake db:migrateはこれまでどおり使えるべき

後方互換性のため、rake db:migrateafter_deployマイグレーションを含むあらゆるマイグレーションを実行できるようにすべきですし、安全のために、デプロイの「ゼロダウンタイム」をないがしろにするアプリを除外できるようにすべきです。

rakeタスクに新しくmigrate:premigrate:postを導入すべき

アプリコードとの互換性を壊さないマイグレーションを実行する場合は、常に次のようにします。

rake db:migrate:pre
# runs all migrations without `after_deploy!`

破壊的な操作を行う場合は、常に次のようにします。

rake db:migrate:post
# runs all migrations with `after_deploy!`

まとめ

今日、ダウンタイムゼロのデプロイを「安全に」始める方法を模索している方には次をおすすめします。

  1. デプロイ前マイグレーションやデプロイ後マイグレーションを実行する形でビルド手順を見直す(OutriggerHandcuffsを使用)
  2. Strong Migrationsで「強制」を徹底する

関連記事

https://techracho.bpsinc.jp/hachi8833/2018_01_30/51106

Rails tips: ビューの`content_tag`のあまり知られていないオプション(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: ビューのcontent_tagのあまり知られていないオプション(翻訳)

一般に、RailsアプリでHTMLコードを生成する方法は主に2つあります。HTMLコードを直に書く方法と、ActionViewモジュールのヘルパーでHTMLを生成する方法です。今回は後者の中からcontent_tagヘルパーを取り上げたいと思います。このヘルパーには、あまり知られていないオプションがいくつかあります。

Railsコンソールを実行してActionView::Helpers::TagHelperモジュールやActionView::Contextモジュールをincludeして、これらのオプションを確かめてみましょう。1番目のモジュールはcontent_tagヘルパーにアクセスできるようにするために、2番目のモジュールはネストしたタグをテストできるようにするためにそれぞれincludeします。

基本的な使い方

content_tagヘルパーでは引数が1つ以上必要です。1番目の引数である要素名は省略できません。

content_tag(:div)
#=> "< div >< /div >"

2番目の引数には要素のコンテンツを渡せます。

content_tag(:div, "content")
#=> "< div >content< /div >"

これだけでは力不足なのであれば、ネストしたタグを渡せます。

content_tag(:div) do
  content_tag(:strong, "header")
end
#=> "< div >< strong >header< /strong >< /div >"

属性

要素にはこの他にもオプションを渡せます。よく使われているのはclassです。これも試してみましょう。

content_tag(:div, "header", class: 'link')
#=> "< div class="link" >header< /div >"

場合によってはクラス名を動的に渡す必要が生じることもあります。その場合、クラス名がなければ空文字ではなくnilを渡すことが重要です。理由は次のコードに示しました。

content_tag(:div, "header", class: "")
#=> < div class="" >header< /div >
content_tag(:div, "header", class: nil)
#=> "< div >header< /div >"

複数のクラスをまとめて渡すこともできます。

content_tag(:div, "header", class: ["one", "two"])
#=> "< div class="one two" >header< /div >"

属性のエスケープ

属性はデフォルトでエスケープされるので、(たとえばユーザーがリッチテキストエディタで入力できるようにした場合にデータベースに保存されている)生のHTMLコードを属性として渡しても、出力はエスケープされます。

content_tag(:div, "")

おそらくこんな出力は欲しくないでしょう。ありがたいことに、こんなときのための4番目のオプションがあります。このオプションは属性をエスケープするかどうかの指定で、デフォルトではtrueになっています。

content_tag(:div, "", {}, false) #=> < div >< div >< /div >< /div >

以上です。content_tagヘルパーはちっぽけですがビューでとても便利ですので、機能を詳しく知っておくと何かと役に立ちます。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

[Rails 5.1] ‘form_with’ APIドキュメント完全翻訳

【速報】Rails 5.2.0正式リリース!Active Storage、Redis Cache Store、HTTP/2 Early Hintほか

$
0
0

こんにちは、hachi8833です。日本時間の今朝方、ついにRails 5.2.0が正式にリリースされました🎉

(私の環境のrbenvに傷が入ってたらしくRuby 2.5.1でインストールしようとするとエラーになったので、2.5.0で動かしました: その後rbenvを全消し&再インストールして解消)

Ruby on Rails 5.2.0 正式にリリース


weblog.rubyonrails.orgより

週刊Railsウォッチなどで目にしたものも多数ありますが、リリースノートから簡単に拾い上げてみました。

文中で触れられていますが、大昔に最初のRailsがリリースされたのは2004年7月だったんですね。

4/17-4/19にピッツバーグで開催されるRailsConf 2018に間に合ったそうです。お疲れさまでした! 同カンファレンスでは、ActiveStorageやWebpackerなど多数のセッションが予定されています。ちょっと数える気にならないくらいセッションが多くてびっくりです。


railsconf.comより

RailsガイドEdge版リリースノート(英語)


edgeguides.rubyonrails.orgより

アップグレードガイド(5.1->5.2)

今回は以下の2点です。

Rails 5.2.0関連の主なTechRacho記事

Rails 5.2新機能を先行チェック!Active Storage/ダイレクトアップロード/Early Hintsほか(翻訳)

Rails 5.2ベータがリリース!内容をざっくりチェックしました

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

Rails 5.2を待たずに今すぐActiveStorageを使ってみた(翻訳)

RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)

Rails tips: RSpecの`let`ブロックや`before`ブロックは基本避けるべき(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecのletブロックやbeforeブロックは基本避けるべき(翻訳)

RSpecの作者はそれなりの理由があってbeforeletブロックを考案しましたが、本当に必要でない限りこれらの利用は避ける方が賢明です。理由は以下のとおりです。

  1. テストの本体からletの定義にジャンプ(またはその逆のジャンプ)することで、テストの自然な流れを妨げることになります。
  2. letを使うとテストが遅くなります。letがシンプルなメソッドに変換されるのは最後の段階だからです(memory usage benchmark)を参照(訳注: リンク切れです)。
  3. specが読みづらくなります。正しいコンテキストを追うにはあちこちにジャンプしなければなりません。
  4. spec同士に依存関係が生じます。specは分離するべきです。

letブロックやbeforeブロックを使った場合と使わない場合でどう変わるかを、以下のテスト例で考えてみましょう。

require 'spec_helper'

describe Users::NameService do
  describe '#name' do
    let(:user) { instance_double(User, posts?: false) }

    it 'ユーザー名を返す' do
      expect(described_class.new(user).name).to eq(user.name)
    end

    context 'ユーザーにpostsがある場合' do
      before { allow(user).to receive(:posts?).and_return(true) }

      it 'ユーザー名を返す' do
        expect(described_class.new(user).name).to eq("#{user.name} (has posts)")
      end
    end
  end
end

次はbeforeletを使わない場合です。

describe Users::NameService do
  describe '#name' do
    it 'ユーザー名を返す' do
      user = instance_double(User, posts?: false)

      expect(described_class.new(user).name).to eq(user.name)
    end

    it 'ユーザーにpostsがある場合ユーザー名を返す' do
      user = instance_double(User, posts?: true)

      expect(described_class.new(user).name).to eq("#{user.name} (has posts)")
    end
  end
end

2番目のテストではストーリーを2つに分けてあり、一方を理解するためにitブロックの外まで読む必要はありません。テストコードの重複は若干ありますが、場合によってはDRY(don’t repeat yourself)ルールを守るよりも読みやすさの方が重要です。しかも2番目のテストの方が若干高速です(テストが大規模になると実感しやすくなります)が、重要なのは、ロジックや読みやすさを損なわない範囲でできるだけspecが高速になるようにテストを書くことです。さらに、2番目のテストではスタブも1個少なくて済むというメリットもあります。

beforeブロックやletブロックをどうやっても省略できない例はあるものでしょうか?もしご存知でしたらぜひコメント欄までどうぞ。

追記(2018/02/10)

本記事にベンチマークの結果を追記しました。記事を書く前にベンチマークを取っておくべきでした。ベンチマークはgistでご覧いただけます(訳注: リンク切れです)。また、多くの方が私の記事を読み誤っています。私は「いついかなる場合でも」beforeletを使うなと申し上げたいのではありません。letbeforeがspecの高速化などに有用な場合は(特に複雑なケースでは)確かにあります。しかし私は常にコードをシンプルに書くようにしており、その方が有用性が高いことに気づいたのです。

RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

テストを不安定にする5つの残念な書き方(翻訳)

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

Rails tips: アプリをYAMLファイルで楽に構成する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: アプリをYAMLファイルで楽に構成する(翻訳)

Railにはymlファイルで定義されている設定を読み込める組み込みメソッドがあることをご存知でしょうか?このメソッドは、外部のgemを一切使わずにアプリで使えます。

やりたいこと

guestbookという名前のアプリで、Guestbook.configでファイルにアクセスしたいと思います。これは設定のネステッドハッシュを返すはずです。

設定

最初に設定ファイルを作成しましょう。configディレクトリにguestbook.ymlという名前の空ファイルを作成します。支払情報やログインcredentialなどのように開発中は不要なものや、production以外で使うのは危険な設定もありますので、この問題を解決するためにファイル内にセクションを設けます。defaultsセクションとdevelopmenttestproductionセクションです。stagingなど、その他の環境も使うことは可能ですが、ここでは普通使われている環境を使うことにしましょう。

defaults: &defaults

development:
  <<: *defaults

production:
  <<: *defaults

test: &test
  <<: *defaults

設定を見るとわかるように、各環境はdefaultsセクションの設定を引っ張ってきています。default設定は他のセクションよりも上にあるので、各環境の設定を簡単に上書きできます。たとえばnameという設定があるとして、productionではこれを別の名前にしたい場合は次のようにします。

defaults: &defaults
  name: GuestBook

development:
  <<: *defaults

production:
  <<: *defaults
  name: GuestBook Production

test: &test
  <<: *defaults

これで、defaultsにあるnameの名前はproduction以外のすべての環境で表示されます。defaultsセクションや環境ごとの個別のセクションはいくらでも柔軟に設定できます。これは、productionやdevelopmentのエンドポイントにアクセスする外部APIを用いる場合に特に便利です。API環境を切り替えたい場合は、コード内のロジックではなく、設定だけを変更するようにしなければなりません。

メソッド作成の設定

作成したファイルの設定はできましたので、今度はGuestbook.configでアクセスできるようにしたいですね。それにはconfig/application.rbファイルに新しいクラスメソッドを追加します。

module Guestbook
  def self.config
    Rails.application.config_for(:guestbook)
  end

  class Application
    ...
  end
end

テストする

動作をテストしたい場合は、まずRAILS_ENV=development bundle exec rails cを実行し、続いてRAILS_ENV=production bundle exec rails cを実行して、Guestbook.conifg[:name]呼び出しの値が異なっていることを確認します。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails: 多機能ベンチマークgem「derailed_benchmarks」README(翻訳)

Rails: rakeタスクをRubyオブジェクトで美しく保つ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。


whitefusion.ioより

記事で取り上げられているHerokuのReview Apps機能、よさそうですね。

Rails: rakeタスクをRubyオブジェクトで美しく保つ(翻訳)

DHHの「Writing Software Well」シリーズの精神に則り、実際に動いているproductionコードを用いてデモいたします。

私はDHHことDavid Heinemeier HanssonのYouTube動画シリーズ「On Writing Software Well」から大いに刺激を受けました。十分に時間をかけて現実世界のproductionコードをひと通り駆け抜けて、コードをそのように書いた理由やそこで発生するトレードオフ、そしてコードをさらに改善する方法についてみっちり議論するところを見るのは、掛け値なしにとても嬉しいことです。

今回は、インフラストラクチャの付属部品を最小限かつきれいに保つ方法について説明したいと思います。Railsで、コードがすぐに肥大化して構造化も満足に行われず、素直にテストできなくなりがちな部分と言えば、rakeタスクもそのひとつです。

それでは、最近私が顧客のプロジェクトで実際にリファクタリングしたrakeタスクを見ていくことにしましょう。私たちのところではHerokuのReview Apps機能を使っています。Review Appsは、GitHub上のどのプルリクからでも新しいアプリを動かせます。QAスペシャリストやプロダクトマネージャーはこれを使って特定のfeature branchの機能を本番とは別にチェックできるのでとても便利です。しかし、そこで実行するデプロイ後rakeタスクでは、正しいサブドメイン/SSL証明書、データの検索用インデックスなどを設定していたのですが、これがだんだんつらくなってきたのです。rakeタスクがコードでぎっしりになってしまっていたので、これはリファクタリングが必要というサインであると私には思えました。

訳注: Heroku Review AppsでPRごとに動作環境を作成せよ - Qiita — 参考

まずは改修前のコードから(デリケートな部分は少し変えてあります)。

namespace :heroku do
  desc "Run as the postdeploy script in heroku"
  task :setup do
    heroku_app_name = ENV['HEROKU_APP_NAME']
    begin
      new_domain = "#{ENV['HEROKU_APP_NAME']}.domain.com"

      # set up Heroku domain (or use existing one on a redeploy)
      heroku_domains = heroku.domain.list(heroku_app_name)
      domain_info = heroku_domains.find{|item| item['hostname'] == new_domain}
      if domain_info.nil?
        domain_info = heroku.domain.create(heroku_app_name, hostname: new_domain)
      end

      key = ENV['CLOUDFLARE_API_KEY']
      email = ENV['CLOUDFLARE_API_EMAIL']
      connection = Cloudflare.connect(key: key, email: email)
      zone = connection.zones.find_by_name("domain.com")

      # delete old dns records
      zone.dns_records.all.select{|item| item.record[:name] == new_domain}.each do |dns_record|
        dns_record.delete
      end

      response = zone.dns_records.post({
        type: "CNAME",
        name: new_domain,
        content: domain_info['cname'],
        ttl: 240,
        proxied: false
      }.to_json, content_type: 'application/json')

      # install SSL cert
      s3 = AWS::S3.new
      bucket = s3.buckets['theres_a_hole_in_the_bucket']
      crt_data = bucket.objects['__domain_com.crt'].read
      key_data = bucket.objects['__domain_com.key'].read
      if heroku.ssl_endpoint.list(heroku_app_name).length == 0
        heroku.ssl_endpoint.create(heroku_app_name, certificate_chain: crt_data, private_key: key_data)
      end

      sh "rake heroku:start_indexing"
    rescue => e
      output =  "** ERROR IN HEROKU RAKE **\n"
      output << "#{e.inspect}\n"
      output << e.backtrace.join("\n")
      puts output
    ensure
      heroku.app.update(heroku_app_name, maintenance: false)
    end
    puts "Postdeploy script complete"
  end

  def heroku
    @heroku ||= PlatformAPI.connect_oauth(ENV['HEROKU_PLATFORM_KEY'])
  end
end

ぐぬぬ、これを読みとおすのは骨が折れます。この時点のタスクがかなり長いのはもちろんのこと、実行するコードのブロック同士にもある種の依存関係が生じていて、軽く読み流すぐらいでは見極めが困難です。

それでは、このコードをどのようにリファクタリングしたかを説明しましょう。まずlibフォルダの下にHerokuReviewAppPostDeployという新しいクラスを作成し、各ブロックを個別のメソッドとしてこのクラスに切り出しました。この新しいオブジェクト内でも同じようなことを行っている(GitHubリポジトリへの接続やプルリクのブランチ名取得など)のにお気づきでしょうか。これによって、Jiraのチケット番号をレビュー対象アプリのサブドメインに置いています。リファクタリングの途中で要件が正しく見えてきたので、コードがさらなる肥大化を回避できたのはありがたいことでした。

完全なクラスは次のとおりです。

class HerokuReviewAppPostDeploy
  attr_accessor :heroku_app_name, :heroku_api

  def initialize(heroku_app_name)
    self.heroku_app_name = heroku_app_name
    self.heroku_api = PlatformAPI.connect_oauth(ENV['HEROKU_PLATFORM_KEY'])
  end

  def turn_on_maintenance_mode
    heroku_api.app.update(heroku_app_name, maintenance: true)
  end

  def turn_off_maintenance_mode
    heroku_api.app.update(heroku_app_name, maintenance: false)
  end

  def determine_subdomain
    new_subdomain = heroku_app_name
    pull_request_number = begin
      heroku_app_name.match(/pr-([0-9]+)/)[1]
    rescue NoMethodError; nil; end
    unless pull_request_number.nil?
      github_info = HTTParty.get('https://api.github.com/repos/organization/reponame/pulls/' + pull_request_number, basic_auth: {username: 'janedoe', password: ENV["GITHUB_API_KEY"]}).parsed_response
      if github_info["head"]
        branch = github_info["head"]["ref"]
        jira_id = begin
          branch.match(/WXYZ-([0-9]+)/)[1]
        rescue NoMethodError; nil; end
        unless jira_id.nil?
          new_subdomain = "#{heroku_app_name.match(/^([a-z]+)/)[1]}-wxyz-#{jira_id}"
        end
      end
    end
    new_subdomain
  end

  def determine_domain
    "#{determine_subdomain}.domain.com"
  end

  def setup_domain_on_heroku(new_domain)
    # set up Heroku domain (or use existing one on a redeploy)
    heroku_domains = heroku_api.domain.list(heroku_app_name)
    domain_info = heroku_domains.find{|item| item['hostname'] == new_domain}
    if domain_info.nil?
      heroku_api.domain.create(heroku_app_name, hostname: new_domain)
    else
      domain_info
    end
  end

  def setup_domain_on_cloudflare(new_domain, heroku_domain_info)
    key = ENV['CLOUDFLARE_API_KEY']
    email = ENV['CLOUDFLARE_API_EMAIL']
    connection = Cloudflare.connect(key: key, email: email)
    zone = connection.zones.find_by_name("domain.com")
    zone.dns_records.all.select{|item| item.record[:name] == new_domain}.each do |dns_record|
      dns_record.delete
    end
    response = zone.dns_records.post({
      type: "CNAME",
      name: new_domain,
      content: heroku_domain_info['cname'],
      ttl: 240,
      proxied: false
    }.to_json, content_type: 'application/json')
  end

  def setup_ssl_cert_on_heroku
    # install SSL cert
    s3 = AWS::S3.new
    bucket = s3.buckets['theres_a_hole_in_the_bucket']
    crt_data = bucket.objects['__domain_com.crt'].read
    key_data = bucket.objects['__domain_com.key'].read
    if heroku_api.ssl_endpoint.list(heroku_app_name).length == 0
      heroku_api.ssl_endpoint.create(heroku_app_name, certificate_chain: crt_data, private_key: key_data)
    end
  end
end

この新しいアプローチのおかげで、オブジェクトを使って細かな機能を単一目的のメソッドに切り分けられるようになりましたが、一部のメソッドでは他のメソッドで生成したデータを必要としているので、そうした変数をメソッドの引数として含めることができます(たとえば setup_domain_on_herokuに明示的にnew_domainを渡すなど)。

リファクタリング後のrakeタスクはどんな姿になったでしょうか?とても、とてもよくなりました。

namespace :heroku do
  desc "Run as the postdeploy script in heroku"
  task :setup do
    heroku_app_name = ENV['HEROKU_APP_NAME']
    post_deploy = HerokuReviewAppPostDeploy.new(heroku_app_name)
    begin
      post_deploy.turn_on_maintenance_mode
      new_domain = post_deploy.determine_domain
      heroku_domain_info = post_deploy.setup_domain_on_heroku(new_domain)
      post_deploy.setup_domain_on_cloudflare(new_domain, heroku_domain_info)
      post_deploy.setup_ssl_cert_on_heroku
      Rake::Task['db:migrate'].invoke
      sh "rake heroku:start_indexing"
    rescue => e
      output =  "** ERROR IN HEROKU RAKE **\n"
      output << "#{e.inspect}\n"
      output << e.backtrace.join("\n")
      puts output
    ensure
      post_deploy.turn_off_maintenance_mode
    end
    puts "Postdeploy script complete"
  end
end

レビューするアプリの設定を完了する処理を追うのに必要な個別の手順を追いやすくなり、あるメソッドから返される変数を設定して別のメソッドに渡すようにしたことで手順同士の依存関係がクリアになりました。さらにHerokuReviewAppPostDeployのメソッド名を、何を行っているのかが正確にわかるように素直に命名したことで、コードの説明に必要だったコメントを大きく削減できました。

この「単独のオブジェクトへの切り出し」テクニックは、アプリの他の部分での肥大化したコードにも適用できます。バックグラウンドジョブもよい例でしょう。私はSidekiqのワーカーを最小限に保っておくのが好きなので、多くの場合、単一のモデル上で単一のメソッドを呼び出せば事足りるようにしています。

本記事が、皆さまがproductionで実際に動かしているコードベースを改善するアイデアのヒントになれば幸いです。本シリーズの今後の記事にもどうぞご期待ください(元記事末尾のINTERSECTニュースレターでぜひご登録をお願いします)。

お知らせ

Intersectは、Whitefusion社が提供するJekyllベースのオープンなWebブログです。弊社についての詳細や弊社の目指すものについては会社概要をご覧ください。

関連記事

[Rails 5] rakeタスクがrailsコマンドでもできるようになった

リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)


週刊Railsウォッチ(20180413)RailsConf 2018、Form Objectの昔と今、rubyreferences.github.ioのドキュメントがスゴイ、GitHubが10歳に、ほか

$
0
0

こんにちは、hachi8833です。心を鬼にして記事数を抑え込みました。

若葉萌える季節のウォッチ、いってみましょう。今回はつっつき成分少なめです。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

⚓Rails: 今週の改修

以下、Rails 6向けのmasterから見繕いました。ドキュメントの修正が増えてます。


つっつきボイス: 「5.2が出たせいかコミットも一息ついた感じがします」「そういえば出てましたね(汗」

【速報】Rails 5.2.0正式リリース!Active Storage、Redis Cache Store、HTTP/2 Early Hintほか

⚓コレクションのキャッシュとHTTPキャッシュを併用するとエラー→修正

# activerecord/test/cases/relation/mutation_test.rb#L62
-    (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache, :skip_preloading]).each do |method|
+    (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache]).each do |method|
       test "##{method}!" do
         assert relation.public_send("#{method}!", :foo).equal?(relation)
         assert_equal :foo, relation.public_send("#{method}_value")
       end
     end

出たばかりの5.2_stablemasterの両方に入りました。

skip_preloading!のアサーションはイミュータブルであるはずが、コントローラでfresh_whenを使うとarelが定義されてしまっていることがある。
生成されたSQLにはskip_preloadingは効かないので、この値を設定するときはチェックをスキップして安全にすべき。
このプルリクでは、assert_immutabilityチェックを使うセッターを定義するのに使われるSINGLE_VALUE_METHODSからskip_preloadingを削除し、リレーションの属性アクセサに置き換えた。
同PRより

⚓dalli gemのエラー修正

# lib/rack/session/dalli.rb#L160
-      # Capture generate_sid's super so we can call it from generate_sid_with
-      alias_method :generate_sid_super, :generate_sid
-
-      def generate_sid
-        # no way to check env['rack.multithread'] here so fall back on
-        # Dalli::Client or ConnectionPool's internal mutex cf. our own
-        @pool.with {|dc| generate_sid_with(dc) }
-      end
-
       def generate_sid_with(dc)
         while true
-          sid = generate_sid_super
+          sid = generate_sid
           break sid unless dc.get(sid)
         end
       end

つっつきボイス: 「Railsではなくdalliっていうgemの修正でした」

その後Rails側の#5bca1f2でdalliがアップデートされたようです。

# Gemfile#L48
-gem "dalli", "< 2.7.7"
+gem "dalli"
 gem "listen", ">= 3.0.5", "< 3.2", require: false
 gem "libxml-ruby", platforms: :ruby
 gem "connection_pool", require: false


同リポジトリより

「dalliって何だろうと思って今回ちょっと下調べしたんですが、memcachedクライアントなんだそうです」「dalliはとってもメジャー: Rails + Memcachedだと大体出てきますね」「でdalliのリポジトリにあるこの超有名な絵↑のタイトルが「記憶の固執」(The Persistence of Memory)だからきっとそこから付けたにキマってる」「へぇぇ!そのまんまw」「固執ってこう、あまり馴染みない言葉: プログラマーだとpersistencyといえばたいてい永続化ですしね」「基本の意味は『頑固な汚れ』の頑固とか『こびりつく』みたいな感じかなー」

いつぞやウォッチで取り上げたsurrealist gem↓は、ダリはダリでもmemcachedとは関係なかったですね。


nesaulov/surrealistより

⚓APIモードにデフォルトでDefaultHeadersを含めた

# actionpack/lib/action_controller/base.rb#L232
       HttpAuthentication::Basic::ControllerMethods,
       HttpAuthentication::Digest::ControllerMethods,
       HttpAuthentication::Token::ControllerMethods,
+      DefaultHeaders,
...
-    def self.make_response!(request)
-      ActionDispatch::Response.create.tap do |res|
-        res.request = request
-      end
-    end
-
     ActiveSupport.run_load_hooks(:action_controller_base, self)
     ActiveSupport.run_load_hooks(:action_controller, self)
   end

つっつきボイス: 「APIモードってRailsをAPIサーバーとして動かすアレですよね?」「そうでしょうね」「Changelogに『デフォルトのヘッダー設定を、コントローラにインクルードされる個別のモジュールに移動した』とある↓」「どうやらリファクタリングの一環のようですね: ActionController::Baseにあったメソッドを引っこ抜いてモジュール化して、APIとかにもインクルードしたってことみたい」「おー、ActionController::Baseに置くと使いもしないところにも読み込まれて大雑把すぎるから、って感じですかね」

# actionpack/CHANGELOG.md
+*   Move default headers configuration into their own module that can be included in controllers.
+
+    *Kevin Deisz*

⚓direct_uploads#create responseからJSON rootを除外

# activestorage/app/controllers/active_storage/direct_uploads_controller.rb#L17
     def direct_upload_json(blob)
-      blob.as_json(methods: :signed_id).merge(direct_upload: {
+      blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
         url: blob.service_url_for_direct_upload,
         headers: blob.service_headers_for_direct_upload
       }) 

つっつきボイス: 「ActiveStorageですね」「テストコードを見るとblob↓がありますけど、レスポンスにまでblobを入れたくないよねという意図みたい」

# activestorage/test/controllers/direct_uploads_controller_test.rb#L133
+    @response.parsed_body.tap do |details|
+      assert_nil details["blob"]
+      assert_not_nil details["id"]
+    end

「これ↓を取りたいということですね」

参考: バイナリ・ラージ・オブジェクト - Wikipedia — blob

⚓changes_to_saveでの属性変更を防止

# activemodel/lib/active_model/attribute_mutation_tracker.rb#L23
       attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
         change = change_to_attribute(attr_name)
         if change
-          result[attr_name] = change
+          result.merge!(attr_name => change)
         end
       end
     end

つっつきボイス:HWIAって何でしたっけ…あ、HashWithIndifferentAccessか」「何つう略し方w」
HashWithIndifferentAccessって死ぬほどわかりづらいからなー: すごくよく間違えるし、名前も長いし」「そんなに手強いのか…RubyじゃなくてRailsのActiveSupportなんですよね: よく使われてるし、Rubyには取り入れないんでしょうかね?」「いやーどうかな…、Rubyには入れなさそうな気がするー…だって(Rubyの文字列やシンボルは)同じものではないから」

「ところで、今日社内勉強会で発表したGoby言語なら文字列とシンボルが同じクラスなんで大丈夫(`・∀・´)エッヘン!!」「そういえばCrystal言語はRubyの遅くなりそうな実装を取り除いた感じのもの』らしいんですけど、Goby言語の話を聞いててそれをちょっと思い出しました」「たしかTuring Complete FMの第5回のmrubyの回でそういう話があったので」「あ、Miuraさんでしたっけ?: 私もその回聞きました♥

Elixir言語はRubyとは似てないんでしたっけ?」「見た目はともかく中身は結構違ってた気がします: ElixirのフレームワークであるPhoenixはRailsに似てるとかじゃなかったでしたっけ?」「そのあたり今度もうちょっと追ってみようかな…」

「ところでPRの方ですが、これもresult[attr_name] = changeで配列をじかに書き換えてるやつですね: 先週のつっつきにも似たようなミューテーションあった」「ハッシュの配列って…?」[{ a: "a" }]のことかー!」「確かにそうなりますよね」「話逸れますが、文章とか翻訳でいつも気にしてるんですけど、所有の「の」って、『ナントカのナントカのナントカ』みたいになったときとか特に、意味が広がって誤解を生じることあるんですよね: 日本語でも英語でも」

参考

⚓テストでヘッドレスChromeのGPUをWindowsで無効化

# actionpack/lib/action_dispatch/system_testing/browser.rb#L33
         def headless_chrome_browser_options
           options = Selenium::WebDriver::Chrome::Options.new
           options.args << "--headless"
-          options.args << "--disable-gpu"
+          options.args << "--disable-gpu" if Gem.win_platform?

           options
         end

つっつきボイス: 「これはトリビアなやつっぽいですね」「"--disable-gpu"は割りと普通に書きますけど、コミットメッセージ↓見ると、そもそも他のプラットフォームのヘッドレスChromeでは当分これが不要だから、Windowsでだけ指定するってことみたい」

Per Chromium team this has not been necessary on other platforms for quite some time: https://bugs.chromium.org/p/chromium/issues/detail?id=737678#c1
IMHO, this should be backported to 5-2-stable
同PRより

⚓Rails

⚓書籍『Docker for Rails Developers』


同サイトより

まだベータ版だそうです。

  • はじめに
  • 開発
    • RubyをインストールせずにRubyを実行
    • Railsイメージの微調整
    • Docker Composeでアプリを宣言的に記述
    • アプリの先: Redisの追加
    • DB追加: Postgres
    • Docker化環境のテスト
    • gem管理の高度な戦略
    • ちょい困ってる点
  • productionに進める
    • productionの概要
    • クラスタの作成
    • アプリのデプロイとスケール

つっつきボイス: 「これは今日morimorihogeさんがSlackに流してくれた情報ですね」「いろいろ盛りだくさんな感じ…でも180ページだから多くはないですね」「Railsがわかってる人なら一気に読み下せそう」「20ドル弱か: どうしようかなー」

⚓Herokuの「Review Apps」


つっつきボイス: 「昨日の記事↓を訳してて知りました」「HerokuのReview Appsって前からあったような…?」「あ、ぐぐったら2016年からリリースされてた」
「今BPS社内のGitLabサーバーにもこれ的な機能を構築してもらってますよね」「言われてみれば」「CIに通ったらレビュー環境を自動構築してくれたりしますね」「それ記事にしましょうよ」

「このしくみ、内部ではSandStarって名前が付いてるんですけど、ちなみにこの名前でググるとけもふれが真っ先に出てきますよ」「やべ、私のカバーできてない分野w: サンドスターでぐぐってみると、何だかガンダムのミノフスキー粒子みたいな趣ですね」「はっは…」

「ところでsand starってヒトデなんですね」「あれ?ヒトデってstarfishだと思ってた」「sandstarの由来はBOTのthumbnail見て分かるとおり完全にけもフレですよねw」

Rails: rakeタスクをRubyオブジェクトで美しく保つ(翻訳)

参考: Heroku Review AppsでPRごとに動作環境を作成せよ - Qiita

⚓Railsの設計の悩みからForm Objectの今と昔

Railsを始めて一ヶ月半経ったので振り返りをする - ゆとり日記

とにかく設計に悩む事が多い。
特に最近悩んでいるのが「controllerがファットになった時の対処法」。Viewに渡す情報が多い画面を作っていると、あっという間にcontrollerの処理が膨らんでしまい、対処法で毎回悩んでいる。

上の記事で以下のTechRacho記事を引用いただいていたのでたどり着きました。『設計に悩む事が多い』のあたりが気になったので。

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)


つっつきボイス: 「『設計に悩む事が多い』のあたりが気になったので」「↑この『肥大化したモデル』記事、BPSに入社する前に読みましたよ」「そういえば訳したのだいぶ前でした」

「『肥大化したモデル』見てて思い出したんですけど、Form Objectって、この記事の頃に比べて必要性がちょっと薄れたような気がしないでもないんですよね」「というと?」「もともとForm Objectって、ビュー画面で昔のform_for(注: 今は非推奨)するときに、フォームとモデルが対応していないからそれをラップして属性を生やすのが主な使い方、みたいに自分は思ってるんですね: ほかにも使い方あるんでしょうけど」「ふむふむ」「でも今はform_withがあるじゃないですか、form_withなら属性が生えてなくてもちゃんとやってくれるんですよ」「ほー!」「だから少なくともForm Object使ってた理由の1つは消えることになるんですよね」「↓あー、form_withドキュメントのこのあたりですね」「ですです、form_withのそういうところってかなりいいと思うんですよ: 極端に言えばモデルなくたって使えるし、だから昔のform_forのときみたいな使い方だけならForm Objectいらないんじゃないかなって」

「あとForm ObjectってService Object的に使うこともできちゃうじゃないですか: で、2017年に誰かが”Form Objectは肥大化しやすい”って記事書いてたように肥大化の原因になったりとか」「なるほどー」「その記事どこにあったかな…Qiitaのアドベントカレンダーとかだった気がするけど見つからない…」

「Form Object、もちろんちゃんと設計して使うならいいんでしょうけど、取りあえず雑に置いとけみたいなレベルだったら使わなくていいんじゃないかなと思ったわけです」「おー!今の話、自分にはいろいろストーリーが見えてきてとてもうれしいです❤」「自分の解釈ですけどねーw」

モデルを複数使うフォームとかなら、Form Objectの出番ありそうですね。

「Form Object肥大化&役割が大きくなりがち問題は同意: ただ、複雑なリクエストを受けるフォーム(よくある「詳細検索」とか)において、acceptableなパラメータとかを一括で管理できる見通しの良さはありますよね」

「あとForm Object作るときに結構大事なのは、Controllerのparams生渡しだけを前提に書かないことかなと思ってます: params前提でやり過ぎるとHTTP Requestの受けでしか使えなくなっちゃうのでつらい」「Jobとかrake taskの中でも同じForm Objectを使えるようにしてたりすると有用」

[Rails 5.1] ‘form_with’ APIドキュメント完全翻訳

⚓dev.toの中の人がRailsConfに登壇

dev.toといえば超速で有名ですね。そういえばdev.toはRailsで構築されているのでした。


railsconf.comより


つっつきボイス: 「dev.toがRailsってのがスゴイですよね」「あちこちで記事になってますけど、あらゆる最適化を尽くしてるんでしょうね」

参考: はてなブックマーク - なぜ dev.to がこんなにも速く、こんなにも自分にとって感動的なのか

「ちなみにそのRailsConf、先日の5.2リリース速報記事でもちょっと紹介したんですけど、セッションの多さがヤバいです↓」「めちゃくちゃ多いじゃないですか!スロット7つあるし、到底見きれないですね…」「これとこれとこれだけ見るって心に誓うぐらいじゃないと能率が落ちそう」「amatsudaさんも登壇するのか」(以下延々)


railsconf.com/scheduleより

「…『Pairing: a guide to fruitful collaboration 🍓🍑🍐』ってペアプロの話っぽいですね」「またしても雑学ですが、fruitfulというと聖書を思い出しちゃうんですよ: 『産めよ増やせよ地に満ちよ』って確か英語だと『Be fruitful and multiply』だった」「へー、じゃ相当ポジティブな意味なんですね」「そんな感じです」(以下延々)

⚓Hanami 1.2リリース

たぶん花見の季節に出したかっただろうと思います。


つっつきボイス: 「HTTP/2 Early Hints、Hanamiにも入るんですね」「そういえばAaron Pattersonインタビューでearly hints知りました」「あれ?form_for↓?ww」「何かいいタイミングというか、歴史逆行してる?中身知りませんが」

<!--同記事より-->
<%=
  form_for :search, "/search", remote: true do
    # ...

    submit "Search"
  end
%>

「Hanamiって、何だか思ったよりずっとRailsの影響大きそう?」「わかりませんが、Railsから離れる方が難しいかも」「form_for使うぐらいだし、もっとRailsから一線を引いた感じなのかと思ってました」「自分も何となくSinatra寄りなのかなと思ってた」「db:migrateもあるみたいですし」

Hanamiフレームワークに寄せる私の想い(翻訳)

⚓Ruby trunkより

⚓Module#refine自身にrefineかけたら参照できなかった

# 同issueより
using Module.new {
  refine Module do
    def refine *;
      puts self
    end
    public :refine
  end
}
Object.refine # => NoMethodError

つっつきボイス: 「ヘ(゚д゚)ノ ナニコレ?まったくわかんないw」「refineできるけど呼べない…?」「bugなんですね」「さすがurabeさん」

⚓フランス語でこんにちは

bonjour,
chaque fois que je lance mailcatcher et que je me rends sur la page : 127.0.0.0:1080, mailcatcher stop et me rapporte un bug.
Ci-joint, un fichier txt du rapport de bug.
Très cordialement,
lflapy


つっつきボイス: 「珍しかったので」「これもフランス語分かんないすねー: 内容はうっすらわからんでもないけど」「Third Party’s Issueにトリアージされた」

⚓提案: ハッシュと配列にも+@-@が欲しい

I like .dup and .freeze, more than + and - on class String.


つっつきボイス: 「最初よくわかんなかったけど、上からしてdupfreezeを記号でやりたいってこと?」「Cのパッチ↓を見ると、+はたとえば配列がfrozenならdupして、frozenでなければ配列自身を返す、と」「あそういうことか」

 * call-seq:
 *   +ary  -> ary (mutable)
 *
 * If the array is frozen, then return duplicated mutable array.
 *
 * If the array is not frozen, then return the array itself.
 */
static VALUE
rb_ary_uplus(VALUE ary)
{
    if (OBJ_FROZEN(ary)) {
    return rb_ary_dup(ary);
    }
    else {
    return ary;
    }
}

「で-はその逆で、freezeしてたら自身を返してそうでなかったらコピーを返す」

/*
 * call-seq:
 *   -ary  -> ary (frozen)
 *
 * If the array is frozen, then return the array itself.
 *
 * If the array is not frozen, return a frozen copy of it.
 */
static VALUE
rb_ary_uminus(VALUE ary)
{
    if (OBJ_FROZEN(ary)) {
    return ary;
    }
    else {
    return rb_ary_freeze(ary);
    }
}

「この書き方は好きになれない…: perlみたいなぱっと見わかりにくい魔術的記号言語にRubyが向かっていく気がして」「ですね: 記号は限りある資源なのに」「ルーン文字のように力を持つ文字みたいな」

⚓Ruby

⚓Rubyのyield

# 同記事より
def one_yield
  yield
end
def multiple_yields
  yield
  yield
end
$> one_yield { puts "one yield" }
one yield
 => nil
$> multiple_yields { puts "multiple yields" }
multiple yields
multiple yields
 => nil

つっつきボイス: 「これはyieldの基本的な使い方の説明ですね」「長くないしすっと読めそうかな」

⚓smart_init: Rubyの初期化コードを簡略化するgem

いま翻訳中のWhy Service Objects are an Anti-Pattern — INTERSECTに出てきました。

def initialize(network_provider, api_token)
  @network_provider = network_provider
  @api_token = api_token
end

# 上を下のように書ける

class ApiClient
  extend SmartInit

  initialize_with :network_provider, :api_token
end

# またはサブクラス化

class ApiClient < SmartInit::Base
  initialize_with :network_provider, :api_token
end

つっつきボイス: 「★は少ないですが」「ふーむ、コンストラクタでそんなにいくつも引数を取らないといけないとしたら、そっちの方が問題かもね」「後、コンストラクタ回りのメカニズムを隠蔽するというのは自分的には何だかオカシイ気がする」「そもそもこのgem、ActiveRecordに乗らないでしょたぶん?」

「でもこのgemを作るのはとっても簡単だと思う: だからこれを使うというより、Ruby初心者がこういうgemを一度自分で書いてみるといいかもね」「なるほど、練習課題として」

「変なこと言いますけど、昔自分もRubyでこうやって引数とインスタンス変数で同じ名前を何度も書くのが何か冗長だな?って思ったことがあって、これ見てたらそれを思い出しちゃいました」「まぁそれがRubyの慣習ですから: Javaもそう」

⚓Rubyメタプログラミングのデバッグ(RubyFlowより)

# 同記事より
require 'csv'
class User < Struct.new(:email, :name)
  def to_csv
    [name, email]
  end
  def self.export(users)
    post_to_api(create_csv(users))
  end
  def self.create_csv(users)
    CSV.generate do |csv|
      csv << ["name", "email"]
      users.each do |user|
        csv << user.to_csv
      end
    end
  end
  def self.post_to_api(string)
    puts "sending to api"
  end
end


同記事より


つっつきボイス: 「メタプログラミングのデバッグぇえ」「ざざっと見てみると、アプリのデバッグというより、メタプログラミングするとどんな挙動になるかをirbで見てみよう、という感じっぽいですね」「class_evalはこんな感じになるんやで、みたいな」

⚓プライベートなRubyサーバーをGCPにデプロイ

Googleの日本語記事とgemです。


つっつきボイス: 「y-yagiさんのツイートですね」「いわゆる社内用gemサーバーなんかを速攻でGCPに立てられるってことですね」「すっげー」「SQLも使えたり」「プライベートgemサーバーのメンテそのものは面倒くさいけどw、今までみたいにメンテもせずgemサーバー立てるのもツライとかよりはずっとマシかもね」「このぐらいスッと立てられるならそうですね」

ドリコムさんとかgemサーバー立ててますよね?」「そうそう、大きい開発会社ならたいてい必要になってくるヤツ」「ドリコムさんはgemをどんどん書く文化って聞いたことありますね」「プライベートなのも公開しているのもあるだろうし」

参考: ドリコムで使ってるgem一覧 #railsdm - くりにっき

⚓🌟「The Ruby Reference」のドキュメントは凄い🌟


rubyreferences.github.ioより


つっつきボイス: 「ネットですごく話題になってますね: 基本的には公式ドキュメントの内容を持ってきて整理した感じですが、ビシっと整形されててすっごく読みやすい」「おーこれは確かによくできてる!」「いいわ〜❤」「で、ブログの方はそれを作ってくださったzverokさんですね: こんな感じで作ったみたいです↓」「エライな〜」

  • ソースリポジトリのRDocが主
  • ruby-lang.orgからも多少
  • 自分でもそこそこ記事書いて埋めた(Rubyのコメントの書き方とか公式ドキュメントにないので)
  • 上から下まですっと読み通せるように整形とかがんばった

「確かに2.5以外のもこうやって見られたらどんなにいいだろうと」「うんうん」

今週の🌟を進呈いたします。おめでとうございます。そういえばTechRachoでも何度かzverokさんの翻訳記事を掲載しました。

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

⚓Rubyの「クラスマクロ」と「クラスインスタンス変数」

# 元記事より
module XmlFormattable  
  def to_xml
    formatter.format
  end

  def render(file_name, object, options)
    formatter.render(file_name, object, options)
  end

  def formatter
    @formatter ||= XmlFormatter.new(self, "format type goes here!!")
  end
end  

# ダメな書き方
class Itunes::Album  
  include XMlFormattable
  XML_FORMAT = "itunes_version_9"
end

class Spotify::Album  
  include XmlFormattable
  XML_FORMAT = "spotify_version_7"
end  
class DDEX::Album  
  include XmlFormattable
  XML_FORMAT = "ddex_version_38"
end

つっつきボイス: 「ほうほう、確かに最初のダメなコード例はヒドイww」「どの辺がひどい感じ?」「だって定数を宣言してそれを書き換えてるだけだし: それ定数にする必要あるー?名前空間切りたいとかかもしれないけど」

「(眺めながら)あーだからクラスマクロなのか!なるほどね」「というと?」「普通ならこういうのはインスタンス変数のスコープなら継承とかmixinでやったりするけど、これはクラスの持ち物だから、マクロという呼び方で共通化しようとしてるんじゃないですかね」「そういえば記事の冒頭に『クラス変数とは、クラス定義時に1回だけ使われるクラスメソッド』ってありますね」「定数名がすべてコピペで同じなのはダサいからそこを何とかしようという趣旨みたい」

「ちなみにこういうの↓をまるっとやってくれるのがActiveSupportのconcernsなんですよね」「そうそう」「そうでしたか!」「要はただのクラスメソッドですけど」

# 同記事より
module XmlFormattable  
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def xml_format(name)
      # coming soon!
    end
  end

「で、それをクラス変数@@使ってやろうとしたらダメだった、さてどうしたらよいでしょうか?という流れですね」「うわーモジュールにクラス変数書いてるゾw」「どんなふうに動くんでしょうねw」

「最終的に、クラスインスタンス変数ならできたぜってことみたい」「謎のクラスインスタンス変数w」「クラスインスタンス変数って、名前につられがちでいつもややこしいっす…」(以下延々)

この記事訳してみようかと思います。

参考: 【まとめ】インスタンス変数、クラス変数、クラスインスタンス変数 - Qiita

⚓SQL

⚓postgresql-topn: 「トップN」問題解決を支援する拡張

先週のウォッチで取り上げたTopN for your Postgres databaseで紹介されていたので。いま翻訳中です。

-- 同記事より
-- Create a roll-up table to capture most popular products
CREATE TABLE popular_products
(
  review_date date UNIQUE,
  agg_data jsonb
);

-- Create different summaries by grouping top reviews for each date (day, month, year)
INSERT INTO popular_products
    SELECT review_date, topn_add_agg(product_id)
    FROM customer_reviews
    GROUP BY review_date;

⚓PostgreSQLのこのクエリが好きやねん(Postgres Weeklyより)

-- 同記事より
dvdrental=> WITH fake_month AS(
SELECT setup::date
FROM generate_series('2007-02-01', '2007-02-28', INTERVAL '1 day') AS setup
)
SELECT date_part('day', p.payment_date)::INT AS legit,
SUM(p.amount),
date_part('day', fk.setup)::INT AS fake
FROM payment AS p
LEFT JOIN fake_month AS fk
ON date_part('day', fk.setup)::INT = date_part('day', p.payment_date)::INT
GROUP BY legit, fake
HAVING SUM(p.amount) > 2000
LIMIT 10;
legit | sum | fake
-------+---------+------
1 | 2808.24 | 1
2 | 2550.05 | 2
6 | 2077.14 | 6
8 | 2227.84 | 8
9 | 2067.86 | 9
17 | 3630.33 | 17
18 | 3977.74 | 18
19 | 3908.59 | 19
20 | 3888.98 | 20
21 | 3786.14 | 21
(10 rows)

つっつきボイス: 「1.は『LEFT JOINなのに右のNULLをよしなにしたい』…?」右にNULL出したいからLEFT JOINなんじゃ?よくわからん」「INNER JOINじゃダメなのかな?」「GROUP BYしてるからNULLが邪魔っけとか?」(以下延々)「時間ないので次いきましょうー」

⚓PostgreSQLのデータ肥大化に立ち向かう(Postgres Weeklyより)

VACUUMも効かないような場合の手法だそうです。


つっつきボイス: 「VACUUMもダメだと相当大ごと」「字ちっちゃいけど、pgcompacttableとpg_repackを使うみたいですね」「ちなみに、postgresqlのVACUUMは雰囲気としては昔懐かしWindowsのデフラグみたいな機能なので、データそのものの肥大化対策にはならないと思って良いですね」「再配置・効率化がメインなので、compressionが目的の機能ではない」

⚓JavaScript

⚓Node.jsプロジェクト構造チュートリアル

Codeshipの記事です。

// 同記事より
'use strict'

// required environment variables
[
  'NODE_ENV',
  'PORT'
].forEach((name) => {
  if (!process.env[name]) {
    throw new Error(`Environment variable ${name} is missing`)
  }
})

const config = {
  env: process.env.NODE_ENV,
  logger: {
    level: process.env.LOG_LEVEL || 'info',
    enabled: process.env.BOOLEAN ? process.env.BOOLEAN.toLowerCase() === 'true' : false
  },
  server: {
    port: Number(process.env.PORT)
  }
  // ...
}

module.exports = config

⚓JavaScript「proxy」入門(JavaScript Weeklyより)


同記事より

⚓JavaScriptのcoercion(JavaScript Weeklyより)


つっつきボイス: 「む、このサイト、重い…」「さっきも重かったです」
「coercionってこういうやつなのか…↓」「えー!」「Ruby脳だとわけわからなくなりそう」

// 同記事より
'2' + 2 // 22
15 + '' // '15'
+'12'   // 12

[1] + [2] – [3] === 9になる理由は一番最後に書いてますね」「絶句」

⚓CSS/HTML/フロントエンド

⚓画像や動画の遅延読み込み

Googleの技術ブログです。

⚓PWA

これもGoogleのサイトです。

⚓MozillaのWebAssembly向けブラウザIDE


webassembly.studioより


つっつきボイス: 「先週のpaiza.ioもそうですけど、ブラウザでやるのが普通の世界になりつつありますね…」

参考: Mozillaが「WebAssembly Studio」発表。C/Rust/AssemblyScript対応のオンラインIDE - Publickey

⚓バズるかどうかを事前に予測ですって

参考: アドビがチラ見せした最新AI機能はまるで「未来」だ ── AIが判定するコンテンツの良し悪し:Adobe Summit 2018 | BUSINESS INSIDER JAPAN


つっつきボイス:BPSのデザインチームの人がアドビ先生凄い!ヤバいって言ってましたね」「今度教えてもらおうっと」

サイト: https://www.adobe.io/apis/cloudplatform/sensei.html

⚓ChromeとFirefoxの新しい認証標準「WebAuthn」(Frontend Focusより)


つっつきボイス: 「WebAuthnって、生体認証をブラウザでできるってことか…」「W3CでCandidate Recommendationになってる↓」

参考: Web Authentication: An API for accessing Public Key Credentials Level 1

⚓Go言語

⚓Go言語の詳細な内部解説

0x0000 TEXT     "".add(SB), NOSPLIT, $0-16
  0x0000 FUNCDATA   $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
  0x0000 FUNCDATA   $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x0000 MOVL       "".b+12(SP), AX
  0x0004 MOVL       "".a+8(SP), CX
  0x0008 ADDL       CX, AX
  0x000a MOVL       AX, "".~r2+16(SP)
  0x000e MOVB       $1, "".~r3+20(SP)
  0x0013 RET

0x0000 TEXT     "".main(SB), $24-0
  ;; ...omitted stack-split prologue...
  0x000f SUBQ       $24, SP
  0x0013 MOVQ       BP, 16(SP)
  0x0018 LEAQ       16(SP), BP
  0x001d FUNCDATA   $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d FUNCDATA   $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
  0x001d MOVQ       $137438953482, AX
  0x0027 MOVQ       AX, (SP)
  0x002b PCDATA     $0, $0
  0x002b CALL       "".add(SB)
  0x0030 MOVQ       16(SP), BP
  0x0035 ADDQ       $24, SP
  0x0039 RET
  ;; ...omitted stack-split epilogue...

つっつきボイス: 「これめちゃ凄いドキュメント!: Gobyの人たちも色めき立ってます」

⚓結城浩さんもすなるGo

参考: 動詞で『すなる』の意味を何方か教えてください。 jfjnc078 - 土佐日記の... - Yahoo!知恵袋

⚓その他

⚓GitHubが10歳に


つっつきボイス: 「まだ10歳なのに、ずっと前からあるような気がしてきました」「もうないと生きていけないヤツ」「そういえばGo言語がで生まれた直後ってGitHubあったかしら?(2009年でした): そんな時期にGitHubに全面的に依存したエコシステム作ったのってある意味英断かも」「Go言語の人たち、バクチ運強いんですよきっと」

⚓四畳半問題


つっつきボイス: 「あ、あ、本当にできないかも!」

⚓ゼロ幅スペースのチート増殖中

詳しくは以下の記事をどうぞ。

Unicodeで絶対知っておくべきセキュリティ5つの注意(翻訳)

⚓WiFiだけで倉庫ロボットを導入

他のインフラがいらないのは画期的かもです。

⚓番外

⚓ドラえもんにこんな道具あった気が

参考: 「現実より広いVR空間を歩ける技術」NVIDIAが発表 | Mogura VR - 国内外のVR/AR/MR最新情報

⚓よい仕事

⚓電磁誘導の最強感

⚓3Dプリンタで家を量産

ちなみにこの動画を見るきっかけになったVOA(Voice Of America)のLearning Englishサイトは英語の自学自習に最適で超おすすめです(オール無料)。何しろ米国の国家予算で運営されてますので、しっかり金かけてるのがよくわかります。動画にきっちり字幕が表示されているのがうれしい点で、私は毎朝これ見ながら5分間シャドウイングしてます。

参考: Wikipedia-ja ボイス・オブ・アメリカ


今週は以上です。

ツィートより

バックナンバー(2018年度)

週刊Railsウォッチ(20180406)ruby-sass gemが非推奨に、Roda gem、paiza.ioは便利、Linuxは/procで遊ぼうほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Focus

frontendfocus_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

Rails tips: Nullalign gemでデータベースのバリデーション漏れを検出(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: Nullalign gemでデータベースのバリデーション漏れを検出(翻訳)

指定されたカラムの値が常に存在することを確認したい場合、presenceバリデータを使うのが普通です。以下は、Userモデルのtitleカラムをバリデーションするシンプルな例です。

class Post < ActiveRecord::Base
  validates :title, presence: true
end

残念ですが、これでは不十分です。タイトルが空欄のままのpostが作成される可能性があるのです。ではどうやって回避すればよいのでしょうか。それには指定のカラムがnull値を持てないようデータベースレベルで確認します。マイグレーションファイルでnull属性を渡すことでこれを設定できます。

class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.string :title, null: false

      t.timestamps null: false
    end
  end
end

既存のアプリ

既存のアプリで作業していて、presenceバリデーションが設定されているものの、同じルールがデータベースレベルに設定されてないか所をすべて更新したい場合は、少しややこしくなります。コードベースが大規模になるとこの作業で相当時間を取られてしまいます。しかし実はそんなことをする必要はありません。

Tom Copeland作のnullalign gemを使えばよいのです。面倒な作業を肩代わりしてくれる、とても使いやすいツールです。さっそく使ってみましょう。

Gemfileにこのgemを追加します。

gem 'nullalign'

後はbundle installを実行すれば準備完了です。ルールはシンプルで、やるべきタスクは1つだけしかありません。bundle exec nullalignを実行すると、以下のような感じで出力されます。

There are presence validators that aren't backed by non-null constraints.
--------------------------------------------------------------------------------
Model              Table Columns
--------------------------------------------------------------------------------
Post               posts: title

このリストを見て、更新したいカラムを見繕います。最後に、カラムを更新するマイグレーションを作成します。

class UpdatePosts < ActiveRecord::Migration
  def change
    change_column :posts, :title, :string, null: false
  end
end

以上でおしまいです。nullalignリポジトリとTomのTwitterをチェックしてみてください。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails tips: カスタムバリデータクラスを作る(翻訳)

Rails tips: テストから共通機能を切り出すリファクタリング(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: テストから共通機能を切り出すリファクタリング(翻訳)

テストもコードなので、アプリのクラスと同じ要領でリファクタリングできます。さまざまなテストで同じコード片が必要になることはざらにあるので、同じコードを繰り返し書かずに済むと助かります。

たとえばコントローラのテストではよく次のような感じのコードを書きます。

get :some_action

expect(JSON.parse(response.body)).to eq({'key' => 'value'})

レスポンスをJSON形式で返すAPIコントローラがある場合のexpectationは普通こんな感じになることでしょう。ヘルパーを使ってこのテストをリファクタリングできます。まずspec/supportディレクトリでファイルを作成し、app/helpersディレクトリのファイルで書くときと同じ要領でシンプルなモジュールとメソッドを作成します。

module ControllerHelpers
  def json_response
    JSON.parse(response.body)
  end
end

このヘルパーをcontroller specでも使えるようにします。それにはspec_helper.rbファイルかrails_helper.rbファイルで次のようにヘルパーをrequireします。

require 'support/helpers/controller_helpers'

同じファイルで、このヘルパーメソッドをspecで使うために以下を書いてRSpecで認識できるようにします。

RSpec.configure do |config|
  ...
  config.include(ControllerHelpers, :type => :controller)
  ...
end

作成したヘルパーはcontroller specでしか使いたくないので、RSpecがヘルパーメソッドをインクルードしてよいspecの種類を指定します。使いたいヘルパーメソッドが多数ある場合は、以下のコードを使って一括読込できます。

Dir[Rails.root.join("spec/support/helpers/*.rb")].each {|f| require f}

リファクタリング

これで、json_responseヘルパーメソッドを使うようにexpectationを更新できます。

expect(json_response).to eq({'key' => 'value'})

以上でおしまいです。よいテストを書く方法もご覧ください。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails tips: RSpecの`let`ブロックや`before`ブロックは基本避けるべき(翻訳)

ActiveRecordのtouchを`no_touching`で一時的に無効にする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

ActiveRecordのtouchをno_touchingで一時的に無効にする(翻訳)

ActiveRecordモデルでのtouchは多くのRailsアプリで広く使われています。特にキャッシュの無効化で便利です。これはデフォルトでは現在時刻でupdated_atタイムスタンプを更新します。以下はモデルでtouchを使う典型例です。

# app/models/photo.rb

class Photo < ApplicationRecord
  belongs_to :user, touch: true
end

新しい写真が作成されたり既存の写真が更新/削除されるたびに、関連付けられているユーザーのupdated_at属性が現在時刻で更新されます。多くの場合これは期待どおりの動作です(これはActiveRecordの珍しいコールバックですが、別に悪いものではありません)が、何らかの理由でtouchしたくないこともあるでしょう。何かいい手はないものでしょうか?

問題の分析

touchを一時的に無効にするのは、パフォーマンス上の理由(大量のレコードを更新するとき)や、after_touchとかafter_commitが何度も実行されないようにするうえで便利です。しかし後者には設計上の問題が潜んでいる可能性があります。というのも、レコードの内部状態を上書きする副作用を引き起こす重要なロジックをActiveRecordのコールバックに配置すると、(特にメール通知をトリガするときに)簡単に地獄を見ることができます。しかし現実には多くのRailsアプリでコールバックがこのように使われてしまっています。

解決方法

ありがたいことに、ごついリファクタリングも書き直しも不要です。代わりに、ブロック内で一時的にtouchを無効にするActiveRecord.no_touchingを利用できます。

あるユーザーに属するすべての写真を更新する必要があるとしましょう。そしてすべての写真が更新されてからtouchする必要があるとしましょう。以下のようにできます。

user = User.find(user_id)

ActiveRecord::Base.transaction do
  User.no_touching do
    user.photos.find_each do |photo|
      # userは`touch`されない
      photo.update!(some_attributes)
    end
  end

  user.touch
end

何らかの理由で全モデルのtouchを無効にしたいのであれば、このメソッドをActiveRecord::Baseで呼べば終わりです。

user = User.find(user_id)

ActiveRecord::Base.transaction do
  ActiveRecord::Base.no_touching do
    user.photos.find_each do |photo|
      # どのモデルも`touch`されなくなる
      photo.update!(some_attributes)
    end
  end

  user.touch
end

できあがりです!

まとめ

ActiveRecord.no_touchingは、トリッキーな問題が潜む可能性のある問題をまさにお手軽に解決してくれます。しかしこれはアプリの設計に潜む問題に対するダーティハックでもあり、その問題は遅かれ早かれ正すべきです。

追記

ActiveRecord.no_touchingのソースはたった1行でした。

# File activerecord/lib/active_record/no_touching.rb, line 22
def no_touching(&block)
  NoTouching.apply_to(self, &block)
end

apply_toは以下でした。

# https://github.com/rails/rails/blob/375a4143cf5caeb6159b338be824903edfd62836/activerecord/lib/active_record/no_touching.rb#L28

    class << self
      def apply_to(klass) #:nodoc:
        klasses.push(klass)
        yield
      ensure
        klasses.pop
      end
...

関連記事

Rails: スコープをモデルの外でチェインするのはやめておけ(翻訳)

Railsのdefault_scopeは使うな、絶対(翻訳)

Rails tips: ActionMailerのstaging向けインターセプターを作る(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: ActionMailerのstaging向けインターセプターを作る(翻訳)

インターセプター(intercept: 途中を捉える)とは、モデルのbefore_saveフックに似たフックの一種です。メールメッセージを配信前に編集したい場合にこのフックがぴったりです。staging環境で、すべてのメールをstaging@yourapp.comのメールボックスの受信箱に送りたいとしましょう。

インターセプターを作る

私はインターセプターをapp/interceptorsディレクトリの下に置いて*_interceptor.rbという命名パターンを用いるのが好みです。ここでは、StagingEmailInterceptorという名前のクラスで保存します。

class StagingEmailInterceptor
  def self.delivering_email(message)
    message.to = ['staging@yourapp.com']
  end
end

インターセプターを登録する

次の手順はインターセプターの登録です。config/initializers/ディレクトリにstaging_email_interceptor.rbという新しいイニシャライザを作成し、これを使いたいことをActionMailerに認識させます。

if Rails.env.staging?
  ActionMailer::Base.register_interceptor(StagingEmailInterceptor)
end

これでstaging環境でテストメールを安心して送れるようになりました。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

【Rails】ActionMailerで突如エラーが起こったら

Viewing all 1381 articles
Browse latest View live