20220114のRubyに関する記事は10件です。

【Ruby】任意の数字が配列の中の何番目に格納されているかを確認 searchメソッド

※初心者向け ※アウトプット練習の為 開発環境 rails 6.1.4.1 ruby 2.6.5 概要 任意の数字が配列の中の何番目に格納されているかを確認できるプログラムを実装。 問題内容 以下の配列から任意の数字を探して何番目に含まれているかという結果を返すsearchメソッドを、each_with_indexを用いて作成しましょう。 input = [3, 5, 9 ,12, 15, 21, 29, 35, 42, 51, 62, 78, 81, 87, 92, 93] 雛形 以下の雛形のコードを参考に解答しましょう。 def search(target_num, input) # 処理を記述 end input = [3, 5, 9 ,12, 15, 21, 29, 35, 42, 51, 62, 78, 81, 87, 92, 93] # 呼び出し例 search(11, input) 出力例 search(5, input) → 2番目にあります search(12, input) → 4番目にあります search(7, input) → その数は含まれていません ヒント each_with_index each_with_indexは、Rubyに標準で組み込まれているメソッドの1つです。要素の繰り返し処理と同時に、その要素が何番目に処理されたのかも表すことができます。 以下のように書きます。 配列名.each_with_index do |item, i| end 具体的には以下のように使うことができます。 fruits = ["メロン", "バナナ", "アップル"] fruits.each_with_index do |item, i| puts "#{i}番目のフルーツは、#{item}です。" end これを実行すると、以下のような出力結果が得られます。 0番目のフルーツは、メロンです。 1番目のフルーツは、バナナです。 2番目のフルーツは、アップルです。 解答 def search(target_num, input) input.each_with_index do |num, index| if num == target_num puts "#{index + 1}番目にあります" return end end puts "その数は含まれていません" end input = [3, 5, 9 ,12, 15, 21, 29, 35, 42, 51, 62, 78, 81, 87, 92, 93] search(11, input) 解説 searchメソッドを呼び出す際の処理とsearchメソッド内の処理、それぞれを分けて解説していきます。 searchメソッドを呼び出す際の処理 配列inputを定義します。 次に、searchメソッドを呼び出す際に、11とinputという変数を実引数としてセットします。 そして、呼び出されたsearchメソッドでは、実引数でセットした値を仮引数target_numとinputとして受け取ります。 searchメソッド内の処理 まず、input.each_with_indexでは、inputに格納されている要素を1つひとつnumとして取り出すと同時に、要素毎に割り当てられている添字をindexとして取得します。 次に、if文でnum == target_numという条件式を設定します。 ここでは、inputから取り出された要素numと、target_numが等しいかを判別しています。 そして、numとtarget_numが等しければ、numがinputの中の何番目に含まれているかが出力されます。 #{index + 1}としているのは、配列が0番目から始まることを考慮するためです。 反対に、numとtarget_numが等くなければ、「その数は含まれていません」と出力されます。 今回は、引数で渡した「11」は配列inputには含まれていないので、条件には当てはまりません。 よって、「その数は含まれていません」と出力されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでコメント機能のajax化

