Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1838 articles
Browse latest View live

あまり知られてないRuby/Railsの便利メソッド5つ(翻訳)

$
0
0

概要

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

あまり知られてないRuby/Railsの便利メソッド5つ(翻訳)

私がRuby on Railsで開発するようになってかれこれ数年が経過しましたが、Ruby世界で何か新しい発見があるたびに感動があります。それこそがRubyであり、Rubyは開発者を幸せにするのが目的です(他にもいろいろ法則はありますが、とりわけ)。Rubyを使っていると、毎年何か新しいものを見つけるたびにそのことを感じます。

本記事では最近私が発見したものをいろいろご紹介いたします。これらはめったに使われていませんし、使わなければいけないものでもありません。ほとんどは「シンタックスシュガー」ですが、いずれにしろコードがとても明確になります。一部は最近のRubyやRailsの新機能です。

1. Hash#dig

Rails開発を7年続けていますが、つい最近初めてこれを見かけたときに当然「これは何?」と思いました。私がRubyで開発を始めた頃は1.8だったのですが、この機能は2.3で導入されました。

次のようなコードを何回書いたか覚えていますか。

... if params[:user] && params[:user][:address] && params[:user][:address][:somewhere_deep]

digはsafe navigation演算子(ぼっち演算子)&.の一種ですが、Hashオブジェクトで使えます。これを使えば上のコードは次のように書き直せます。

... if params.dig(:user, :address, :somewhere_deep)

2. Object#presence_in

これは、Query Objects in Ruby on Railsというお気に入りの記事を書いたときに見つけました。存在チェックの結果(論理値)が欲しいのではなく、チェックされたオブジェクトそのものが欲しい場合に、条件(多くは三項演算子)を1つのメソッドで書けます。次のコードをご覧ください。

sort_options = [:by_date, :by_title, :by_author]
...

sort = sort_options.include?(params[:sort])
  ? params[:sort]
  : :by_date

# もうひとつの方法
sort = (sort_options.include?(params[:sort]) && params[:sort]) || :by_date

上のコードを下のように書き直せまず。ずっとよくなりましたよね?

params[:sort].presence_in(sort_options) || :by_date

3. Module#alias_attribute

この便利さに気づいたのは、ある案件でレガシーなデータベースを扱っているときでした。そのデータベースのあるテーブルでは、SERNUM_0ITMDES1_0といった気持ち悪いカラム名が大量に使われていました。このテーブルをActiveRecordモデルにマップするときに、WeirdTable.where(SERNUM_0: '123')のようなクエリやスコープを書く代わりに、#alias_attributeを使うことにしました。このメソッドのよさは、述語メソッドやゲッターやセッターを生成するだけではなく、クエリで次のように使える点です。

alias_attribute :name, :ITMDES1_0
...
scope :by_name, -> (name) { where(name: name) }

4. Object#presence

このメソッドは他のものよりは知られています。ApiDockにわかりやすい説明があります。object.presenceは次と同等です。

object.present? ? object : nil

5. Module#delegate

理由はわかりませんが、このメソッドを使っている開発者をめったに見かけません。このメソッドの主な目的は、結合を弱めてデメテルの法則に沿うようにすることです。デメテルの法則についての良記事はいろいろありますが、その中でもAvdi Grimm氏の「デメテルは単なるよいアイデアではない: 法則である」が思い出されます。Rails Best Practicesの記事でも、デメテルの法則を適用するときに#delegateを使っているものがありますのでご覧ください。以下のスニペットでもおわかりいただけます。

class Profile < ApplicationRecord
  belongs_to :user

  delegate :email, to: :user
end

...
profile.email # profile.user.emailと同等

本記事でご紹介したヒントがお役に立てば幸いです。楽しくコーディングしましょう!

本記事を気に入っていただいた方は、ぜひ元記事の[✋]をクリックして応援してください。

関連記事

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

3年以上かけて培ったRails開発のコツ集大成(翻訳)


メモリを意識したRubyプログラミング(翻訳)

$
0
0

概要

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

メモリを意識したRubyプログラミング(翻訳)

— メモリ使用量を節約する方法と戦略

Rubyのプログラミングでメモリが大量に消費されるのは当たり前で、避けられないと考えている人がたくさんいますが、メモリ使用量を削減するさまざまな方法や戦略が利用できます。本記事ではその中からいくつかをご紹介いたします。

Rubyの内部を常に意識する

TrueClassFalseClassNilClassIntegerFloatSymbolStringArrayHashStructといったRubyの主要なビルトインクラスは、実行パフォーマンスやメモリ使用量について高度に最適化されています。なお、ここではCRuby(MRI)を扱いますので、その他のRuby実装についてはほとんど該当しないと思われます。

Rubyの各オブジェクトへの参照は、内部(Cコード内など)ではVALUE型を経由します。これは、必要な情報をすべて含むCの構造体へのポインタです。

以下のすべての数値は64ビットLinuxプラットフォームで有効ですが、その他の64ビットシステムにも適用されるはずです。

niltruefalse、そして一部のInteger

クラスによっては、オブジェクト作成時にCの構造体へのメモリ割り当てが不要なものがあります。これは、オブジェクトがVALUEで直接表現されているためですNilClass型(nil値など)、TrueClass型(true値など)、FalseClassfalse値など)のオブジェクトがこれに該当します。

-262 〜262-1の範囲に収まる小さな整数も、同様にVALUE値で表されます。

これはどういうことなのでしょうか。これらのオブジェクトは最小限のメモリで表現できるため、これらの値を用いるときにメモリを気にする必要はないということです。

オブジェクトで使われているメモリ量を返すObjectSpace.memsize_ofメソッドを使ってこのことを確認できます。

2.4.2 > require 'objspace'
 => true
2.4.2 > ObjectSpace.memsize_of(nil)
 => 0
2.4.2 > ObjectSpace.memsize_of(true)
 => 0
2.4.2 > ObjectSpace.memsize_of(false)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62-1)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62)
 => 40

結果からわかるように、整数の値が大きくなりすぎた最後の例を除いてメモリはまったく追加されていません。VALUE構造体が必要になると、少なくとも40バイトのメモリがオブジェクトで使われます。

ArrayStructHashString

これらの4クラスのオブジェクトは一般的な方法ではなく特別(に用意された専用の)C構造体を使います。値によっては、追加メモリを割り当てずにこれらの構造体に保存できます。

Arrayの要素が3つ以内であればメモリ効率が向上します。これを超えてしまうと、要素1つにつき8バイトの追加メモリが必要になります。

2.4.2 > ObjectSpace.memsize_of([])
 => 40
2.4.2 > ObjectSpace.memsize_of([1])
 => 40
2.4.2 > ObjectSpace.memsize_of([1, 2])
 => 40
2.4.2 > ObjectSpace.memsize_of([1, 2, 3])
 => 40
2.4.2 > ObjectSpace.memsize_of([1, 2, 3, 4])
 => 72

Structも同様に、要素3つ以内ならメモリ効率が向上します。この場合Structのメモリは40バイトで済みます。

2.4.2 > X = Struct.new(:a, :b, :c)
 => X
2.4.2 > Y = Struct.new(:a, :b, :c, :d)
 => Y
2.4.2 > ObjectSpace.memsize_of(X.new)
 => 40
2.4.2 > ObjectSpace.memsize_of(Y.new)
 => 72

Hashの場合はやや異なります。もっとも重要なのは要素をまったく含まないハッシュは最小の40バイトで済む点です(したがってデフォルト値などで大きなペナルティは生じません)。

2.4.2 :044 > ObjectSpace.memsize_of({})
 => 40
2.4.2 :045 > ObjectSpace.memsize_of({a: 1})
 => 192
2.4.2 :046 > ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4})
 => 192
2.4.2 :047 > ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4, e: 5})
 => 288

上でわかるように、ハッシュの要素が4つ以内であれば192バイトで済みます。これが、空でないハッシュで必要な最小メモリサイズです。

最後のStringでは、23バイトまでの文字列なら文字列オブジェクトを表すRString構造体に直接保存できます。

2.4.2 :062 > ObjectSpace.memsize_of("")
 => 40
2.4.2 :063 > ObjectSpace.memsize_of("a"*23)
 => 40
2.4.2 :064 > ObjectSpace.memsize_of("a"*24)
 => 65

この知識は何の役に立つのでしょうか。このような制限に厳密に沿って設計しなければならないということではありませんが、最適な実装方法を選ぶときの決め手になることがあります。

普段使いのオブジェクト

あらゆる「普通の」オブジェクト(特殊なC構造体を使わないものなど)については、一般的なRObject構造体が使われます。これではメモリを意識しようがないと思われそうですが、そんなことはありません。この構造体にも「メモリ効率の高い」モードがあるのです。

arrayを使用する場合、arrayのメモリはエントリ(を指すVALUEポインタ)を保存するのに使われます。同様に、stringを使う場合、stringのメモリはstringを構成するバイトを保存するのに使われます。では一般的なオブジェクトの場合、メモリはどんな目的に使われるのでしょうか。インスタンス変数です。

インスタンス変数の値はそのオブジェクトのそばに保存されますが、インスタンス変数の名前は、関連するクラスオブジェクトのそばに保存されます(通常、1つのクラスからインスタンス化されたオブジェクトはどれも同じ名前のインスタンス変数を持つため)。
arrayの場合と同様、あるオブジェクトのインスタンス変数が3つ以内であればメモリは40バイトで済みますが、インスタンス変数が4つや5つの場合は80バイトになります。

2.4.2 > class X; def initialize(c); c.times {|i| instance_variable_set(:"@i#{i}", i)}; end; end
 => :initialize
2.4.2 :064 > ObjectSpace.memsize_of(X.new(0))
 => 40
2.4.2 :065 > ObjectSpace.memsize_of(X.new(1))
 => 40
2.4.2 :066 > ObjectSpace.memsize_of(X.new(2))
 => 40
2.4.2 :067 > ObjectSpace.memsize_of(X.new(3))
 => 40
2.4.2 :068 > ObjectSpace.memsize_of(X.new(4))
 => 80
2.4.2 :069 > ObjectSpace.memsize_of(X.new(5))
 => 80
2.4.2 :070 > ObjectSpace.memsize_of(X.new(6))
 => 96

戦略

クラスやシステムの設計

多くのオブジェクトを作成する必要のないアプリやライブラリの開発であれば、そこまでメモリを意識する必要はありません。しかし、オブジェクトを多数作成する必要があるなら、クラスやクラス間のやりとりを設計するときに本記事の情報を頭に置いておくとよいでしょう。

次の例で考えてみましょう。CSSのmargin値を表すクラスの作成が必要になったとします。CSSの仕様に基づき、値を1つから4つまで持てるようにするにはどうすればよいでしょうか。

  • 単にarrayを使う方法が考えられます。この方法では抽象化が不十分ですが、メモリは40バイトまたは72バイト(値が4つの場合)に収まるのでメモリに優しくなります。
  • ただし、実際にはほとんどのarrayメソッドはほとんど適用できないため利用に向いていないため、このarrayは別のクラスにラップすべきです。このクラスのオブジェクトは、arrayのサイズに応じて80バイトまたは112バイトのメモリを使います。

  • 初期化時に4つの値をインスタンス変数に保存するクラスを作成する方法も考えられます。この場合、オブジェクトは常に80バイトのメモリを使います。

  • 最後は、メンバを4つ持つStructをクラスの代わりに使う方法です。この場合オブジェクトのメモリは72バイトで済みます。

この例は無理やりかもしれませんが、次の2つの点がよく示されています。1つ目は、抽象化を犠牲にしてもよいのであれば、ビルトイン型を使うのが最善のメモリ節約方法であるということです。2つ目は、Rubyの内部を意識したクラス設計はメモリ使用量の節約に役立つということです(上の例では純粋なクラスの代わりにStructを使うことでオブジェクトごとのメモリを10%節約できました)。

オブジェクトの再利用

メモリ節約のもうひとつの方法は、可能な場合にオブジェクトを再利用することです。イミュータブルなオブジェクトなら再利用は簡単ですが、イミュータブルでないオブジェクトにも適用可能です。

オブジェクト再利用の典型例として、GUIテキストエディタを考えてみます。このテキストエディタでは、文字ごとの利用可能なビジュアル表示(グリフ)情報が必要です。このグリフ情報をキャッシュして再利用すれば、多くの場所から参照されてもグリフごとにインスタンスを1つ作るだけで済みます。

他の例としては、文字列のフリーズや複製防止があります。これは個別に指定することも、frozen_string_literal: trueマジックコメントを書いたRubyのソースファイル内でグローバルに指定することもできます。これによってインタプリタで文字列の複製が防止され、メモリ使用量を削減できます。Ruby 2.5からは、String#-@の結果を使って(-strなど)文字列の複製を防止することもできます。

メソッドやアルゴリズムの適切な利用

メモリ節約のための最善の方法は、追加オブジェクトを一切割り当てないことです。たとえば、あるarrayに含まれる各値をmapしなければならない場合、Array#mapArray#map!のいずれかを使えます。前者はarrayを新しく作るのに対し、後者はarrayを直接変更する点が異なります。後者は多くの場合、他の部分を書き換えずに簡単にコードに導入できます。もしArray#mapなどの変換メソッドが使われているホットスポットがあれば、メモリ効率の高い別のメソッドに置き換えられるかどうかを検討しましょう。

適切なアルゴリズムの選択も、メモリ使用量を大きく削減するうえで有効です。たとえば、暗号化されたPDFファイルをHexaPDF gemで変更する場合、暗号解除と再暗号化についてはデータストリームが不要になることがあります。その点を見極められれば、入力データストリームを単に出力ファイルにコピーすることでメモリ使用量を削減でき、処理速度も向上します。この方法で暗号化ファイルを最適化すると、HexaPDFでC++ライブラリよりもメモリが少なくて済みます

メモリ使用量を測定する

プログラムのメモリ割り当てを調べるgemはいろいろあります。もっともよく使われるgemは、allocation_tracermemory_profilerです。

どちらのgemも、プログラム全体を測定することも、プログラムの特定部分でオン/オフしてその箇所だけを測定することもできます。これらの方法でプログラムのホットスポットを特定し、その情報に対して操作を行えます。たとえば、私たちが数年前にkramdownというgemを開発したとき、破棄した文字列が大量にHTMLコンバーターのクラスに割り当てられていることに気づきました。改良された別のkramdownでこのホットスポットを変更したことで速度が向上し、メモリ使用量も削減できました。

これら2つのgemを初めて使う方のために、2とおりのファイルを用意しました。これらは、Rubyバイナリの-rスイッチでプリロードすることが前提です(ruby -I. -ralloc_tracer myscript.rbなど)。

BEGIN {
  require 'allocation_tracer'
  ObjectSpace::AllocationTracer.setup(%i{path line type})
  ObjectSpace::AllocationTracer.trace
}

END {
  require 'pp'
  results = ObjectSpace::AllocationTracer.stop
  results.reject {|k, v| v[0] < 10}.sort_by{|k, v| [v[0], k[0]]}.each do |k, v|
    puts "#{k[0]}:#{k[1]} - #{k[2]} - #{v[0]}"
  end
  puts "Sum: " + results.inject(0) {|sum, (k,v)| sum + v[0]}.to_s
  pp ObjectSpace::AllocationTracer.allocated_count_table
  pp :total => ObjectSpace::AllocationTracer.allocated_count_table.values.inject(:+)
}
BEGIN {
  require 'memory_profiler'
  MemoryProfiler.start
}

END {
  report = MemoryProfiler.stop
  report.pretty_print
}

この2つのファイルを使えば、プログラムを変更せずにメモリ使用量をプロファイリングできます。

まとめ

Rubyのライブラリやアプリ開発でメモリ使用量を削減する方法はたくさんあります。Rubyインタプリタ内部を少し知っておけば、Rubyのメモリ利用の仕組みと、その活用方法の理解に役立ちます。
また、Rubyコアメソッドがパフォーマンスやメモリに与える影響を知っておけば、適切なメソッドを選択するときに役立ちます。

関連記事

Rubyのヒープをビジュアル表示する(翻訳)

Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

[インタビュー] Aaron Patterson(後編): Rack 2、HTTP/2、セキュリティ、WebAssembly、後進へのアドバイス(翻訳)

週刊Railsウォッチ(20171201)JSON PatchでRails高速化、Knapsack Proでテスト高速化、Decorator/Presenter gem比較ほか

$
0
0

こんにちは、hachi8833です。マカーな皆さまはHigh Sierraアップデートお済みでしょうか。

続報: macOS High Sierra脆弱性パッチに別の不具合、ファイル共有できなくなる恐れ。ただし修正は簡単

12月最初のウォッチ、いってみましょう。

Rails: 今週の改修

MemCacheStoreでexpiringカウンタに機能追加

expires_in: [seconds]#increment#decrementオプションが追加されました。
Rails.cache.increment("my_counter", 1, expires_in: 2.minutes)のように使えます。

# activesupport/lib/active_support/cache/mem_cache_store.rb#125
        options = merged_options(options)
         instrument(:increment, name, amount: amount) do
           rescue_error_with nil do
-            @data.incr(normalize_key(name, options), amount)
+            @data.incr(normalize_key(name, options), amount, options[:expires_in])
           end
         end
       end

