Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20201012前編)Railsの隠し機能routing visualizer、action_args gem、N+1用goldiloader gemほか

こんにちは、hachi8833です。Kaigi on Railsのスライドをまとめてくださった記事を見つけました🙇。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

Rails: 先週の改修(Rails公式ニュースより)

久しぶりにRails公式の更新情報が出ていました↓。


つっつきボイス:「ちなみに上のほとんどを先週のウォッチで先取りしていてちょっと嬉しいです」「ホントだ、バックグラウンドジョブもintervalデータ型もActive Storageコンフィグも先週やってるし」「こうやって毎週追っていると新しい機能とかの話がしやすくなるのがいいですよね」

以下のコミットリストのChangelogを中心に見繕いました。

「6.1のリリースはそう遠くないのかな?」「6.1はだいぶ機能が増えているのでproductionで使う前に検証しておきたいですね」


「そういえば今週は@amatsudaさんが集中的にコミットしていて何事かと思ったらsendpublic_sendに変更していました↓」「たしかに、可能ならsendよりpublic_sendを使うべき」


同コミットリストより

参考: When to use send and public_send methods in ruby? - Stack Overflow

Active Storage、Action Text、Action MailboxのActiveRecord::BaseRecordに切り出した


つっつきボイス:「ActiveStorageActionTextActionMailboxで使われているActiveRecord::BaseRecordに置き換えたのね」「それぞれActiveStorage::RecordActionText::RecordActionMailbox::Recordという形になってる」

# actionmailbox/app/models/action_mailbox/inbound_email.rb#L27
- class InboundEmail < ActiveRecord::Base
+ class InboundEmail < Record
    self.table_name = "action_mailbox_inbound_emails"

    include Incineratable, MessageId, Routable
    has_one_attached :raw_email
    enum status: %i[ pending processing delivered failed bounced ]
    def mail
      @mail ||= Mail.from_source(source)
    end
    def source
      @source ||= raw_email.download
    end
    def processed?
      delivered? || failed? || bounced?
    end
  end
end
# actionmailbox/app/models/action_mailbox/record.rb
+# frozen_string_literal: true
+
+module ActionMailbox
+  class Record < ActiveRecord::Base #:nodoc:
+    self.abstract_class = true
+  end
+end
+
+ActiveSupport.run_load_hooks :action_mailbox_record, ActionMailbox::Record

「Active RecordにおけるApplicationRecord↓と同じような形に設計を整理したということでしょうね」

# models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

cache-controlヘッダーでno-storeを指定したときに他の設定を含まないよう修正

Railsアプリケーションをデフォルトで一切キャッシュしないよう設定することが望ましいケース(PCI DSSなどへのコンプライアンス)では、キャッシュなしをシンプルに設定できるとありがたい。
最も手っ取り早くやれそうに思えるのは、config/application.rbの設定を以下のように変更する方法。

config.action_dispatch.default_headers.merge!('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')

しかしこうするとCache-Control: private, no-storeのような結果になって微妙にうまくいかない。
このユースケースにおけるprivateキーワードは、no-storeと併用されると少なくとも余計であり、最悪の場合は危険ですらある(ブラウザが何らかの形で解釈に混乱をきたした場合)。

自分はMDNのリファレンスを参照してこの結論に達した。

このプルリクは、デフォルトのCache-Controlヘッダーで単にno-storeディレクティブを設定したときはprivateを含まないようにする。
同PRより大意


つっつきボイス:「そういえばちょうどこの間Cache-Controlヘッダー周りを調べたんですよ↓」「ホントだ、no-storeも載ってますね」「キャッシュディレクティブってこんなにいっぱいあるんですか!」

# リクエスト時のキャッシュディレクティブ
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-Control: no-cache 
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: only-if-cached
# レスポンス時のキャッシュディレクティブ
Cache-Control: must-revalidate
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: public
Cache-Control: private
Cache-Control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-Control: s-maxage=<seconds>

参考: Cache-Control - HTTP | MDN

no-store