コメント機能のajax化 前回のajax化の記事にてどんなものかある程度理解したので、この記事ではお気に入りボタンの実装を例としてしたが、今回はコメント機能の投稿と削除のajax化の備忘録として記載。 手順 ①コントローラーの修正 ②remote: trueの追加やclass、idの指定 ③js.erbファイルの追加 ①コントローラーの修正 redirect先を削除 修正前comments_controller.rb class CommentsController < ApplicationController def create comment = current_user.comments.build(comment_params) if comment.save redirect_to board_path(comment.board), success: t('defaults.message.created', item: Comment.model_name.human) else redirect_to board_path(comment.board), danger: t('defaults.message.not_created', item: Comment.model_name.human) end ↓このように修正。 修正後comments_controller.rb class CommentsController < ApplicationController def create @comment = current_user.comments.build(comment_params) @comment.save end ②remote: trueの追加やclass、idの指定 コメントフォームにid:'new_comment'、 テキストエリアにid:"js-new-comment-body"を追加。 またform_withからlocal: trueを削除。form_withは元からremote :tureとなっているのでわざわざ新たに記入する必要はない。 エラーメッセージはformではなく後に出てくるjs.erbファイルに記載する。 form.html.rb <%= form_with model: comment,url: [board, comment],id:'new_comment' do |f|%> <div class="form-group"> <%= f.label :body%> <%= f.text_area :body,class:"form-control mb-3",id:"js-new-comment-body",row:4,placeholder: "コメント"%> </div> <%= f.submit t('defaults.post'),class:"btn btn-primary"%> <%end%> 削除ボタンにclass: 'js-delete-comment-button'とremote: trueを追加。 delete_button.rb <%= link_to comment_path(comment), class: 'js-delete-comment-button', method: :delete, data: { confirm: "コメントを削除します。よろしいですか?" }, remote: true do %> <%= icon 'fa', 'trash' %> <% end %> ③js.erbファイルの追加 コメントviewファイル直下にcreate.js.erbとdestroy.js.erbを作成。 create.js.erb # ↓既に表示されているエラーメッセージがあった場合は削除する $("#error_messages").remove() # ↓コメント作成処理の結果によって処理を分岐 <% if @comment.errors.present? %>     # ↓エラーがある、処理失敗時にはエラーメッセージのパーシャルを表示 $("#new_comment").prepend("<%= j(render('shared/error_messages', object: @comment)) %>") <% else %>     # エラーがない、処理成功時には作成されたコメント内容をHTML要素として追加する $("#js-table-comment").prepend("<%= j(render('comments/comment', comment: @comment)) %>")      # ↓コメント入力フォームのテキストは表示する必要がないので、空文字に置き換えて内容をクリアにする $("#js-new-comment-body").val('') <% end %> destroy.js.erb $("tr#comment-<%= @comment.id %>").remove()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker compose環境のrailsでDB関連のエラーが出たら確認することまとめ

環境 docker compose mysql rails6 rails初心者が色々迷走しているうちに様々なエラーに出会ったのでまとめてみます。 エラー データベースがない時に出るエラーです。 error.rb ActiveRecord::NoDatabaseError: Unknown database 対応 データベースがあるかを確認します。 checkdb.sh #execコマンドでrailsコンテナの中に入ります。 $ docker exec -it xxxxxxxxxx bash #railsコンテナの中からmysqlコンテナに接続しつつmysqlを立ち上げます。 root@xxxxxxxxxx:/app# mysql -u root -p -h db #ここでmysqlが立ち上がらない場合そもそもdockerコンテナ同士の通信がうまく行っていないので下の「dockerコンテナ間の通信を確認する」を見てください。 #mysqlが立ち上がった場合はshow databasesでデータベースがあるか確認します。データベース名はrailsアプリの/config/database.ymlでdatabase: rails_sample_dvなどと記載されています。 MySQL [(none)]> show databases; #データベースがなかった場合は作成します。 MySQL [(none)]>exit root@xxxxxxxxxx:/app# rails db:create root@xxxxxxxxxx:/app# rails db:migrate これでデータベースが作成されるので0.0.0.0:3000などでアクセスができるようになります。 dockerコンテナ同士がつながっていなそうな場合 docker-compose.ymlの設定を確認します。 以下db接続関連の項目のみを書き出してみます。 これらがない場合は書き足してみてdockerを再構築 (環境によってはimage、volumeを削除してdocker compose build --no-cacheなどとすると確実かもしれません。) してみて再度ウェブコンテナからmysqlに接続できるかを確認します。 docker-compose.yml version: "3" services: app: depends_on: - mysql mysql: volumes: - mysql-volume:/var/lib/mysql environment: - MYSQL_ALLOW_EMPTY_PASSWORD=yes ports: - "3306:3306" volumes: mysql-volume: railsのdatabase.ymlの記述が間違っていたときのエラー 自分の場合は以下のエラーがブラウザでアクセスした時に出ました。 error1.rb ActiveRecord::ConnectionNotEstablished No connection pool for 'ActiveRecord::Base' found. error2.rb Puma caught this error: Cannot load database configuration: そしてその状態でrails consoleを立ち上げようとして以下のエラーに遭遇 error3.sh webapp/config/database.yml:33:in `<main>': Cannot load database configuration: undefined method `[]' for nil:NilClass (NoMethodError) ちなみにその時のdatabase.ymlは以下 database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: 5 timeout: 3306 host: mysql development: <<: *default username: root host: mysql database: rails_sample_dv socket: /tmp/mysql.sock # test: # <<: *default # username: root # host: <%= ENV['DB_HOST'] %> # database: rails_sample_dv # socket: /tmp/mysql.sock # production: # <<: *default # username: <%= Rails.application.credentials.db[:user] %> # host: <%= Rails.application.credentials.db[:host] %> # password: <%= Rails.application.credentials.db[:password] %> # database: <%= Rails.application.credentials.db[:name] %> これはなぜかコメントアウトしている行が読み込まれているようで 以下のようにコメントアウトしている部分を削除したらとりあえずエラーは解消されました。 database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: 5 timeout: 3306 host: mysql development: <<: *default username: root host: mysql database: rails_sample_dv socket: /tmp/mysql.sock master.key、credentials.yml.encを作り直してみる railsアプリの/configディレクトリにmaster.key、credentials.yml.encというファイルがあります。 これらがなかったり、なんらかの影響で変更されたりした場合railsアプリが立ち上がらない場合があります。 そんな時は念の為2つのファイルをバックアップしてから 作成し直すことでアプリが立ち上がる場合もあります。 作成し直す際は前述同様docker execコマンドでrailsコンテナにログインしてから以下のコマンドで再生成します。 recreate.sh EDITOR=vim rails credentials:edit 場合によってはsudoをつけたほうが良いかもしれません。 そもそもmaster.key、credentials.yml.encとは? いわゆるサーバで設定する環境変数をrails内で設定できるという仕組みで master.key(秘密鍵)、credentials.yml.enc(公開鍵)といった具合に 1組の対になっています。 ですから何らかの拍子で一つが変わったりする場合エラーになります。 またmaster.keyは必ず.gitignoreなどに記載し、公開されないように デプロイツールに登録しなければなりません。 番外編|コンテナが立ち上がらない railsのWebサーバが立ち上がっていない状態です。 docker.sh $ docker compose up -d $ docker container ls cfxxxxxxa776 mysql:5.7 "docker-entrypoint.s…" 1 minutes ago Up 1 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp webapp-db-1 対応方法 /tmp/pids/server.pid というファイルを削除しコンテナを再度立ち上げると無事ウェブサーバが立ち上がりました。 これはrails特有のお作法でよくある原因とのことでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby]配列のアレコレ

