ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

【Rails 6.1】AS 句で作ったカラムに DB の型情報はない

本記事の環境

  • Rails 6.1.1

結論

9sako6 です。突然ですが、

AS 句で作ったカラムに DB の型情報はありません。

次の例をご覧ください。スキーマに日時型で定義されている created_at カラムは TimeWithZone オブジェクトが返るのに対し、AS 句で作った latest_created_at カラムは文字列が返されます。

> Foo.select('created_at').first.created_at
  Foo Load (0.2ms)  SELECT "foos"."created_at" FROM "foos" ORDER BY "foos"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> Mon, 08 Feb 2021 16:56:10.531831000 JST +09:00 # ActiveSupport::TimeWithZone

> Foo.select('MAX(created_at) AS latest_created_at').first.latest_created_at
  Foo Load (0.3ms)  SELECT MAX(created_at) AS latest_created_at FROM "foos" ORDER BY "foos"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> "2021-02-08 07:56:10.591486"                   # String
# スキーマ
  create_table "foos", force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

さらに、AS 句で作ったカラムは、limit, precision, scale のようなカラム修飾子の情報も持ちません。

この挙動を把握していないと、次のような問題が起こってしまうかもしれません。

  • Rails アプリに設定したタイムゾーンではなく、DB のタイムゾーンで日付が表示される

なぜこの挙動になるのかに興味のある方は以降のおまけをご覧ください。

おまけ: コードリーディング

次のコードを実行したとき、なんのメソッドが呼ばれるか追っていきます。

# DB からデータを取ってくるフェーズ
foo = Foo.select('created_at, created_at AS created_at_clone').first 

# 値を取得するフェーズ
foo.created_at_clone                                                 

その過程で、AS 句で作ったカラムに型情報がつかない理由もわかります。 スキーマにないカラムはデフォルトで ActiveModel::Type::Value 型になるから、です。

※ 記事中で書いた foos テーブルが定義されているものとします。

環境

  • Rails 6.1.1
  • SQLite3

DB からデータを取ってくるフェーズ

Foo.select('created_at, created_at AS created_at_clone').first で何が起こるかを追っていきます。

select メソッドから始まり、おおよそ次の順に呼ばれていきます。

ActiveRecord::ConnectionAdapters::DatabaseStatements#select

ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements#exec_query

ActiveRecord::ConnectionAdapters::AbstractAdapter#build_result

ActiveRecord::Result#initialize

次に、select の戻り値 Foo::ActiveRecord_Relation のインスタンスに対して first メソッドから次の順に呼ばれていきます。

ActiveRecord::FinderMethods#first

ActiveRecord::FinderMethods#find_nth

ActiveRecord::FinderMethods#find_nth_with_limit

ActiveRecord::Relation#to_ary

ActiveRecord::Relation#records

ActiveRecord::Relation#load

ActiveRecord::Relation#exec_queries

これが Foo のインスタンスを返します。上記の ActiveRecord::Relation#exec_queries の中で呼ばれる ActiveRecord::Querying#find_by_sql の中の ActiveRecord::Persistence::ClassMethods#instantiate_instance_offoo (Foo のインスタンス)のインスタンス変数 @attributes に DB からとってきたデータや型情報を格納しています。

ActiveRecord::Querying#find_by_sql

ActiveRecord::Persistence::ClassMethods#instantiate_instance_of

ここまでが DB からデータを取ってくるフェーズです。

まとめると、DB からとってきたデータやカラムの型情報などをfoo ( Foo インスタンス)のインスタンス変数 @attributes に格納しました。

@attributes は以下のようになっています。

> pp Foo.select('created_at, created_at AS created_at_clone').first.instance_variable_get(:@attributes)
  Foo Load (0.4ms)  SELECT created_at, created_at AS created_at_clone FROM "foos" ORDER BY "foos"."id" ASC LIMIT ?  [["LIMIT", 1]]
#<ActiveModel::LazyAttributeSet:0x00007feb86be6788
 @additional_types={},
 @attributes={},
 @casted_values={},
 @default_attributes=
  {"id"=>
    #<ActiveModel::Attribute::FromDatabase:0x00007feb82d6a9c8
     @name="id",
     @original_attribute=nil,
     @type=
      #<ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer:0x00007feb8662a470
       @limit=nil,
       @precision=nil,
       @range=-9223372036854775808...9223372036854775808,
       @scale=nil>,
     @value_before_type_cast=nil>},
 @materialized=false,
 @types=
  {"id"=>
    #<ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer:0x00007feb8662a470
     @limit=nil,
     @precision=nil,
     @range=-9223372036854775808...9223372036854775808,
     @scale=nil>,
   "created_at"=>
    #<ActiveRecord::Type::DateTime:0x00007feb82d6b378
     @limit=nil,
     @precision=6,
     @scale=nil>,
   "updated_at"=>
    #<ActiveRecord::Type::DateTime:0x00007feb82d6b378
     @limit=nil,
     @precision=6,
     @scale=nil>},
 @values=
  {"created_at"=>"2021-02-08 07:56:10.531831",
   "created_at_clone"=>"2021-02-08 07:56:10.531831"}>

