ken2merのブログ

Webエンジニア, Ruby on Rails, Go

ActiveRecord::Batches のメソッドについて

はじめに

バッチ処理などで大量のレコードに対して処理を行わなければいけない時、全てのレコードを一度に読み込んでしまうとメモリ負荷の観点から非効率的である。

そのような時に使えるのが ActiveRecord::Batches のメソッド (in_batches, find_in_batches, find_each) である。

これらのメソッドを使えば、大量レコードをいくつかの単位(デフォルトで1000件)に分割して処理を行うことができる。

in_batches

in_batches メソッドでは、分割単位ごとに絞り込まれた ActiveRecord::Relation オブジェクトをブロックの引数として処理を行う。

使い方として、おおきく3つを挙げる。

①分割したレコードごとに一括処理

User.in_batches do |users|
  users.update_all atrributes
end

②分割してレコードを読み込み、それを使い回すような処理

User.in_batches do |users|
  users = users.to_a

  OneService.call users
  AnotherService.call users
end

③分割してレコードを読み込み、その1件ずつに対する処理

User.in_batches do |users|
  users.each do |user|
    user.do_something!
  end
end

このうち②と③は、それぞれ find_in_batches, find_each で置き換えることができる。

find_in_batches

find_in_batches メソッドでは、ブロックが渡された場合、処理の中で in_batches が呼ばれるようになっている。 https://github.com/rails/rails/blob/v7.0.1/activerecord/lib/active_record/relation/batches.rb#L137-L139

      in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore, order: order) do |batch|
        yield batch.to_a
      end

これを踏まえると②のコードは以下のように書き換えられる。

User.find_in_batches do |users|
  OneService.call users
  AnotherService.call users
end

find_each

find_each メソッドでは、ブロックが渡された場合、 処理の中で find_in_batches が呼ばれるようになっている。 https://github.com/rails/rails/blob/v7.0.1/activerecord/lib/active_record/relation/batches.rb#L70-L72

        find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do |records|
          records.each(&block)
        end

これを踏まえると③のコードは以下のように書き換えられる。

User.find_each do |user|
  user.do_something!
end

まとめ

ActiveRecord::Batches に3つののメソッドがあり、それらは find_eachfind_in_batchesin_batches のような関係性にあることがわかった。

Railsアプリケーションで大量レコードの処理を行う際はこれらのメソッドの違いを理解し、処理内容によってどのメソッドが適切か考えた上で使うようにする。

参考URL

Goの標準パッケージを試す (sync.RWMutex, net.Listen, net.Accept, net.Dial)

sync.RWMutex

https://github.com/Ken2mer/go-example/commit/b22404c36603c2d927567aa597a941ac78251a97

みんなのGo言語 1.4章で map排他制御の例が載っていたので、それを参考に Example テストを実装した。あとで sync.RWMutex がない場合や、うまく動かなかった場合にFailとなるようなテストコードを追加してみたい。

net.Listen, net.Accept

https://github.com/Ken2mer/go-example/commit/50791c9ca06a5f21a5ddbe212e27538399816762

この記事 を参考に net.Listennet.Accept でHTTP Serverを実装した。テストの途中でサーバーを停止させたかったため channel や signal.Notify といった処理を追記している。テストでは SIGINT を無理やり?発行させているが、もっと良いやり方がないか模索したい。

Windowsでは syscall.Kill が使えず テストで落ちる ことが分かったので、これも何とかしたい。

net.Dial

https://github.com/Ken2mer/go-example/commit/a7de1051ea8c341b5a8a706cc143b19cdd8da01d

こちらも この記事 を参考に net.Dial を使ってHTTPクライアントを実装してみた。

strings.Split(url, "://")[1] としていたところは url.Parse を使うように直した。 https://github.com/Ken2mer/go-example/commit/cb746e697bfb798dd8577a1ef67354291a4d41ce

Daprをローカルマシンにインストールして試した

getting-started

https://docs.dapr.io/getting-started/

ここからバイナリをダウンロード可能。 https://github.com/dapr/cli/releases

Linux/MacOS の場合は /usr/local/bin などに配置して実行可能にしておけばOK。

dapr init を実行してから docker ps を実行してみて以下のような出力を確認する。

CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS         PORTS                              NAMES
0dda6684dc2e   openzipkin/zipkin        "/busybox/sh run.sh"     2 minutes ago   Up 2 minutes   9410/tcp, 0.0.0.0:9411->9411/tcp   dapr_zipkin
9bf6ef339f50   redis                    "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes   0.0.0.0:6379->6379/tcp             dapr_redis
8d993e514150   daprio/dapr              "./placement"            2 minutes ago   Up 2 minutes   0.0.0.0:6050->50005/tcp            dapr_placement