レスポンスをキャッシュに保存することはできません。他のディレクティブを設定することもできますが、最近のブラウザーではレスポンスがキャッシュされることを防ぐために必要なディレクティブはこれだけです。 max-age=0 が暗黙で含まれますmust-revalidate は意味を持ちません。再検証を行うにはレスポンスがキャッシュに格納されている必要がありますが、 no-store はこれを抑止するからです。

private

レスポンスが通常はキャッシュ可能でなくても、ブラウザーのキャッシュにのみ格納することができます。レスポンスがどのキャッシュにも保存されないようにするには、代わりに no-store を使用してください。このディレクティブにはレスポンスがキャッシュに保存されないようにする効果はありません。
developer.mozilla.orgより

「プルリクメッセージを見ると、修正前はCache-Control: private, no-storeと両方が設定されていたけど、MDN↑にも書かれているように、サーバーサイドでno-storeを指定するときにそれ以外の指定が重複するとCache-Controlヘッダの意図としては矛盾してしまうので、no-storeprivateが同時に指定される状況は好ましくないということだと思います」「ブラウザは恐らく、よりセキュアで強い指定であるno-store指定を優先して、private指定を無視して動作するでしょうけど、念のためサーバーからのレスポンスにprivate設定を含めないようにしたんでしょうね」

「修正は、:no_storeが設定されていれば_cache_controlNO_STOREで上書きして、それ以外の場合はその時の設定値に合わせて複数ディレクティブを設定するように変更されている↓」「なるほど!」「ちょうど最近この辺を調べたばかりでよかった: こうやって思い出すことでより頭に残りやすくなりますし✨」

# actionpack/lib/action_dispatch/http/cache.rb#L197
-         if control[:no_cache]
+         if control[:no_store]
+            self._cache_control = NO_STORE
+         elsif control[:no_cache]
             options = []
             options << PUBLIC if control[:public]
             options << NO_CACHE
             options.concat(control[:extras]) if control[:extras]

「ちなみにPragma: no-cacheという記述はHTTP/1.0の古い仕様との互換性用です↓」

参考: Pragma - HTTP | MDN

メモ: Pragma は HTTP レスポンスには指定されていないため、リクエストの Cache-Control ヘッダーフィールドが省略されている場合は Cache-Control: no-cache と同じように動作しますが、一般的な HTTP/1.1 Cache-Control ヘッダーの代わりに信頼できるものではありません。Pragma は HTTP/1.0 クライアントとの下位互換性のためにのみ使用してください。
developer.mozilla.orgより

fill_in_rich_text_area<label>テキストで探索


つっつきボイス:「Action Textか」「久しぶりに機能が追加されたようです」「HTML5のfor属性を<label>要素で指定した場合にTrixエディタが対応したのね↓」

# actiontext/lib/action_text/system_test_helper.rb#L3
module ActionText
  module SystemTestHelper
    # Locates a Trix editor and fills it in with the given HTML.
    #
    # The editor can be found by:
    # * its +id+
    # * its +placeholder+
    # * the text from its +label+ element
    # * its +aria-label+
+   # * the +name+ of its input
    #
    # Examples:
    #
    #   # <trix-editor id="message_content" ...></trix-editor>
    #   fill_in_rich_text_area "message_content", with: "Hello <em>world!</em>"
    #
    #   # <trix-editor placeholder="Your message here" ...></trix-editor>
    #   fill_in_rich_text_area "Your message here", with: "Hello <em>world!</em>"
    #
+   #   # <label for="message_content">Message content</label>
+   #   # <trix-editor id="message_content" ...></trix-editor>
+   #   fill_in_rich_text_area "Message content", with: "Hello <em>world!</em>"
+   #
    #   # <trix-editor aria-label="Message content" ...></trix-editor>
    #   fill_in_rich_text_area "Message content", with: "Hello <em>world!</em>"

参考: for属性 ≪ label要素 ≪ メタデータ ≪ 要素 ≪ HTML5入門

<!-- html5.cyberlab.info より -->
<label for="sampleId">チェックボックス: </label>
<input type="checkbox" name="sampleName" value="sampleValue" id="sampleId">