基礎的だから難しくはないけど、使ってないと何気忘れそうなので記事に。 配列に値を追加する場合 <<の演算子を使用する 例 irb(main):001:0> pencil_case = ["ペン", "消しゴム", "定規"] => ["ペン", "消しゴム", "定規"] irb(main):002:0> pencil_case << "えんぴつ" => ["ペン", "消しゴム", "定規", "えんぴつ"] #配列に"えんぴつ"が追加される 配列の値を指定して取得する場合 添字を使用する #配列の要素を取得 配列[添字] #配列の要素を取得し変数に代入 変数 = 配列[添字] 配列の値を変更する場合 変更したい要素を取り出して、新しい要素を代入する #配列の要素を変更 配列[添字] = 値   以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのinitializeの可視性の変更

initializeは特別なメソッドとして扱われるので、 publicやprotecdの配下であってもprivateメソッドのままになる。 class Public protected def initialize end end p Public.new.private_methods.include?(:initialize) #=> true p Public.new.public_methods.include?(:initialize) #=> false p Public.new.methods.include?(:initialize) #=> false class Protected protected def initialize end end p Protected.new.private_methods.include?(:initialize) p Protected.new.protected_methods.include?(:initialize) p Protected.new.methods.include?(:initialize) ただし、未だRuby2.1のRuby技術者認定試験Goldの勉強をしてる人は、 protectedについて注意が必要である。 2.1の頃は、バグか何かで、protecdの配下にあるとprotectedメソッドになってしまう。 RExの練習問題で出題されるので、気をつけた方がいい。 2.3の頃には、privateのままで、protectedに変更できなくなっていた。 また、余談であるが、publicはメソッドであり、 publicに引数をとる形でシンボルの:initializeを指定すると、 initializeメソッドがpublicメソッドになる。 class Public public :initialize end p Public.new.private_methods.include?(:initialize) #=> false p Public.new.public_methods.include?(:initialize) #=> true p Public.new.methods.include?(:initialize) #=> true p Public.new.initialize #=> nil
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのprependによる定数探索バグ(3.1で修正済)