値を取得するフェーズ

foo.created_at_clone で何が起こるかを追っていきます。 まず、ActiveModel::AttributeMethods#method_missing が呼ばれます。

ActiveModel::AttributeMethods#method_missing

    def method_missing(method, *args, &block)
      if respond_to_without_attributes?(method, true)
        super
      else
        match = matched_attribute_method(method.to_s)
        match ? attribute_missing(match, *args, &block) : super
      end
    end

次に、以下のメソッドが呼ばれます。

ActiveModel::AttributeMethods#attribute_missing

    def attribute_missing(match, *args, &block)
      __send__(match.target, match.attr_name, *args, &block)
    end

このとき、match.target'attribute'となっており、attributeメソッドが呼ばれます。 attributeメソッドの定義は ActiveRecord::AttributeMethods::Read#_read_attribute にあります。

ActiveRecord::AttributeMethods::Read#_read_attribute

      def _read_attribute(attr_name, &block) # :nodoc
        @attributes.fetch_value(attr_name, &block)
      end

      alias :attribute :_read_attribute
      private :attribute

次に呼ばれるのが fetch_value です。この fetch_value の戻り値が foo.created_at_clone の戻り値になります。

ActiveModel::LazyAttributeSet#fetch_value

  class LazyAttributeSet < AttributeSet # :nodoc:
    ...

    def fetch_value(name, &block)
      if attr = @attributes[name]
        return attr.value(&block)
      end

      ...

name'created_at_clone' です。attr = @attributes[name] に、created_at_clone カラムのデータをインスタンス化したものが格納されています。 そして、attr.value(&block) が、foo.created_at_clone として返ります。

ん...?ちょっと待ってください! 前節、「DB からデータを取ってくるフェーズ」で最後に記載した @attributes をもう一度思い出してみます。 前節で登場した @attributesfoo のインスタンス変数であり、 LazyAttributeSet のインスタンスです。

よく見ると、@attributes 自身もインスタンス変数 @attributes を持っています。そしてその中身は空の Hash です。

#<ActiveModel::LazyAttributeSet:0x00007feb864a16a8
 @additional_types={},
 @attributes={},       # HERE!!!
 @casted_values={},
 @default_attributes=
  {"id"=>
    #<ActiveModel::Attribute::FromDatabase:0x00007feb82d6a9c8
     @name="id",
     @original_attribute=nil,
     @type=
      #<ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer:0x00007feb8662a470
       @limit=nil,
       @precision=nil,
       @range=-9223372036854775808...9223372036854775808,
       @scale=nil>,
     @value_before_type_cast=nil>},

 ...

話を戻します。

  class LazyAttributeSet < AttributeSet # :nodoc:
    ...

    def fetch_value(name, &block)
      if attr = @attributes[name]
        return attr.value(&block)
      end

      ...

ここで登場した @attributes は、前節で登場した @attributes のインスタンス変数です。つまり、foo.instance_variable_get(:@attributes).instance_variable_get(:@attributes) です。 そして、中身は空でした。

fetch_value@attributes[name] すると値は取れるので、どこかで値を格納しているはずです。

値の格納は、一番最初に呼ばれた method_missingmatched_attribute_method(method.to_s) で行われます。次の流れです。

ActiveModel::AttributeMethods#matched_attribute_method

ActiveRecord::AttributeMethods::PrimaryKey#attribute_method?

ActiveRecord::AttributeMethods#attribute_method?

ActiveModel::LazyAttributeSet#key?

ActiveModel::AttributeSet#[]

ActiveModel::LazyAttributeSet#default_attribute

      def default_attribute(
        name,
        value_present = true,
        value = values.fetch(name) { value_present = false }
      )
        type = additional_types.fetch(name, types[name])

        if value_present
          @attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])

        ...

@attributes[name] = Attribute.from_database(name, value, type, @casted_values[name]) で値が格納されます。

typecreated_at_clone カラムの型です。types['created_at_clone'] が格納されます。

types は前節の @attributes のもつインスタンス変数 @types です。つまり、foo.instance_v ariable_get(:@attributes).instance_variable_get(:@types) です。

前節で @attributes が設定された際に @types も初期化されました。DB のもつカラムである id, created_at, updated_at の型情報が格納されています。

@types はただの Hash ですが、デフォルト値が ActiveModel::Type::Value のインスタンスになっています。スキーマにない 'created_at_clone' カラムの型は ActiveModel::Type::Value になるというわけです。

ちなみに、id の型は ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integercreated_at の型は ActiveRecord::Type::DateTime です。

以降、次のように呼ばれます。

ActiveModel::Attribute.from_database

ActiveModel::Attribute#value

ActiveModel::Attribute::FromDatabase#type_cast

ActiveModel::Attribute::FromDatabase#type_cast の戻り値が foo.created_at_clone です。

        def type_cast(value)
          type.deserialize(value)
        end