このコミットは、アクセシビリティを意識する形でrich_text_areaへの呼び出しをテストする方法を改善することにフォーカスするという点で#38551と調和する。
適切なaria-label属性を持つ<trix-editor>要素を検索する他に、対応する<label>のテキストとマッチする要素の探索もサポートする。
basecamp/trix#829がマージおよびリリースされたことで、<trix-editor>要素を参照する<label>要素をクリックするとその<trix-editor>要素にフォーカスが移動する。
アクセシビリティを改善可能なラベルテキストは他にもいくつかあるが、手始めにfill_in_rich_text_areaを拡張して<label for="...">要素を扱えるようにするのがよいだろう。
同PRより大意

uniquenessバリデーターのconditions:オプションにレコードを渡せるようになった

uniquenessバリデーターのconditions:オプションにレコードを渡せるようサポート。
これによって、レコードの属性に基づいて条件をビルドできるようになる。
例: 出版期間内でユニークでなければならないslugをバリデーションする場合は以下のようになる。

class Article < ApplicationRecord

  validates_uniqueness_of :slug,
    conditions: ->(record) {
      where(published_at: record.published_at.beginning_of_day..record.published_at.end_of_day)
    }

この機能追加を歓迎してもらえるのであればもう少し手を加えてテストカバレッジも追加する。
同PRより大意


つっつきボイス:「上みたいな書き方って今までできなかったんですか?」「たぶんカスタムバリデーターを書けば今までもできたと思いますけど、そうしなくてもできるようになったんでしょうね」「なるほど、必要だったら自分で書いちゃうかも」「あると嬉しい機能👍」

「diffを見ると↓今までもuniquenessの条件をlambdaで渡せたみたいだけど、lambdaの中で当該レコード自身を参照できなかったので、ブロック引数で受けられるようにしたのがポイントかな」「なるほど」

# activerecord/lib/active_record/validations/uniqueness.rb#L19
      def validate_each(record, attribute, value)
        finder_class = find_finder_class_for(record)
        value = map_enum_attribute(finder_class, attribute, value)
        relation = build_relation(finder_class, attribute, value)
        if record.persisted?
          if finder_class.primary_key
            relation = relation.where.not(finder_class.primary_key => record.id_in_database)
          else
            raise UnknownPrimaryKey.new(finder_class, "Cannot validate uniqueness for persisted record without primary key.")
          end
        end
        relation = scope_relation(record, relation)
-       relation = relation.merge(options[:conditions]) if options[:conditions]
+
+       if options[:conditions]
+         conditions = options[:conditions]
+
+         relation = if conditions.arity.zero?
+           relation.instance_exec(&conditions)
+         else
+           relation.instance_exec(record, &conditions)
+         end
+       end

        if relation.exists?
          error_options = options.except(:case_sensitive, :scope, :conditions)
          error_options[:value] = value
          record.errors.add(attribute, :taken, **error_options)
        end
      end

「たしかにpublished_at: record.published_at.beginning_of_day..record.published_at.end_of_dayっていう条件を渡すならrecordも渡さないとできませんよね」「recordに対する相対的なuniquenessをチェックするというのはuniquenessの判定方法としてはやや特殊な気はしますけど」

バッチのupdate_alldelete_allが非バッチ版と同様に行数を返すよう修正

#40287を修正する。
従来はnilという戻り値がドキュメントに書かれておらず、バッチでないバージョンのメソッドと整合していなかった。
また、eachでバッチを作成できるようにし、BatchEnumeratorupdate_alldelete_alldestroy_allにAPIドキュメントを追加した。
同PRより大意


つっつきボイス:「#40287↓を見ると、in_batches.update_allnilが返されていたらしい」

「SQL的な作法としては、UPDATE文やDELETE文はレスポンスとして表を返さない代わりに『何行処理したか』という行数を返すものなんですけど、その『何行処理したか』が返されていなかったのを返すようにしたようです」「ああ、MySQLとかならxx rows affectedのように確実に数値が返されますけど、今までの実装ではそこでnilを返していたのが問題だったんですね」「たしかにSQL的な作法としては欲しい」