アイドリング中のDB接続を解除

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#959
+      # Disconnects all currently idle connections.
+      #
+      # See ConnectionPool#flush! for details.
+      def flush_idle_connections!
+        connection_pool_list.each(&:flush!)
+      end

つっつきボイス: 「これまではとりあえずconfigで設定したpool数分だけ常にDBのconnction poolを確保していたのが、idle_timeoutの間使われていないconnectionは切断するようになったということかな」「大規模なシステムだと影響ありそう: abstractの中で定義されているので、ほぼすべてのDBMSに影響するのかも」

ActiveRecordのDB接続のfork周りを改善

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb より
+      # Discards all connections in the pool (even if they're currently
+      # leased!), along with the pool itself. Any further interaction with the
+      # pool (except #spec and #schema_cache) is undefined.
+      #
+      # See AbstractAdapter#discard!
+      def discard! # :nodoc:
+        synchronize do
+          return if @connections.nil? # already discarded
+          @connections.each do |conn|
+            conn.discard!
+          end
+          @connections = @available = @thread_cached_conns = nil
+        end
+      end

つっつきボイス: 「これは上の#31221に関連する修正のようだ」「最近DBアダプタ周りの改修が目につきますね」

form_withヘルパーでデフォルトでidを生成するよう変更

configでid生成を無効にすることもできるそうです。

# actionview/lib/action_view/helpers/form_helper.rb#1676
      def initialize(object_name, object, template, options)
         @nested_child_index = {}
         @object_name, @object, @template, @options = object_name, object, template, options
-        @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {}
+        @default_options = @options ? @options.slice(:index, :namespace, :allow_method_names_outside_object) : {}

         convert_to_legacy_options(@options)

つっつきボイス: 「あ…これform_forform_withに代えたときにはまった気がする」「今度からはデフォルトでidが有効ですね」

ActiveRecord::RecordNotFoundに引数を追加

# activerecord/lib/active_record/associations/collection_association.rb#79
       def find(*args)
         if options[:inverse_of] && loaded?
           args_flatten = args.flatten
-          raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+          model = scope.klass
+
+          if args_flatten.blank?
+            error_message = "Couldn't find #{model.name} without an ID"
+            raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
+          end
+

つっつきボイス: 「これはデバッグでありがたい: どのidで失敗したかがわかるのは親切」
「その代わり今後はこういう雑なコード↓を書くとリクエストidがお漏らししてしまいますけどね」

raise => e
  render :index, alert: e.inspect

variable_size_secure_compareをpublicに変更

publicに変更してもSHA256ダイジェストの長さは暴露されないからという理由です。長さを手がかりにした攻撃方法があった気がします。

# actionpack/lib/action_controller/metal/http_authentication.rb#70
             before_action(options.except(:name, :password, :realm)) do
               authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
                 # This comparison uses & so that it doesn't short circuit and
-                # uses `variable_size_secure_compare` so that length information
+                # uses `secure_compare` so that length information
                 # isn't leaked.
-                ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
-                  ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
+                ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
+                  ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
               end
             end
           end

つっつきボイス: 「長さを手がかりにした攻撃って何て名前だったかなー」「んーと」「そうだ、timing attack

以下の記事によると、アプリの応答時間がパスワード比較の前方一致に比例してしまうとパスワードが推測されやすくなってしまうので、それを防ぐためにRails 4.2.5.1でvariable_size_secure_compareが導入されたという経緯でした。

参考: Rails CVE-2015-7576 で見る タイミングアタック(Timing Attack)

既存の認証情報をデフォルトで上書きしないよう修正

昨日mergeされていました。

# railties/lib/rails/generators/rails/credentials/credentials_generator.rb#8
   module Generators
     class CredentialsGenerator < Base
       def add_credentials_file
-        unless credentials.exist?
+        unless credentials.content_path.exist?
           template = credentials_template

           say "Adding #{credentials.content_path} to store encrypted credentials."
...
       def add_credentials_file_silently(template = nil)
-        credentials.write(credentials_template)
+        unless credentials.content_path.exist?
+          credentials.write(credentials_template)
+        end
       end

つっつきボイス: 「うっぷ、これは普通にバグ」「y-yagiさんが秒殺で修正」

Rails

Decorator/Presenter gem 6種を比較(Awesome Rubyより)

以下の6つを比較していますが、他にもあるそうです。Lulalalaは手作りなようです。

  • ActiveDecorator
  • Draper
  • Oprah
  • Display-case
  • Lulalala Presenter
  • RailsCasts

つっつきボイス: 「RailsCast?と思ったらgemじゃなくて本当にRailsCastsだった」「↓この図、DecoratorとPresenterの違いがよくわかってとてもいい」「Presenterは機能を絞り込んでる感じ」「Decoratorは強力な分ビューで無茶なことできちゃったりする」


lulalala.logdown.comより

Rails 5.2を待たずにActiveStorageを使ってみる(Ruby Weeklyより)

すぐ使える手順です。

# 同記事より
Rails.application.routes.draw do
  resources :posts
end

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

JSON-PatchでRailsのパフォーマンスを向上(Ruby Weeklyより)

見出しから単にJSONにパッチを当てるのかと思ったら、RFC6902: JSON Patchを元に実装したfast-JSON-Patchというnpmパッケージでした。hanaというgemで導入しています。

// 同記事より
import { compare as jsonPatchCompare } from 'fast-json-patch'

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields_patch = jsonPatchCompare(
    previousTemplate.fields.toJS(), template.fields.toJS())
}

つっつきボイス: 「おー、JSONを毎回まるごと投げる代わりに差分だけを投げるのか」

ブラウザごとのsession cookie上限を調べてみた(Ruby Weeklyより)

Rack::Protection::MaximumCookieというRackミドルウェアの利用を勧めています。

# mwpastore/rack-protection-maximum_cookie より
use Rack::Protection::MaximumCookie

つっつきボイス: 「巨大なsession cookie食わせると普通にぶっ壊れますね」「クライアント側で対処しないといけなくなるとつらいです」

Amazon API GatewayでRuby SDK生成をサポート(Ruby Weeklyより)

aws.amazon.comより

# 同記事より
gem install pet-sdk-1.0.0.gem

つっつきボイス: 「おー、gemとして生成してくれるのがよさそう」「そういえば昔SOAPとかでこういうSDK生成的なのが流行った気がする: Java界隈とかで特に」

RailsEventStore.orgで監査ログを無料で取得


railseventstore.orgより

RailsEventStore.orgはオープンソースをベースにしています。


つっつきボイス: 「これ使いみちあります?」「悪くなさそう: 某社案件で使いたかったなこれ」「pub/sub好きな人にはいいかも」「Auditingは大体以下の2つをやりたいケースが多い:」

  • データに対する参照や変更ログを自動で網羅的に取りたい
  • 特定の操作に対して手動で特別なログを出したい

「前者はARのModelに対してであればpaper_trailとかがメジャーで、良い」「後者は正直あんまり決め手がないというかLoggerでよくね?という話になったりする」


後でRailsのPublish/Subscribeについて検索したところ、The Publish-Subscribe Pattern on Rails: An Implementation Tutorialという記事でwisperというgemがあるのを知りました。RailsのActionCableにもPub/Subがありますね。

Knapsack Pro: 複数のCIノードにテストを分散するサービス


knapsackpro.comより


つっつきボイス: 「Knapsack Pro、以下がとりあえずわかりやすかった」

参考: CircleCI + KnapsackProでRailsのテストを高速化させる

comfortable-mexican-sofa: Rails 5.1対応のマルチリンガルCMSエンジン(RubyFlowより)

以前からあったようです。★2100超え。


github.com/comfy/comfortable-mexican-sofaより


つっつきボイス: 「名前が凄いな: comfyという略し方も」「mexican sofaってこういう形なのか」「Railsエンジンのようだけどルーティングは自分で足すのかな↓」「マルチリンガル対応みたいだし、使いどころがありそうならチェックしてもいいかも」

# 同記事より
comfy_route :cms_admin, path: "/admin"
comfy_route :cms, path: "/"

derailed_benchmarks: Railsアプリ全体のベンチマークgem

Railsのベンチマークをさっと取れます。作者はRichard Schneemanさんです。

$ bundle exec derailed bundle:mem
TOP: 54.1836 MiB
  mail: 18.9688 MiB
    mime/types: 17.4453 MiB
    mail/field: 0.4023 MiB
    mail/message: 0.3906 MiB
  action_view/view_paths: 0.4453 MiB
    action_view/base: 0.4336 MiB

つっつきボイス: 「gem名はちょっとひねりすぎかなー」「『脱線』w」「自分でもちょっとだけ動かしてみました」

duckrails: Rails APIのモックを急いで作りたいときに(Ruby Weeklyより)

GUIでAPIモック作れます。docker pull iridakos/duckrailsでお試しできます。


github.com/iridakos/duckrailsより


つっつきボイス: 「これ教育用にならいいかもしれない」

enumerize: ActiveRecordなどの属性をi18n化

class User < ActiveRecord::Base
  extend Enumerize
  enumerize :sex, :in => [:male, :female], scope: true
  enumerize :status, :in => { active: 1, blocked: 2 }, scope: :having_status
end

User.with_sex(:female)
# SELECT "users".* FROM "users" WHERE "users"."sex" IN ('female')

User.without_sex(:male)
# SELECT "users".* FROM "users" WHERE "users"."sex" NOT IN ('male')

User.having_status(:blocked).with_sex(:male, :female)
# SELECT "users".* FROM "users" WHERE "users"."status" IN (2) AND "users"."sex" IN ('male', 'female')

つっつきボイス: 「名前からenumの拡張かと思ったら、enumerizeのkeyはstringとしてDBに保存されるということかな」
「なお、自分はこのkeyをstringのままenumとして扱う方が好き: DBでSELECTしたときにわかるので」「keyが0とか1とかだと見たときにわからないですしね」「そういえば今のRailsはenumで_prefix_suffixが使えるのでとても助かってます↓」「enumってRails 4.1からだったのか」

参考: Rails Enum with prefix/suffix

social-share-button: Railsに各種SNSボタンを追加するgem(Awesome Rubyより)


github.com/huacnlee/social-share-buttonより


つっつきボイス: 「今ならgemよりWebpackでインストールしたいですね」

本当にあったRailsの怖い話

Ruby trunkより

提案: attrattr_readerattr_writerをpublicに変えよう

2.5で採用されるようです。

Here are 15k+ examples of send :attr_accessor in the wild:
https://github.com/search?utf8=%E2%9C%93&q=language%3Aruby+%22send+%3Aattr_accessor%22&type=Code
15k+ examples of send :attr_writer in the wild:
https://github.com/search?utf8=%E2%9C%93&q=language%3Aruby+%22send+%3Aattr_writer%22&type=Code
15k+ examples of send :attr_reader in the wild:
https://github.com/search?utf8=%E2%9C%93&q=language%3Aruby+%22send+%3Aattr_reader%22&type=Code

つっつきボイス:send :attr_accessorとかでやっている事例がこんなにたくさんあるとは↑」「それならpublicにしてもよさそうですね」

提案: Exception#displayのエラー出力をフォーマットしたい(継続)

def the_program
  # ...
  raise "failure!"
  # ...
rescue RuntimeError => e
  $stderr.puts "#{e.message} (#{e.class})\n\t#{e.backtrace.join("\n\t")}"
  retry
end

# こう書けば済むようにしたい

rescue RuntimeError => e
  e.display
  #

つっつきボイス:sorahさんからの提案だ」「『そらは』って読むのか」

今年3月の大江戸Ruby会議でキーノートスピーチを務めたのがsorahさんでした。

大江戸Ruby会議 06 に行ってまいりました

Ruby

クラスメソッドをclass << selfで定義する理由(Awesome Rubyより)

RubocopのRubyスタイルガイドではdef self.methodが推奨されていることを踏まえた記事です。いかにも議論になりそうです。


つっつきボイス: 「わかるー: 自分もclass << selfにしたい派」「self.だとクラスのどこにでもクラスメソッドを書けてしまうから散らかりそう」「self.使うこともあるかな」「個数にもよるかも: 1つのクラス内でself.が3つ以上になると耐えられなくなりそう」

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

Ruby 2.5の新機能: Dir.childrenDir.each_childRuby Weeklyより)

# 同記事より
> Dir.each_child("/Users/mohitnatoo/Desktop/test") { |child| puts child }
.config
program.rb
group.txt
test2

つっつきボイス:Dir.childrenDir.each_child、なぜ今までなかったんだと思っちゃいますね」

Rubyでfreezefrozen?を使うタイミング

# 同記事より
MY_CONSTANT = "foo".freeze
MY_CONSTANT << "bar" # => RuntimeError: can't modify frozen string

平易で読みやすい内容です。


つっつきボイス: 「Ruby最近始めた人やPHPとかから来た人には読んでおいて欲しいですね」

chewy: Ruby製Elasticsearchフレームワーク(Awesome Rubyより)

Elasticsearch公式のelasticsearch-rubyODMでありラッパーだそうです。

# toptal/chewyより
class UsersIndex < Chewy::Index
  define_type User.active.includes(:country, :badges, :projects) do
    field :first_name, :last_name # multiple fields without additional options
    field :email, analyzer: 'email' # Elasticsearch-related options
    field :country, value: ->(user) { user.country.name } # custom value proc
    field :badges, value: ->(user) { user.badges.map(&:name) } # passing array values to index
    field :projects do # the same block syntax for multi_field, if `:type` is specified
      field :title
      field :description # default data type is `string`
      # additional top-level objects passed to value proc:
      field :categories, value: ->(project, user) { project.categories.map(&:name) if user.active? }
    end
    field :rating, type: 'integer' # custom data type
    field :created, type: 'date', include_in_all: false,
      value: ->{ created_at } # value proc for source object context
  end
end

つっつきボイス: 「インターフェースが素直で普通っぽく書けるのがよさそう」

bunny: RabbitMQのRuby製クライアント


rubybunny.infoより

ドキュメントがかなり充実しているようです。

# ドキュメントより
require "bunny"

conn = Bunny.new
conn.start

ch   = conn.create_channel
q = ch.queue("", :exclusive => true)
x = ch.fanout("logging.events")

q.bind(x)

つっつきボイス:RabbitMQはメッセージングミドルウェアとして有名」「そういえばIBMのWebSphere MQというのもありますね」


www.rabbitmq.comより

参考: RabbitMQを導入すると… “依存しないカンケイ”でもっと幸せになれる!?

RubyHack.com: 米ソルトレイクシティで開催されるRubyカンファレンス


rubyhack.comより

昨年に続き、来年5月中旬に第2回が開催されます。詳細は未定のようです。


つっつきボイス: 「ソルトレイクシティというとロケットカーのイメージ」「ユタ州なのか」

@a_matsudaさんの福岡Ruby会議02スライド


つっつきボイス: 「a_matsudaさんの作ったライブラリこんなにある↓」

RubyConf 2017に参加しての雑感(Ruby Weeklyより)

ざっとしか見ていませんが、進むに連れてだんだん落ち込み気味になってきて、ちょっとどきどきしてしまいました。コメント欄の励ましが泣けます。

mruby向けIDE

ちょっとだけ動かしてみました。


つっつきボイス: 「組み込み分野でIDE欲しい人はそれなりにいると思うので、そういう人向けかも」

データベース

PostgreSQLの設定を1箇所変えたら速度が50倍になった(Postgres Weeklyより)

これだけ速くなったそうです。


amplitude.engineeringより


つっつきボイス: 「まさしくexplainが役に立つ例」

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

PostgreSQLのAutovacuumのビジュアル表示とチューニング(Postgres Weeklyより)


つっつきボイス: 「Vacuumって何だろうと思って」「↓: うかつにVACUUM FULLすると終わるまでテーブル全部ロックされるから注意」「こ、怖」

参考: PostgreSQL: VACUUM

PostgreSQLのトランザクション分離レベル(Postgres Weeklyより)

repmgr 4.0がリリース(Postgres Weeklyより)

PostgreSQL標準のツールです。dry-runできる操作が増えたようです。


つっつきボイス: 「dry-runマジありがたい」「レプリケーション方向逆にして自爆するのを防げる」

PgBouncerとAWS RDSでデータベーストラフィックのセキュリティを向上(Postgres Weeklyより)

参考: PgBouncer とは何ですか

スライド: 人間のためのPostgreSQL設定(Postgres Weeklyより)


つっつきボイス: 「やっぱりPostgreSQLはいい情報あるなー」

JavaScript

JSのletconst解説

ブラウザからBluetooth機器につないでみた


blog.vertica.dkより


つっつきボイス: 「こんなことできるのか!」

CSS/HTML/フロントエンド

Webサイトを簡単にPWA(Progressive Web App)に変える方法

Googleが推進しているProgressive Web Appの記事です。


pwa.rocksより


つっつきボイス: 「Progressiveといえばプログレ」「私もろその世代」「ピンク・フロイドとかキング・クリムゾンとかでしたっけ」「今はJoJoの影響でキング・クリムゾンの意味が全然違っちゃいましたね: 本家クリムゾンもそれに反応してたり」

参考: いまさら聞けないPWAとAMP

HTTP/2 pushは思ったより手強い(Frontend Weeklyより)