typeActiveModel::Type::Value が入るのでしたね。この deserialize メソッドは型ごとに定義されており、データベースから得た値を Ruby 型に変換します。

ActiveModel::Type::Valuedeserialize は、引数 value をそのまま返すメソッドです。 というわけで、foo.created_at_clone は文字列として返ります。

ActiveModel::Type::Value#deserialize

      def deserialize(value)
        cast(value)
      end

ActiveModel::Type::Value#cast

      def cast(value)
        cast_value(value) unless value.nil?
      end

ActiveModel::Type::Value#cast_value

        # Convenience method for types which do not need separate type casting
        # behavior for user and database inputs. Called by Value#cast for
        # values except +nil+.
        def cast_value(value) # :doc:
          value
        end

以上です。お疲れ様でした。

おまけのおまけ: コードを読んだ際の手の動き

EDITOR=code bundle open activerecord で、ランタイムで実際に動いている gem のコードを開きます。

明らかに呼ばれそうなメソッドに byebug を仕込み、up, down コマンドでスタックフレームを移動しながら呼ばれるメソッドを追います。これを繰り返します。

他にヒントが欲しければ Method#source_location で調べます。

最後に gem pristine activerecord して自分の編集箇所を残さないように掃除します。


永和システムマネジメント アジャイル事業部は Ruby とアジャイルの精鋭集団です。

一緒に働くことに興味をもってくださった方、ぜひ一度お話してみませんか!

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2021年2月のお知らせ

2021年2月の Rails / OSS パッチ会を 2月25日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Zoom あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントが必要になると思います。

以下、前回の活動が関わる成果です。

koic: RuboCop

github.com

okuramasafumi: Audited

github.com

osyo-manga: Ruby

github.com

yahonda: activerecord-oracle_enhanced-adapter

github.com github.com

昨年末にリリースされた Ruby 3.0、Rails 6.1 の実プロジェクトへの導入状況や、開発版となる Ruby 3.1 や Rails 7.0 に向けた話題などあるかもしれません。

その他の開催方針については以下の Gist に記していますので、ご参照ください。

Reboot Rails/OSS meetup online · GitHub

BuriKaigi2021 で Emoji の話をしました

はじめに

はじめまして。 @ima1zumi と申します。2021 年 1 月に株式会社永和システムマネジメントに中途入社しました。好きな gem は Relineirb で、最近は文字コードに興味を持っています。どうぞよろしくお願いします。

さて、先日オンラインで開催された BuriKaigi2021 で Unicode Emoji の話をしました。Emoji についてはスライドを読んでいただくとして、ここでは BuriKaigi で Emoji の話をした経緯と準備について書きました。

この記事を書くにあたり、 kasumi8pon から「ima1zumi さんが普段どのようなことを考えているのか分かるような記事になっているとよいですね」とアドバイスをいただきました。そこで発表するまでにどのような準備をしたのか、何を考えていたのか、に焦点を当てて書いてみました。この記事が何かを発表する方の参考になれば幸いです。

BuriKaigi とは

富山のおいしいもの(鰤など)を食べながらITに関することを勉強する会です。 .NET, Java, Ruby などさまざまな言語のセッションがあります。懇親会でおいしいぶりしゃぶを食べることで有名です。 毎年富山県で開催されていますが、今年は新型コロナウイルスの影響でオンラインでの開催となりました。

【オンライン開催】Burikaigi2021 - connpass

f:id:imaizumimr:20210203172146p:plain

ちなみに、懇親会参加者向けに冷凍ぶりしゃぶセット(有料)の案内もありました。豪華ですね!

登壇するきっかけ

BuriKaigi の 6 日前に知人から「 BuriKaigi の登壇枠が空いているので登壇しませんか?」と誘っていただきせっかくなので話すことにしました。本来は 30 分の枠でしたが、準備期間が短いため 15 分枠なら登壇できそう、ということで調整していただき登壇が決まりました。

テーマ選定

登壇する時に、私は以下のような内容で何か話せないか?という観点でテーマを考えています。

  • 最近学んだこと・わかったことで面白く感じていること
  • ハマったことと解決方法
  • 登壇をゴールに開発する・調べる*1
  • 自分の経験をまとめる

また、そのテーマを選んだときにどれだけ情熱を持って話せるか、何か伝えたいことがあるのかという点を重視しています。 テーマについてよく知っていたり語りたいことがあると、モチベーションが上がります。モチベーションが高いと準備も捗りますし、話にも熱が入りいい話になるのではないかと思っています。

具体的なテーマ選定

例えば今回の場合はこのように考えていました。

  • 文字コードや Emoji の話は Reline を通して学んだため少し知見があるし面白いと思っている
  • Ruby の String の話もしたいが、 Ruby ユーザー以外も多く参加するのであまり合わないかもしれない
  • Reline の概要はどこかで話したいが、しっかり準備したいので今回は難しそう

