builderscon tokyo 2019に行ってきました

f:id:sktktk1230:20180726121247p:plain

2019/08/23のbuildersconに参加してきました
今回は、非同期処理や分散システムなどに興味があったので、そこをメインで見てきました

参加したセッション

メルペイ開発の裏側

t.co

参加にあたって、特に気になっていたところ

  1. データ整合性の担保をどうしているのか?
  2. ロールバックはどうしているのか?

1. データ整合性の担保をどうしているのか?

基本はリトライし、継続不可能なときのみ、処理をやめるようです

リトライと冪等性 - どんなエラーが出ても基本的にリトライする - 冪等性を担保して二重処理されないようにする - 継続不可能なときのみ処理をやめる

引用:P.22

リコンサイル

[名](スル)《一致させる、の意》複数帳簿間で残高照合を行うこと。また、金融関連では外国の銀行に保有する口座の取引明細と、自行の処理した取引明細と照合すること。 引用:リコンサイル

することで、データの整合性を担保しているそうです

データの整合性を確認すること - 会計データが完全な状態だということを保証する - 各マイクロサービスと会計システム間

引用:P.29

2. ロールバックはどうしているのか?

状態遷移モデルを採用し、どんな処理でも意図せず落ちることがある為、処理単位を記録するように状態を定義しているようです

ロールバックも状態遷移で定義 - 途中で継続不可能(tryが失敗)した場合も遷移先が異なるだけ - Cancelを行う状態を定義して一貫したモデルで扱う 引用:P.25

感想

トランザクション管理はやはり難しそうで、データ整合性を担保する為に、かなり安全に倒して設計されているんだなと思いました
また開発もサービス安定性、品質を重視されているので、リリースまで重厚なプロセスがあると感じました


RDBのトラブルの現場を追え!

speakerdeck.com

参加にあたって、特に気になっていたところ

  • どうやって障害理由を切り分け、そして対応してきたのか?

1. どうやって障害理由を切り分け、そして対応してきたのか?

壊れたとは何かをちゃんと特定すること たとえば、

  1. 突然パフォーマンスが悪化した
  2. データの不整合が発生している
  3. データベースが応答を返さない
  4. コネクションが溢れている
  5. 間違えてDROP TABLEしちゃった(バルス)

引用

感想

これは知らないDBの機能でした

  1. MySQLだと beginしても DROP TABLEするとauto commitが走って消える
  2. バルクインサートより、MySQLならLOAD、PostgreSQLならCOPYが早い
    MysqlSQLはこちら

    テキストファイルからテーブルをロードする場合は LOAD DATA INFILE を使用します。通常、これは INSERT ステートメントを使用する場合より、20 倍速くなります。セクション13.2.6「LOAD DATA INFILE 構文」を参照してください。

    引用:8.2.2.1 INSERT ステートメントの速度

    PostgreSQLはこちら

    PostgreSQL に大量のデータを高速に取り込む方法を紹介します。 COPY という専用のコマンドを使うと INSERT よりもずっと高速です。 また、COPY を使う際にひと工夫すると、さらに速くなります。

    引用:大量のデータを高速に投入するには

  3. B-Treeインデックスは全体の10%くらいのデータ量じゃないとインデックス効かない

Web API に秩序を与える Protocol Buffers 活用法

speakerdeck.com

参加にあたって、特に気になっていたところ

  • Web APIのSchema管理について

1. Web APIのSchema管理について

  • .proto ファイルを記述して、コードを自動生成する
  • 豊富なpluginでswagger.jsonの生成も可能

f:id:sktktk1230:20190830151249p:plain

感想

実装よりも振る舞いに集中するして開発できるのはかなり楽だなと思いました .proto file 自体もシンプルに見えるので、学習コストもあまりかからず導入できそうだなと感じました


Optimizing Ruby with JIT - 最速の言語を目指して

t.co

参加にあたって、特に気になっていたところ

  • RubyのJust-In-Timeコンパイラがいかにしてそのような言語の高速化を実現しているかのエッセンス

