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_each
⊃ find_in_batches
⊃ in_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.Listen
と net.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)を得る箇所が以下。
req.Host, req.URL, err = Server(req.Query)
Server
関数の内容
// 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サーバを取得する。取得できなければここでエラーとなる。
Railsアプリケーションでsteep checkを試す
Ruby 3関連でRailsアプリケーション上で何か試したいと思っていたら以下の記事を発見したので、これをそのまま試してみる。
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
このコミットから、元記事の解説と実際に行われた処理を照らし合わせてみる。
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
今回使用したアプリケーションに含まれるモデルは User
と Message
のみ。それに対応する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:install
で create_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) ...
参考情報
'19/'20年振り返り&今後
ここ一年ほど仕事でRailsの案件をやっていて、そのためのインプットにかなり時間を費してしまっていた。元がレガシーなWebアプリケーションの作り替えなのだが、もろもろの事情によりDBのレイアウトはほぼ元のままで開発が進んでいて、ちょっと(かなり)癖のある設計を理解するのにしばらく悩まされていたという具合だ。
おかげでこんなデータベース設計は嫌だというアンチパターンを身をもって体験することができた。そろそろ次のステージに行きたい気持ちが高まってきているので、ここで少しばかり振り返りしておく。
コーディングについて
特定の言語やフレームワークのお作法に慣れてきた。仕事では、言語はRubyとJavaScript、フレームワークはRailsとVueを中心にやってきた。これらは人並みに扱えるようになったと言って良いだろう。人並みというと何だか基準があいまいだが、たとえばRuby書ける?と聞かれたら、書けますと答えたいくらいには自信が持てたと言える。
今後はRubyとRailsの技術をさらに高めたいたいと考えていて、その一環として書籍を読み始めた。まず今年7月に改訂版が出ていたパーフェクトRuby on Railsを読んだ。つぎにパーフェクトRubyを読んだ。これからメタプログラミングRubyを読もうと考えている。参考にしたのはこちらの学習ガイドの"Rubyの技術を伸ばす"の章。
コードレビュー
レビューに注力した時期があった。結構もやもやを抱えるようなことが多かったように思える。
もやもやというのは、たとえば他人のプルリクエストを見ていて「ここは何でこうしたんだろう。何か事情があるのか?」とか「この部分って結局は何がしたいんだろうか。コメントがない…」みたいなコードが多いときに発生する。
レビューしていて意図が伝わってこないコードが多いとつらい
— 佐原義雄 (@Ken2mer) 2020年6月9日
このまま、よく分からないので解説してもらえますか?と聞いてしまうのが良いのだが、なぜよく分からないと感じたのか、まずはこちらから説明(フィードバック)するのが筋だろう。このフィードバックというのが厄介で、何とも説明し難い(違和感のような)ものを感じたときにどのように伝えようかと悩んでしまうときがあった。これは自分自身の未熟さから来るものでもあって、やっているうちに説明の仕方が分かってくる場合が多かったのでまだよかった。
問題なのは、このもやもやは自分自身のコードを読む能力足りていないために発生しているのでは?と考えてしまったときである。つまり自分の考えに自信がない場合なのだが、そのまま素直に「こう思いました」と伝えられればよいものの、それが無知の露呈になることを恐れてしまうのである。
自分の考えは正しいんだよな…?とあれこれ調べたりしてしまい、気づけばレビューにかけられる時間の半分を費していたことがある。これは良くなくて、たとえば普通にレビュイーに聞き返せば説明してくれてすぐに納得することが多かったりする。効率的かつ、レビュアーとレビュイーの双方にとって学びのあるレビューのためには、"無知の露呈への恐れ"はなるべく排除したほうがよいというのは教訓になった。
チーム開発
複数人でGithubのリポジトリを使って開発するスタイルにも慣れてきた。現在の開発チームではプルリクエストベースで個人の作業を進めるフローになっていて、GithubのIssue上でのコミュニケーションが身についたと感じる。
近況と今後
直近では、仕事の方はRailsのActiveStorageを導入してファイルアップロードの機能を作ったり、外部のAPIとの連携処理を書いたりしている。
個人では、仕事で使い慣れたRubyやたまに触っているGoに関して、書籍を読んで知識を深めるようにしている。駆け出しの頃はとにかくやる、という感じでネット上のチュートリアル記事を見つけては試したり、分からないところを調べて見つけたブログ記事を真似たりといったインプットが多かった。しかし、ある時を境に、このままでは断片的なTipsしか身につかない、もっと体系的に学びたいという気持ちが高まり、それからは書籍を使ってまとまった知識を取り入れるようになった。
今後は新しい技術を積極的に取り入れていくようなチャレンジングな開発に関わりたいと考えている。いまの開発チームではそれは叶わないので、別のチームに移る計画を立てている。所属している会社の中で移ることも考えたが、組織の体質からみて恐らく他のチームも同じだろう。そのため転職も視野に入れて動いている。考えの軸としては、プロダクトにおいては最新の動向を踏まえて技術選定しているか、文化としては外部へのアウトプット(OSSやコミュニティへのコミット、カンファレンスのスポンサー参加など)が見られるか、色々とオープンな雰囲気か、といったところは条件に入れておきたい。