hello-world

https://github.com/dapr/quickstarts/blob/v1.0.0/hello-world/README.md

# clone the repo, then navigate to the Hello World quickstart
git clone https://github.com/dapr/quickstarts && cd quickstarts/hello-world

# Install dependencies
npm install

# Run Node.js app with Dapr
dapr run --app-id nodeapp --app-port 3000 --dapr-http-port 3500 node app.js

以下のコマンドでPOSTリクエストの送信が試せる。

dapr invoke --app-id nodeapp --method neworder --data '{"data": { "orderId": "42" } }'

また、上記と同様のリクエストを curl で行う場合は以下のようになる。

curl -XPOST -d @sample.json -H "Content-Type:application/json" http://localhost:3500/v1.0/invoke/nodeapp/method/neworder

curl で行う場合と比べ、かなりシンプルで直感的なコマンド記述が可能なことが分かった。

Go製whoisクライアントの実装をみてみた

https://github.com/domainr/whois

Go製のwhoisクライアントを見かけたので実装を見てみた。 Ruby製のものにインスパイアされているらしい。

Trending Go repositories on GitHub today で見かけた ditto というツールの中で使われていて興味を持った。

利用方法

以下のようにするだけで指定したクエリに対するドメイン情報の出力が得られる。

query := "example.com"
response, _ := whois.Fetch(query)
fmt.Println(response.String())
//    Domain Name: EXAMPLE.COM
//    ...

whois-parser-go を使えば出力をパースすることができる。1

import (
    whoisparser "github.com/likexian/whois-parser-go"
    "github.com/k0kubun/pp"
)

parsed, err := whoisparser.Parse(res.String())
pp.Println(parsed)
// whoisparser.WhoisInfo{
//   ...
// }

実装について 2

zonedb というライブラリを使ってWhoisサーバを識別しているところが特徴。上記例ではクエリに "example.com" を与えているが、その場合は "whois.verisign-grs.com" というホストが得られる。ここのポート43にTCP接続してクエリをやり取りすることでドメイン情報が得られる。

以下にソースコードの一部を抜粋しておく。

Whoisサーバを識別してホスト(とURL)を得る箇所が以下。

request.go#L32

   req.Host, req.URL, err = Server(req.Query)

Server 関数の内容

whois.go#L22

// Server returns the whois server and optional URL for a given query.
// Returns an error if it cannot resolve query to any known host.
func Server(query string) (string, string, error) {
    // Queries on TLDs always against IANA
    if strings.Index(query, ".") < 0 {
        return IANA, "", nil
    }
    z := zonedb.PublicZone(query)
    if z == nil {
        return "", "", fmt.Errorf("no public zone found for %s", query)
    }

    // Try whois URL first (these are relatively rare)
    wu := z.WhoisURL()
    if wu != "" {
        u, err := url.Parse(wu)
        if err == nil && u.Host != "" {
            return u.Host, wu, nil
        }
    }

    // Then try host (more common)
    h := z.WhoisServer()
    if h != "" {
        return h, "", nil
    }

    return "", "", fmt.Errorf("no whois server found for %s", query)
}

指定したクエリに . が含まれていなければ、リクエスト先は常に IANA ("whois.iana.org" という文字列の定数) になる。

. が含まれる場合は後続の処理を行う。先にレアケースであるWhois URL3が得られるか確認し、なければWhoisサーバを取得する。取得できなければここでエラーとなる。


  1. structの出力用に pp を使用

  2. 2021年2月1日時点のmainブランチを参照している。

  3. サブドメインwhois情報を検索するためのURLのことらしい (参照)

Railsアプリケーションでsteep checkを試す

Ruby 3関連でRailsアプリケーション上で何か試したいと思っていたら以下の記事を発見したので、これをそのまま試してみる。

pocke.hatenablog.com

Gemfileに必要なGemを追加

https://github.com/Ken2mer/rails_master_test_app/commit/9d12ab40fa3aaadf18fcb823c52ddae4ea0bfb66

Rake Taskの追加

https://github.com/Ken2mer/rails_master_test_app/commit/479b06a43c9badf91b42f9acebd4a8646db6d60a

Rake Taskの実行(bin/rake rbs_rails:all)

https://github.com/Ken2mer/rails_master_test_app/commit/cdd9d753eb8665ab7eb70bb8e2124db969b60bed