Ruby技術者認定試験の対象バージョンは、いまだに2.1である。 2.1にバグがあり、 Ruby技術者認定試験の有名なRExという練習問題で、 バグが混在されたまま出題されて、 勉強中の受験生を悩ませている定数探索の問題がある。 実際、ruby-jpというSlackで、同じ質問が3回されているのを見た。 そのバグは3.1で修正されたのだが、そのバグについて解説する。 module PrependedModule CONST = "PrependedModule" end class Parent CONST = "Parent" prepend PrependedModule end class Child < Parent p Module.nesting #=> [Child] # レキシカルスコープは、Childのみ。 def self.const CONST end end p Child.ancestors #=> [Child, PrependedModule, Parent, Object, Kernel, BasicObject] p Child.const #=> "Parent" (誤: Ruby 3.0) #=> "PrependedModule" (正: Ruby 3.1) 上記のChildクラス内で定義されたメソッドで参照されるCONST定数がある。 まず、レキシカルスコープで、Child内のCONST定数を探すが、ChildにCONST定数は定義されていない。 また、他のレキシカルスコープも存在しないので、継承順にCONST定数を探索していくことになる。 ここで、prependで継承したモジュールは、そのクラスよりも優先度が高い。 実際、ancestorsメソッドで確かめると、ParentクラスよりもPrependedMoudleモジュールの方が優先順位が高くなっている。 そのため、PrependedModule内にある定数を探索すべきだが、3.0まではParentクラスにあるCONST定数を先に見つけてしまっていた。これが3.1で修正されPrependedModuleの方が先に見つかるようになっている。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】メソッド名を付けるときに気をつけること

最近、1つのメソッドで色々なことをしてしまってるメソッドのリファクタリングをしていて、 その過程で処理ごとにメソッドに切り出すことが多く、 メソッド名の付け方で迷うことがあったのでまとめてみる。 オブジェクトを別の形に変換するメソッド名はto_hogeにする # hashにして返すメソッド # 悪くはない例 def convert_to_hash end # より良い例 def to_hash end 否定形のメソッド名はなるべく作らない 判定が複雑になると可読性が下がってしまうこともある。 否定形のメソッドを作りたくなったときは、逆の意味(肯定)のメソッドを作って!hogeの形にして使う。 # 良くない例 book.not_published? # 良い例 !book.published? 判定形のメソッドの末尾には疑問符?をつける trueかfalseを返すメソッドの末尾にだけ「?」をつける。それ以外にはつけない。 shop.open? メソッド名に疑問符をつけられるのはRubyだけらしい。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2022年最新】Rails6にBootstrap5を導入する方法

※ 本記事はRails6でBootstrap5を導入したい方向けの記事です! ※ Rails5やBootstrap4など、バージョン違いのものを導入する場合とは手順が異なります! 開発環境&前提条件 Ruby 3.0.2 Rails 6.1.4.4 導入したBootstrap 5.1.3 Rails6でrails newしてある状態 Bootstrap5のインストール terminal % yarn add bootstrap 上記コマンドでBootstrapの最新バージョンがインストールされます! バージョンを指定する場合? terminal % yarn add bootstrap@バージョン番号 ドロップダウンなどのポップアップ要素をいい感じにしてくれるpopper.jsもインストールします! terminal % yarn add @popperjs/core application.html.erbに追記 app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title>Title</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <% stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <%# ここを追加? %> <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>         <%# ここを追加? %> </head> <body> <%= yield %> </body> </html> 上記の通り、stylesheet_pack_tagの一文を追加します! application.scssの作成・追記 Bootstrapを読み込むためのファイルを作成します! app/javascript配下に、stylesheetsフォルダを作成、そのフォルダの中にapplication.scssファイルを作成します! terminal % mkdir ./app/javascript/stylesheets % touch ./app/javascript/stylesheets/application.scss 作成したapplication.scssに以下を追記します! app/javascript/stylesheets/application.scss @import "~bootstrap/scss/bootstrap.scss"; application.jsに追記 上記application.scssを読み込む記述をapplication.jsに追記します! app/javascript/packs/application.js import Rails from "@rails/ujs" import Turbolinks from "turbolinks" import * as ActiveStorage from "@rails/activestorage" import "channels" // ここを追加? import "bootstrap"; import "../stylesheets/application.scss"; // ここを追加? Rails.start() Turbolinks.start() ActiveStorage.start() これでBootstrap5の導入は完了です! あとはいつも通りBootstrapクラスの指定をしてあげれば適応されます? 最後に Rails6にBootstrap5を導入する記事が少なかったので本記事を作成しました。 本記事がBootstrapの導入で悩んでいる方に届けば幸いです?‍♂️
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