その他

正規表現の背後を深掘り


つっつきボイス: 「この間翻訳したJSの正規表現記事より深い内容っぽいので、これも翻訳してみます」

VSCodeの表示をかっこよくするCSS

VSCodeをアップデートするとCSSが元に戻っちゃうそうです。

コーダーがスランプを理解して克服する方法

最初何のblockかと思ってしまいました。

  • パソコンの電源を切ってみる
  • あえて紙と鉛筆でやってみる
  • などなど

つっつきボイス: 「紙と鉛筆はスランプ以外でも有効ですねー」

番外

どうぶつの森

100ドルで買えるミューオン検出器

12月


今週は以上です。

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

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Mac: homebrew caskを一括アップデートできるhomebrew-cask-upgrade

$
0
0

こんにちは、hachi8833です。小ネタで恐縮です。

Homebrew-Caskを使うと、ChromeやSublime Textなど多くのMacアプリをHomebrewと同じ方法でインストール/管理できます。App Storeでポチポチクリックするより楽なので、私はなるべくMacのアプリはCaskでインストールするようにしています。Homebrew-Caskについての説明は省略します。


caskroom.github.ioより

そのHomebrew-Caskをさらに便利にしてくれるのが、このhomebrew-cask-upgradeです。

インストール方法

homebrew-cask-upgradeはRubyで書かれていますが、インストールはgem installではなくbrew tapを使います。

brew tap buo/cask-upgrade

使い方

基本的にbrew cuを入力すれば、後は言われたとおりにするだけで、Caskでインストールしたアプリ一覧を表示し、必要なものをひととおりアップデートしてくれます。

brew cu

-a, --all
自動アップデート
--cleanup
アップデート後にキャッシュとシンボリックリンクをクリア
-f, --force
強制インストール
--no-brew-update
Homebrew/tap/formulaを更新しない
-y, --yes
アップデート確認にすべてyesと答える
-q, --quiet
アプリ一覧やオプションの情報を表示しない

合言葉はbrew cu

Macに素のDockerをHomebrewでインストールする

$
0
0

こんにちは、hachi8833です。アドベントカレンダー3日目です。

半年ほど前からなぜかDocker for Macが起動できなくなってしまい、issue #1760にも音沙汰がないので、仕方なく緊急避難として素のDockerをHomebrewでインストールしました。

本来なら正規版であるDocker for Macを使う一手なのですが、やむを得ずdocker-toolboxを使うことにしました。一応docker-toolboxは一応現在もメンテはされているようですが、条件を満たせないMac向けのソフトウェアなので、その点ご了承ください。

docker pullできるようにするのが目標です。

必要なもの

  • Mac OS X (High Sierra使用)
  • Homebrew — リンク先の「インストール」に貼ってあるシェルスクリプトでインストールできます
  • VirtualBoxbrew cask install virtualboxでインストールしました

コマンドはいずれもbash用です。

インストール手順

1. VirtualBoxとdocker-toolboxのインストール

  • caskでVirtualBoxとdocker-toolboxをインストールします。ハイフンをお忘れなく。
$ brew cask install virtualbox docker-toolbox

やってみると、docker-toolboxをインストールすればdocker本体やdocker-machine、docker-composeなども全部インストールされたので、brew install docker docker-machineは不要でした。いつの間にか随分楽になりました。

バージョンを確認しておきます。

$ docker --version
Docker version 17.10.0-ce, build f4ffd25
$ docker-compose --version
docker-compose version 1.16.1, build 6d1ac21
$ docker-machine --version
docker-machine version 0.13.0, build 9ba6da9

2. Docker-machineのセットアップ

HomebrewでDockerをインストールすると、brew servicesでdocker-machineを管理できます。

$ brew services list
Name           Status  User      Plist
postgresql     started hachi8833 /Users/hachi8833/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
docker-machine stopped
memcached      stopped
nginx          stopped
mysql          stopped
  • brew services start docker-machineを実行し、docker-machineを起動します。以後docker-machineは常駐します。
  • Dockerを初めて使う場合はVirtualBoxに仮想マシンを作成しておきます。名前は自由に付けられますが、ここではdefaultにしておきます。

$ docker-machine create --driver virtualbox default
``

`docker-machine ls`で仮想マシンが作成されたことを確認します。