これらのテーマを候補として考えつつ、BuriKaigi にどういう参加者が集まるのか、他の方がどういうテーマで話すのか運営の方に聞いてみました。 Ruby 以外の言語を使う人も多いことと、マニアックな話は歓迎ということでした。それなら Emoji の話が良さそうだと思い Emoji の話をすることにしました。

テーマの深堀り

まだまだスコープが粗い状態なのでもう少し具体的な内容を考えます。

最初はざっくりと何を話すか考えました。LT では何度か登壇したことがあるので、5 分 x 3 本ならどんなことを話せるかというイメージで決めていきました。

例えば今回は、5 分ずつ以下のテーマで話せそうだと思いました。

  • Emoji の歴史
  • よく問題になる Emoji
  • Emoji の仕様

その後もう少し肉付けして、

  • Emoji に興味を持ったきっかけ
    • Reline
  • Emoji の歴史・前提
    • エモティコン、顔文字
    • ドコモの Emoji
    • Unicode に入って普及
    • どんどん増える Emoji
    • Emoji の特徴
    • 書記素
  • よく問題になる Emoji
    • ZWJ
    • SKINTONE
    • REGIONAL
    • VALIATION
    • KEYCAP
  • Emoji の仕様
    • Unicode の technical standard
    • Emoji テーブル
    • Emoji 採択のプロセス
  • Emoji は面白い!

こういう内容で話そう、と仮決めしました。

スライドを作る

上の箇条書きをもとに、スライドを作っていきます。

スライド作成ツール

私はよく reveal.js を使っています。 Markdown で書けるので、さっと書けて書き心地がよいです。公式ドキュメントが充実しているため、こういうことをしたいときにはどうすればいいんだろう?と思ったときに迷わなくてよいので便利です。

revealjs.com

スライド作成と練習

私は初稿は思うまま書いて、後から修正を繰り返してブラッシュアップしていくスタイルです。最初は自分の素直な言葉でスライドを書いていきます。口語になっていたり、よくわからなくてもとりあえず言葉に起こしていきます。

途中で筆が止まったら一旦声に出して読んでみて構成におかしな点がないか、時間がどれくらいかかるか確かめます。 今回は 2/3 くらい書いたところで筆が止まったので、一旦声に出してみることにしました。すると思ったより時間がかかることがわかり、Emoji の仕様に関するスライドは作らないことにしました。

声に出して読み上げると、スライドに書いてある構成では話の流れがおかしい、というところがわかります。私はその場でアドリブで話を組み立てるのは苦手なので、スライドを見て読み上げれば自然と構成が正しくなるように、脱線しないように意識してスライドを書いています。そのため、一旦完成したら声を出して読みながら違和感があるところを修正する、を繰り返しながら仕上げていきます。

また、これは実践できていないことも多いのですが、スライドの情報だけ見て理解できるような要約を書くことも意識しています。そのようなスライドは、イベントが終わった後に参照しても役に立つからです。音声で補足しないと伝わらない情報があれば、できるだけ文字に書き起こすようにしています。

発表前にやること

オンライン登壇の発表前にやっていることです。

  • 画面共有できることを確認
    • 全画面を共有する場合、通知や映ってはいけないものが映らないように設定
      • Slack, Discord などを停止する、おやすみモードにする
      • ブラウザで共有する場合、ブックマークバーを非表示にする
  • マイクの音量を確認
  • カメラを確認
  • インターホンの音量を下げる、止める
  • ストップウォッチを準備
  • スライドをアップロードする
  • スマートスピーカーを切る

私は Amazon Echo Dot をデスクの上に置いているのですが、本番中に Alexa が桃太郎の解説を始める*2というトラブルがあり非常に焦りました。スマートスピーカーは本番前に止めておきましょう。

発表中にやること

ストップウォッチを見ながら時間を調整しつつ話します。

余裕があるときは Twitter やチャット欄を見ながら話しますが、余裕がない時はスライドに集中します。今回は練習量が足りず緊張していたので、スライドの他は何も見ず話していました。

発表後にやること

スライドを公開します。Twitter でイベントのハッシュタグをつけて tweet することが多いです。connpass でのイベントの場合はイベントページに資料のリンクを貼れるのでそちらからも貼ります。

発表を終えてみて

発表前は、15分聞いていただけるだけの内容にできるかとても不安でした。ですがなんとかまとまった内容を発表できてよかったです!スライドを作るにあたり Unicode の technical standard を読んで学んだり、Emoji の歴史について知ることができて良かったです。 Emoji への知識と愛がより深まりました。反省点としては、準備時間が足りず用語の使い方が怪しいところがあったことです。普段から正確な言葉を使えるよう意識していきたいです。

このようなしっかりした場で発表するのは初めてでしたが、なんとか無事終えることができました。誘ってくださった方、こんな機会をくださった BuriKaigi 運営のみなさま、ありがとうございました!いつかぶりしゃぶを食べに富山に行きたいです🐟

*1:いわゆる登壇駆動開発

*2:なぜ桃太郎なのかは不明

コミットハッシュからプルリクエストを特定する

Dolce Gustoを買ってからコーヒーを飲みすぎてしまっている wat-aro です。