assertEqualsやassert_equalの引数はなぜ expected, actual の順なのか、調べてみた

はじめに xUnit系のテスティングフレームワークでは、2つの値が等しいことを検証するメソッド(assertEqualsやassert_equalなど)の引数が expected, actual(期待する値、実際の値)の順に並びます。 // Java (JUnit) assertEquals(2, calculator.add(1, 1)); # Ruby (minitest または test-unit) assert_equal(2, calculator.add(1, 1)) しかし、この引数の順番は直感に反するので、逆に書きたい、と思う人も中にはおられるようです(というか、僕も昔そう思っていました)。 // actual、expectedの順がいい! assertEquals(calculator.add(1, 1), 2); # actual、expectedの順がいい! assert_equal(calculator.add(1, 1), 2) そこで、expected, actualの順で引数が並ぶのには何か理由があるのか、ちょっと調べてみました。 調査方法 "unit test assert equal argument order why" みたいなキーワードでググってみました。検索の上位に毎度お馴染みのStack Overflowのリンクが上がってきたので、いくつか回答を覗いてみました。 Kent Beck氏は何と言っているか? JavaのJUnitやRubyのtest-unit/minitestなど、いわゆるxUnit系のテスティングフレームワークはKent Beck氏が始祖の一人であると言われています。 1997年に、Smalltalk のためのユニットテストのフレームワークであるSUnitをもとにして、エーリヒ・ガンマと、SUnitの開発者のケント・ベックが中心となって開発された。 JUnit - Wikipedia そして、こちらのStack Overflowの中に、この疑問に対するKent Beck氏の回答(のリンク)が載っていました。その回答がこちらです。 Line a bunch of assertEquals in a row. Having expected first makes them read better. And yes, it's too late to change it. (筆者訳) 大量の assertEquals をひとまとめにして並べてみる。expectedを最初に持ってきた方が読みやすい。(1つ前のスレッドの意見に対して)そしておっしゃるとおり、今から変更するのはもう手遅れです。 JUnit / RE: [Junit-devel] why not assertEquals(actual, expected)? コメントが短く、サンプルコードもないのでちょっと意図がつかみにくいのですが、Stack Overflowの回答を見る限り、こういうことのようです。 actual, expected の順だと読みにくい(?) def test_bit_length assert_equal((-2**12-1).bit_length, 13) assert_equal((-2**12).bit_length, 12) assert_equal((-2**12+1).bit_length, 12) assert_equal(-0x101.bit_length, 9) assert_equal(-0x100.bit_length, 8) assert_equal(-0xff.bit_length, 8) assert_equal(-2.bit_length, 1) assert_equal(-1.bit_length, 0) assert_equal(0.bit_length, 0) assert_equal(1.bit_length, 1) assert_equal(0xff.bit_length, 8) assert_equal(0x100.bit_length, 9) assert_equal(0x101.bit_length, 9) assert_equal((2**12-1).bit_length, 12) assert_equal((2**12).bit_length, 13) assert_equal((2**12+1).bit_length, 13) assert_equal((-2**10000-1).bit_length, 10001) assert_equal((-2**10000).bit_length, 10000) assert_equal((-2**10000+1).bit_length, 10000) assert_equal((2**10000-1).bit_length, 10000) assert_equal((2**10000).bit_length, 10001) assert_equal((2**10000+1).bit_length, 10001) 2.upto(1000) {|i| n = 2**i assert_equal((-n-1).bit_length, "(#{-n-1}).bit_length", i+1) assert_equal((-n).bit_length, "(#{-n}).bit_length", i) assert_equal((-n+1).bit_length, "(#{-n+1}).bit_length", i) assert_equal((n-1).bit_length, "#{n-1}.bit_length", i) assert_equal((n).bit_length, "#{n}.bit_length", i+1) assert_equal((n+1).bit_length, "#{n+1}.bit_length", i+1) } end expected, actual の順だと読みやすい(?) def test_bit_length assert_equal(13, (-2**12-1).bit_length) assert_equal(12, (-2**12).bit_length) assert_equal(12, (-2**12+1).bit_length) assert_equal(9, -0x101.bit_length) assert_equal(8, -0x100.bit_length) assert_equal(8, -0xff.bit_length) assert_equal(1, -2.bit_length) assert_equal(0, -1.bit_length) assert_equal(0, 0.bit_length) assert_equal(1, 1.bit_length) assert_equal(8, 0xff.bit_length) assert_equal(9, 0x100.bit_length) assert_equal(9, 0x101.bit_length) assert_equal(12, (2**12-1).bit_length) assert_equal(13, (2**12).bit_length) assert_equal(13, (2**12+1).bit_length) assert_equal(10001, (-2**10000-1).bit_length) assert_equal(10000, (-2**10000).bit_length) assert_equal(10000, (-2**10000+1).bit_length) assert_equal(10000, (2**10000-1).bit_length) assert_equal(10001, (2**10000).bit_length) assert_equal(10001, (2**10000+1).bit_length) 2.upto(1000) {|i| n = 2**i assert_equal(i+1, (-n-1).bit_length, "(#{-n-1}).bit_length") assert_equal(i, (-n).bit_length, "(#{-n}).bit_length") assert_equal(i, (-n+1).bit_length, "(#{-n+1}).bit_length") assert_equal(i, (n-1).bit_length, "#{n-1}.bit_length") assert_equal(i+1, (n).bit_length, "#{n}.bit_length") assert_equal(i+1, (n+1).bit_length, "#{n+1}.bit_length") } end サンプルコードの引用元: https://github.com/ruby/ruby/blob/master/test/ruby/test_integer.rb 引数の位置が安定しやすいから、この順番? 上のサンプルコードを見ただけではまだピンと来ないかもしれません。 StackOverflowの回答にも書いてあるんですが、expectedは文字列や数値といった単純なリテラルを書くことが多く、actualは複雑なメソッド呼び出しや、さまざまな引数を与えることが多いです(もちろん状況によりますが)。 結果、「expectedは短くて長さも安定しやすい」「actualは長くて長さも変わりやすい」ということになります。 そのため、assertEqualsやassert_equalを大量に並べる場合は、expectedを最初に持ってきた方が、引数の位置を安定させやすい(そしてexpectedをさっと把握しやすい)、ということのようです。 もちろん、例外パターンもあると思うので、「見ろ、こういう場合はactualが最初に来た方が読みやすいぞ!」という反論はいくらでもできそうですが、Kent Beck氏に言わせるとexpectedが最初に来た方が読みやすくなることが多いのかもしれません。 他のツールはおそらくJUnitを踏襲しただけ JUnitはもともとSmalltalk用のSUnitをJavaに移植したものですが、assertEqualsが導入されたのはJUnitが最初のようです(参考)。それゆえ、assertEqualsの元祖はJUnitになると思われます。 Rubyのtest-unit/minitestやPythonのunittestライブラリがexpected, actualの順になっているのは、おそらくJUnitの引数の順番を踏襲しただけだと思います。 まとめ というわけで、この記事ではassertEqualsやassert_equalの引数はなぜ expected, actual の順なのか調査した結果を書いてみました。 StackOverflowの回答を読んでいると、回答者自身の「俺はこう思う」的な意見は多いのですが、原典にさかのぼって調査するような回答はほとんど見つけられませんでした。その中で唯一見つけられたのが、前述のKent Beck氏の回答です。 とはいえ、その回答も短く、僕個人も「わかるような、わからんような」という感が否めません。なんとなくの予想ですが、Kent Beck氏自身も「まあ、どっちでもいいんだけどね」ぐらいに考えているような気がします(確固たる理由があるなら、もっと詳しいコメントが見つかりそうなので)。 この件についてもし詳しい理由をご存じの方がいたら、コメント欄等で情報源を教えてもらえると幸いです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQL RubyのDataloaderを使ってみる