「batchedでないときはちゃんと件数返していたようです」「なるほど、update_alldelete_allで取れてなかったのはin_batchesの場合なのね↓」「あまり使われてなさそうなケースだから気づきにくいかも」「これは直ってよかった🎉」

# #40287より
class BugTest < Minitest::Test
  def test_update_all
    assert_equal 3, User.update_all('id = id')  # 成功する
  end

  def test_update_all_in_batches
    assert_equal 3, User.in_batches.update_all('id = id') # 失敗する(nilが変える)
  end
end

in_batchesって何をするんだろうと思って調べてみると、デフォルトで1000件ずつ分割して処理するのか↓」

# api.rubyonrails.orgより
Person.where("age > 21").in_batches do |relation|
  relation.delete_all
  sleep(10) # Throttle the delete queries
end

Rails

delegated_typeを使ってみた


つっつきボイス:「前にも話題に出たRailsのdelegated_type記事です(ウォッチ20200601)」「STIではないpolymorphic associationsですね」

「Railsでpolymorphic associationsというとSTIという印象が強いんですけど、STI以外にも実装方法があってdelegated_typeはそのひとつ」「polymorphic associationsでググってみるとほとんどがRailsとSTIの記事ですね😆」

「この図の赤で囲まれている部分↓はSTIだとひとつのテーブルになりますけど、記事ではこれをdelegated_typeで実装したんですね」「記事長い...これ後でちゃんと読みます」


同記事より

「なお、そーだいさんの書籍『失敗から学ぶRDBの正しい歩き方』↓の『第7章:隠された状態』には、STIではないpolymorphic associationsについて解説が載っているので読むとよいと思います」

goldiloader: N+1を回避するgem

salsify/goldiloader - GitHub

以下の記事で知りました。


つっつきボイス:「ゴルディローダー?」「前からあるみたいですが初めて知りました」「コードを変えずに自動的にeager loadingしてくれるgemみたい」「fully_load: trueのようにヒントも付けられるのね」

# 同記事より: goldiloaderなしの場合
> blogs = Blogs.limit(5).to_a
# SELECT * FROM blogs LIMIT 5

> blogs.each { |blog| blog.posts.to_a }
# SELECT * FROM posts WHERE blog_id = 1
# SELECT * FROM posts WHERE blog_id = 2
# SELECT * FROM posts WHERE blog_id = 3
# SELECT * FROM posts WHERE blog_id = 4
# SELECT * FROM posts WHERE blog_id = 5
# 同リポジトリより: goldiloaderありの場合
> blogs = Blogs.limit(5).to_a
# SELECT * FROM blogs LIMIT 5

> blogs.each { |blog| blog.posts.to_a }
# SELECT * FROM posts WHERE blog_id IN (1,2,3,4,5)

「ちゃんと動くならよさそう😋」「最近のActive Recordは@kamipoさんたちが多くのチューニングをかけているので、もしかすると既に似たようなことをある程度内部でやってるかもしれないという気がしますね」「あ〜やってそう!」

flyerhzm/bullet - GitHub

「goldiloaderは以下の翻訳記事を見ると、ぱっと見bulletのオルタナかなと思ったのですが...」「bulletはN+1を検出するgemだけどgoldiloaderはN+1を自動回避するgemなので、オルタナではなくてN+1対応の別アプローチと考える方がいいでしょうね」「なるほど!」

Rails: パーシャルと`collection:`でN+1クエリを回避してビューを高速化(翻訳)

N+1を今後も解決するためにBullet gemを導入します。私はBulletが大好きです❤️。次の3つの理由からGoldiloaderよりもBulletが好みです。
同記事より

「goldiloaderは、N+1を解決するという最終目的だけに着目すればbulletのオルタナと言えなくもありませんけど、方法としてはオルタナではないでしょうね」「たしかに」「bulletはN+1を人間が解決するために検出して教えてくれるけど、goldiloaderはN+1をいい感じに自動解決する代わりにN+1を隠蔽するので」「翻訳記事は隠蔽より解決を志向してbulletを選んだんでしょうね」

Railsルーターの隠し機能