開発をしていると書かれているコードの意図がわからないことがあります。
そういうときは git blame で該当コミットを確認しますが、コミットを確認しても意図がわからない場合にはプルリクエストを確認したくなります。
ここではプルリクエストを確認しやすくするためにコミットハッシュからプルリクエストを特定する方法を2点紹介します。

Merge pull request コミットから特定する

まずは コミットハッシュ プルリクエスト 検索 などで検索すると見つかるやり方*1です。
GitHub でプルリクエストをマージすると

Merge pull request #ISSUE_NUMBER from OWNER/BRANCH

のようなコミットメッセージがついたコミットが作成されます。

指定したコミットハッシュから HEAD までのログの中から最初に Merge pull request が含まれているコミットを見つければそこにプルリクエストの番号が入っています。

$ git config --global alias.searchpr '!f(){ git log --oneline --merges --reverse --ancestry-path $1..HEAD | grep "Merge pull request" | head -n 1 | cut -d" " -f 5;};f'
$ git searchpr b71abb3bb8cd
#33137

上記コマンドに GitHub CLI コマンドを合わせることでブラウザで開くこともできます。

$ git config --global alias.openpr '!f(){ git searchpr $1 | xargs -I ISSUE_NUMBER gh pr view ISSUE_NUMBER --web;};f'

しかし、この方法では Rebase and mergeSquash and merge には対応できません。

GitHub API から特定する

Rebase and merge Squash and merge の場合でも GitHub から検索することで該当プルリクエストを特定することができます。
GitHub CLI の gh pr view でコミットハッシュからプルリクエストを見ることができればよいのですが、残念ながらそのようなオプションはありません。

$ gh pr view --help
Display the title, body, and other information about a pull request.

Without an argument, the pull request that belongs to the current branch
is displayed.

With '--web', open the pull request in a web browser instead.


USAGE
  gh pr view [<number> | <url> | <branch>] [flags]

FLAGS
  -c, --comments   View pull request comments
  -w, --web        Open a pull request in the browser

INHERITED FLAGS
      --help                     Show help for command
  -R, --repo [HOST/]OWNER/REPO   Select another repository using the [HOST/]OWNER/REPO format

LEARN MORE
  Use 'gh <command> <subcommand> --help' for more information about a command.
  Read the manual at https://cli.github.com/manual

しかし GitHub CLI v0.10.0 から GitHub GraphQL API を叩けるようになりました。*2
これを使って、コミットハッシュを渡すと GitHub GraphQL API を叩いてプルリクエストの番号を返す alias を登録してみます。
取得した JSON からプルリクエストの番号を抽出するため jq が必要です。
以下は fish shell での記述になります。

$ gh alias set --shell searchpr "gh api graphql -F owner=':owner' -F repo=':repo' -F hash=\$1 -f query='
  query(\$repo:String!, \$owner:String!, \$hash:String) {
    repository(name: \$repo, owner: \$owner) {
      object(expression: \$hash) {
        ... on Commit {
          associatedPullRequests(first: 1) {
            edges {
              node {
                number
              }
            }
          }
        }
      }
    }
  }
' | jq .data.repository.object.associatedPullRequests.edges[0].node.number" 

zsh では ! もエスケープしなければいけません。

$ gh alias set --shell searchpr "gh api graphql -F owner=:owner -F repo=:repo -F hash=\$1 -f query='
  query(\$repo:String\!, \$owner:String\!, \$hash:String\!) {
    repository(name: \$repo, owner: \$owner) {
      object(expression: \$hash) {
        ... on Commit {
          associatedPullRequests(first: 1) {
            edges {
              node {
                number
              }
            }
          }
        }
      }
    }
  }
' | jq .data.repository.object.associatedPullRequests.edges[0].node.number"

bash では \! でエスケープした場合にそのまま \! という文字列になってしまうためこの書き方では実行できません。
この alias は次のように使うことが出来ます

$ gh searchpr b71abb3bb8cd
33137

後はプルリクエストを開く alias を登録するだけです。

$ gh alias set --shell openpr "gh searchpr \$1 | xargs -I ISSUE_NUMBER gh pr view ISSUE_NUMBER --web"
$ gh openpr b71abb3bb8cd

GitHub CLI は GitHub の OAuth access token を ~/.config/gh/hosts.yml に保持しているため、プライベートリポジトリでもこの方法でプルリクエストを特定することができます。

おわりに

GitHub CLI コマンドを使ってコミットハッシュからプルリクエストを特定する方法について紹介しました。
gh api コマンドを使うことで他にも色々なリソースを操作することができます。
GraphQL API の Explorer で query を作成して GitHub CLI を便利にしていきましょう。

この記事が開発の助けになれば幸いです。

standard librariesとdefault gemsとbundled gemsの違い

日本のハワイ、宮崎よりお送りします。yoshinoです。

先日、XMLでレスポンスを返すAPIがあったので、require 'rexml/document'でロードしてXMLをパースしようとすると、cannot load such file -- rexml/documentが起きてしまう事象に遭遇しました。