1. RubyのJust-In-Timeコンパイラがいかにしてそのような言語の高速化を実現しているかのエッセンス

  1. 機械語にしただけでは、早くならない
    • 処理を減らすことが最適化への道

感想

JIT コンパイラが何をやっているのか?を知らなかったので、ここが学べたのは、非常に大きいなと思いました


ウォレットアプリ「Kyash」の先 〜「Kyash Direct」のアーキテクチャ〜

t.co

参加にあたって、特に気になっていたところ

  • いろいろな設計を行っているが運用してみてどうだったのか?
    • DDD
    • Clean Architecture
    • Microservices
    • Orchestration/Choreography
    • Message Pub/Sub
    • Event Driven Architecture/Event Sourcing
    • CQRS
    • 分散Tracing

1. いろいろな設計を行っているが運用してみてどうだったのか?

運用はこれからなので、まだまだ試行錯誤が続きそう。。。

とのことでした

マイクロサービスの選択理由

  • モノリスにするには巨大で複雑すぎる
  • 部分的な変更が全体に影響を及ぼすのを避けたい
  • 部分ごとに負荷が大きく異なる
  • 外部サービスとの接続部分を切り離しておきたい
  • 機能追加のスピードは極力落としたくない
Pros.
  • 各サービス単体では、小さくてシンプル
  • 変更の影響を局所化できる
  • スケールしやすい
  • デプロイしやすい
  • リソース配分を最適化しやすい
Cons.
  • 設計・実装が難しい

感想

イベントドリブンアーキテクチャの設計を見たことがなかったので、ここまで難しいのかということと、かなり複雑なアーキテクチャで運用がつらそうと思ったが、エンジニアとしてはかなり刺激的だなと思いました

今日一日の感想

仕事でもすぐに生かしていけそうな知識からエンジニアとしての基礎力を上げるような内容まで幅広く非常に楽しめた一日でした

f:id:sktktk1230:20190830165635j:plain

※ 当日貰ったTシャツとバック

f:id:sktktk1230:20190830151909j:plain

f:id:sktktk1230:20190830152036j:plain

Railsのソースコード読んでみる | Active Support delegate_missing_to編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

delegate_missing_to を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

UserオブジェクトにないものをProfileにあるものにすべて委譲したいとしましょう。

delegate_missing_toマクロを使えばこれを簡単に実装できます。

class User < ApplicationRecord
  has_one :profile
 
  delegate_missing_to :profile
end

オブジェクト内にある呼び出し可能なもの(インスタンス変数、メソッド、定数など)なら何でも対象にできます。対象のうち、publicなメソッドだけが委譲されます。

引用:RAILS GUIDES:delegate_missing_to

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def delegate_missing_to を探してみます

f:id:sktktk1230:20181206152315p:plain

2. 該当箇所が1箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > module > delegation.rb
# frozen_string_literal: true

require "set"

class Module

省略

def delegate_missing_to(target)
    target = target.to_s
    target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def respond_to_missing?(name, include_private = false)
        # It may look like an oversight, but we deliberately do not pass
        # +include_private+, because they do not get delegated.

        #{target}.respond_to?(name) || super
      end

      def method_missing(method, *args, &block)
        if #{target}.respond_to?(method)
          #{target}.public_send(method, *args, &block)
        else
          begin
            super
          rescue NoMethodError
            if #{target}.nil?
              raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
            else
              raise
            end
          end
        end
      end
    RUBY
  end
end

まずはここまで見ていきます

def delegate_missing_to(target)
  target = target.to_s
  target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

引数targetを文字列へ変換しています
そしてtargetは DELEGATION_RESERVED_METHOD_NAMES に含まれているのかチェックしています

定数 DELEGATION_RESERVED_METHOD_NAMES がどのようなものか調べる為、記述箇所付近を見てみます

class Module
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
  # option is not used.
  class DelegationError < NoMethodError; end

  RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
  else elsif END end ensure false for if in module next nil not or redo rescue retry
  return self super then true undef unless until when while yield)
  DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
  DELEGATION_RESERVED_METHOD_NAMES = Set.new(
    RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
  ).freeze

定義箇所はこちらです