つっつきボイス:「そうそう、Kaigi on Rails冒頭のキーノートスピーチで@tenderloveさんが発表していたこれはスゴい💪」「あのときその場で動かそうとして四苦八苦しました😅」「当時Twitterにも貼りましたけど、これを動かすにはgraphvizが必要なんですよ↓」「あ〜、それで動かなかったのか...」

「これなかなか優秀な機能で、Railsのルーティングエンジンに入ったものをこうやってビジュアル表示してくれるんですよ、しかもdevise_forのようなDSL処理されているルーティングも表示できますし」「おおスゴい!ちょうどこういうのを使いたいプロジェクトがあるんですよ😂」「rails routesで表示されるルーティングだとややフラットに表示されるんですけど、特にルーティングがめちゃくちゃ大きいときはツリーで見たいですよね」「同意です!」「こういう隠し機能をさらりと紹介する@tenderloveさんさすが」

「Railsのtransition_table.rbにあるvisualizer、思ったよりコードがシンプルかも↓」

# actionpack/lib/action_dispatch/journey/gtg/transition_table.rb#78
        def to_svg
          svg = IO.popen("dot -Tsvg", "w+") { |f|
            f.write(to_dot)
            f.close_write
            f.readlines
          }
          3.times { svg.shift }
          svg.join.sub(/width="[^"]*"/, "").sub(/height="[^"]*"/, "")
        end

        def visualizer(paths, title = "FSM")
          viz_dir   = File.join __dir__, "..", "visualizer"
          fsm_js    = File.read File.join(viz_dir, "fsm.js")
          fsm_css   = File.read File.join(viz_dir, "fsm.css")
          erb       = File.read File.join(viz_dir, "index.html.erb")
          states    = "function tt() { return #{to_json}; }"

          fun_routes = paths.sample(3).map do |ast|
            ast.map { |n|
              case n
              when Nodes::Symbol
                case n.left
                when ":id" then rand(100).to_s
                when ":format" then %w{ xml json }.sample
                else
                  "omg"
                end
              when Nodes::Terminal then n.symbol
              else
                nil
              end
            }.compact.join
          end

後でMac環境でやってみました。brew install graphvizでGraphvizをインストールし、以下を実行するとout.htmlが生成され、ボックスにルーティングを入力してsimulateボタンをクリックするとルーティングが緑色にハイライトされました。

$ bin/rails r 'File.binwrite "out.html", <アプリ名>::Application.routes.router.visualizer'

action_args gem

asakusarb/action_args - GitHub


つっつきボイス:「この間の感想戦締めくくりのキーノートスピーチ↓でamatsudaさんが触れていたaction_args gemが気になってピックアップしてみました」「action_args gemは自分も結構好きです」

「このときのメモを読み返すと『action_argsはRailsのコントローラをMerb風にするgem』とあります」「Merbは昔Railsと統合されたフレームワークですね↓」

参考: MerbがRails 3に統合、人気Rubyフレームワークが合体へ - ITmedia エンタープライズ

「amatsudaさんが『この構文を選ばなかったのはDHHの選択ミスだったと思っている』とお話しされてたのが気になりました↑」「これは自分も共感する部分がありますね: 現在のRailsのようなaction methodとStrong Parametersが分離している書き方よりも、こういうaction_args的な書き方の方が本来あるべき姿だったのかもしれないという気持ち」

# 同リポジトリより
class UsersController < ApplicationController
  permits :name, :age, :email

  # GET /users
  def index
    @users = User.all
  end

  # GET /users/1
  def show(id)
    @user = User.find(id)
  end

  # GET /users/new
  def new
    @user = User.new
  end

  # GET /users/1/edit
  def edit(id)
    @user = User.find(id)
  end

  # POST /users
  def create(user)
    @user = User.new(user)

    if @user.save
      redirect_to @user, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  # PUT /users/1
  def update(id, user)
    @user = User.find(id)

    if @user.update(user)
      redirect_to @user, notice: 'User was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /users/1
  def destroy(id)
    @user = User.find(id)
    @user.destroy

    redirect_to users_url, notice: 'User was successfully destroyed.'
  end