Ruby 3.0.0 Releasedによれば、Ruby3.0からrexmlrssはbundled gemになったようです。

このリリース記事では、libraryとして次のような言葉が使われていて、

  • standard libraries ( stdlib files )
  • default gems
  • bundled gems
  • default gemsでもbundled gemsでもないgem (その他のgem )

「具体的に何が違うんだっけ?」となったので、この4つの言葉について調べました。

standard libraries

Ruby 3.0.0 Standard Library Documentation

Ruby standard library (found in lib/ directory of the Ruby source code, or lib/ruby/1.x/ of a Ruby installation)

GitHubのRubyリポジトリruby/libディレクトリ下にあるコードを指すようです。 

default gems

standard librariesの中でgem化されたライブラリを指します。

gem listを実行した時に、csv (default: 3.1.9)のように、defaultが出力されることからも確認することができます。

$ gem list | grep default
abbrev (default: 0.1.0)
base64 (default: 0.1.0)
benchmark (default: 0.1.1)
.
.
.

default gemsは消すことができません。

$ gem uninstall abbrev
Gem abbrev-0.1.0 cannot be uninstalled because it is a default gem

Ruby3.0の時点では、standard librariesのうち88%(101/114)がgem化されていて、standard librariesの開発に依存しないで、Rubyの開発ができるように、残りの部分に関してもgem化が目指されているようです。

Ruby3.0のリリースでも、たくさんのstandard librariesがgem化されたことがアナウンスされています。

The following stdlib files are now default gems and are published on rubygems.org.
English
abbrev
base64
drb
.
.

例えばbase64はRuby3.0からgem化されていることがわかります。

$ ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
$ gem contents base64
Unable to find gem 'base64' in default gem paths
$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]
$ gem list | grep base64
base64 (default: 0.1.0)

bundled gems

Rubyをインストールした時に一緒にインストールされる点が、その他のgemとは異なります。

例えば、Ruby3.0ではrssはbundled gemになったため、Rubyと一緒にインストールされますが、default gemではないため、後から削除することができます。

$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux]
$ gem list | grep rss
rss (0.2.9)
$ gem uninstall rss
Successfully uninstalled rss-0.2.9

Railsへの影響など

  • bundled gemになったgemはGemfileで指定して、bundle installする必要があります
  • gem化されたstandard libraryは、Gemfileに指定することで、bundlerで管理することができるようになります

リファレンス

Railsのバグレポートの書き方

こんにちは。Railsを普段使いしていて、想定しない挙動をしたり/この挙動は不具合ではないかと疑うことがあるかと思います。 今回enum関連の不具合報告をしました。本記事では、バグレポートの書き方と実際に報告した不具合の内容を説明したいと思います。

バグレポートの書き方

バグレポートは、基本的に Rails Guide の Creating a Bug Report に沿って報告するのが良いです。 付け加えると、実際に再現するようなRailsアプリや再現コードがIssueに記載出来るとベストです。

私が実際に挙げたIssueはUse non-exist enum string to get unrelated record in My SQL となります。

Issueの内容

まず、以下のユーザーモデルが定義されていたとします。

class User < ActiveRecord::Base
  enum status: { active: 0, non_active: 1 }
end

この時 User.find_by(status: :non_exist_status) とコードを書くとステータスが active のデータが取れます。原因は SELECT * FROM users WHERE status = 'non_exist_status' が発行されるのですが、文字列から数字への暗黙の変換がされてしまい SELECT * FROM users WHERE status = 0 と同じ意味になってしまうからです。

終わりに

バグ報告をまとめるのは、結構大変な行為かと思います。なので、無理せずに余裕がある時やモチベーションが高い時などに挑戦してみるのはいかがでしょうか?

コミュニケーションに必要なもの

こんにちは。 はじめまして。tkywtnb です。

ソフトウェア開発を行っていると様々な場面で人とのコミュニケーションが発生します。 コミュニケーションが上手くいっているかどうかがソフトウェア開発に大きな影響を与えることがあるのは、みなさんも心当りがあるのでないでしょうか?

今回は、コミュニケーションに必要なものは何なのか、私が考えていることを言語化してみたいと思います。

以降の文中に何度かコンテキストという言葉が出てきますが、知識、経験、立場、価値観などを統合した概念として、コンテキストを使っています。

そもそもコミュニケーションって何だろう?

先に進む前に、そもそもコミュニケーションとは何なのかについて確認しておきます。

コミュニケーション【communication】

1 社会生活を営む人間が互いに意思や感情、思考を伝達し合うこと。言語・文字・身振りなどを媒介として行われる。「コミュニケーションをもつ」「コミュニケーションの欠如」

2 動物どうしの間で行われる、身振りや音声などによる情報伝達。

引用元: デジタル大辞泉

今回取り扱う範囲のコミュニケーションは、 1 の 「社会生活を営む人間が互いに意思や感情、思考を伝達し合うこと」にあたります。

さっそく、伝達することの結果である「伝わる」までを詳しくみていくことにします。