```bash
$ docker-machine ls  # 仮想マシン一覧表示
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER        ERRORS
default   *        virtualbox   Running   tcp://192.168.99.100:2376           v17.10.0-ce

なお、この時点で仮想マシンにsshでログインすることもできます。

$ docker-machine ssh
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/
 _                 _   ____     _            _
| |__   ___   ___ | |_|___ \ __| | ___   ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__|   <  __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 17.10.0-ce, build HEAD : 34fe485 - Wed Oct 18 17:16:34 UTC 2017
Docker version 17.10.0-ce, build f4ffd25
docker@default:~$
  • 次のコマンドで、その仮想マシン用の環境変数を設定します。Dockerの利用頻度が高いのであれば、.bashrcに書いておくとよいでしょう。
eval $(docker-machine env)

おまけ1: docker pullしてみる

ここまで終わればdocker pullできるようになっているはずです。この間のPostgreSQL 10記事を元にPostgreSQL 10のDockerイメージをpullして動かしてみました。

$ docker pull postgres:10
$ docker run --name new-postgres -d postgres:10

以下でPostgreSQL 10にログインできます。

$ docker run -it --rm --link new-postgres:postgres postgres:10 psql -h postgres -U postgres

Dockerイメージとコンテナを表示してみます。

$ docker images  # イメージ一覧
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
postgres            10                  599272bf538f        13 days ago         287MB

$ docker ps -a   # コンテナ一覧(停止しているものも含む)
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
636a1f9212c0        postgres:10         "docker-entrypoint..."   About a minute ago   Up About a minute   5432/tcp            new-postgres

やっとDockerが帰ってきた…

おまけ2: イメージとコンテナを削除する

確認できたのでイメージとコンテナを削除します。

$ docker stop 636a1f9212c0  # コンテナを停止
$ docker rm 636a1f9212c0    # コンテナを削除
$ docker rmi postgres:10    # イメージを削除

イメージやコンテナ以外も全部きれいにしたいときは以下を実行します。

$ docker system prune

関連記事

Dockerでsupervisorを使う時によくハマる点まとめ

HomebrewからDockerにMySQL環境を移行する

[翻訳] Dockerについてよくある勘違い

Rails 3.2を4.0にアップグレードする(翻訳)

$
0
0

概要

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

Rails 3.2を4.0にアップグレードする(翻訳)

本記事はUpgrade Railsシリーズの1つです。シリーズの他の記事についてはこちらをクリックしてください

前回の記事では、マイグレーションを考慮する際の一般的なヒントについて触れましたので、本記事ではもう少し先に進みます。最初にRailsを3.2から4.0にアップグレードし、次に4.1、最終的に4.2にアップグレードします。所要時間はアプリの複雑さに応じて、開発者1人で1週間かかることもあれば、開発者2人で数か月かかることもあります。

  1. Rubyバージョン
  2. Gem
  3. Configファイル(config/)
  4. アプリケーションコード
    • a. モデル(app/models/)
    • b. コントローラ(app/controllers/)
  5. テスト
  6. その他
  7. 次のステップ

1. Rubyバージョン

Ruby 3.2.xは、Ruby 1.8.7をサポートする最後のバージョンです。アプリでRuby 1.8.7を使っている場合、Rubyを1.9.3以上にアップグレードする必要があります。本ガイドではRubyのアップグレードについて扱いませんので、詳しくはこちらのガイドをご覧ください。

2. Gem

rails4_upgradeというgemをRails 3プロジェクトのGemfileに追加して、アップデートの必要なgemを確認します。

$  myproject git:(develop) ✗ bundle exec rake rails4:check

** GEM COMPATIBILITY CHECK **
+------------------------------------------+----------------------------+
| Dependency Path                          | Rails Requirement          |
+------------------------------------------+----------------------------+
| devise 2.1.4                             | railties ~> 3.1            |
| devise-encryptable 0.2.0 -> devise 2.1.4 | railties ~> 3.1            |
| friendly_id 4.0.10.1                     | activerecord < 4.0, >= 3.0 |
| strong_parameters 0.2.3                  | actionpack ~> 3.0          |
| strong_parameters 0.2.3                  | activemodel ~> 3.0         |
| strong_parameters 0.2.3                  | activesupport ~> 3.0       |
| strong_parameters 0.2.3                  | railties ~> 3.0            |
+------------------------------------------+----------------------------+

現在バンドルされているgemやGemfile.lockを手動で調べなくても、アップグレードの必要なgemのレポートを生成できます。

3. Configファイル

Railsにはrails:updateというタスクが含まれています。これを、こちらの記事で説明されているガイドとして使うことができます。configファイルやinitializerから不要なコードやモンキーパッチを取り除くときに役立ちます。特に、Rails 2で動いていたRails 3アプリので有用です。

他の方法として、RailsDiffというサイトで基本的なRailsアプリの3.2から4.0への変更点をチェックすることもできます。このサイトでは他のバージョン間の変更点もチェックできます。

冒険してみたい方は、このスクリプトをお試しください。これは(RailsDiffで表示されているパッチと同じような感じで)Railsアプリにこのgitパッチ の適用を試行し、3.2から4.0に移行します。ただし複雑なアプリや成熟したアプリに適用するとコンフリクトが発生するためおすすめしません。

4. アプリケーションコード

a. モデル

  • .find_by_...を除くすべての動的finderメソッドが非推奨になります。
# 移行前
Authentication.find_all_by_provider_and_uid(provider, uid)

# 移行後
Authentication.where(provider: provider, uid: uid)

これらのfinderメソッドは、activerecord-deprecated_finders gemを追加することで使えるようになります。

  • ActiveRecordのスコープではlambdaが必要になりました。
# 移行前
default_scope where(deleted_at: nil)

# 移行後
default_scope { where(deleted_at: nil) }

# 移行前
has_many :posts, order: 'position'

# 移行後
has_many :posts, -> { order('position') }

(なお、default_scopeはくれぐれも慎重にお使いください)

  • 保護された属性は非推奨になりましたが、protected_attributes gemをインストールして引き続き使うことは可能です。ただし、RailsコアチームはRails 5.0からこのgemをサポート対象外にしているため、いずれにしろそのモデルはStrong Parametersに移行するべきですではありません

モデルを移行するには、attr_accessibleへの呼び出しをモデルから削除し、モデルに対応するコントローラにuser_paramsモデル名_paramsといった名前で新しいメソッドを追加する必要があります。

class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:name, :email)
  end
end

最後に、params[:user]への(ほとんどの)参照を、コントローラのアクション内のuser_paramsに変更します。この参照が更新や作成に使われる場合は(user.update_attributes(params[:user])など)、user.update_attributes(user_params)に変更します。この新しいメソッドは、Userモデルのname属性やemail属性の利用を許可し、Userモデルのその他の属性(idなど)への書き込みをすべて禁止します。

  • ActiveRecord ObserversはRails 4.0のコードベースから削除され、gemに切り出されました。Gemfileに以下を追加することで使えるようになります。
gem 'rails-observers' # https://github.com/rails/rails-observers

別の方法として、wisper gemか、もう少し違うアプローチとしてRailsのconcern(Rails4.0以降で追加)をチェックしてみてもよいでしょう。

  • ActiveResourceは削除され、独自のgemに切り出されました。
gem 'active_resource' # https://github.com/rails/activeresource

b. コントローラ

  • ActionController Sweeperはrails-observers gemに切り出されました。Gemファイルに以下を追加することで使えるようになります。
gem 'rails-observers' # https://github.com/rails/rails-observers

  • Action cachingは独自のgemに切り出されました。この機能をもう一度使いたい場合は、以下のいずれかの方法を使います。
caches_page   :public

または

caches_action :index, :show

その場合次のgemを追加する必要があります。

gem 'actionpack-action_caching' # https://github.com/rails/actionpack-action_caching

5. テスト

Ruby 1.9からはtest-unit gemをGemfileに追記する必要があります(標準ライブラリから削除されたため)。または、MinitestRSpecなど好みのテストフレームワークに移行します。

6. その他

  • ルーティングでリクエストメソッドの指定が必須になったため、デフォルトの「catch-all」動作に依存できなくなりました。
# これは以下のいずれかに変更する
match '/home' => 'home#index'

# こちら
match '/home' => 'home#index', via: :get

# またはこちら
get '/home' => 'home#index'
  • Rails 4.0からはプラグインのサポートが廃止されたため、プラグインをgemに置き換える必要があります。RubyGemsGithubでgemを探すか、プラグインをlibディレクトリに移動してRailsアプリで必要な箇所でrequireします。

7. 次のステップ

以上の手順が無事終了すれば、Rails 4.0を実行できるはずです。

アプリを微調整したい場合は、ぜひFastRuby.ioであなたのアップグレード結果をお気軽にお知らせください。

関連記事

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

アトミックなトランザクションで冪等APIを強化する(翻訳)

$
0
0

概要

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

アトミックなトランザクションで冪等APIを強化する(翻訳)

ソフトウェア業界ではいろんな人がさまざまなことに取り組んでいますが、新しい埋め込みファームウェアの開発者でもなければ、現代のソフトウェア開発の根幹であるHTTP上でリクエストを処理するCRUDアプリを構成する要素は10個程度です。多くのアプリの背後にはRuby on RailsやASP.NETといったMVCフレームワークや、PostgreSQLやSQL ServerといったACID準拠のリレーショナルデータベースがあります。

過酷な本番環境は、HTTPリクエストの処理中にありとあらゆる不測の事態を呼び起こす可能性があります。クライアントの切断、リクエスト処理中に失敗するアプリケーションのバグ、そしてタイムアウトは、いずれも平常時のリクエスト量でも十分発生しうる特殊条件です。データベースはトランザクションによって完全性の問題からアプリを保護できるので、トランザクションを最大限に活用するために、このことについて少し時間を割く価値は十分あります。

1件のHTTPリクエストと1件のデータベーストランザクションの間には驚くべき対称性が存在します。(データベース)トランザクションと同様に、HTTPリクエストもやはり処理のトランザクション単位であり、開始/終了/結果がはっきりしています。クライアントは一般にリクエストがアトミックに実行されることを期待しており、そしてクライアントはそうであるかのように振る舞います(もちろん実装にもよりますが)。ここではあるサービスを例に、HTTPリクエストとトランザクションを互いにうまく作用する方法を見ていくことにしましょう。

「1対1対応」モデル

ある典型的な冪等(idempotent)HTTPリクエストを例に考えます。HTTPリクエストは背後のトランザクションと1対1対応しなければなりません。どんなHTTPリクエストも、その内部の単一トランザクションに含まれるあらゆる操作は「コミット」か「失敗」のどちらかになります。

3つのトランザクション(tx1、tx2、tx3)に1対1対応するHTTPリクエスト

一見すると、冪等性を要求するというのは大げさな注意事項に思えますが、多くのAPI操作の冪等性は、エンドポイントの動作や振る舞いのメッセージのやりとりの後で、冪等でない操作(ネットワーク経由でのバックグラウンドジョブ呼び出しなど)に進むことによって保つことができます。

冪等でないAPIの場合はもう少し配慮が必要になります。本記事ではそのために必要な概要を解説します。詳しくは今後別記事でフォローしようと思います。

単純なユーザー作成サービス

それでは、単純な「create user」エンドポイントを提供する簡単なテストサービスを作ります。クライアントがemailパラメータ付きでリクエストを送信すると、エンドポイントは201 Createdステータスでレスポンスを返してユーザーが作成されたというシグナルを伝えます。エンドポイントは冪等でもあり、クライアントが同じパラメータでエンドポイントにリクエストを送ると200 OKステータスでレスポンスを返して問題がないことを伝えます。

PUT /users?email=jane@example.com

この動作の背後では以下の3つが行われています。

  1. ユーザーが既に存在するかをチェックし、存在する場合は中断して何もしない。
  2. ユーザーのレコードを1つ新規作成する。
  3. 新しい「ユーザーアクション」レコードを1つ挿入する。これはユーザーIDやアクション名やタイムスタンプへの参照を持つ監査ログでも使われる。

ここではPostgreSQL、Ruby、ORM(ActiveRecordまたはSequelスタイル)で実装しますが、上のコンセプトはどんな技術を使った場合にも応用できます。

データベーススキーマ

このサービスではシンプルなPostgreSQLスキーマを定義し、usersuser_actionsのテーブルを含めます1

CREATE TABLE users (
    id    BIGSERIAL PRIMARY KEY,
    email TEXT      NOT NULL CHECK (char_length(email) <= 255)
);

--  "user action"監査ログ
CREATE TABLE user_actions (
    id          BIGSERIAL   PRIMARY KEY,
    user_id     BIGINT      NOT NULL REFERENCES users (id),
    action      TEXT        NOT NULL CHECK (char_length(action) < 100),
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

バックエンドの実装

サーバーのルーティングではユーザーが存在するかどうかをチェックします。存在する場合はすぐにレスポンスを返し、存在しない場合はユーザーとユーザーのアクションを作成してからレスポンスを返します。トランザクションのコミットはどちらの場合も成功します。

put "/users/:email" do |email|
  DB.transaction(isolation: :serializable) do
    user = User.find(email)
    halt(200, 'User exists') unless user.nil?

    # ユーザーを作成
    user = User.create(email: email)

    # ユーザーのアクションを作成
    UserAction.create(user_id: user.id, action: 'created')

    # 成功のレスポンスを返す
    [201, 'User created']
  end
end

このときに生成されるSQLでは、おおよそ以下のような感じでINSERTに成功します。

START TRANSACTION
    ISOLATION LEVEL SERIALIZABLE;

SELECT * FROM users
    WHERE email = 'jane@example.com';

INSERT INTO users (email)
    VALUES ('jane@example.com');

INSERT INTO user_actions (user_id, action)
    VALUES (1, 'created');

COMMIT;

並列性の保護

察しのよい方なら、ある問題がここに潜んでいることに気づくでしょう。usersテーブルのemailカラムにはUNIQUE制約がありません。この制約がない場合、2つの別のトランザクションがSELECT部分を同時に実行すると結果が空になる可能性が生じます。どちらのトランザクションもその後にINSERTを実行するので、行が重複したままになってしまいます。

2つの並列HTTPリクエストが同じ行を挿入するためにデータが競合する

幸い、上の例では既にUNIQUE制約よりも強力なメカニズムを用いてデータの正しさを保護しています。DB.transaction(isolation: :serializable)でトランザクションを呼び出すと、トランザクションがSERIALIZABLEという独立性(isolation)レベルで開始されるので、魔法のように強力にデータの正しさを保証します。この独立性レベルは、未決の各トランザクションが同時ではなく順次実行されているかのようにトランザクションの直列実行をエミュレーションします。仮に上の例で競合が発生すると、トランザクションの1つが他方の結果を汚してしまうので、一方が次のメッセージを表示してコミットに失敗します。

ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.

ここではSERIALIZABLEの仕組みには立ち入りませんが、SERIALIZABLEが多種多様なデータ競合を検出できることと、競合時にコミットしようとするとトランザクションが失敗することを知っておけばよいでしょう。

失敗のリトライ

このコード例でも競合はめったに発生しませんが、競合によってHTTP 500エラーがクライアントに表示されないようアプリのコードで正しく扱いたいと思います。これを行うには、リクエストのコアとなる操作をループでラップします。

MAX_ATTEMPTS = 2

put "/users/:email" do |email|
  MAX_ATTEMPTS.times do
    begin
      DB.transaction(isolation: :serializable) do
        ...
      end

      # 成功: ループを終了する
      break

    rescue Sequel::SerializationFailure
      log.error "Failed to commit serially: #{$!}"
      # 失敗: 次のループにフォールスルーする
    end
  end
end

この例で、HTTPリクエストにマップされる同じトランザクションが次のように複数発生したとします。

失敗したトランザクションを同じリクエスト内でリトライする

これらのループは通常よりもコストがかかりますが、上述のとおり異常な競合から保護するために行っています。実際には、呼び出し側がよほど連続でリクエストをかけない限りめったに発生しません。

これはSequelなどのgemを使えば自動的に行えます(このコードは先のループと同様に振る舞います)。

DB.transaction(isolation: :serializable,
    retry_on: [Sequel::SerializationFailure]) do
  ...
end

レイヤーでのデータ保護

ここまでシリアライズ可能なトランザクションの威力をお目にかけましたが、現場ではシリアライズ可能な独立性レベルを使いながら同時にemailUNIQUE制約もかけたいと思うでしょう。INSERTの重複はSERIALIZABLEで保護できますが、誤ったトランザクション呼び出しやバグを含むコードからもアプリを保護するチェックとしてUNIQUE制約を追加することには価値があります。

バックグラウンドジョブ

重い操作でクライアントを待たせないために、HTTPリクエスト中にバックグラウンドキューにジョブを追加して帯域外で実行するのはよく使われるパターンです。

上述のuserサービスにもうひとつ手順を追加しましょう。userやuserアクションのレコードの作成に加えて、新しいアカウントが1つ作成されたことを外部のサポートサービスに通知するAPIリクエストを作成します。このジョブをリクエスト帯域内で行わなければならない理由はないため、バックグラウンドジョブとしてキューに入れることにします。

put "/users/:email" do |email|
  DB.transaction(isolation: :serializable) do
    ...

    # 新しいユーザーが作成されたことを
    # 外部サポートサービスに通知するジョブをキューに入れる
    enqueue(:create_user_in_support_service, email: email)

    ...
  end
end

これをSidekiqなどの一般的なジョブキューで行うと、トランザクションのロールバック時(上述の2トランザクション競合の場合など)にキューのジョブが不正になってしまうかもしれません。ジョブが参照しようとするデータは既に存在しないので、ジョブワーカーが何度リトライしても成功するはずはありません。

トランザクションをステージングするジョブ

これを回避する方法の1つは、データベースにジョブのステージング用テーブルを作成することです。ジョブをキューに直接送信するのではなく、最初にステージング用テーブルに送り、キュー追加を担当するenqueuerが後でテーブルを一括読み出ししてジョブキューに置きます。

CREATE TABLE staged_jobs (
    id       BIGSERIAL PRIMARY KEY,
    job_name TEXT      NOT NULL,
    job_args JSONB     NOT NULL
);

enqueuerはジョブを選択してキューに置き、その後ステージング用テーブルからジョブを削除します2。以下はおおまかな実装です。

loop do
  DB.transaction do
    # 長大なバッチからジョブを読み出す
    job_batch = StagedJobs.order('id').limit(1000)

    if job_batch.count > 0
      # それぞれを実際のジョブキューにINSERTする
      job_batch.each do |job|
        Sidekiq.enqueue(job.job_name, *job.job_args)
      end

      # これらのレコードを同じトランザクションから削除する
      StagedJobs.where('id <= ?', job_batch.last).delete
    end
  end
end

ジョブは1つのトランザクション内でステージング用テーブルに挿入されるため、独立性(ACIDの「I」)によって、INSERTトランザクションがコミットされるまで他のトランザクションから見えなくなることが保証されます。ロールバックしたステージングジョブはenqueuerからは決して見えないため、ステージングからジョブキューが作成されることもありません。

私はこのパターンを「transactionally-staged job drain」と呼んでいます(訳注: 著者の考案したパターンのようです)。

Queなどのライブラリを使ってジョブキューをデータベースに直接置く方法も一応可能ですが、PostgreSQLなどのシステムでは肥大化の危険があるため、おそらくよいアイデアにはならないでしょう。

冪等でないリクエスト

本記事で取り上げた方法は、互いに冪等なHTTPリクエスト同士をうまく扱うことができます。よく設計されたAPIの大半はおそらくこのように健全でしょう。しかしエンドポイントが冪等でないことも必ずありえます。例としては、クレジットカードの外部支払いゲートウェイへの問い合わせや、プロビジョニングされるサーバーへのリクエスト、その他同期的なネットワークリクエストが必要なすべてのケースが考えられます。

そのようなリクエストの場合はもう少し洗練された手法が必要ですが、本記事のシンプルなケースと同様、本記事で使ったデータベースではその点をカバーしています。本記事のパート2では、ステージングを複数持つ(マルチステージング)トランザクションの最上位で冪等なキーを実装する方法を取り上げます。

関連記事

Railsのトランザクションと原子性のバグ(翻訳)

Ruby on Rails 4.0.1リリース!大量のバグ修正、3系からの移行も少し簡単になりました


  1. このシンプルなコード例の目的のためにSQLをおそらくもっと素朴にすることも可能ですが、健全さを失わないため、多少見た目が煩雑になるのを承知で長さチェック、NOT NULLチェック、外部キー制約を使います。 
  2. 他の多くのジョブキューと同様、enqueuerが保証するのは「一回のみ」というセマンティクスではなく「少なくとも1回」というセマンティクスであることを思い出しましょう。したがって、ジョブ自身は冪等でなければなりません。 

Rails+PostgreSQLのパーティショニングを制覇する(翻訳)

$
0
0

概要

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

原文タイトルはおそらくCommand&Conquerのもじりと思われます。

Rails+PostgreSQLのパーティショニングを制覇する(翻訳)

前書き

本記事は実際の出来事をヒントにしたデータベースパーティショニングについて書いたものです。productionアプリ、すなわちRuby on RailsとPostgreSQLの速度を低下させる巨大なテーブルを分割する方法について手順を追って学びます。

これ以上大きくなっては困るとき

データベースは肥大化する傾向があります。データベースのサイズはある時点から負債と化しますが、主キー数が上限に達するような極端な状況をなかなか想定できません(そしてこれは実際に起きます)。本記事は、私達の一顧客であるGettでの経験を元にしています。このときはデータベーステーブルが危険水域に達するほど肥大化し続けていました。

行数が数百万行に達すると、クエリによっては完了に数時間を要することもあります。これによって生じた技術的な困難をデータベースパーティショニングによって解決しました。

1個の巨大なテーブルを多数の小さなテーブルに分割するというのは標準的な技法ですが、特に本番稼働中のデータが危機にひんしている場合は注意深く行う必要があります。本記事では、よくある落とし穴を回避してデータロスなしで移行できるようにする方法を解説します。何らかのハンズオンをやってみるのが学習法として最善なので、最初にフェイクデータで巨大なテーブルを作成して問題を作り出します。続いて、PostgreSQLのマジックを武器としてこの問題を皆さんと一緒に解決します。

実際のフェイクテーブル

まずデータが、それも大量のデータが必要です。改善前のテーブルが含むordersには、通常のビジネスロジックを模したカラムがあります。

訳注: 上の一文目は、おそらく映画「マトリックス」のセリフ(Guns, Lots of Guns)のもじりです。

CREATE TABLE orders (
  id SERIAL,
  country VARCHAR(2) NOT NULL,                        -- 国コード
  type VARCHAR NOT NULL DEFAULT 'delivery',           -- orderの種類
  scheduled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,  -- orderの作成時刻
  cost NUMERIC(10,2) NOT NULL DEFAULT 0,              -- orderのコスト
  data JSONB NOT NULL DEFAULT '{}'                    -- 追加データ
);

: リファクタリングでは主にPostgreSQLを考慮するため、以後クエリは純粋なSQLで、関数はPL/pgSQLでそれぞれ表記します。作業が終わった後は、ActiveRecord経由でデータをRailsで扱えるようになります。

最初に、実行頻度が最も高いクエリを次のように決めます。

  • idorder_byしてorderを1件取得
  • ある期間(精度は分単位)の特定の国についてのordersをすべて取得する
  • orderのcostや関連付けられたデータを変更する

ordersテーブルを検討すれば、最も多いクエリの速度を向上させるにはcountryscheduled_atをインデックス化するのが妥当であることが即座にわかります。

CREATE INDEX index_orders_on_country_and_scheduled_at ON orders (country, scheduled_at);

準備が整ったので、以下のようにランダムな値を用いてgenerate_seriesでテーブルの値を埋めます。

INSERT INTO orders (country, type, scheduled_at, cost)
SELECT
  ('{RU,RU,RU,RU,US,GB,GB,IL}'::text[])[trunc(random() * 8) + 1],
  ('{delivery,taxi}'::text[])[trunc(random() * 2) + 1],
  CURRENT_DATE - (interval '1 day' * (n / 100000)) + (interval '1 second' * (random() * 86400)),
  round((100 + random() * 200)::numeric, 2)
FROM
  generate_series(1,30 * 1000000) s(n);

分割

目標はストレートに設定しなければなりません。ここではordersを分割して次のようにしたいと考えています。

  • 生成されるテーブルには特定の月の特定の国のordersがすべて含まれること
  • アプリのロジックがほぼ変わらないようにすること

最も達成しやすいのは、子テーブルを作成し、対応するトリガを作成し、テーブル全体にレコードを分散させるトリガ関数を作成する方法です。

しかしこの方法でActiveRecordでデータベースにクエリをかけたい場合、ひとつ面倒な点があります。純粋なSQLでは、同じレコードを2回INSERTする(マスターテーブルで1回、子テーブルで1回)のを避けるため、トリガプロシージャはNULLを返す必要があります。しかしこれはActiveRecordとの相性がよくありません。ActiveRecordはINSERT文でRETURNING文を使った場合に新規レコードの主キーを1つ返すことを期待するからです。解決方法はいくつか考えられます。

レガシーなスキーマで(データベース)ビューを使う方法の例はRailsガイド(英語)をご覧ください。

  1. ActiveRecordにおまかせする: NULLの代わりに新規レコードを返します。新規レコードをマスターテーブル子テーブルにそれぞれ配置したら、マスターテーブルから即座に削除します。つまり1つの操作を3つに分けて行うことになります。この方法を選んでもパフォーマンスが必然的に著しく低下するため、ほとんどのRails開発者はパーティショニング自体を諦めざるを得なくなるでしょう。
  2. ActiveRecord PostgreSQLアダプタで切り抜ける: Rails 4.0.2以降なら設定ファイルでinsert_returningfalseに設定すればよいので、これは難しくありません。これはうまくいきますが、その代わりアプリの全テーブルの振る舞いが変わってしまいます。また、(主キーの現在の値を取得するため)INSERT操作ごとにリクエストを1つ余分に受け取ることになります。

  3. (データベース)ビューを使う: これならデータベースレベルのリファクタリングだけでできるようになります。しかも、ビューは「普通の」テーブルであるかのように扱えるため、ActiveRecordはビューと自然に協調動作でき、既存アプリのロジック変更は最小限で済みます。

第3の方法を使うことにします。最初に、テーブルを複製する必要があります。テーブルを複製するメリットは次のとおりです。

  • 既存データの完全性を保ち、他のモデルからの参照が安全に保たれる
  • パーティショニングのデプロイ中(マイグレーション後からリスタートまでの間)に既存アプリを生かしておくことができる
  • 作業中に問題が発生しても元のテーブルにフォールバックできる

複製は次の方法で行います。

CREATE TABLE orders_partitioned (LIKE orders INCLUDING ALL);

クローンされたテーブルの主キーは、元のテーブルと同じorders_id_seqシーケンスを参照します。これにより、古いテーブルから新しいテーブルにデータを移動するときに衝突を回避できます。

データベースビューはマジでいいやつ

今度は新しいテーブルでビューを作成する必要があります。新しいテーブルはまだ空ですが、変更をデプロイするとすべての新規レコードがそこに配置され、対応する複数の子テーブルにも直ちに同じレコードが配置されます。

CREATE OR REPLACE VIEW orders_partitioned_view AS SELECT * FROM orders_partitioned;

まだ何か足りないようです。デフォルト値はどうすればよいでしょうか。明らかに主キーのデフォルト値が必要ですし、デフォルト値がないとActiveRecordでINSERTが効かなくなってしまいます(orders_id_seqにご注目ください)。

ALTER VIEW orders_partitioned_view
ALTER COLUMN id
SET DEFAULT nextval('orders_id_seq'::regclass);

理論上は、他のカラムはデフォルト値なしでActiveRecordによって扱われますが、アプリのコードではまだ必要になる可能性があります。
また、先のトリガ関数はデフォルト値がないと動作しません(フィールド値がNULLのままになるため、INSERTNot Null Violationで失敗します)

ALTER VIEW orders_partitioned_view
ALTER COLUMN cost
SET DEFAULT 0;

ALTER VIEW orders_partitioned_view
ALTER COLUMN type
SET DEFAULT 'delivery';

ALTER VIEW orders_partitioned_view
ALTER COLUMN data
SET DEFAULT '{}';

次は、以下を行うトリガプロシージャが必要です。

  • 子テーブルをどのレコードを配置できるかを決定する
  • テーブルが存在しない場合は作成する: これがないと、思いつく限りすべてのテーブルを手動で作成しなければならなくなります。もちろんアプリで自動化もできますが、余分なコードは書かずに済ませたいものです。

データベースの(ストアド)プロシージャのコードを書くときが来ました。PL/pgSQLを使います。

CREATE OR REPLACE FUNCTION orders_partitioned_view_insert_trigger_procedure() RETURNS TRIGGER AS $BODY$
  DECLARE
    partition TEXT;
    partition_country TEXT;
    partition_date TIMESTAMP;
  BEGIN

    /* 新しいテーブルの名前を作成する*/

    partition_date     := date_trunc('month', NEW.scheduled_at);
    partition_country  := lower(NEW.country);
    partition          := TG_TABLE_NAME || '_' || partition_country || '_' || to_char(partition_date, 'YYYY_MM');

    /*
    必要な場合に子テーブルを作成する。関連するすべての部分に通知する。
    */

    IF NOT EXISTS(SELECT relname FROM pg_class WHERE relname = partition) THEN

      RAISE NOTICE 'ordersの新しいパーティションを作成します: %', partition;

      /*
      ここでは以下を行います:
      * マスターテーブルからテーブルを1つ継承
      * 生成されるテーブルにCHECK制約を作成
        (条件を満たさないレコードが挿入されないようにするため)
      * 必要なインデックスを作成
      * 引用符が3つあるのは機能であり、バグではありません
      */

    EXECUTE 'CREATE TABLE IF NOT EXISTS ' || partition || ' (CHECK (
      country = ''' || NEW.country || ''' AND
      date_trunc(''minute'', scheduled_at) >= ''' || partition_date || ''' AND
      date_trunc(''minute'', scheduled_at)  < ''' || partition_date + interval '1 month' || '''))
      INHERITS (orders_partitioned);';

    EXECUTE 'CREATE INDEX IF NOT EXISTS ' || partition || '_scheduled_at_idx ON ' || partition || ' (scheduled_at);';

  END IF;

  /* And, finally, insert. */

  EXECUTE 'INSERT INTO ' || partition || ' SELECT(orders  ' || quote_literal(NEW) || ').*';

  /*
  注意: NULLではなく、新規レコードが返されます。
  これによりActiveRecordとの相性がよくなります。
  */

  RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;

これで以下ができました。

  • テーブルとして使えるビュー
  • 新規レコードを配置するトリガ関数

それではここまでの成果をまとめてみましょう。

CREATE TRIGGER orders_partitioned_view_insert_trigger
INSTEAD OF INSERT ON orders_partitioned_view
FOR EACH ROW EXECUTE PROCEDURE orders_partitioned_view_insert_trigger_procedure();

ところで、Rubyらしく行うには、元のテーブルの代わりにorders_partitioned_viewを使うようOrderモデルに指示するだけでできます。Railsはこうした操作を自然に行なえますので、モデルのtable_nameにビュー名を与えるだけでおしまいです。
この機能はレガシーなスキーマを用いるときによく使われます。

ところで元のデータは?

そうそう、既存のデータを忘れてはいけません。まだ元のテーブルにそのまま残っているので、注意深く新しいテーブルに移動しなければなりません。問題はデータ量が非常に大きい(数百万行でしたよね)ことで、対処方法はいくつも考えられます。ありがたいことに、すべてのデータを移行しなければならないことはめったにありません。通常、直近のいくつかの月に対応する子テーブルにデータを入れる必要があります。先月分のデータでやってみましょう。

INSERT INTO orders_partitioned_view
       SELECT * FROM orders
       WHERE scheduled_at >= date_trunc('month', now());

後悔しないデプロイ方法

手順はできあがりましたが、本番環境で行うのはまったく別の話です。予想もつかないような問題がいくつも起きるでしょう。

私たちが本番でこの手法を使ったとき、まさにそれが起きたのです。パーティショニングが必要なサービスは「High Availability(高可用性)」を謳っていました。つまりダウンタイム15分以上は許されないということです。

本番サービスへの全リクエストはMessage Queueを経由しますので、少し待てば作業完了後にリクエストを実行できます。しかし実際には長時間待つしかありませんでした。

アプリはActiveRecordに全面的に依存していたのではなく、生のデータベースクエリも使われていたため、ダウンタイムは避けられませんでした(生SQLを避けることができれば、多くのトラブルに遭わずに済みます)。そういうわけで、Railsでテーブル名を単に切り替えるという選択肢は使えませんでした。また、テーブルの複製も避けたかったため、いくつかの手順を組み合わせました。

ALTER TABLE orders RENAME TO orders_partitioned;
CREATE OR REPLACE VIEW orders AS SELECT * FROM orders_partitioned;

/* ...省略 */

常に操作が繰り返されている稼働中のビジネスデータを扱うので、変更のリリースは「リスキー」とマーキングすべきです。理想的なデプロイチェックリストを以下に示します。

  • 運用エンジニアは今度のリリース内容を把握し、かつ立ち会うこと
  • データベース管理者も同様にリリース内容を把握し、かつ立ち会うこと
  • 監視可能なものはすべて監視対象に含めること: ディスクアクセス操作、RAMやCPUの使用量、ネットワークリクエスト、データベースクエリ、アプリリクエスト、バックグラウンドタスク、ログ
  • 別の開発者かチームリーダーに精査してもらった「リリースプラン」を用意すること
  • マイグレーションコードを書いた開発者に連絡を取れること
  • チームリーダーはこれらを把握し、立ち会うこと
  • あらゆる手順を手動で再現できるようにしておくこと
  • ロールバックプランを用意すること

大げさに見えるかもしれませんが、自信過剰は禁物です。十分時間をかけて正しいデプロイ手順を策定しましょう。

パーティショニングすべきかどうかの決定

テーブルのサイズを検査するのはもちろんですが、最も重要なのは時間の経過に伴うデータ増加を見積もることです。どんなクエリが最も多いかを検討します。主キーや外部キーによるSELECT文しか実行しないのであれば、おそらくパーティショニングは不要です。データを何らかの形でグループ化し(上の例のような期間ごとのグループ化など)、グループに関連するクエリを常用するのであれば、パーティショニングすることでかなり楽になるでしょう。

「これ使っちゃダメですか?」

より複雑で粒度の高いパーティショニングが必要になることがあります。しかし一度に1つのカラムしか作成しないのであれば使ってもよいでしょう。

  • PostgreSQL 10(とお遊びでpg_partyも)

宣言的パーティショニング(declarative partitioning)は本当に便利です。使える状況であれば遠慮なくどうぞ。ただし、PostgreSQLにかぎらず、最新バージョンを無条件に使える幸せな状況はめったにありません。(PostgreSQL 10へ)アップグレードできるのであれば、上述のデプロイプランを用意しておきましょう。


パーティショニングを試す準備は整いましたか?マイグレーションコードの作成は仕事の半分でしかありません。残り半分はチームが正しく作業を実行できるよう準備することです。

もっと詳しく知りたい方へ


スタートアップをワープ速度で成長させられる地球外エンジニアよ!Evil Martiansのフォームにて待つ。

関連記事

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

Rails: PostgreSQLのマイグレーション速度を改善する(翻訳)

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

Rails開発者のためのPostgreSQLの便利技(翻訳)


Ruby正規表現の後読みでは長さ不定の量指定子は原則使えない

$
0
0

こんにちは、hachi8833です。「ライフ」カテゴリの記事でアドベント書きたかったのですが、こちらの小ネタにします。

正規表現の先読みと後読みについては「正規表現の先読み・後読み(look ahead、look behind)を活用しよう」をご覧ください。

以下は基本的にRubyの正規表現(onigmo)を使います。他の正規表現ライブラリではこのとおりにならない可能性があります。

Rubyの正規表現の後読みは長さを不定にできない

以下の文字列が対象です。

word work wording working interesting partitioning subscribe subscriber subscription

たとえば、ingで終わる1文字以上の長さの英単語のingだけにマッチさせたいと思って次の正規表現を書いたとします。+は1文字以上のマッチを表します。

(?<=[\p{L}]+)ing

しかしやってみると、Invalid pattern in look-behind.と表示されます。なお、+を最小一致の+?に変えてもだめでした。

任意の長さの代わりに、長さの異なる特定の語のリストを後読みで使うとどうなるでしょうか。

(?<=(word|work|interest|partition))ing

これも同じくInvalid pattern in look-behind.になります。なお、リスト内の各語の長さをすべて同じにすると通ります。

後読みは本体がマッチした後で文字どおり遡ってチェックされるはずなので、量指定子(quantifier: 量化子とも呼ばれます)の長さが不定だと効率が非常に落ちることは想像がつきます。Onigmoの仕様まではチェックしていませんが、おそらくそうした理由で長さ不定の後読みをサポートしていないのではないかと推測しています。

ちょっとだけPerlでも試してみましたが、こちらもnot implementedだそうです。

$ perl -e '"word work wording working interesting partitioning subscribe subscriber subscription" =~ /(?<=[\\p{L}]+)ing/;'
Variable length lookbehind not implemented in regex m/(?<=[\\p{L}]+)ing/ at -e line 1.

後読みに使える量指定子

Rubyの場合、少なくとも次のように{, 10}などで長さに制約を与えた場合は後読みが機能します。これがなかったらわたし的につらいです。

(?<=[\p{L}{, 10}])ing

他にも使えるものがあるかもしれませんが、いずれにしろ量指定子を不用意に使うと効率が落ちるので、あまりやんちゃしないようにしましょう。

先読みでは長さを不定にできる

Ruby正規表現の先読み(look ahead)では、次のように長さ不定の量指定子を使えます。

work(?=[\p{L}]+)

おまけ: .NET Frameworkだとできる

遠い昔の記憶では、.NET Frameworkでは後読みで長さ不定の量指定子を使えたはずだったので、チェックしてみました。当時はこれが当たり前だと思っていたので、他のライブラリでできないことを知ったときはショックでした。

たった今見つけたregexstorm.netというサイトで.NET Frameworkの正規表現をチェックしたところ、後読みであっさり長さ不定の量指定子を使えました。Mac環境だとおいそれと.NET Frameworkの正規表現を確認できないので、このサイトは助かります。

また、.NET Frameworkの正規表現ライブラリをGo言語に移植したdlclark/regexp2という私の大好きなパッケージで試したところ、こちらでも長さ不定の量指定子を使えました。

package main

import (
    "fmt"

    "github.com/dlclark/regexp2"
)

func main() {
    re, err := regexp2.Compile("(?<=[\\p{L}]+)ing", 0)
    if err != nil {
        fmt.Println("err compile: ", err)
    }

    ma, err := re.FindStringMatch("word work wording working interesting partitioning subscribe subscriber subscription")
    if err != nil {
        fmt.Println("err match: ", err)
    }

    fmt.Println(ma)
}
$ go run regexp2.go
ing

効率を犠牲にしても後読みで長さ不定の量指定子をサポートしているのか、それとも実装が凄いのかは調べていませんが、私の中ではやはり.NET Frameworkの正規表現が今のところ最強です。

関連記事

正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号

正規表現の先読み・後読み(look ahead、look behind)を活用しよう

Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック

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

$
0
0

概要

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

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

DHHは今年ActiveStorageという新しいコンポーネントの導入をアナウンスしました. ActiveStorageは、写真などのアップロードをRailsで直接管理します。

以来、ActiveStorageをRailsに統合するため多くの改良が加えられ、ActiveStorageは事実上利用可能になっています。本記事では、ActiveStorageを使うためにRailsをアップデートする方法を調べてみました。

警告: bleeding edgeバージョンのRailsを使うため、見たこともないような問題が引き起こされる可能性があります。

Rails向けにActiveStorageをセットアップする

  • 5.1.14より前のRailsを使っている場合は、Gemfileを変更して5.1.14にアップデートします。
gem 'rails', '~> 5.1', '>= 5.1.4'
  • $ bundle update railsを実行します。
  • $ rails app:updateを実行してコードの差分をすべて解決します。
  • 5.1.14へのアップグレードで問題ないことを確認します。
  • bleeding edgeバージョンのRailsにアップデートします。

Gemfileを以下のように変更します。

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end
...
gem 'rails', github: 'rails/rails'
gem 'arel', git: 'https://github.com/rails/arel.git'
gem 'bootsnap', '~> 1.1', '>= 1.1.5', require: false
  • $ bundle update railsを実行します。
  • $ bundle exec rails -vでbleeding edgeバージョンRails 5.2.0.alphaの表示を確認します。
  • アプリケーションのconfigを更新し、$ bundle exec rails app:updateを実行します。
  • アプリ起動前に$ ./bin/rails --tasksrails active_storage:installが実行可能タスクに表示されることを確認します。
  • $ ./bin/rails active_storage:installを実行し、マイグレーションファイルを生成します。
  • $ ./bin/rails db:migrateを実行します。これでSQliteデータベースでActiveStorageがサポートされます。

ActiveStorageでシンプルな画像をアップロードしてみる

ここまではRailsアプリでActiveStorageをサポートするための準備でした。アプリでActiveSupportが使えるようになったので、ActiveStorageを使って画像のpostを作成できるようにする簡単な機能を作ってみましょう。

  • $ ./bin/rails g model postでPostモデルを作成します。
  • 次のマイグレーションファイルでPostのテーブルにtitlebodyの2つのカラムを追加します。
# db/migrate/20171114063756_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :body

     t.timestamps
    end
  end
end
  • $ ./bin/rails g controller postsでPostsリソースのコントローラを作成します。
  • Postsリソースへのルーティングをconfig/routes.rbに追加します。

Rails.application.routes.draw do
  resources :posts
end
  • 画像とpostの関連付けが必要です。
class Post < ApplicationRecord
  has_many_attached :images
end
  • indexshowcreateアクションのコードを追加します。
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # postのフォームをここで表示する
  def index
    @post = Post.new
  end
  # ここでpostを作成する
  def create
    post = Post.create! params.require(:post).permit(:title, :body)
    post.images.attach(params[:post][:images])
    redirect_to post
  end
  # 写真付きのpostをここで表示する
  def show
    @post = Post.find(params[:id])
  end
end
  • 以下のコードを持つindexのビューをpostに追加します。アップロード用フォームはlocalhost:3000/postsに表示されます。
# app/views/posts/index.html.erb
<%= form_with model: @post, local: true do |form| %>
  <%= form.text_field :title, placeholder: "Title" %><br>
  <%= form.text_area :body %><br><br>
  <%= form.file_field :images, multiple: true %><br>
  <%= form.submit %>
<% end %>
  • postを表示するビューを追加します。
# app/views/posts/show.html.erb
<%= image_tag @post.images.first %>
  • 写真を1枚送信してみると、ビューに画像が表示されます。

この画像は、アプリのルートレベルのstorageというディレクトリにローカル保存されますが、このファイルをAWS S3やGoogle Cloud、Azureなどのクラウドファイルストレージシステムにプッシュするよう設定することもできます。

ご覧のとおり、ActiveStorageのおかげでRailsのActiveRecordコンポーネントにうまく統合されたシンプルなファイル管理システムを使えました。ActiveStorageが成熟すれば、ファイル管理で主要なユースケースをカバーできるようになるでしょう。Rails向けのファイル管理システムは他にもいろいろありますが、最初にActiveStorageを検討することをおすすめします。

ActiveStorageのドキュメントもご覧ください。

本記事の最終的なコードはGitHubリポジトリでご覧いただけます。

自身のプロジェクトでActiveStorageを使った経験を共有してくださった@jeffreyguentherに感謝いたします。

関連記事

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

週刊Railsウォッチ(20170721)ActiveStorageは5.2で正式導入、Onigmoの脆弱性が修正、この夏読みたい名作Ruby本ほか

Ruby: Chain of Responsibilityパターンの解説(翻訳)

$
0
0

概要

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

Ruby: Chain of Responsibilityパターンの解説(翻訳)

本記事ではChain of Responsibilityパターンについて説明します。このパターンのRubyでの実装方法と、どのような場合にRubyアプリに適用可能かを判別する方法について学びます。

Chain of Responsibilityパターンの目的は、1つのリクエストを複数のオブジェクトで扱えるようにすることで、リクエストの送信側を受信側から切り離す(結合を断つ)ことである。

この定義では堅苦しすぎると思った方も心配は無用です。わかりやすいいくつかの例を使って考察します。

例#1

顧客からの送金を受け付けるアプリを書いているとしましょう。金額や通貨に応じてお金を扱う支払いプロバイダを切り替えたいと思います。

特定のトランザクションごとに支払いプロバイダを定義するには、いくつかの条件ロジックを実行する必要があります。このコードは次のような感じになります。

if ...some logic for transaction
  use payment provider 1
elsif ...logic
  use payment provider 2
elsif ...logic
  use payment provider 3
end

ロジックが複雑になるとコードは扱いにくくなり、リファクタリングも面倒になります。

Chain of Responsibilityパターンを使うことで、ハンドラをチェインできるようになります。それぞれのハンドラには、ハンドラでトランザクションを処理するかどうかを定義するロジックが含まれます。

1つのトランザクションは、トランザクションを処理できるハンドラが見つかるまでチェインに沿って進みます。図にすると次のようになります。

Ruby - Chain Of Responsibility

各ハンドラには、そのハンドラがトランザクションに適用可能かどうかを判定するロジックが含まれており、適用できない場合はチェインしている次のハンドラを実行します。

このチェインの場合、ハンドラ#1が最初にトランザクションを処理しようとします。トランザクションを処理できない場合、ハンドラ#2を実行します。#2でもトランザクションを実行できない場合は、ハンドラ#3を実行します。

この方法には次のメリットがあります。

  • ハンドラの順序を定義できる
  • ハンドラごとに独自のロジックを含めることができる
  • ハンドラを簡単に追加できる
  • 特殊なハンドラから一般性の高いハンドラへと処理を進められる

この例に沿ってChain of Responsibilityを実装してみましょう。

最初に、トランザクション用のシンプルなクラスを作成します。

class Transaction
  attr_reader :amount, :currency

  def initialize(amount, currency)
    @amount = amount
    @currency = currency
  end
end

次にハンドラのインターフェイスを決定します。ハンドラはcan_handle?メソッドとhandleメソッドに応答するのがよさそうです。ハンドラがトランザクションを扱えない場合は、次のハンドラを呼び出す必要がありますので、チェインのsuccessorで次のハンドラを呼び出します。私はこのロジックをBaseHandlerクラスに切り出すことにしました。各ハンドラはこのクラスを継承します。

class BaseHandler
  attr_reader :successor

  def initialize(successor = nil)
    @successor = successor
  end

  def call(transaction)
    return successor.call(transaction) unless can_handle?(transaction)

    handle(transaction)
  end

  def handle(_transaction)
    raise NotImplementedError, 'Each handler should respond to handle and can_handle? methods'
  end
end

だいぶコード量が増えましたが、動作を1行ずつ理解してみましょう。

  def initialize(successor = nil)
    @successor = successor
  end

初期化中にsuccessorを受け取ることで、チェインを形成できるようになります。たとえば次のように書けます。

chain = StripeHandler.new(BraintreeHandler.new)
chain.call(transaction)

call(transaction)メソッドを呼び出すための実装は次のとおりです。

  def call(transaction)
    return successor.call(transaction) unless can_handle?(transaction)

    handle(transaction)
  end

最初のハンドラでcall(transaction)を呼ぶと、このトランザクションを扱ってよいかどうかをチェックし、扱えない場合はsuccessor.call(transaction)を呼んでチェインの次のハンドラにフローを進めます。

以上で、各ハンドラがBaseHandlerを継承することと、can_handle?メッセージやhandleメッセージに応答することを理解できました。ハンドラをいくつか実装してみましょう。

class StripeHandler < BaseHandler

  private

  def handle(transaction)
    puts "トランザクションをStripe支払いプロバイダで扱います"
  end

  def can_handle?(transaction)
    transaction.amount < 100 && transaction.currency == 'USD'
  end
end

class BraintreeHandler < BaseHandler

  private

  def handle(transaction)
    puts "トランザクションをBraintree支払いプロバイダで扱います"
  end

  def can_handle?(transaction)
    transaction.amount >= 100
  end
end

transaction = Transaction.new(100, 'USD')

chain = StripeHandler.new(BraintreeHandler.new)
chain.call(transaction)
# => トランザクションをBraintree支払いプロバイダで扱います

トランザクションを2つ作成しました。ハンドラがこのトランザクションを扱ってよいかどうかを決定するロジックはcan_handle?メソッドにあります。支払い処理はhandleメソッドにあります。

上の例では、BraintreeHandlerクラスのオブジェクトでStripeHandlerのオブジェクトを作成し、リストの次のハンドラを表しています。

続いてcallを呼び出します。StripeHandlerにはcallは実装されていないため、BaseHandlerに進んで以下のコードが実行されます。

  def call(transaction)
    return successor.call(transaction) unless can_handle?(transaction)

    handle(transaction)
  end

StripeHandlerクラスのオブジェクトのcan_handle?(transaction)を実行すると、トランザクションの総数が99を超えたので応答はfalseになります。

このようにしてsuccessor.call(transaction)が実行され、次のハンドラはBraintreeHandlerクラスのオブジェクトになります。このハンドラはトランザクションを扱えるので、handle(transaction)が実行されます。

例#2

Chain of Responsibilityパターンというアイデアの理解に役立つ別の例を考えてみましょう。

あるオンラインストアを運営していて、ある顧客の個人ディスカウント額の算出が必要になりました。ディスカウント額は、顧客の忠実度(loyalty)、前回の注文数といった多くの要素によって決まります。

これは顧客の最終的なディスカウント額を算出するハンドラのチェインを作成するのにうってつけの機会です。ディスカウントの種類によっては適用できないものがあります。たとえばブラックフライデー割引は年に1回まで、忠実な顧客は5回以上購入した場合にディスカウントが効くという具合です。

これを実装してみましょう。このアイデアをそのまま写し取ったシンプルなクラスを作成します。

class Customer
  attr_reader :number_of_orders

  def initialize(number_of_orders)
    @number_of_orders = number_of_orders
  end
end

話を簡単にするため、1人の顧客については注文数(number_of_orders)のみをトラックすることにします。

先ほどの例と同様にBaseDiscountクラスを作成し、他のディスカウントはこれを継承することにします。

class BaseDiscount
  attr_reader :successor

  def initialize(successor = nil)
    @successor = successor
  end

  def call(customer)
    return successor.call(customer) unless applicable?(customer)

    discount
  end
end

続いて、ディスカウントを必要な分追加します。

class BlackFridayDiscount < BaseDiscount

  private

  def discount
    0.3
  end

  def applicable?(customer)
    # ... 当日がブラックフライデーかどうかをチェック
  end
end

class LoyalCustomerDiscount < BaseDiscount

  private

  def discount
    0.1
  end

  def applicable?(customer)
    customer.number_of_orders > 5
  end
end

class DefaultDiscount < BaseDiscount

  private

  def discount
    0.05
  end

  def applicable?(customer)
    true
  end
end

これで、非常に特殊なディスカウントから一般的なディスカウントまで自由に扱えるようになりました。

chain = BlackFridayDiscount.new(LoyalCustomerDiscount.new(DefaultDiscount.new))

顧客にとってディスカウント額が最も大きいのはブラックフライデーなので、最初にブラックフライデーを処理します。続いて忠実な顧客向けのディスカウント適用を試みて、2つとも適用できない場合はデフォルトのディスカウントを使います。Chain of Responsibilityは「特殊な条件から一般的な条件」の順に処理されるよう実装すべきです。

たとえばブラックフライデーのディスカウントを廃止するという業務命令が下されても大丈夫。チェインからブラックフライデーのハンドラを削除するだけでおしまいです。

chain = LoyalCustomerDiscount.new(DefaultDiscount.new)

これでチェインは2つになりました。簡単ですね。

このパターンは、アプリがクライアントからの問い合わせに回答しなければならないようなシステムで、特殊な回答から一般的な回答までをチェインするのに適しています。問い合わせがこのチェインを進むと、システムは最も適した回答を見つけます。特定の質問には特定の回答を返し、他によい回答がなければ一般的な回答を返します。

お読みいただきありがとうございました。このパターンがうまくはまる場所がアプリで見つかって、皆さまのコードが改善されることを願っています。

追伸: 私はニューオリンズで開催されたRubyConfに参加して、多くの素晴らしいエンジニアの皆さまに感謝を伝える機会に恵まれました。Rubyコミュニティの素晴らしさを改めて実感したこと、そしてRuby言語、gem、ツールなどを支えているすべての人たちへの感謝の気持ちをここに述べたいと思います。MatzがRuby開発者たちに「素晴らしい開発者になろう」と呼びかけたように😊

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#3: 振舞い系(翻訳)

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

シェルのPowerlineをPowerline-goに置き換えてみた

$
0
0

こんにちは、hachi8833です。

Powerline使ってますか?私もシェルのプロンプトを便利かつかっこよく表示できるPowerlineを半年ぐらい前から使っています。特にGitのディレクトリでステータスを表示できるところが非常にありがたいです。

これまで使っていたLokaltog/vim-powerlineを開いてみるとdeprecation warningが表示されていることに気づき、そして他の変種も含めて本家Powerlineに収束したらしいことを今になって知りました。

しかし本家Powerlineに差し替えてみようとしたところ、なまじpyenvをインストールしていることもあってか、Python周りの設定がいろいろ面倒で途中でくじけました。それにpowerline-daemonを常駐させないとやはり重いらしく、常駐させるのは自分的に残念です。

そんなときに見つけたのが、Go言語で書かれたpowerline-goでした。


github.com/justjanne/powerline-goより

以前にも別のGo製Powerlineを見つけたことがあったのですが、機能が少なすぎてあきらめたことがありました。これはいけるかも。

インストール

releaseディレクトリからバイナリを取ってきてもよいのですが、Go環境があるので以下で即インストールしました。

go get -u github.com/justjanne/powerline-go

私の場合Powerlineで必要なフォントは既にインストール済みだったので省略します。必要な方はpowerline-fontsなどでお探しください。

後は.bashrcに以下を追加してsource .すれば完了です。

# .bashrc
function _update_ps1() {
    PS1="$(<パス>/powerline-go -error $?)"
}

if [ "$TERM" != "linux" ]; then
    PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND"
fi

あっさり動きました。

カスタマイズ

後は好みに合わせてカスタマイズです。表示をあまりギンギラギンにしたくなかったのと、なるべくPowerlineが幅を取らないようにしたかったのですが、既存のオプションだと少々力不足だったので、forkして雑にカスタマイズしました。完全に自分用なのでプルリクはしません。

  • セグメントの前後のスペースを削除
  • ディレクトリ表示の...を削除
  • プロンプト$の色を変更
  • デフォルト設定を自分好みに変えてオプションを与えなくて済むようにした

powerline-goのディレクトリでgo installを実行してインストールします。

ワンバイナリできびきび動いてくれて安心かつ満足です。AWSやDockerにも対応しているようなのでそのうちチェックしてみます。

Vue.jsサンプルコード(24)テキストフィールドの文章量に応じて縦幅を自動拡張する

$
0
0

24. テキストフィールドの文章量に応じて縦幅を自動拡張する

  • Vue.jsバージョン: 2.5.2
  • テキストフィールドの文章量が増えるとフィールドが下に拡張し、文章量が減ると上に縮小します。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード


ポイント: 前回の23. テキストフィールドの文章量に応じて縦幅を自動拡張するほど厳密ではありません。行数を予測可能なテキストについてのみ使えます。

<textarea class="form-control" v-model="a" :rows="4 + a.length / 40"></textarea>

バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

Rails: Service Objectはもっと使われてもいい(翻訳)

$
0
0

概要

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

なお、Aaron Lasseigne氏の言うService ObjectとAvdi Grimm氏の言うService Objectは違うものを指しているのではないかという意見がBPS社内でありました。

Rails: Service Objectはもっと使われてもいい(翻訳)

Avdi Grimm氏は最近のブログ記事で、Service Objectの盛り上がりに苦言を呈していました。Service Objectを擁護しようと思い、Service Objectにどんな欠点があるのか読んでみたくなりました。私の使うこのツールの長所と短所を知りたかったのです。しかし記事の内容は私の期待とは異なり、私の経験と真っ向からぶつかるものでした。

Avdiは冒頭で、PayPalのIPNデータを処理する「重すぎるコントローラ」を取り上げていました。皆さんもこの巨大過ぎるコントローラアクションを記事でご覧になったでしょう。これはよくある問題であり、修正したくなる問題です。彼はService Objectを1つ作成してコントローラのコードのほとんどをそこに移動しました。移動後の構造は次のような感じです。

class IpnProcessor
  def process_ipn(...)
    # ここにコントローラのコードを書く
  end
end

続いてIpnProcessor.new.process_ipnには「コードの匂い」があることを指摘しています。この命名は冗長であり、同義反復的な悪いコードに見えるとのことです。

ipn processor new process ipn

まさしく彼の言うとおりです。この命名はよくないものであり、Service Objectで通常使われている命名法からも外れています。チームでService Objectを使う場合、callまたはrunといったメソッド名で統一するのが普通です。

class IpnProcessor
  def call(...)
    ...
  end
end

ポイントは、publicなメソッドを1つに限定し、クラス名を見ればそのメソッドの動作がわかるようにすることです。IpnProcessorオブジェクトは、アプリで行う操作の1つを表現します。モデル名は名詞で表すべきであるのと同様に、Service Object名は動詞で表されるべきです。

Avdiは続いて、彼がよりよいソリューションと考えるアイデアを提唱しています。

ところで私のアプリでは、アプリのコードに総合的な名前空間を与えるモジュールを1つ作ることがよくあります。このアプリは「perkolator」(訳注: percolator: 濾過器のもじり)と呼ばれていたので、このモジュール名はPerkolatorとしました。

こうして作成されるモジュールはアプリ名を踏襲し、IPN処理のメソッドを1つ持ちます。

module Perkolator
  def self.process_ipn(...)
    # ...
  end
end

このアプローチのメリットは、「責務の増加」や「Service間の結合」をうまく避けられることです。

彼は、定義に不備のあるオブジェクトは危険であると主張しています。そうしたオブジェクトは本質的にあいまいになるため、本当に追加してよいかどうか疑問の残る機能が追加されやすくなってしまいます。その点には私も賛成です。しかしアプリケーション名のモジュールに全てを入れてしまえば、それこそそのモジュールが乱雑なコード置き場になってしまうのではないでしょうか。多くのRails開発者は、そこにふさわしくない機能が山積みされる「夢の島」と化したapp/controllers/application_controller.rbのことを思い出すでしょう。

Service Objectであれば、IpnProcessorオブジェクトは1つしかなく、publicに呼び出せるメソッドも1つしかありません。これなら、そこにメソッドをさらに追加しようとするときに違和感を覚えてコードレビューで相談を持ちかけるでしょう。(メソッドを増やさずに)機能を追加するには、1個のpublicメソッドの機能を増やすしかありませんが、直ちにそれもおかしいと思えてくるでしょう。1つのメソッドにオプション引数を大量に追加する方法は正しくないと感じられるものです。

2番目の大きな懸念は「Service間の結合」です。Serviceにはアプリのさまざまな操作がカプセル化され、最終的にそれらの操作が組み合わせられて動作します。2つの操作がデータベーステーブルを共有したり、2つのステップとしてさらに大きな処理に組み込まれるかもしれません。コントローラで起きているのはこれではないでしょうか?Service Objectをモジュールに置き換えたところで、この点が変わるでしょうか?何も変わりません。

私の経験では、Service Objectはアプリを介したパスを定義するのに有用です。Service Objectを使って次のようにわかりやすい操作のリストを作成できます。

CreateUserCreateGroupAddUserToGroupBanUserFromGroup

ある処理の手順が正確に定まっており、それらの手順をいつもとは異なる方法で互いに結合したい場合は、それらの手順をservicesディレクトリ内のフォルダに配置して、次のように名前空間を与えます。

Purchase::MadePurchase::Redeemed

Service Objectを利用しても、モジュールが利用できなくなるわけではありません。購入で使う承認トークンを理解して扱えるモジュールならおそらく意味があるでしょう。この場合、Service Objectはこのモジュールを承認に活用して他の処理を行えます。もちろん、これは何がしたいかによってまったく変わってきます。ハンマーしか入っていない道具箱は、本当の道具箱ではありません。

私の経験から

わぉ(いい意味の「わぉ」ではありません)。私はとあるスタートアップ企業でずっと働き続けています。開発者は4人で、従業員は20人そこそこでしょうか。

その企業では、あるRailsアプリを何年も前から使っていました。そのアプリは、コンピュータサイエンス専攻の学生たちが学位を終了する前に無償で作ってリリースしたものでした。学生たちは頭脳明晰でしたが、経験についてははなはだ不十分でした。もし私がその場にいたとしても、もっとうまくやれたとは思えません。実際、彼らは動く製品を作ったのであり、収益にもつながっていました。DHHの言う「期待を遥かに上回る」というやつです。

しかし機能の作り込みはだんだん困難になり、バグ修正もどんどんトリッキーになっていきました。サイトの実行速度も以前より落ちてしまい、仕切り直しが必要になりました。

作業は大変でしたが、それでも進捗はありました。会社は成長し、チームも拡大しました。私たちはコードをモジュールに移動し、巨大なオブジェクトのいくつかを分割しました。そしてある日のこと、ある開発者が「Service Objectを使ってみてはどうか」と持ちかけてきました。やがてチームは、その決定が勝利を導いたことに気づきました。

あるグループにユーザーを1人追加する方法が必要だとしましょう。追加するのはUser#join_groupGroup#add_memberでしょうか?違います。追加するのは、グループとユーザーを1つずつ受け取るAddGroupMemberというService Objectです。今度は、送信の必要なメールアドレスについてはどうでしょう。新しいメンバーにはグループから紹介メールを送信すべきであり、グループは新メンバー追加のメールを受け取るべきです。これも問題ありません。グループに新メンバーを追加する処理に含めることができます。

私たちは、このような状況をある程度避ける方法を見つけました。操作を無様な方法でモデルにアタッチするのではなく、それにふさわしい住み家を与えました。私たちはここから最終的にActiveInteractionというgemを作り上げました。このgemは私たちにとって非常にうまく動きましたし、他の人にとっても有用です。ActiveModelが名詞を扱うように、ActiveInteractionでは動詞を扱います。グループで利用できるインターフェイスも統一されました。私たちはこれらの扱い方、エラーハンドリング、呼び方を理解して、作成のための枠組みを手に入れました。

訳注: ActiveInteraction gemは「Commandパターン」を実装したものだそうです。

クラスからの操作の切り出しも素直に行えました。こうしたコードは簡単にテストできます。私たちは巨大なクジラを一口ずつ、おいしく味わいました。

これを手続きとして実現することもできましたが、もしそうしていたら枠組みは失われていたでしょう。ある開発者はRubyエラーをraiseし、別の開発者はエラーをreturnオブジェクトにアタッチし、また別の開発者はタプルの一部として送信するというようにバラバラになっていたことでしょう。チーム開発において、コードの標準化は成功に欠かせない重要な部分のひとつです。

彼が間違っているとも限らない

私はAvdiをとても尊敬していますが、この件に関して彼と私の意見は明らかに異なっています。もしかすると、彼が気づいていることで私がまだ見落としていることがあるのかもしれません。私はこれまでにもいろんなことに気づきましたし、今後も気づくことがあるでしょう。しかし今のところ、彼の主張から学べる点がまだ見つかりません。

私にとって確かなのは、Service Objectは業務を改善してくれたということです。Service Objectを使っている他の人に尋ねてみた結果も、肯定的な経験が圧倒多数でした。私はフリーランス開発者として、コードをきれいにするためにチームにService Objectを導入する価値はおそらく今後数年変わらないと思います。本記事への肯定的な評価が多い方に賭けます。

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Ruby: Chain of Responsibilityパターンの解説(翻訳)

週刊Railsウォッチ(20171208)最近のRailsフロントエンド事情、国際化gem、mallocでRubyのメモリが倍増、るびま記事募集ほか

$
0
0

こんにちは、hachi8833です。

Railsウォッチ公開前に行われているつっつき会の後、軽呑みになだれこみました。「Service ObjectはパターンじゃないんではGoFデザインパターンほど定義のコンセンサスが取れていないのでデザインパターンの一種のように扱わない方がいいのでは」「名前変えて欲しい」みたいな話が飛び交ったような気がします。

12月のRailsウォッチ2回目、いってみましょう。年の瀬の足音が聞こえる…

Rails: 今週の改修

add_indexでPostgreSQLの演算子クラスをサポート

# activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb#249
       def test_index_with_opclass
         with_example_table do
-          @connection.add_index "ex", "data varchar_pattern_ops"
-          index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
-          assert_equal "data varchar_pattern_ops", index.columns
+          @connection.add_index "ex", "data", opclass: "varchar_pattern_ops"
+          index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+          assert_equal ["data"], index.columns

-          @connection.remove_index "ex", "data varchar_pattern_ops"
-          assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
+          @connection.remove_index "ex", "data"
+          assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
         end
       end

つっつきボイス: 「PostgreSQLのoperator classってこれか↓」

演算子クラスにより、その列のインデックスで使用される演算子が特定されます。 例えば、int4型に対するB-treeインデックスには、int4_opsクラスを使用します。 この演算子クラスには、int4型の値用の比較関数が含まれています。 実際には、通常、列のデータ型のデフォルト演算子クラスで十分です。 演算子クラスを持つ主な理由は、いくつかのデータ型では、複数の有意義なインデックスの振舞いがあり得るということです。
https://www.postgresql.jp/document/9.6/html/indexes-opclass.htmlより

「それにしてもこのissue、随分スレが伸びてますね」

PostgreSQLの外部キーをバリデーションなしでも作成できる機能

外部キー追加によるパフォーマンス低下回避のためだそうです。

  • valid: falseオプションを指定すると無効な外部キーを作成できる機能
  • 外部キーバリデーション用のvalidate_foreign_keyメソッドを追加
# activerecord/test/cases/migration/foreign_key_test.rb#230
+        if ActiveRecord::Base.connection.supports_validate_constraints?
+          def test_add_invalid_foreign_key
+            @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+            foreign_keys = @connection.foreign_keys("astronauts")
+            assert_equal 1, foreign_keys.size
+
+            fk = foreign_keys.first
+            refute fk.validated?
+          end

つっつきボイス: 「PostgreSQL寄りの改修ほんとうに増えましたね」

preload_link_tagヘルパーを追加

HTTP/2 Early hintsがプロキシでサポートされている場合に対応するそうです。

preload_link_tag("custom_theme.css")
# => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />

preload_link_tag("/videos/video.webm")
# => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />

preload_link_tag(post_path(format: :json), as: "fetch")
# => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
...
# actionview/lib/action_view/helpers/asset_tag_helper.rb#260
+        early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
+        early_hints_link += "; type=#{mime_type}" if mime_type
+        early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
+        early_hints_link += "; nopush" if nopush
+
+        request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request

つっつきボイス: 「PreloadってW3Cの仕様にあるんですね↓: すごく新しい」

Preload: W3C Editor’s Draft 30 August 2017

ActiveRecordのスコープ名に予約名を使えないよう修正

#
+  def test_scopes_name_is_relation_method
+    conflicts = [
+      :records,
+      :to_ary,
+      :to_sql,
+      :explain
+    ]
+
+    conflicts.each do |name|
+      e = assert_raises ArgumentError do
+        Class.new(Post).class_eval { scope name, -> { where(approved: true) } }
+      end
+      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
+    end
+  end

つっつきボイス: 「Rails開発普通にやっていれば、使ってはいけない名前ってだいたい見当は付きますけどね」「それでもありがたいです」

Rails

Railsの現代的なフロントエンド事情を理解する

TechRacho翻訳記事でお世話になっているEvil Martiansの記事です。Asset PipelineからWebpackに移行した事情などを解説しています。


「Sprocketsの改修(左)が盛り下がる一方、Webpackの改修頻度は高まっている」
同記事より


つっつきボイス: 「これはいいまとめ記事」「PostCSSって初めて知った」「そういえばSassとかCompassってRubyで書かれているけど、そういうのもJSでやりたいってことか: 気持わかる」「Compassって一見便利だけどバッドノウハウの塊ですね」「(Compassに入れ込んだ日々を返してくれ…)」

参考: PostCSS まとめ


postcss.orgより

ニューオリンズRubyカンファレンス動画47本(Ruby Weeklyより)

他にRubyConf 2017を振り返る記事も紹介されていました。


つっつきボイス: 「時間をかけて動画を見る余裕はなかなかないなー」「ところで最近YouTubeの自動字幕の日本語機械翻訳、前より良くなったっぽいですね」「それでも英語の自動字幕の方が精度高いですね: 大文字小文字を区別してたりして驚異」

RailsのモデルidにPostgreSQLのUUIDを使う(Ruby Weeklyより)

短い記事です。新規プロジェクト以外はやめておくほうがよいかもだそうです。


つっつきボイス: 「そりゃもう、IDを後から変えるとか自殺行為w」「ところでID生成については以下の記事↓がとてもよくまとまってますね」

参考: Qiita ID生成大全

RailsでHTTP OPTIONSをうまく扱う方法(Ruby Weeklyより)

# route.rb
  match '*path', {
    controller: 'application',
    action: 'options',
    constraints: { method: 'OPTIONS' },
    via: [:options]
  }

# コントローラ
  class Api::V1::UsersController < ApplicationController
    options do
      {
        schemas: {
          accepts: Company.json_schema,
          returns: Company.json_schema
        },
        meta: { max_per_page: 100 }
      }
    end
  end

つっつきボイス: 「あー、確かにRailsでHTTP OPTIONS扱うの割りと面倒ではある」

参考: MDN HTTP OPTIONS

ActionCableを単独で使ってみた(RubyFlowより)

ActionCableそのものの解説も充実しています。

# 同記事より
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :uuid

    def connect
      self.uuid = SecureRandom.urlsafe_base64
    end
  end
end

つっつきボイス: 「ストリーミングだけやりたいときなんかはRailsなくてもいいのかも: それこそmrubyでやれればよさそう」

Rails 5.2の新機能: HashWithIndifferentAccessfetch_values

とても短い記事です。


つっつきボイス:fetch_values今までなかったのかー」「記事からリンクされている今年4月の#28316の方がわかりやすいですね↓」

# 更新前
hash = ActiveSupport::HashWithIndifferentAccess.new
hash[:a] = 'x'
hash[:b] = 'y'
hash.fetch_values('a', 'b') # => KeyError: key not found: "a"

# 更新後
hash = ActiveSupport::HashWithIndifferentAccess.new
hash[:a] = 'x'
hash[:b] = 'y'
hash.fetch_values('a', 'b') # => ["x", "y"]
hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
hash.fetch_values('a', 'c') # => KeyError: key not found: "c"

asset_sync: RailsとS3の間でアセットを同期するgem

  #config/environments/production.rb
  config.action_controller.asset_host = "//#{ENV['FOG_DIRECTORY']}.s3.amazonaws.com" # S3の場合
  config.action_controller.asset_host = "//#{ENV['FOG_DIRECTORY']}.storage.googleapis.com" # Google Cloud Storageの場合

つっつきボイス: 「これなかなかよさそう: たとえばアセットをS3に置いてRails側でアセットのプリコンパイルをしなくて済むようにすればデプロイも速くなるし」「yarnただでさえ重いし」「★も1600超えてるし使って大丈夫そう」

多言語化gem「Mobility」が0.3にバージョンアップ

Module Builderパターン」のshioyamaさんがメンテしている多言語化gem: Mobilityがバージョンアップされたとのことです。

  • Rails 5.2とSequel 5をサポート
  • ActiveRecordのdirty系メソッド
  • ロケールのフォールバック
  • モデルをdupすると翻訳も複製
  • etc

つっつきボイス: 「gemがメンテされてるのはいいこと」

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

i18n-tasks: 国際化/多言語化を支援する静的分析gem


github.com/glebm/i18n-tasksより


つっつきボイス: 「このgem今まで知らなかったんですが、前から結構使われてるみたい: 訳ぬけチェックはありがたいです」「さすがに翻訳ミスまではチェックできないだろうけど」「画像に埋まってる文字の翻訳も手に余るでしょうね」

今見ると、Google翻訳で雑に訳を埋める機能までありました。くれぐれもそのまま本番で使わないようご注意ください。

$ i18n-tasks translate-missing
# accepts from and locales options:
$ i18n-tasks translate-missing --from base es fr

activerecord-cause: ActiveRecordのSQL発行タイミングをログ出力するgem

morimorihogeさんが見つけました。

# spec/spec_helper.rb
User.all

# output to log file.
# D, [2015-04-15T22:13:46.928908 #66812] DEBUG -- :   User Load (0.1ms)  SELECT "users".* FROM "users"
# D, [2015-04-15T22:13:46.929038 #66812] DEBUG -- :   User Load (ActiveRecord::Cause)  SELECT "users".* FROM "users" caused by /Users/joker/srcs/activerecord-cause/spec/activerecord/cause_spec.rb:16:in `block (3 levels) in <top (required)>'