このコミットから、元記事の解説と実際に行われた処理を照らし合わせてみる。

RBS Railsから、解析に必要なRBSファイルをコピーしてくる

Rake Taskの中で以下の処理が呼ばれるよう。 https://github.com/pocke/rbs_rails/blob/1b6e4f7bc21624c9fabac122152e97794c5ca1b7/lib/rbs_rails.rb#L14-L18

上記により「解析に必要なRBSファイル」は以下のディレクトリ配下のものを示すことがわかる。 https://github.com/pocke/rbs_rails/tree/master/assets/sig

データベースのスキーマ定義から、対応するモデルのRBSファイルを生成する

今回使用したアプリケーションに含まれるモデルは UserMessage のみ。それに対応するRBSファイルが生成されているのがわかる。

routes定義から、パスヘルパーのRBSファイルを生成する

ルーティングの定義をしていなかったので確認できず。あとでやってみる。

git submoduleを使ってgem_rbsをダウンロード

https://github.com/Ken2mer/rails_master_test_app/commit/1de8e4c56098fe8ab980f1dc0197d375c8cb7931

rbs validate のエラー解消

https://github.com/Ken2mer/rails_master_test_app/commit/0f2f461fea069b889a47de368100ef2ecede356d

このコミットでやっているのは、元記事でも触れられている ApplicationRecord の型定義と、まだ未対応っぽいActiveStorageの型定義の手動追加である。これで一旦 rbs validate コマンドを通すことができた。

Steepfileの設定

https://github.com/Ken2mer/rails_master_test_app/commit/bf45d698aab6b55ed08668ec2caa570d2ff62856

このコミットではSteepfileは元記事と同じ内容のままで使っている。また steep init コマンドで生成できるとあり、その場合はデフォルトで以下のような内容となる。

# target :lib do
#   signature "sig"
#
#   check "lib"                       # Directory name
#   check "Gemfile"                   # File name
#   check "app/models/**/*.rb"        # Glob
#   # ignore "lib/templates/*.rb"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "strong_json"           # Gems
# end

# target :spec do
#   signature "sig", "sig-private"
#
#   check "spec"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "rspec"
# end

Steepの実行(bundle exec steep check)

このコマンドで型検査ができる。いまのリポジトリの状態では以下のような出力が得られた。

% bundle exec steep check
app/models/message.rb:2:2: NoMethodError: type=singleton(::Message), method=has_many_attached (has_many_attached :images)
app/models/user.rb:2:2: NoMethodError: type=singleton(::User), method=has_one_attached (has_one_attached :avatar)

ActiveStorage関連のメソッドで型エラーが出ていることがわかる。これに対応する型定義を書いていくことになるということか。

やり残し

bin/rake rbs_rails:all の実行時にエラーが出ていた。 https://github.com/Ken2mer/rails_master_test_app/commit/c81966d029b2029c17461942d375d6ff1ea4d7bb

メッセージは PG::UndefinedTable: ERROR: relation action_text_rich_texts does not exist というもの。

action_text_rich_texts というテーブル定義がないとのこと。これはActionText関連のものっぽいので、とりあえず action_text:install してみたが解消されず。というのが上記のコミット。

action_text:installcreate_action_text_tables.action_text.rbマイグレーションファイルができていないのがおかしそう。なので再調査してみる必要がある。

参考情報

ruby-buildをいち早く最新にして rbenv install する方法 (Ruby 3.0.0をインストールした)

Ruby 3.0.0がリリースされたので早速使ってみたいと思い、日頃使っているrbenvでインストールするにはどうすればいいんだっけ、となった(毎回なる)のでメモ。

Homebrewで管理している場合は brew update && brew upgrade rbenv ruby-build とすれば良い。ただしリリースされたばかりのバージョンの場合は、Homebrewに反映されるまで少し時間がかかることがあり、いますぐに最新バージョンを使いたいという時のために次の方法でセットアップしておきたい。

それはruby-buildのリポジトリをrbenvのプラグインとしてローカルマシン上に置いておく方法である。

以下のようにしてローカルの決められたディレクトリにruby-buildをクローンしておく。ほぼこれだけ。

$ mkdir -p "$(rbenv root)"/plugins
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

あとはruby-buildのリリースを見ておいて1更新があったら以下を実行する。

$ git -C "$(rbenv root)"/plugins/ruby-build pull

これで rbenv install --list の出力に載ってくるので欲しいやつをインストールして完了。

$ rbenv install 3.0.0
$ rbenv local 3.0.0
$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) ...