伝わるということ

何かが伝わるまでは、どのような事が行われているのでしょう? 「わたし」から「あなた」へ何かが伝わるまでは、おおよそ次のようなプロセスを経ていると考えられます。

  1. 「わたし」が伝えるべき事を認識する
  2. 「わたし」が伝えるべき事を表現する
  3. 「あなた」が表現されたものから伝えられた事を読み取る

これはかなり単純化したモデルです。 実際のコミュニケーションにおいては、単純に 1 から 3 の順序に進むとは限りません。 例えば、伝えることを表現しようとすることによって伝えるべきことがよりはっきりと認識出来たり、表現したことによって伝えるべきことが違っていると感じたりすることがあります。それでも、この単純なモデルを使ってコミュニケーションで発生する問題を考えることが出来ます。

どんな問題が起きるのかひとつずつ確認していきましょう。

「「わたし」が伝えるべき事を認識する」段階で発生する問題

ここで起きそうな問題は、次のようなものでしょう。

  • 何を伝えるべきなのかわからない
  • 伝えるべき事を誤認する

これらの問題はなぜ発生するのでしょう?

まっさきに挙げられるのは事実と感情を区別できていないケースです。 個人的に観測している範囲では、かなりの割合でこの問題が発生しているように見えます。

また、何を取り扱うのかについて、伝える相手との間でまったく共有されていない状況だとしたら、容易に発生しそうです。 取り扱う対象への理解が十分でないことによっても引き起こされるでしょう。 相手が取り扱う対象をどのように理解しているのかについての認識が不十分であっても問題が発生すると考えられます。

さらに、何かを教えたりする状況において、相手のスキルや知識に適さないことを伝えるべき事として誤認してしまうことがあります。 学習する人が何かを学ぶ為には、前提となる知識やスキルが必要となることがほとんどです。 相手の状態を十分に把握していないと、あまり効果がない指導になってしまうでしょう。

「「わたし」が伝えるべき事を表現する」段階で発生する問題

ここで起きそうな問題は、次のようなものでしょう。

  • どのように表現したら良いかわからない
  • 表現の仕方を間違える

これらの問題の要因は、自身のスキルと知識によるものです。

どのように表現したら良いかわからないことの要因は、表現の手段により異なります。

まず言葉による表現を考えると、必要な語彙を持っていないことによって引き起こされます。 語彙が乏しいことによる影響は、表現が出来ないだけに留まらず、もっと大きなものになります。 語彙が乏しいことは、言葉の違いによる微妙なニュアンスの違いを認識出来ないことにも繋がり、先に述べた取り扱う対象などへの理解が浅くなる要因にもなり得ます。

言葉による表現は、細心の注意をはらったとしても曖昧さを含んでしまうものであり、図などを用いて表現することが有効なケースがしばしばあります。 プログラマ同士のコミュニケーションの場合、具体的なコードで表現することが可能なものはコードで表現するのが一番でしょう。 しかし、図解を用いたり適切なコードで表現するためには、ある程度のスキルが必要になります。 これらのスキルが不足していると、問題が発生することになります。

表現の仕方については、単に事実などの情報を誤って表現してしまうというのも問題となりますが、もう少しややこしい問題を含んでいます。

攻撃的な表現を使ってしまったり明言を避ける為に曖昧な表現を使ってしまったりといった問題も、適切な表現でないという意味で、表現の仕方を間違えていると言えるでしょう。 例えば、ただ相手の理解を確認すれば良いケースにおいて、まだ理解できないのか?といったプレッシャーを掛けてしまうようなものが、攻撃的な表現にあたります。 このような態度は意図が正しく伝わらないだけでなく、相手が理解することを諦めてしまう事などに繋がるのでとても有害です。 ここで挙げた表現の差異は性格の問題のように捉えられがちですが、スキルの問題です。

相手のスキルや知識に適していない表現を使ってしまうのも、表現の仕方の間違いの一種でしょう。 これは相手に関する理解が不十分な為に引き起こされます。

「「あなた」が表現されたものから伝えられた事を読み取る」段階で発生する問題

ここで起きそうな問題は、次のようなものでしょう。

  • 表現に使用された言葉を知らない
  • 表現に使用された言葉などの解釈が「わたし」と異なる

これらの問題の要因は、相手のコンテキストに適さない表現を選択したことによるものと考えられます。 つまり、このステップで問題が発生しているわけではなく、前の段階である表現の選択時点で問題が発生していることになります。

意図したとおりに伝わったかどうかは相手に確認してみるしかないのですが、その事が問題をややこしくしています。 この後に何が起きるのかをみてみましょう。

  1. 「あなた」が伝えられた事をもとに「わたし」へ伝える事を認識する
  2. 「あなた」が伝えるべき事を表現する
  3. 「わたし」が表現されたものから伝えられた事を読み取る