DELEGATION_RESERVED_METHOD_NAMES = Set.new(
  RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze

class Set (Ruby 2.6.0)は集合を表すクラスになります
Set.new で引数のオブジェクトを要素として集合を作ります

Set.new に配列を与えると

Setの挙動
Setの挙動

このような集合を作成します

RUBY_RESERVED_KEYWORDS, DELEGATION_RESERVED_KEYWORDS で定義されている文字列を見てみると

RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
else elsif END end ensure false for if in module next nil not or redo rescue retry
return self super then true undef unless until when while yield)
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)

こちらの文字列に含まれているかをチェックしているようです

ここまでを踏まえてもう一度、こちらを見てみると

target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

targetが含まれている場合は、self.target で自身のメソッドを呼び出します

そして次を見ていきます

module_eval <<-RUBY, __FILE__, __LINE__ + 1
  def respond_to_missing?(name, include_private = false)
    # It may look like an oversight, but we deliberately do not pass
    # +include_private+, because they do not get delegated.

    #{target}.respond_to?(name) || super
  end

  def method_missing(method, *args, &block)
    if #{target}.respond_to?(method)
      #{target}.public_send(method, *args, &block)
    else
      begin
        super
      rescue NoMethodError
        if #{target}.nil?
          raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
        else
          raise
        end
      end
    end
  end
RUBY

instance method Module#class_eval (Ruby 2.6.0)を使用して動的にメソッドを定義しています
ヒアドキュメント <<-RUBY 〜 RUBY に記載されている FILE , LINE は疑似変数というものです

FILE
現在のソースファイル名

フルパスとは限らないため、フルパスが必要な場合は File.expand_path(FILE) とする必要があります。

LINE
現在のソースファイル中の行番号
引用:Ruby 2.5.0 リファレンスマニュアル#疑似変数

動的に定義するメソッドを1つずつ見ていきます

def respond_to_missing?(name, include_private = false)
  # It may look like an oversight, but we deliberately do not pass
  # +include_private+, because they do not get delegated.

  #{target}.respond_to?(name) || super
end

実際に動作する部分はこちらで

#{target}.respond_to?(name) || super

targetにnameメソッドが存在するか、もしなければ、継承チェーン内の親の instance method Object#respond_to_missing? (Ruby 2.6.0)を実行します

次にもうひとつ定義されるメソッドを見てみます

def method_missing(method, *args, &block)
  if #{target}.respond_to?(method)
    #{target}.public_send(method, *args, &block)
  else
    begin
      super
    rescue NoMethodError
      if #{target}.nil?
        raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
      else
        raise
      end
    end
  end
end

instance method BasicObject#method_missing (Ruby 2.6.0)はメタプログラミングで利用されるテクニックで、オーバーライドすることで、存在しないメソッドを呼び出したときに、エラーとなる前に特定の処理を実行することができます

targetのpublicなmethodに存在しているか確認しています instance method Object#public_send (Ruby 2.6.0)
存在している場合は、そのメソッドを呼び出すという処理です

if #{target}.respond_to?(method)
  #{target}.public_send(method, *args, &block)

else句を見てみると、 NoMethodError が発生した場合、 targetがnilであれば、DelegationError とするようです

else
  begin
    super
  rescue NoMethodError
    if #{target}.nil?
      raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
    else
      raise
    end
  end
end

メタプログラミングについてはこちらの書籍がわかりやすかったので、もし読んでない方は読んでおくといいかもしれないです

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

Railsのコミットでわからないものを調べてみた | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

日課で@y_yagiさんのrails commit log流し読みを読んでいるのですが、 コミットの内容を読んでみて、どうしてその修正でバグが直るのかわからないものがありました
そこで、その修正内容をしっかりと理解する為に調査したりしたので、どうやって理解するに至ったかなどを書いていきたいと思います
今回読んでいたエントリはこちらです

issue

github.com

該当PR

github.com

修正内容を抜粋

f:id:sktktk1230:20180822134705p:plain

2. PRを理解するためにやってみたこと

1. まず、PRのコメントを読んでみる

Fix merging relation that order including `?`