参考情報


  1. フィード購読するならこっち

'19/'20年振り返り&今後

ここ一年ほど仕事でRailsの案件をやっていて、そのためのインプットにかなり時間を費してしまっていた。元がレガシーなWebアプリケーションの作り替えなのだが、もろもろの事情によりDBのレイアウトはほぼ元のままで開発が進んでいて、ちょっと(かなり)癖のある設計を理解するのにしばらく悩まされていたという具合だ。

おかげでこんなデータベース設計は嫌だというアンチパターンを身をもって体験することができた。そろそろ次のステージに行きたい気持ちが高まってきているので、ここで少しばかり振り返りしておく。

コーディングについて

特定の言語やフレームワークのお作法に慣れてきた。仕事では、言語はRubyJavaScriptフレームワークRailsとVueを中心にやってきた。これらは人並みに扱えるようになったと言って良いだろう。人並みというと何だか基準があいまいだが、たとえばRuby書ける?と聞かれたら、書けますと答えたいくらいには自信が持てたと言える。

今後はRubyRailsの技術をさらに高めたいたいと考えていて、その一環として書籍を読み始めた。まず今年7月に改訂版が出ていたパーフェクトRuby on Railsを読んだ。つぎにパーフェクトRubyを読んだ。これからメタプログラミングRubyを読もうと考えている。参考にしたのはこちらの学習ガイドの"Rubyの技術を伸ばす"の章。

コードレビュー

レビューに注力した時期があった。結構もやもやを抱えるようなことが多かったように思える。

もやもやというのは、たとえば他人のプルリクエストを見ていて「ここは何でこうしたんだろう。何か事情があるのか?」とか「この部分って結局は何がしたいんだろうか。コメントがない…」みたいなコードが多いときに発生する。

このまま、よく分からないので解説してもらえますか?と聞いてしまうのが良いのだが、なぜよく分からないと感じたのか、まずはこちらから説明(フィードバック)するのが筋だろう。このフィードバックというのが厄介で、何とも説明し難い(違和感のような)ものを感じたときにどのように伝えようかと悩んでしまうときがあった。これは自分自身の未熟さから来るものでもあって、やっているうちに説明の仕方が分かってくる場合が多かったのでまだよかった。

問題なのは、このもやもやは自分自身のコードを読む能力足りていないために発生しているのでは?と考えてしまったときである。つまり自分の考えに自信がない場合なのだが、そのまま素直に「こう思いました」と伝えられればよいものの、それが無知の露呈になることを恐れてしまうのである。

自分の考えは正しいんだよな…?とあれこれ調べたりしてしまい、気づけばレビューにかけられる時間の半分を費していたことがある。これは良くなくて、たとえば普通にレビュイーに聞き返せば説明してくれてすぐに納得することが多かったりする。効率的かつ、レビュアーとレビュイーの双方にとって学びのあるレビューのためには、"無知の露呈への恐れ"はなるべく排除したほうがよいというのは教訓になった。

チーム開発

複数人でGithubリポジトリを使って開発するスタイルにも慣れてきた。現在の開発チームではプルリクエストベースで個人の作業を進めるフローになっていて、GithubのIssue上でのコミュニケーションが身についたと感じる。

近況と今後

直近では、仕事の方はRailsのActiveStorageを導入してファイルアップロードの機能を作ったり、外部のAPIとの連携処理を書いたりしている。

個人では、仕事で使い慣れたRubyやたまに触っているGoに関して、書籍を読んで知識を深めるようにしている。駆け出しの頃はとにかくやる、という感じでネット上のチュートリアル記事を見つけては試したり、分からないところを調べて見つけたブログ記事を真似たりといったインプットが多かった。しかし、ある時を境に、このままでは断片的なTipsしか身につかない、もっと体系的に学びたいという気持ちが高まり、それからは書籍を使ってまとまった知識を取り入れるようになった。

今後は新しい技術を積極的に取り入れていくようなチャレンジングな開発に関わりたいと考えている。いまの開発チームではそれは叶わないので、別のチームに移る計画を立てている。所属している会社の中で移ることも考えたが、組織の体質からみて恐らく他のチームも同じだろう。そのため転職も視野に入れて動いている。考えの軸としては、プロダクトにおいては最新の動向を踏まえて技術選定しているか、文化としては外部へのアウトプット(OSSやコミュニティへのコミット、カンファレンスのスポンサー参加など)が見られるか、色々とオープンな雰囲気か、といったところは条件に入れておきたい。