つっつきボイス: 「欲しい機能をさっと作る、さすがjoker1007さん」「ビューが重いと思ったら実は背後のSQLが遅いとかざらにあるけど、そういう問題の解明で便利そう」「bulletでカバーしきれないときとか」

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

Rails Developers Meetup 2017明日12/09開催

錚々たるメンバーが登壇します。キャンセル待ち223人と大盛況です。


techplay.jp

Ruby trunkより

やっぱりwarn_past_scope: trueしたい

3年前にボツになった同様の#10661を引用し、デフォルトオフでいいので変数名の衝突をチェックできるようにしたいという提案です。


つっつきボイス: 「parse.yだ」「parse.y…」「むかーしparse.yを読んでみたことあるけど手強い」「10000行超えてますしね」

Ruby

mallocでマルチスレッドのRubyプログラムのメモリ使用量が倍増することがある

Puma/Unicorn/Passengerの効率を最大化する設定」のNate Berkopecさんの記事です。


つっつきボイス: 「なかなかヘビーかつ濃厚な内容だけどためになりそう」

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

Ruby 3×3の進捗ってどうよ

Ruby 3×3の動きに注目しつつ、昨年提案されたGuildという手法(参考: 「Concurrency in Ruby 3 with Guilds」)による並列化がRuby trunkに見当たらないのを残念に思っているそうです。