GraphQL Rubyを使っているときにSQLのN+1問題を解決するためにバッチロードを利用すると思います。 有名なgemとしては GraphQL::Batch や BatchLoader、 Dataloaderなどがあります。 個人的には使ったことがあるのはDataloaderなのですが、2018年から更新されていないようなのと、去年GraphQL Ruby自体にDataloaderが入ったので今回はこちらを触ってみました。 GraphQL::Dataloader の概要 ドキュメントはこちらです。 GraphQL::Dataloaderはデータベースアクセスを効率的に行うためのツールで、RubyのFiberを使っており、Ruby 3のノンブロッキングFiberもサポートしているようです。影響を受けたものとしては以下2つが挙げられています。 https://github.com/bessey/graphql-fiber-test/tree/no-gem-changes https://github.com/shopify/graphql-batch Dataloader vs. GraphQL-Batchでも、GraphQL-Batchなどの他のローダーはPromiseを使っている一方で、このDataloaderはFiberを使っているということが特徴として挙げられています。committerのRobert Mosolgoによると、Promiseを使うと複雑になりやすいところを、Rubyにもともと備わっている機能であるFiberを使うことで、他に何も使わずとも並列I/Oが使えるというところで選んだようです。 前提 graphql v1.13.2 rails v7.0.0 前準備 サンプルのデータをもとに検証してみます。 シンプルにUser, Aritcle, Likeの3つのテーブルを作ります。 class User < ApplicationRecord has_many :articles, foreign_key: 'author_id' end class Article < ApplicationRecord belongs_to :author, class_name: 'User' has_many :likes end class Like < ApplicationRecord belongs_to :article, validate: true belongs_to :user, validate: true end フィールドはこんな感じです。 module Types class UserType < Types::BaseObject field :id, ID, null: false field :name, String, null: false field :email, String, null: false field :articles, Types::ArticleType.connection_type, null: false end end module Types class ArticleType < Types::BaseObject field :id, ID, null: false field :title, String, null: false field :body, String, null: false field :author, Types::UserType, null: false field :liked, Types::LikeConnection, null: false, method: :likes end end module Types class LikeType < Types::BaseObject field :id, ID, null: false field :user, Types::UserType, null: false field :article, Types::ArticleType, null: false field :liked_at, Int, null: false, method: :created_at end end 今回欲しいクエリはこんな感じで、現在のユーザーが投稿した記事一覧とその記事にLikeしたユーザーの一覧を取得します。一部ページネーションでConnectionを使っています。 query { currentUser { name articles { edges { node { title body liked { count edges { node { user { name } } } } } } } } } ダミーデータも生成しておく 取得するデータを生成しておきます。 クエリの取得結果はこのようになりました。 { "data": { "currentUser": { "name": "test_user1", "articles": { "edges": [ { "node": { "title": "first article", "body": "first article body", "liked": { "count": 4, "edges": [ { "node": { "user": { "name": "test_user2" } } }, { "node": { "user": { "name": "test_user3" } } }, { "node": { "user": { "name": "test_user4" } } }, { "node": { "user": { "name": "test_user5" } } } ] } } }, { "node": { "title": "second article", "body": "second article body", "liked": { "count": 1, "edges": [ { "node": { "user": { "name": "test_user2" } } } ] } } }, { "node": { "title": "third article", "body": "third article body", "liked": { "count": 2, "edges": [ { "node": { "user": { "name": "test_user3" } } }, { "node": { "user": { "name": "test_user4" } } } ] } } }, { "node": { "title": "forth article", "body": "forth article body", "liked": { "count": 0, "edges": [] } } }, { "node": { "title": "fifth article", "body": "fifth article body", "liked": { "count": 1, "edges": [ { "node": { "user": { "name": "test_user5" } } } ] } } } ] } } } } まずクエリを投げてみる このまま素直にクエリを投げると、素直にN+1が発生してくれるのでこれを解消していきます。 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."author_id" = ? LIMIT ? [["author_id", 1], ["LIMIT", 20]] Like Load (0.1ms) SELECT "likes".* FROM "likes" WHERE "likes"."article_id" = ? LIMIT ? [["article_id", 1], ["LIMIT", 20]] Like Load (0.1ms) SELECT "likes".* FROM "likes" WHERE "likes"."article_id" = ? LIMIT ? [["article_id", 2], ["LIMIT", 20]] Like Load (0.2ms) SELECT "likes".* FROM "likes" WHERE "likes"."article_id" = ? LIMIT ? [["article_id", 3], ["LIMIT", 20]] Like Load (0.2ms) SELECT "likes".* FROM "likes" WHERE "likes"."article_id" = ? LIMIT ? [["article_id", 4], ["LIMIT", 20]] Like Load (0.5ms) SELECT "likes".* FROM "likes" WHERE "likes"."article_id" = ? LIMIT ? [["article_id", 5], ["LIMIT", 20]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]] User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]] Dataloaderの導入 新規プロジェクトでrails generate graphql:installした場合はすでに有効化されていますが、schemaに以下の行を追加するだけです。 class MySchema < GraphQL::Schema # ... + use GraphQL::Dataloader end シングルレコードを取得 まずはbelongs_toの関係になっている、like.user部分のシングルレコードの取得部分からから準備します。 実装するものとしては、 Source resolverのメソッド の2つです。 說明する前に先にコードを載せます。 こちらはほぼドキュメントに乗っているサンプルそのままです。 app/graphql/sources/user_by_id.rb module Sources class UserById < GraphQL::Dataloader::Source def initialize @model_class = ::User end def fetch(ids) records = @model_class.where(id: ids) ids.map { |id| records.find { |r| r.id == id.to_i } } end end end app/graphql/types/like_type.rb module Types class LikeType < Types::BaseObject field :user, Types::UserType, null: false + def user + dataloader.with(::Sources::UserById).load(object.user_id) + end end end これによって、Userの取得が以下のように1つのクエリで完結します。 User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?) [["id", 2], ["id", 3], ["id", 4], ["id", 5]] 解説 まずSourceについてです。 Sourceはバッチロードする処理を記述するクラスで、GraphQL::Dataloader::Sourceを継承し、fetchメソッドを実装します。 このSourceはGraphQL::Dataloaderによってインスタンス生成され、fetchメソッドが呼ばれるのですが、引数としては取得するためのkey(今回でいうとids)が渡されます。今回はこのkeyからUserのデータを取得します。 返り値は渡された引数のkeyと同じ順番でオブジェクトを返す必要があります。 そしてresolver側では以下のように取得します。 def user dataloader.with(::Sources::UserById).load(object.user_id) end loadで裏側でFiberのキューに入れて遅延ロードを行っているようです。この例でいくとobject.user_idがSourceのfetchメソッドの引数として渡されます。 マルチレコードを取得 こちらも先にコードを載せます。 基本的にはシングルレコードとあまり変わりません。 app/graphql/sources/likes_by_user_id.rb module Sources class LikesByUserId < GraphQL::Dataloader::Source def initialize @model_class = ::Like end def fetch(keys) records = @model_class.where(article_id: keys) .group_by { |record| record.article_id } keys.map { |key| records[key] || [] } end end end app/graphql/types/article_type.rb module Types class ArticleType < Types::BaseObject field :liked, Types::LikeConnection, null: false + def liked + dataloader.with(::Sources::LikesByUserId).load(object.id) + end end end 少し変わったのはfetchメソッドくらいですね。 こちらは1つのkeyに対して複数レコードが返ってくる可能性があるのでgroup_byをしています。 返り値は同様にkeyの順番に合わせて配列を返しています。 これだけでマルチレコードも遅延ロードができました。 結果以下のようになり、かなり効率的なクエリになりました。 User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Article Load (0.1ms) SELECT "articles".* FROM "articles" WHERE "articles"."author_id" = ? LIMIT ? [["author_id", 1], ["LIMIT", 20]] Like Load (0.1ms) SELECT "likes".* FROM "likes" WHERE "likes"."article_id" IN (?, ?, ?, ?, ?) [["article_id", 1], ["article_id", 2], ["article_id", 3], ["article_id", 4], ["article_id", 5]] User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?) [["id", 2], ["id", 3], ["id", 4], ["id", 5]] まだ分かっていないこと GraphQL::Batchなどに比べるとGraphQL Dataloaderはまだ導入されたばかりです。そのため、今回はかなりシンプルなものしか試していないので、実際の運用で出てくるような複雑なものまで実用に耐えられるのかは不明です。 また、ドキュメントを見ていると1Source 1クラスで処理は似たものでもクラスが増えていきそうな印象でした。Sourceが増えてきたときに効率的に実装して管理できるかはまだ不明です。 最後に GraphQL::Batchなど他のGemはまだ触っていないのですが、GraphQL Rubyに入っているというだけあって、導入はめちゃくちゃ楽でした。お手軽に使いたいのであればとてもいいと思います。 また他のLoaderも触ってみて比較したいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む