The `Relation::Merger` has a problem that order values would be merged
as nested array.

That was caused an issue #33664 since if array value is passed to
`order` and first element in the array includes `?`, the array is
regarded as a prepared statement and bind variables.

https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order

Just merging that as splat args like other values would fix the issue.

Fixes #33664.

ネストしたArrayで最初の値に'?'が含まれている場合、prepared statementとみなされてエラーとなるというバグとのことでした

issueには再現コードも書かれていました

github.com

どういったバグになるのかが何となく理解できたところで、コードを追ってみます

2. 変数の前に*をつけるとどんな動きなのか調べてみる

修正の対象になった箇所はこちらです

        def merge_multi_values
          if other.reordering_value
            # override any order specified in the original relation
            relation.reorder!(*other.order_values)
          elsif other.order_values.any?
            # merge in order_values from relation
            relation.order!(*other.order_values)
          end

          extensions = other.extensions - relation.extensions
          relation.extending!(*extensions) if extensions.any?
        end

Ruby * 等でGoogleで調べてみると、こちらの記事がヒットしたので、読んでみました alpaca.tc

変数の前に* を書くと、配列に変換してくれるようです
なんとなく修正内容を理解したところで、コードを読んでみます

3. relation.reorder! を読んでみる

rails/activerecord/lib/active_record/relation/query_methods.rb

# Same as #reorder but operates on relation in-place instead of copying.
def reorder!(*args) # :nodoc:
  preprocess_order_args(args)

  self.reordering_value = true
  self.order_values = args
  self
end

reorder! の引数をそのまま preprocess_order_argsに渡しているので、こちらの処理を追ってみます

4. preprocess_order_args を読む

rails/activerecord/lib/active_record/relation/query_methods.rb

def preprocess_order_args(order_args)
  order_args.map! do |arg|
    klass.sanitize_sql_for_order(arg)
  end
  order_args.flatten!

  @klass.enforce_raw_sql_whitelist(
    order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
    whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
  )

省略

end

引数を.map! して 配列の要素を sanitize_sql_for_order に渡しているようなので、こちらを読んでみます

5. sanitize_sql_for_order を読む

PRのコメントに書いてあったエラーの原因のメソッドまでいきつきました

That was caused an issue #33664 since if array value is passed to order and first element in the array includes ?, the array is regarded as a prepared statement and bind variables.

https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order

こちらのドキュメントを読んで、挙動を確認してみます

sanitize_sql_for_order(condition)

Accepts an array, or string of SQL conditions and sanitizes them into a valid SQL fragment for an ORDER clause.

sanitize_sql_for_order(condition)

sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
# => "field(id, 1,3,2)"

sanitize_sql_for_order("id ASC")
# => "id ASC"

引用:Ruby on Rails 5.2.1#sanitize_sql_for_order

そしてコードを読んでみると、

rails/activerecord/lib/active_record/sanitization.rb

def sanitize_sql_for_order(condition)
  if condition.is_a?(Array) && condition.first.to_s.include?("?")
    enforce_raw_sql_whitelist([condition.first],
      whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
    )

    # Ensure we aren't dealing with a subclass of String that might
    # override methods we use (eg. Arel::Nodes::SqlLiteral).
    if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
      condition = [String.new(condition.first), *condition[1..-1]]
    end

    Arel.sql(sanitize_sql_array(condition))
  else
    condition
  end
end

conditionが配列かつ要素の最初の値に'?'が含まれている場合に、prepared statementと判断し、処理をやってくれるみたいです

なので、ここまで読んでみて推測すると、正しい挙動では引数conditionは こちらの条件

 if condition.is_a?(Array) && condition.first.to_s.include?("?")

には該当せず、

else
  condition
end

こちらになるのが正しい挙動のようです

ここまで読んでみて仮設を立てるとpreprocess_order_args(order_args) の引数が

  • バグのケースだと [['?', '!']] が引数となり、
  • 正しいケースだと ['?', '!'] となるのではないかなと思いました

しかしなぜこちらのパターン[['?', '!']] になるのかわからず困っていたのですが、