end

「上のshow(id)みたいにメソッド定義にパラメーターが示される方がparams経由の参照に比べるとメソッドとして読みやすいと思います。現在のRailsのStrong Parametersのようなメソッド定義のパラメーターに書かれていないグローバル的な変数をactionのコード内で参照するのはあんまりキレイじゃないかもという気持ちがあります」「ふむふむ」「とは言えWebリクエストのパラメーターはすぐネストが深くなるので、show(id)のようなaction_args的な書き方ですべてうまくいくとは限らないのもわかりますし、悩ましいところではあります」「う〜む」「パラメーターの個数なりフォーマットなりが固定されているのであれば、個人的にはaction_args的な書き方の方がキレイだと思います」

「それにaction_args的な書き方なら必須パラメーターもこういうふうにRubyの構文で自然に書けますし↓」「たしかにキーワード引数とかデフォルト値とかを普通に書けますね」

# 同リポジトリより
class CommentsController < ApplicationController
  def create(post_id:, comment:)
    post = Post.find post_id
    if post.create comment
      ...
  end
end

「現在のRailsの*_paramsというprivate methodを使ったStrong Parametersによるパラメーターアクセスは、アクション間で定義を共有できるメリットがあるとも言えますし、開発者もそれに慣れていますけど、プログラムとして自然な書き方という点ではこのaction_args的な書き方に惹かれるところはあります」「言われてみれば、今の*_paramsなメソッドって毎回書いていますけど、パラメータ数が増えたりネストしたパラメータを許可したりしていると、この書き方で本当にいいんだろうかと思うことがありますね」「ある意味スキーマレスになっていくというか」「それそれ!」「まあStrong Parametersでフィルタすることで多少スキーマを通している感じにはなりますけどね」

「ただ、スキーマ的なものを通せる前提があるんだったら生のparams自体はアクションのメソッド内から直接参照できなくてもいいんじゃないの?って思うわけですよ: 実際action_argsをうまく使えば以下のようにparamsが出現しないように書けますし↓、個人的にもその方がキレイだと思うんですけど」「リクエストされたパラメータがフィルタされずに入っているparamsを参照するようなコードを書くのが微妙に居心地悪い感じがするというのは何だかわかります」

# 同リポジトリより(コメントは本記事にて加筆)
# action_argsのconvensionとして、`#{name}`、または`#{name}_params`という引数名で受けると
# params[:#{name}]相当の`#{name}`のmodel objectが渡されたかのように自動展開できる
# 以下は同じ挙動になる
# without _params
def create(user)
  @user = User.new(user)
  ...
end

# with _params
def create(user_params)
  @user = User.new(user_params)
  ...
end
# 同リポジトリより
# Strong Parameters相当のパラメータフィルタは
# Controllerに`permits`で記述できる
class UsersController < ApplicationController
  # allow-lists User model's attributes
  permits :name, :age

  # the given `user` parameter would be automatically permitted by action_args
  def create(user)
    @user = User.new(user)
  end
end

paramsって、PHPにおける$_POSTみたいな感じで、クライアントからのリクエストがそのまま入っていて直接参照するのが危険なイメージがあるんですよね😆」「それもわかります!」「Strong Parametersでフィルタした*_paramsの方はフィルタされた結果なので、原則こちらしか触らないようにしたい」「コントローラークラスならたしかにparamsを参照できる必要のあるユースケースはあるんですけど、だからといってコントローラのアクションがカジュアルにこれらを参照していいんだろうかという気持ちはあります」

参考: PHP: $_POST - Manual

その他Rails


つっつきボイス:「Rails以外の人にもRailsチュートリアルをすすめていたので」「徳丸先生も書いているように、RailsチュートリアルのいいところはWeb開発で常識的に必要なものがとりあえず全部入っていることですね👍」「たしかに!」「Web開発についてここまでまとまっている資料って他にあまりありませんし」


前編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201006後編)Rubyの`defined?`キーワード、Ractorベースのジョブスケジューラ、Caddy Webサーバーほか

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

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

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。