olivierlacan.comより

参考: A proposal of new concurrency model for Ruby 3(RubyKaigi 2016資料: PDF)

RubyでDSLを書く

# 同記事より
ConstructionGirl.create_structure(:owner) do
  name { "Michael number: #{Time.now.to_i}" }
  age { [20, 18, 30].sample }
end
# => #<Owner:0x000000040d3258 @name=”Michael number: 1511421963", @age=30>

つっつきボイス: 「オレオレDSL、Rubyistなら一度は通る道ですね」

Embulk: Javaで書かれた一括読み込みツール


embulk.orgより

最初気が付かなかったのですが、Java製です。joker1007さんやmgi166さんなどがRubyでプラグインを書いています。


つっつきボイス: 「↓この図が一目瞭然ですね: Fluentdみたいに常に流し込むのでなく、バッチでアップロード&変換までやってくれるやつです」「うまくはまれば某案件で使ってみようかと思っているところ」


www.embulk.orgより

参考: Fluentdのバッチ版Embulk(エンバルク)のまとめ

るびま執筆者募集


つっつきボイス: 「TechRachoからも記事出してみようか」「Railsネタでもいいのかしら」

そういえば今のるびまはMarkdownで書けるようになったのでした↓。

ダウンロードの多いgemオールタイムトップテン(Ruby Weeklyより)