会社の先輩に相談したところこちらの記事を教えていただきました qiita.com

変数の前に* を書くと、配列に変換してくれるではなく、 可変長引数を受け取るメソッドに、配列を渡す際に変数に* をつけないともう一回り配列で包まれてしまうというこちらの挙動ではないかとのことでした

なので、こちらの修正を行うことで f:id:sktktk1230:20180822134705p:plain

other.order_values の戻り値の配列がさらにもうひと回り配列で包まれることが無くなるということだと思われます
本当に戻り値が配列なのか確認するために、さらにコードを読んでいきます

6. other.order_values の戻り値を確認してみる

order_valuesの定義箇所を探してみようと思い def other.order_values でgrepしてみてもヒットしませんでした

activerecord/lib/active_record/relation/merger.rb ファイルの中を見てみると、lock_value, create_with_valueなどのメソッドがあり、こちらも定義箇所が見つからなかったので、define_method などでsuffixに_valueを設定し、動的にメソッド定義しているのではないかと予想し探し方を変えてみました

Merger クラス内で other を初期化している箇所を探してみると(RubyMineで⌘+B)

こちらにいきつきました

activerecord/lib/active_record/relation/merger.rb

省略

    class Merger # :nodoc:
      attr_reader :relation, :values, :other

      def initialize(relation, other)
        @relation = relation
        @values   = other.values
        @other    = other
      end

省略

Mergeクラスを初期化する際に引数でotherを入れているので、初期化処理の箇所がないか調べてみました f:id:sktktk1230:20180823105415p:plain

こちらを見てみるとotherはRelationクラスの可能性がありそうです

rails/activerecord/lib/active_record/relation/spawn_methods.rb

省略

def merge!(other) # :nodoc:
      if other.is_a?(Hash)
        Relation::HashMerger.new(self, other).merge
      elsif other.is_a?(Relation)
        Relation::Merger.new(self, other).merge
      elsif other.respond_to?(:to_proc)
        instance_exec(&other)
      else
        raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
      end
    end

省略

こちらのファイルを見てみると、

activerecord/lib/active_record/relation.rb

# frozen_string_literal: true

module ActiveRecord
  # = Active Record \Relation
  class Relation
    MULTI_VALUE_METHODS  = [:includes, :eager_load, :preload, :select, :group,
                            :order, :joins, :left_outer_joins, :references,
                            :extending, :unscope]

    SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
                            :reverse_order, :distinct, :create_with, :skip_query_cache]

    CLAUSE_METHODS = [:where, :having, :from]
    INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]

    VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS

    include Enumerable
    include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation

定数の中にorder とあるので、この定数を利用してメソッド定義しているのではないかと予想しました MULTI_VALUE_METHODS で検索すると こちらでしか利用してなさそうなので、 ruby VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS activerecord/lib/active_record配下で、 VALUE_METHODSを使用しているところを探してみます

f:id:sktktk1230:20180823110148p:plain

こちらで定義していました

rails/activerecord/lib/active_record/relation/query_methods.rb