これらのステップは「わたし」から「あなた」への逆となっているだけで、1.の表現が少し異なるものの同じものと言えるでしょう。 よって、これらのステップにおいてもここまで見てきた問題が発生する可能性があり、2重3重に問題が積み重なってしまうこともあります。 その為、問題が発生した場合にどの時点で発生した問題かを特定するのは困難です。

実際のコミュニケーションにおいては、何か噛み合っていないな?という違和感に気が付き、ひとつずつ確認することによって問題を把握することになると思います。

問題の原因とその対策

各段階で発生する問題と原因についてみてきました。 改めて原因を分類しつつまとめます。

  • 相手への理解に関するもの
    • 相手が取り扱う対象をどのように理解しているか、十分に把握出来ていない
    • 相手のスキルや知識を十分に把握出来ていない
  • 自身のスキルに関するもの
    • 事実と感情を混同して捉えてしまう
    • アサーティブでない態度になっている
    • 表現するのに使用するスキルが十分でない
  • 事前準備に関するもの
    • 何を取り扱うのかについて共有されていない
    • 取り扱う対象への理解が不十分である
    • 取り扱う対象を含む分野に関する語彙が乏しい

ここからは原因の分類ごとに対策を考えてみます。

相手への理解に関するもの

ここに挙げたものは、相手に尋ねるなどする必要があるものになります。 つまり、対策する為にコミュニケーションが必要になります。 コミュニケーションの矛盾ともいうべきところがここです。 冗長な表現になってしまうのを覚悟で矛盾について説明するならば、次のようになります。 総じてコミュニケーションの目的は何らかの共通理解を築くことだと捉えることができますが、共通理解を築かなければならない状況とは共通理解が築かれていない状況なのです。

ここに挙げられているような要因を解消していくことが、コミュニケーションだと言い換えられるかもしれません。

共通して理解出来る表現を探りながら、 それを足がかりとしてお互いのコンテキストについて理解を深めるよう、 丁寧に対話を重ねていく。 これ以外の方法は無さそうです。

自身のスキルに関するもの

ここに挙げたものも、基本的には事前に対策が可能なものです。 ただし、対策がスキルの習得となる為、必要な期間が長くなります。 それでは個別に見ていきます。

事実と感情を混同して捉えてしまう

客観的事実と、どのように感じたのかという感情を区別し整理することで、このケースは避けることが出来ます。 次に取りあげるアサーティブコミュニケーションのスキルを身に付けることで、自然と実践出来るようになるでしょう。

アサーティブでない態度になっている

突然アサーティブという言葉が出てきましたが、これは何でしょうか?

アサーティブネス(Assertiveness)の訳語は、「自己主張すること」。でも、アサーティブであることは、自分の意見を押し通すことではありません。 自分の気持ちや意見を、相手の気持ちも尊重しながら、誠実に、率直に、そして対等に表現すること を意味します。

引用元: はじめに | アサーティブジャパン

ポイントは性格を変えるのではなく伝え方を変えることにあります。 アサーティブなコミュニケーションはスキルとしてトレーニングする事が可能です。 詳細については、書籍*1などを参照いただければと思います。

表現するのに使用するスキルが十分でない

問題の記述の中では、図解とコードで表現するスキルを取りあげました。 これらは日頃の訓練によって身に付けておく必要があります。 特に図解するスキルについては、プログラマ以外の方とのコミュニケーションにも有効なので、積極的に身に付けると良いでしょう。

事前準備に関するもの

ここに挙げたものは、次のように事前に準備しておくことで対策が可能なものです。

  • 何を取り扱うのか事前に共有しておく
  • 取り扱う対象について調査するなどして理解を深めておく

もちろん、シンプルな状況においてはこのような準備が必要ないことも多いでしょう。 しかし、思い込みによって本来は準備が必要だった状況をみすごしてしまうこともあるので、注意が必要です。

コミュニケーションにおいて一番大切なこと

色々な問題に対する対策を見てきましたが、コミュニケーションにおいて一番重要な事はなんでしょうか?

対策の中で触れたように、スキルはもちろん必要です。 ですが、それ以上に大切なのは異なるコンテキストを持つことを受けいれ理解しようとする姿勢にあると、私は考えています。 どちらかが正しいというのではなく、違いを知り理解しない事にはすり合わせる事すらできません。

違いを受け入れることによって、異なる視点をもつことを強みに変えていくことが出来るでしょう。

おわりに

ソフトウェア開発は創造的な問題解決と捉えることが出来ます。 創造的な問題解決には多様性に富む集団の方がそうでない集団と比べると、 たいてい高い成果を出すという実験結果があるそうです*2。 しかし、多様性は摩擦の増加の要因になります。 異質な人同士がひとつの問題に取り組み成果を出すのは容易な事ではありません。 多様な視点から新たな気づきを得て成果につなげるには、異なるコンテキストを持つ事を前提としたコミュニケーションが不可欠です。

豊かなコミュニケーションにより多様性を生かして素晴しいものを生み出していく。 少しでもそのヒントになれば幸いです。

*1:

*2:出典: 「Learn Better――頭の使い方が変わり、学びが深まる6つのステップ」 P.221