つっつきボイス: 「このダウンロード数、ほとんどはCIが回しているやつでしょうねー」「人気の指標としては当てにできない感じ」

海外のRubyアドベントカレンダー


つっつきボイス: 「雑にしか探していませんが、こんなに少ないと思わなかった:技術向けアドベントカレンダーはほぼ日本だけのような印象でした」「後は韓国に1つあったぐらい」「もともと西洋のアドベントカレンダーはこういう実物↓ですしね: お菓子やおもちゃ入れたりとか」


businessinsider.comより

BPSアドベントカレンダー2017もどうぞよろしく。

データベース

PostgreSQL: NOT NULL制約を追加して高速化(Postgres Weeklyより)

フランス企業のブログ記事はちょっと珍しい気がしました。


medium.com/doctolib-engineeringより


つっつきボイス: 「NOT NULLしないとたいてい遅くなりますね」

Pgexercise.com: PostgreSQLの出題サイト(Postgres Weeklyより)



pgexercises.comより


つっつきボイス: 「おー、これいいじゃない! 採用面接で目の前でやってもらうとか」「インターフェイスもいいですね」

一同でとりあえずいくつか解いてみたりしました。

pg_hexedit: PostgreSQLのリレーションファイル向け16進エディタ(Postgres Weeklyより)

wxHexEditorという16進エディタを元にしているようです。

pgeoghegan.blogspot.jpより


つっつきボイス: 「こういうのを持ち出すときは最後の手段ですねw」

check_pgactivity: NagiosのPostgreSQLプラグイン(Postgres Weeklyより)

# OPMDG/check_pgactivityより
check_pgactivity -p 5433 -h slave --service hit_ratio --dbexclude idelone --dbexclude "(?i:sleep)" -w 90% -c 80%

JavaScript

JavaScriptのthisって結局何?

割と短い記事です。詳しくはMDN: thisを見て欲しいとのことでした。

CSS/HTML/フロントエンド

カスタムプロパティ(CSS変数)入門

/* 同記事より */
    --width: 80%
    @media screen and (min-width: 768px) and (max-width: 1020px) {
        --width: 60%;
    }
    @media screen and (min-width: 1020px) {
        --width: 40%
    }

つっつきボイス: 「最初CSS変数という言葉を見ていつの間に?と思ったら、babaさんに『普通カスタムプロパティって言いますね』とツッコまれました」「記事にも書いてありますね」「ただしまだ使うには早い」「IE11で動かないんですね」「それにしても--で書くのかー」「Sassみたいに$にして欲しかった」

Sonarwhal: Webサイトをチェックするサービス(Frontend Focusより)


sonarwhalのマスコットnellieちゃん
24ways.orgより

オンライン版の他にコマンドライン版もあるのが特徴です。


24ways.orgより


つっつきボイス: 「CLIで動くならCIと連携できるということだから、どっかに出力しておくのは悪くない気がする: 全部の項目に対応することもないとは思うけど」

(遠い)未来のCSS(Frontend Focusより)


つっつきボイス: 「Houdiniというタスクフォースをこれで知りました: 仕様書いてる人のスライドだそうです」「Houdiniは結構有名ですね」「それにしてもこのスライド…めくりのアニメーションが見づらい」

参考: CSSのHoudiniとは何者か

Houdiniは明らかにマジシャンのハリー・フーディーニですね。

BootstrapよりCSS Gridの方がレイアウト作成に向いていると思う理由(Frontend Focusより)


hackernoon.comより

その他

Kata Container: コンテナ実装のニューフェイス


katacontainers.ioより

コンテナ間でカーネルを共有しないタイプの実装で、OCI(Open Container Initiative)に準拠しているそうです。


www.opencontainers.orgより

参考: コンテナの軽量さと仮想マシンの堅牢さを兼ね備えた新しいコンテナ実装「Kata Containers」、OpenStack Foundationが発表

deep-image-prior: 相当崩れた画像も復元するニューラルネットワークツール(Python)


つっつきボイス: 「↓このレベルで復元するのか」「しかも学習不要らしいです」「この辺は研究でもホットな分野なので、多分論文読めば何やってるかぐらいはふんわりわかる(アルゴリズムはともかく概要ぐらいは)」

Matzインタビュー

Project-modeからProduct-modeの時代へ


martinfowler.comより

かのMartin Fowler先生のサイトの記事です(筆者は別の人)。ソフトウェア開発をプロジェクトではなくProduct-modeというコンセプトで運営する方法論について解説しています。

QNNcloud: NTTが無料で公開した量子ニューラルネットワーク



qnncloud.comより

まだアカウントを作ってみたところです。

番外

日本語の学習はトップレベルに難しいらしい


www.boredpanda.comより

ヨーロッパ住民にとって。

考えるだけで楽器を演奏

ee_at_9e2_short from Thomas Deuel on Vimeo.


つっつきボイス: 「ヘッドギア付けてるとどうしても患者さんっぽく見える」「演奏していると言えるんだろうか」

年号

シンプル化とは

http://gkojax-text.tumblr.com/post/168287716115

鳥のような恐竜の化石がモンゴルで見つかる

作り物感満載。


今週は以上です。

バックナンバー(2017年度後半)

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured


Rails5: ActiveRecord標準のattributes API(翻訳)

$
0
0