省略

    Relation::VALUE_METHODS.each do |name|
      method_name = \
        case name
        when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
        when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
        when *Relation::CLAUSE_METHODS then "#{name}_clause"
        end
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{method_name}                   # def includes_values
          get_value(#{name.inspect})         #   get_value(:includes)
        end                                  # end

        def #{method_name}=(value)           # def includes_values=(value)
          set_value(#{name.inspect}, value)  #   set_value(:includes, value)
        end                                  # end
      CODE
    end

省略

動的にメソッドを定義し、メソッド内部では get_value を呼び出しているので、こちらの定義箇所を読んでみます

    # Returns a relation value with a given name
    def get_value(name) # :nodoc:
      @values.fetch(name, DEFAULT_VALUES[name])
    end

fetchの第二引数を指定すると、キーが存在しない場合、デフォルト値を返すようになっているので、DEFAULT_VALUES を探してみます

DEFAULT_VALUES ハッシュにはorderが定義されていないのですが

      DEFAULT_VALUES = {
        create_with: FROZEN_EMPTY_HASH,
        where: Relation::WhereClause.empty,
        having: Relation::WhereClause.empty,
        from: Relation::FromClause.empty
      }

      Relation::MULTI_VALUE_METHODS.each do |value|
        DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
      end

こちらで追加していました

      Relation::MULTI_VALUE_METHODS.each do |value|
        DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
      end

配列をループしている内部の処理は、左辺が未定義または偽の場合に右辺の値を代入するという意味になります
Rubyの||=というイディオムは左辺が未定義または偽なら代入の意味 -- ぺけみさお

なので配列の要素のこちらをひとつずつ取り出しながら

    MULTI_VALUE_METHODS  = [:includes, :eager_load, :preload, :select, :group,
                            :order, :joins, :left_outer_joins, :references,
                            :extending, :unscope]

未定義の場合は、FROZEN_EMPTY_ARRAYを追加していくという処理です
DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY

FROZEN_EMPTY_ARRAYは空の配列になります

    FROZEN_EMPTY_ARRAY = [].freeze

なので、other.order_values の戻り値は配列になるということでした

今回のPRの修正、reoader! メソッドの引数に * をつけることで配列で包まれることなく、配列を渡せるようになったというのが修正内容でした

Rubyで文字列内の\nが改行コードとして認識されないとき

様々な改行コードを統一して\n に変換して、DB保存したいと思ったが、改行コードではなく文字列として保存されてしまいハマりました

最初に書いたコード

str = 'Hello! \nWorld'
str.gsub(/(\\r\\n|\\r|\\n)/, '\n')
User.name = str
User.save!

=> "Hello! \\nWorld"

これだと\n が文字列として保存されてしまいました。
\n は特殊文字だからエスケープとかすればいいのかな?と思い次にこんなコードを書きました

次に試してみたコード

str = 'Hello! \nWorld'
str.gsub(/(\\r\\n|\\r|\\n)/, '\\\\n')

=> "Hello! \\nWorld"

こちらもうまく動かず よく調べてみると

正しく改行されるコード

ダブルクォーテーションにすればよいだけでした

バックスラッシュ記法は文字列をダブルクオーテーションで囲った場合とシングルクオーテーションで囲った場合で扱いが異なります。
引用:バックスラッシュ記法によるエスケープ

str = 'Hello! \nWorld'
str.gsub(/(\\r\\n|\\r|\\n)/, "\n")
User.name = str
User.save!

=> "Hello! \n" + "World"

Railsで許可するハッシュキーを設定する | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいての学びを書いてみます

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

PRの中の処理に書かれていた.assert_valid_keys ってどんな処理か

対象のPR

github.com

記述内容

def _define_before_model_callback(klass, callback)
  klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
    options.assert_valid_keys :if, :unless, :prepend
    set_callback(:"#{callback}", :before, *args, **options, &block)
  end
end

4. 調べてみた

assert_valid_keys が分からなかった為、どんなメソッドなのか調べてみました

Validates all keys in a hash match *valid_keys, raising ArgumentError on a mismatch.

Note that keys are treated differently than HashWithIndifferentAccess, meaning that string and symbol keys will not match.

{ name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
{ name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
{ name: 'Rob', age: '28' }.assert_valid_keys(:name, :age)   # => passes, raises nothing

引用:Ruby on Rails 5.2.0

assert_valid_keys の引数に入れた値がレシーバのkeyに存在するかチェックするメソッドです
存在しない場合は、ArgumentErrorをraiseする仕様です
シンボルと文字列は区別します またoptionsハッシュにassert_valid_keys の引数に設定した値のうち1つしか含んでいない場合

f:id:sktktk1230:20180724134134p:plain

となります

5. さきほどのソースコードを読んでみる

さきほどの記述の assert_valid_keys の部分を見てみると

options.assert_valid_keys :if, :unless, :prepend

optionsに設定した値にif, unless, prepend 以外のキーが存在しないかチェックしており、それ以外がある場合は、ArgumentErrorをraiseします

Hash#fetchでブロックを記述した場合の挙動 | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいての学びを書いてみます

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

  1. PRの中の処理に書かれていたHash#fetchにブロックを渡すとどうなるか
def log_to_stdout?
  options.fetch(:log_to_stdout) do
    options[:daemon].blank? && environment == "development"
  end
end

4. PRを読んでみる

対象のPR

github.com

1. どんな修正内容?

railties/lib/rails/commands/server/server_command.rbの修正です。
rails serverでlogをstdoutに出力するかどうかをrails serverコマンドの引数で指定出来るようにしています。
rails serverコマンドに--no-log-to-stdoutを引数に指定した場合、developmentでもlogがstdoutに出力されないようになっています。
引用:rails commit log流し読み(2018/07/09)

5. PR読んでてわからない部分調べてみた

1. Hash#fetch にブロックを渡すとどんな挙動になる?

fetch(key, default = nil) {|key| ... } -> object[permalink][rdoc]
key に関連づけられた値を返します。該当するキーが登録されてい ない時には、引数 default が与えられていればその値を、ブロッ クが与えられていればそのブロックを評価した値を返します。
fetchはハッシュ自身にデフォルト値が設定されていても単に無視します(挙動に変化がありません)。
引用:Ruby 2.5.0 リファレンスマニュアル Hash#fetch

ハッシュにkeyが存在しない場合は、ブロックの評価値が戻り値となるようです

実際の挙動を確認してみました

f:id:sktktk1230:20180710113633p:plain

ブロックの評価値trueが戻り値となりました

ここまでを踏まえ、さきほどのPRの内容を見てみると、

def log_to_stdout?
  options.fetch(:log_to_stdout) do
    options[:daemon].blank? && environment == "development"
  end
end

optionsに log_to_stdout が設定されていない場合、options[:daemon]がblankかつ環境がdevelopmentだとログ出力をするという処理になります

to_symを使わずに文字列からシンボルを生成 | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

概要

日課のrails commit log流し読みを読んでいて、文字列からシンボルに変換する方法が to_sym 以外にあることがわかったので、備忘録も兼ねて書いてみました

対象のコミット

github.com

to_sym を使わずに、シンボルを作る

今までは "hoge".to_sym で文字列からシンボル作っていたのですが、

to_symメソッドまたはinternメソッドは、文字列に対応するシンボル(Symbolオブジェクト)を返します。

s = "hello"
p s.to_sym
s = "symbol with spaces"
p s.to_sym

引用:Rubyリファレンス#to_sym,intern

:"hoge" でもシンボルを作れるようです

f:id:sktktk1230:20180529104606p:plain

[:blank:][:alnum:]ってなんだろう | rails commit log流し読みを読んでみた

f:id:sktktk1230:20180726121250p:plain

概要

y_yagiさんのrails commit log流し読みを読んでいてわからなかったこと調べてみました

y-yagi.hatenablog.com

わからなかったこと

  1. POSIX文字クラス

POSIX文字クラス

github.com:word: という記述を初めてみました

Unicodeプロパティと 似た機能を持つ記法として、POSIX 文字クラスと呼ばれるものがあります。 これらは上の省略記法とは異なり、文字クラスの中でしか用いることが できません。これらは [:クラス名:] という記法を持ちます。 また、[:^クラス名:]という記法でその否定を意味します。 以下の括弧では実際にどの文字にマッチするかが Unicode プロパティや Unicode コードポイントで示されています。
引用:https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html

種類は以下のものがあります

文字 説明
[:alnum:] 英数字
[:alpha:] 英字
[:ascii:] ASCIIに含まれる文字 (0000 - 007F)
[:blank:] スペースとタブ
[:cntrl:] 制御文字
[:digit:] 数字 (Decimal_Number)
[:graph:] 空白以外の表示可能な文字(つまり空白文字、制御文字、以外)
[:lower:] 小文字
[:print:] 表示可能な文字(空白を含む)
[:punct:] 句読点
[:space:] 空白、改行、復帰
[:upper:] 大文字
[:xdigit:] 16進表記で使える文字
[:word:] 単語構成文字

Rubyのバージョンによって挙動が変わったりすることもあるみたいです qiita.com