ActiveRecordに任意の属性を定義したり既存の属性を上書きしたりできるRails標準機能です。

概要

MITライセンスに基いて翻訳・公開いたします。

参考: Rails 5のActive Record attributes APIについて y-yagiさんの良記事です。

Rails5: ActiveRecord標準のattributes API(翻訳)

メソッド

定数

NO_DEFAULT_PROVIDED = Object.new

publicインスタンスメソッド

attribute(name, cast_type = Type::Value.new, **options)Link

型を持つ属性をこのモデルに定義します。必要な場合、既存の属性の型をオーバーライドします。これにより、モデルへの代入時に値がSQLと相互に変換される方法を制御できるようになります。また、ActiveRecord::Base.whereに渡される値の振る舞いも変更されます。これを使って、実装の詳細やモンキーパッチに依存せずに、ActiveRecordの多くに渡ってドメインオブジェクトを使えるようになります。

  • name” 属性メソッドの定義対象となるメソッド名、およびこれを適用するカラム。
  • cast_type: この属性で使われる:string:integerなどの型オブジェクト。利用例について詳しくは以下のカスタム型オブジェクトの情報をご覧ください。

オプション

以下のオプションを渡せます。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。
  • array:(PostgreSQLのみ)array型にならなければならないことを指定する(以下の例を参照)。

  • range:(PostgreSQLのみ)range型にならなければならないことを指定する(以下の例を参照)。

ActiveRecordで検出される型はオーバーライド可能です。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.decimal :price_in_cents
end
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end
store_listing = StoreListing.new(price_in_cents: '10.1')

# 変更前
store_listing.price_in_cents # => BigDecimal.new(10.1)

class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :integer
end

# 変更後
store_listing.price_in_cents # => 10

デフォルト値を指定することもできます。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.string :my_string, default: "original default"
end

StoreListing.new.my_string # => "original default"
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :my_string, :string, default: "new default"
end

StoreListing.new.my_string # => "new default"

class Product < ActiveRecord::Base
  attribute :my_default_proc, :datetime, default: -> { Time.now }
end

Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600

属性の背後にデータベースのカラムがなくても構いません。

# app/models/my_model.rb
class MyModel < ActiveRecord::Base
  attribute :my_string, :string
  attribute :my_int_array, :integer, array: true
  attribute :my_float_range, :float, range: true
end
model = MyModel.new(
  my_string: "string",
  my_int_array: ["1", "2", "3"],
  my_float_range: "[1,3.5]",
)
model.attributes
# =>
  {
    my_string: "string",
    my_int_array: [1, 2, 3],
    my_float_range: 1.0..3.5
  }

カスタム型の作成

値型で定義されるメソッドと対応していれば、独自の型を定義することもできます。この型オブジェクトでは、deserializeメソッドまたはcastメソッドが呼び出され、データベースやコントローラから受け取った生の入力を取ります。前提とされるAPIについてはActiveModel::Type::Valueをご覧ください。型オブジェクトは既存の型かActiveRecord::Type::Valueのいずれかを継承することが推奨されます。

class MoneyType < ActiveRecord::Type::Integer
  def cast(value)
    if !value.kind_of?(Numeric) && value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      super(price_in_dollars * 100)
    else
      super
    end
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :money
end
store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

カスタム型の作成について詳しくは、ActiveModel::Type::Valueのドキュメントをご覧ください。型をシンボルで参照できるように登録する方法については、ActiveRecord::Type.registerをご覧ください。シンボルの代わりに型オブジェクトを直接渡すこともできます。

クエリ

ActiveRecord::Base.whereが呼び出されると、そのモデルクラスで定義された型が使われ、型オブジェクトでserializeを呼んで値がSQLに変換されます。次の例をご覧ください。

class Money < Struct.new(:amount, :currency)
end
class MoneyType < Type::Value
  def initialize(currency_converter:)
    @currency_converter = currency_converter
  end

  # deserializeまたはcastの結果が値になる
  # ここではMoneyのインスタンスになることが前提
  def serialize(value)
    value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
    value_in_bitcoins.amount
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/product.rb
class Product < ActiveRecord::Base
  currency_converter = ConversionRatesFromTheInternet.new
  attribute :price_in_bitcoins, :money, currency_converter: currency_converter
end
Product.where(price_in_bitcoins: Money.new(5, "USD"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.02230

Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.03412

dirtyトラッキング

属性の型には、dirtyトラッキングの実行方法を変更する機会が与えられます。ActiveModel::Dirtychanged?changed_in_place?が呼び出されます。これらのメソッドについて詳しくはActiveModel::Type::Valueをご覧ください。

define_attribute( name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true )Link

これはattributeの背後にある低レベルAPIです。型オブジェクトのみを受け取り、スキーマの読み込みを待たずに即座に動作します。自動スキーマ検出と#attributeはどちらもこのメソッドを背後で呼び出します。このメソッドが提供されていることでプラグイン作者によって使われる可能性もありますが、おそらくアプリのコードで#attributeを使うべきです。

  • name: 定義される属性の名前。Stringで定義します。
  • cast_type: この属性で使う型オブジェクト。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。procを渡すことも可能であり、新しい値が必要になるたびにprocが1度ずつ呼び出される。

  • user_provided_default: デフォルト値がcastdeserializeでキャストされるべきかどうかを指定。

  • GitHubソース

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

Rails: JSON Patchでパフォーマンスを向上(翻訳)

$
0
0

概要

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

Rails: JSON Patchでパフォーマンスを向上(翻訳)

FormAPIはPDFを生成するサービスです。バックエンドはRuby on Railsで書かれており、PDFテンプレートエディタはReactで作られています。PostgreSQLを用いてテンプレートのフィールドデータをjsonbカラムに保存しています。

テンプレートエディタの最初のバージョンでは、素朴なアプローチでフィールドデータを保存していました。フィールドが変更されるたびにarray全体をサーバーにpostしていたのです。立ち上げ当初のMVP(Minimum Viable Product)はこの方法で十分動きましたが、やがて顧客がPDFテンプレートに500個ものフィールドを設定するようになり、実行に5〜10秒も要するリクエストにアラートが出始めました。

明らかな解決法は、変更点だけをサーバーに送信することです。ReactアプリではReduxを使っているので、Reduxのアクションを送信することも考えましたが、Reduxのアクションには複雑なロジックが若干含まれているため、私たちのFormAPIではその手が使えませんでした。コードをRubyで書き直したくなかったのですが、仮にNode.jsを使っていたなら同じJavaScriptコードをサーバー側で再利用できたかもしれません。

他の選択肢としては、純粋なJSONオブジェクトの差分を取り出してそれをサーバーに送信する方法が考えられます。その場合JSON Patchを利用できます。

JSON Patchは、JSONドキュメントの変更点を記述するフォーマットです。JSONの一部だけが変更された場合にドキュメント全体の送信を避けるために利用できます。

こういうものは独自にこしらえる1よりも、IETF標準(RFC6902)を利用する方がほとんどの場合よい結果が得られると思います。以下のオープンソースライブラリを利用しました。

  • fast-json-patch — クライアント側でJSON Patchを生成するNPMパッケージです
  • hana — RailsサーバーでJSON Patchを適用するRuby gemです

以下のようにしてRailsモデルのjsonカラムにJSON Patchを適用しました。

class Template < ApplicationRecord
  # エラーが発生したらここでエラーを保存し、
  # バリデーション中にそれらを追加する
  attr_accessor :json_patch_errors
  validate :add_json_patch_errors
  after_save :clear_json_patches

  attr_reader :fields_patch

  # メソッドが呼び出されると即座にJSON Patchが適用される
  def fields_patch=(patch_data)
    # 後でアクセスしたい場合
    @fields_patch = patch_data
    self.json_patch_errors ||= {}
    json_patch_errors.delete :fields

    unless patch_data.is_a?(Array)
      json_patch_errors[:fields] =
        'JSON patch data was not an array.'
      return
    end

    hana_patch = Hana::Patch.new(patch_data)
    begin
      hana_patch.apply(fields)
    rescue Hana::Patch::Exception => ex
      json_patch_errors[:fields] =
        "Could not apply JSON patch to \"fields\": #{ex.message}"
    end
  end

  # データ再読み込み時にすべてのJSON Patchとエラーをクリア
  def reload
    super
    clear_json_patches
  end

  private

  def add_json_patch_errors
    return unless json_patch_errors.present?
    json_patch_errors.each do |attribute, errors|
      errors.add(attribute, errors)
    end
  end

  def clear_json_patches
    @fields_patch = nil
    self.json_patch_errors = nil
  end
end

こちらのRSpecテストをコピーして実装が正しいことを確認できます。そのうちこれをgemとしてリリースするかもしれません。

permittedパラメータとしてfields_patchをコントローラに追加しました。

params.require(:template).permit(
  fields: {},
).tap do |permitted|
  # arrayやhashのネストはややこしい
  if params[:template][:fields_patch].is_a?(Array)
    permitted[:fields_patch] = params[:template][:fields_patch].
      map(&:permit!)
  end
end

上のコードは、:fields_patchを通常の属性として扱い、update_attributesの間にJSON Patchを適用することを示しています。JSON Patchの適用に失敗すると、バリデーション中にエラーが追加されます。

フロントエンド側の実装は実に簡単です。改修前のコードは次のようになっていました。

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields = template.fields.toJS()
}

新しいコードでは、JSON Patchをfields_patch属性として送信します。

import { compare as jsonPatchCompare } from 'fast-json-patch'

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields_patch = jsonPatchCompare(
    previousTemplate.fields.toJS(), template.fields.toJS())
}

以下は新しいコードから送信されるAJAXリクエストの例です。

{
  "template": {
    "fields_patch": [
      {
        "op": "replace",
        "path": "/2/name",
        "value": "image_field_2"
      }
    ]
  }
}

コードの変更はわずか数行で済んだにもかかわらず、送信されるデータを大きく削減できました。

JSON Patchのもうひとつのメリットは、複数ユーザーによる同時編集をずっと楽にサポートできることです。JSON Patchは多くの場合任意の順序で適用することができます。replaceinsert操作しか含まれていない場合は特にそうです。衝突が発生した場合、最新のデータをリロードしてユーザーにやり直してもらうだけで済みます。また、WebSocketを使えばサーバーからブラウザにJSON Patchを送信して全クライアントを同期することもできます。

お読みいただきありがとうございました。コメントはHacker Newsまでどうぞ。

関連記事

Rails: ActiveModelSerializersでAPIを作る–Part 1(翻訳)

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

Ruby on Railsで使ってうれしい19のgem(翻訳)


  1. FormAPIの最初のバージョンではフィールド名記述に独自文法を使っていましたが、その後でJSON Pointersというものを発見しました。
    それまでスラッシュ文字/をエスケープするなど思いもよりませんでしたが、仕様の一部に含まれています。 

Ruby 2.5新メソッド: Dir.children と Dir.each_child(翻訳)

$
0
0

概要

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

Ruby 2.5新メソッド: Dir.children と Dir.each_child(翻訳)

本記事はRuby 2.5シリーズのひとつです。

先ごろRuby 2.5.0-preview1がリリースされました

Dir.entriesはRuby 2.4にあるメソッドで、シェルコマンドls -aの結果をarrayで返します。

 > Dir.entries("/Users/john/Desktop/test")
 => [".", "..", ".config", "program.rb", "group.txt"]

ls -aコマンドの結果の各値を列挙してyieldするDir.foreachメソッドもあります。

> Dir.foreach("/Users/john/Desktop/test") { |child| puts child }
.
..
.config
program.rb
group.txt
test2

結果には、カレントディレクトリを表す.や親ディレクトリを表す..が含まれています。

子ファイルや子ディレクトリだけにアクセスしたくない場合、arrayの[".", ".."]の部分は不要です。

これは非常によくあるユースケースであり、欲しい結果を得るためにDir.entries(path) - [".", ".."]のような方法を使わなければならないでしょう。

この問題を解決するためにRuby 2.5でDir.childrenが導入されました。ls -aコマンドの出力からカレントディレクトリを表す.や親ディレクトリを表す..を除いた結果を返します。

irb> Dir.children("/Users/mohitnatoo/Desktop/test")
 => [".config", "program.rb", "group.txt"]

その他に、列挙中にカレントディレクトリや親ディレクトリのyieldを回避するDir.each_childメソッドも利用できるようになりました。

irb> Dir.each_child("/Users/mohitnatoo/Desktop/test") { |child| puts child }
.config
program.rb
group.txt
test2

#11302の議論で言及されているように、これらの名前は既存のPathname#childrenメソッドやPathname#each_childメソッドに合わせてあります。

シンプルな機能追加に見えますが、実際は2年以上前にあげられたissueです。

関連記事

Ruby 2.5のパフォーマンス改善点(翻訳)

Ruby: ぼっち演算子`&.`の落とし穴(翻訳)

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

$
0
0

概要

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

なお、原文のbefore_validateは訳文でbefore_validationに修正しました。

Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

ActiveRecordのコールバックが多くのプロジェクトで乱用され、もっとよい方法で簡単に回避できるユースケースであっても誤った理由で使われているのは今に始まったことではありません。実行される処理と関係のない、かなり逸脱した理由で多用される、特殊なコールバックが1つあります。それがbefore_validationコールバックです。

データ整形

データ整形、特に文字列のストリップは、アプリの主要な部分を占めることが多い処理です。たとえば、スペースで問題が生じないように URLをストリップすることを考えてみましょう。どのようなアプローチが考えられるでしょうか。

1つの方法は、before_validationを使うことです。特にデータ形式のバリデーションを行っている場合です。

# app/models/my_model.rb
class MyModel
  before_validation :strip_url

  private

  def strip_url
    self.url = url.to_s.strip
  end
end

これで処理は完了します。しかしテストはどうすればよいでしょうか。そのモデルでvalid?メソッドを呼び、URLがストリップされたかどうかをチェックする必要があるのでしょうか?これはいかにも変ですし、次のspecを見れば違和感がもっとよくわかるでしょう。

# spec/models/my_model_spec.rb
require "rails_helper"

RSpec.describe MyModel, type: :model do
  it "バリデーション前にURLをストリップする" do
    model = MyModel.new(url: "  http://rubyonrails.org")

    model.valid?

    expect(model.url).to eq "http://rubyonrails.org"
  end
end

このコードがTDDの結果であるとはちょっと考えられません。では他に方法はあるでしょうか。

次のように、単に専用の属性ライターメソッド(#url=)を書く方法ならどうでしょう。

# app/models/my_model.rb
class MyModel
  def url=(val)
    super(val.to_s.strip)
  end
end

この機能に対応するspecとして次が考えられます。

# spec/models/my_model_spec.rb
require "rails_helper"

RSpec.describe MyModel, type: :model do
  it "strips URL" do
    model = MyModel.new(url: "  http://rubyonrails.org")

    expect(model.url).to eq "http://rubyonrails.org"
  end
end

この実装とspecならどちらもずっとシンプルになりますし、ずっと自然です。データ整形はバリデーションと何の関係もないので、このようなユースケースを扱うためにバリデーションがらみのコールバックを使う必要はありません。

属性やリレーションシップを代入する

もうひとつのよくあるシナリオは、属性やリレーションシップの代入です。たとえば、contentを1つ持つコメントと、current_userになる著者(author)を作成し、かつパフォーマンス上の理由から何らかのdenormalization(非正規化)を行って、current_userが属するコメントにgroupを直接代入したいとします。以下はbefore_validationコールバックを少し使った例です。

Comment.create!(
  content: content,
  author: current_user,
)
# app/models/my_model.rb
class MyModel
  before_validation :assign_group

  private

  def assign_group
    self.group = author.group if author
  end
end

これも先のデータ整形のユースケースとかなり似ています。この機能のテストを書くためにvalid?を呼ぶ必要があるのでしょうか?バリデーションは、属性やリレーションシップの代入とは何の関係もないのですから、必要性はさほど感じられません。これは、次のようにもっとシンプルで明示的な方法で扱えます。

Comment.create!(
  content: content,
  author: current_user,
  group: current_user.group,
)

マジックを使わない単なる代入なので、テストも理解も簡単です。

まとめ

before_validationコールバックが考えうる限り最善の選択になることがもしかするとあるかもしれません(私はそのような状況に出会ったことがありませんが)。しかし私は、データ整形や属性/関連付けの代入は、before_validationコールバックに適していないとある程度確信しています。

関連記事

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

3年以上かけて培ったRails開発のコツ集大成(翻訳)

Vue.jsサンプルコード(25)クイズの答えをダイアログに入力しないと送信できないようにする

$
0
0

25. クイズの答えをダイアログに入力しないと送信できないようにする

  • Vue.jsバージョン: 2.5.2
  • [送信]ボタンを押すとダイアログにクイズが表示されます。正解しない場合は「不正解」と表示されます。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード


ポイント: formタグで@submit="c"のようにイベントと関数を指定できます。イベントはform以外のタグでも設定できます。

<form @submit="c" method="POST" action="" ref="form">

サンプルでは正解した場合の送信も抑制してあります。

...
else {e.preventDefault()}
...

@で指定できるイベントは他にもありますが、以下によるとイベントの種類の公式なリストはまだないようです。


バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

Viewing all 1838 articles
Browse latest View live