- 投稿日:2021-01-24T23:35:21+09:00
DBのトランザクションがコミットされる前に非同期ジョブを実行してしまっていた、、、
はじめに
DBの更新処理が完了したら、非同期ジョブを実行するようにしていたが、更新前の状態で非同期ジョブが実行されていた、、
ここでは例として、下記要件があるとする
- メッセージが送られたら、ユーザーアプリへ非同期ジョブにてプッシュ通知を送る
- 全てのメッセージ送信に対して、ログテーブルで履歴を残す
- 特定のメッセージの送信が完了したら、ポイントがつく
動作環境
- ActiveJob
- Sidekiq
- Redis
更新処理の実装
class Message < ApplicationRecord enum status: { prepare: 0, # 下書き complete: 1 # 送信完了 } def complete_message! transaction do complete! update_message_log! # メッセージログテーブルを更新する処理 end MessagePushJob.perform_later(self) # 非同期ジョブにキューする end def send_message_with_point! transaction do message_complete! give_point! # 送信者にポイントを与える end end end(give_point!もcomplete_message!メソッドに書いてないのは、ポイントがつかないメッセージ送信にcomplete_message!が使われているからとする)
ジョブの実装
class MessagePushJob < ApplicationJob queue_as :message_push_notice def perform(message) # messageのステータスがcompleteの状態だったら if message.complete? # 受信者にpush通知を送る end end endここでポイントなのが、引数のmessageのstatusの値が何になるかということ!
想定では、completeステータスになっていることを期待していたが、なんとprepareステータスとなっていた、、
なので、後述の処理が実行されてなかった!!?引数でcompleteステータスのインスタンスを渡しているのになんで!?
ジョブの引数を変換している処理が下記である
今回の場合は、argumentsにMessageのインスタンスが格納されている# arguments = [ Messageのインスタンス ] def serialize { "job_class" => self.class.name, "job_id" => job_id, "queue_name" => queue_name, "priority" => priority, "arguments" => serialize_arguments(arguments), "executions" => executions, "locale" => I18n.locale.to_s } endserialize_argumentsの中を追っていくと
def serialize(arguments) arguments.map { |argument| serialize_argument(argument) } end def serialize_argument(argument) case argument when *PERMITTED_TYPES argument when GlobalID::Identification convert_to_global_id_hash(argument) when Array argument.map { |arg| serialize_argument(arg) } when ActiveSupport::HashWithIndifferentAccess serialize_indifferent_hash(argument) when Hash symbol_keys = argument.each_key.grep(Symbol).map!(&:to_s) aj_hash_key = if Hash.ruby2_keywords_hash?(argument) RUBY2_KEYWORDS_KEY else SYMBOL_KEYS_KEY end result = serialize_hash(argument) result[aj_hash_key] = symbol_keys result when -> (arg) { arg.respond_to?(:permitted?) } serialize_indifferent_hash(argument.to_h) else Serializers.serialize(argument) end endActiveRecordのインスタンスの場合は、GlobalID::Identificationの分岐に入り、出力内容はGlobalIDのインスタンスが返却される(詳しくは後述)
よく見ると、テーブル名とIDしか渡されていない!?そもそもGlobalIDとは
モデルのインスタンスを一意に識別するためのURI
デフォルトでActiveRecordにインクルードされている今回の場合は下記のような値となる
> Message.find(1).to_global_id #<GlobalID:0x00007f19ccb9d178 @uri=#<URI::GID gid://test/Message/1>>https://github.com/rails/globalid
ジョブの実行時はGlobalIDインスタンスをどうしている?
ジョブが実行する前に、デシリアライズされている
def deserialize_argument(argument) case argument when String argument when *PERMITTED_TYPES argument when Array argument.map { |arg| deserialize_argument(arg) } when Hash if serialized_global_id?(argument) deserialize_global_id argument elsif custom_serialized?(argument) Serializers.deserialize(argument) else deserialize_hash(argument) end else raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" end enddef deserialize_global_id(hash) GlobalID::Locator.locate hash[GLOBALID_KEY] enddef locate(gid, options = {}) if gid = GlobalID.parse(gid) locator_for(gid).locate gid if find_allowed?(gid.model_class, options[:only]) end endhttps://github.com/rails/globalid/blob/master/lib/global_id/locator.rb#L15
ちょっと省略するが、結局下記のコードにいきついて、findしている
def locate(gid) gid.model_class.find gid.model_id endhttps://github.com/rails/globalid/blob/master/lib/global_id/locator.rb#L129
修正を検討
問題を振り返る
問題のコードを再掲
def complete_message! transaction do complete! update_message_log! # メッセージログテーブルを更新する処理 end MessagePushJob.perform_later(self) # 非同期ジョブにキューする end def send_message! transaction do message_complete! give_point! # 送信者にポイントを与える end end
- send_message!を実行
- complete!メソッドでmessageステータスをcompleteステータスに変更(この時点ではDBに変更が反映されていない)
- update_message_log!でログテーブルを更新
- MessagePushJob.perform_laterで非同期ジョブをキューする
- 非同期ジョブが実行される(この時点でも、まだDBに変更が反映されていない)
- give_point!で送信者のポイントを更新
- トランザクションが完了したので、ここでDBに変更が反映される
ということで、DBの更新前に非同期ジョブが実行されてしまっていることが問題となっている
修正案 2案
案1 send_message!のトランザクションを外す
仕様次第でばあるが、ポイントの更新が完了しないとメッセージが送れないというのはよくないので、トランザクションをはずす
(ポイントの処理に問題が出れば、ログテーブルから再付与すればいいだろう)
ちなみに、実際はステータス更新ともっと関係ない処理にトランザクションが貼られていたので、こちらの案1を採用したdef complete_message! transaction do complete! update_message_log! # メッセージログテーブルを更新する処理 end MessagePushJob.perform_later(self) # 非同期ジョブにキューする end def send_message! message_complete! give_point! # 送信者にポイントを与える end案2 send_message!に処理をまとめる
ポイントの更新も完了して、DBに変更が更新されてから、非同期ジョブをキューするようにする
ちなみに、complete_message!は他の箇所で使用しているので消せないので、重複したコードになってしまうdef complete_message! transaction do complete! update_message_log! # メッセージログテーブルを更新する処理 end MessagePushJob.perform_later(self) # 非同期ジョブにキューする end def send_message! transaction do complete! update_message_log! # メッセージログテーブルを更新する処理 give_point! # 送信者にポイントを与える end MessagePushJob.perform_later(self) # 非同期ジョブにキューする endまとめ
インスタンスを渡しているはずが、インスタンスのIDだけ渡しているっていうのが、暗黙的な仕様で初見殺しだなと思った(当たり前の人にとっては当たり前なのかな?)
ジョブ実行時に明示的にfindする必要はあるけど、明示的に引数にはインスタンスではなく、IDを渡すように変更してもいいのかもしれない
- 投稿日:2021-01-24T23:28:48+09:00
Railsのpresentメソッドの使い方
presentメソッドとは
オブジェクトであるレシーバーの値が存在すればtrue、存在しなければfalseを返すメソッドです。条件分岐(if文等)をプログラムで書くときによく使います。
例えばアプリの購入機能において「もし選択した商品に紐づく購入記録が存在していたら(空ではなかったら)、"sold out"と表示する」を実装したい場合は該当のビューファイルにおいて
index.html.erb<% if item.purchase.present? %> <div class='sold-out'> <span>Sold Out!!</span> </div> <% end %>という様に記述します。
<% if item.purchase.present? %>
の部分ですが
itemモデルとpurchaseモデルにテーブル間のアソシエーションが組んであれば
商品(item)に紐づく購入記録(purchase)が存在していたら(空ではなかったら)=true
という解釈になります。
- 投稿日:2021-01-24T23:28:48+09:00
Railsのpresent?メソッドの使い方
present?メソッドとは
オブジェクトであるレシーバーの値が存在すればtrue、存在しなければfalseを返すメソッドです。条件分岐(if文等)をプログラムで書くときによく使います。
例えばアプリの購入機能において「もし選択した商品に紐づく購入記録が存在していたら(空ではなかったら)、"sold out"と表示する」を実装したい場合は該当のビューファイルにおいて
index.html.erb<% if item.purchase.present? %> <div class='sold-out'> <span>Sold Out!!</span> </div> <% end %>という様に記述します。
<% if item.purchase.present? %>
の部分ですが
itemモデルとpurchaseモデルにテーブル間のアソシエーションが組んであれば
商品(item)に紐づく購入記録(purchase)が存在していたら(空ではなかったら)=true
という解釈になります。以下のサイトにはpresent?メソッドと逆のblank?メソッドも紹介されています。
- 投稿日:2021-01-24T23:21:59+09:00
ActionMailerを使用した問い合わせ機能の実装
実装機能
問い合わせフォームからメールを送信、それを指定したメールアドレスで受信する。
(アプリの利用者から管理者に向けてのメール機能として実装)ActionMailerでは他にもメルマガ配信のように運営側からユーザーに対してメール送信を行ったり、問い合わせに対するメールの返信機能なども実装が可能とのことだが、今回は一番シンプルなこちらの機能を実装。
またGmailアカウントでのメールの受信方法の為、前提としてGmailのアカウントを持っている体で話を進めていく。完成イメージ
実装内容
1.コントローラの作成
使用するビューファイルはnewのみな上にルーティングの修正が面倒な為、アクションの指定はせずにコントローラファイル単体で作成。
$ rails g controller inquiries2.ルーティングの作成
今回使用するアクションは以下の2つ
・問い合わせ作成画面に使用するnewアクション
・問い合わせのデータを作成する為のcreateアクション
のみconfig/routes.rbresources :inquiries, only[:new, :create]3.モデルの作成
最低限必要な情報としてユーザーの名前とメッセージ内容を保存するカラムを用意するが、今回は送信したユーザーが問い合わせに関して返答を要求する場面を想定し、emailのカラムも作成しておく。
$ rails g model Inquiry name:string email:string message:textマイグレーションファイルに特に変更がなければ
bundle install
を実行してテーブルを作成。その後必要なカラムにバリデーションをかけておく。
app/models/inquiryclass Inquiry < ApplicationRecord validates :name, presence: true validates :email, presence: true validates :message, presence: true end4.Mailerの作成
ActionMailerという機能がRailsに標準搭載されており、下記のコマンドで作成が出来る
terminal$ rails g mailer inquiry以下のファイルが作成されるのを確認
terminalcreate app/mailers/inquiry_mailer.rb invoke erb create app/views/inquiry_mailer invoke test_unit create test/mailers/inquiry_mailer_test.rb create test/mailers/previews/inquiry_mailer_preview.rb
このうちの
inquiry_mailer.rb
は、メール送信機能を実装するための空のクラス5.Mailerにメソッドを定義
InquiryMailer
内でメソッドを定義する。
ここで定義したメソッドを実際にデータの作成、送信を実行するInquiriesコントローラ
内で使用するapp/mailers/inquiry_mailer.rbclass InquiryMailer < ApplicationMailer def send_mail(inquiry) @inquiry = inquiry mail to: 'メールアドレス', subject: '【サイト名】お問い合わせ通知' end end上記のmailメソッドで指定されているプロパティの内容は以下の通り
・to = 送信先(メールアドレス)の指定
・subject = メールの件名
要はここで指定したアドレスから指定した件名でメールが届くということなる。
6.コントローラにメソッドを定義
app/controllers/inquiries_controller.rbclass InquiriesController < ApplicationController def new @inquiry = Inquiry.new end def create @inquiry = Inquiry.new(inquiry_params) if @inquiry.save InquiryMailer.send_mail(@inquiry).deliver redirect_to new_inquiry_path flash[:email] = "Your message was successfully sent." else render 'new' end end private def inquiry_params params.require(:inquiry).permit(:name, :email, :message) end endアクション内の処理の流れは通常の投稿作成とほぼ同じだが、createアクション内の処理で先程作成した
InquiryMailer
のメソッドを使用する。InquiryMailer.send_mail(@inquiry).deliverコードの文末にあるdeliverメソッドはメールの送信に関わるメソッドになる。
因みに、当然の事ながらこの行の処理が完了するまで以降のredirect等の処理が行われない為、フラッシュメッセージが現れるまで数秒間掛かることもある。
それを回避する為にActiveJob
と連携して非同期にメール送信を行う為のdeliver_laterというメソッドも存在するらしいが、今回はこちらを採用。7.development.rbにメール送信設定を記述する
ドメインの指定等の設定をconfigディレクトリ配下のdevelopment.rbファイルに記述する。
config/environment/development.rbconfig.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', port: 587, domain: 'gmail.com', user_name: 'メールアドレス', password: 'パスワード', authentication: 'plain', enable_starttls_auto: true }このあたりはこちらの参考記事の説明がわかりやすかったのでそのまま引用させて頂くことにする。
ここでは、config.action_mailerというパラメーターに色んなオプションを指定してます。
1行目 raise_delivery_errors
メールの送信に失敗した時にエラーを出すかどうか (出したいので true)2行目 delivery_method
メールの送信方法。 デフォルトで :smtd なので気にする必要もないのですが、
わたしみたいに「なにそれ!?」ってなった方は以下の引用を読んでみてください。「SMTP」とは「Simple Mail Transfer Protocol(シンプル・メール・トランスファー・プロトコル)」の略で、あえて>訳せば「簡単なメールの送信の手順」というところだろうか。お約束ごとと考えてもいい。
あなたがメールを書き、宛先のアドレスを入力して「送信」アイコンをクリックする。このとき、あなたのスマホやパソコン>は、この「SMTP」のお約束ごとに従って、あなたが契約しているメールサーバーと、こんなやり取りをするのである。
「メールを送るよ〜」「ええで!」「宛先は〇◯だよ」「りょ」「本文はかくかくしかじかだよ」「受け取ったで!」――とまぁそんな具合。出典: メール設定で最初につまずく「SMTP」「POP」「IMAP」。その意味&設定方法は?
3行目 smtp_settings
smtpの詳細設定って感じです。port => SMTPサーバーのポート番号
address => SMTPサーバーのホスト名
domain => HELOドメイン
user_name => メール送信に使用するgmailのアカウント
password => メール送信に使用するgmailのパスワード
authentication => 認証方法
enable_starttls_auto => メールの送信にTLS認証を使用するか※前項のMailerやここで使用するメールアドレスやパスワードはGithubにpushする際には環境変数化しておく必要があるので注意
8.ビューを作成する
問い合わせフォーム用のビュー画面を作成する。
コードは冒頭で添付した画面で使用したものなので、ご自身のアプリに合わせて作成。
通常のフォーム画面の作成と変わらないはず。app/views/inquiries/new.html.erb: <div class="contact_form text-center"> <%= form_with model: @inquiry, local:true do |f| %> <%= render 'layouts/error_messages', model: f.object %> <h3>NAME</h3> <%= f.text_field :name %> <h3>EMAIL</h3> <%= f.email_field :email %> <h3>MESSAGE</h3> <%= f.text_area :message %> <div> <p><%= f.submit "送信" %></p> </div> <% end %> </div> :9.メール画面を構成するファイルを作成
上記とは別に実際に送られてくるメールのレイアウトを構成するファイルも作成する必要がある。
ActionMailerのコマンドを実行した際にviewsディレクトリ
の中にinquiry_mailerディレクトリ
が作成されているため、そこにファイルを作成する。
今回はアプリの管理者が受け取る想定の為、構成には必要最低限しか気を配っていない。因みに場合によってはHTML形式のメールを受け取ることができない場合もあるらしく、そのためテキスト形式のファイルも用意しておくのがベスト。
HTML形式
app/views/inquiry_mailer/send_mail.html.erb<h5>お問い合わせ内容</h5> <P>-----------------------------------------</P> <p>名前:<%= @inquiry.name %></p> <p>メールアドレス:<%= @inquiry.email %></p> <p>お問い合わせ内容:<%= @inquiry.message %></P> <p>-----------------------------------------</p>テキスト形式
app/views/inquiry_mailer/send_mail.text.erbお問い合わせ内容 ----------------------------------------- 名前:<%= @inquiry.name %> メールアドレス:<%= @inquiry.email %> お問い合わせ内容:<%= @inquiry.message %> -----------------------------------------実際に届くメール画面がこちら(HTML形式)
10.環境変数を使用
Githubにpushしたり、本番環境でアプリを使用する際はこれまでに使用したメールアドレスやパスワードはセキュリティを考慮し環境変数化しておく必要がある。
gemのdotenv-rails
をinstallしている前提での説明な為、gemをインストールしていない場合は導入する。
.envファイル
に実際のメールアドレスやパスワードを記述.envSEND_MAIL="使用するメールアドレス" GMAIL_PASSWORD="使用するGmailアカウントのパスワード"定義した環境変数をコード内で使用
app/mailers/inquiry_mailer.rb: mail to: ENV['SEND_MAIL'], subject: '【サイト名】お問い合わせ通知' :config/environment/development.rb: user_name: ENV['SEND_MAIL'], password: ENV['GMAIL_PASSWORD'], :
.envファイル
を.gitignoreファイル
に記述して完了補足
gmailのアカウントの設定の関係でアクション実行の際に以下のようなサーバーエラーが発生する可能性がある。
・
Net::SMTPAuthenticationError (535-5.7.1 Username and Password not accepted. Learn more at
・Net::SMTPAuthenticationError 534-5.7.9 Application-specific password required.
上記エラーが現れた際は、別途Gmailアカウントの設定変更が必要となる可能性が高い(パスワードやメールアドレスのスペルミス等の可能性もあるため、まずはそこを疑う)。
個人的には実装よりもむしろこれらのエラーの解消に手こずったので、参考になれば。参考記事
ActionMailer参考記事
・Action Mailer でメール送信機能をつくる
・【Rails入門説明書】Action Mailerについて解説
サーバーエラーの解決のために使用
・Googleで2段階認証を使っているときにRailsのActionMailerでGmailを使う方法
・[Rails5]deviseでgmailを送ろうとしたがsmtp認証のエラーが出た時にした事。
- 投稿日:2021-01-24T23:17:31+09:00
特定のディレクトリ内のファイルを一括でrequireした話
元々の状況・課題
Rubyでチケット料金モデリングに挑戦中。
特定のディレクトリ内のファイルを一つずつrequireするのが面倒。
今後特定ディレクトリ内にファイルが増えた際に、追加するのも面倒。やったこと
下記コードを読み込みする側(User.rb)の一番上に書くことで、特定のディレクトリ内のコードを一括でrequireできました。
Dir[File.expand_path("../user_type", __FILE__) << "/*.rb"].each do |file| require file endディレクトリ構成は、ざっくりですがこんな感じです。
├── sample │ ├── user.rb ←読み込みする側(requireする側) │ └── user_type │ ├── user_a.rb ←requireしたいファイル │ └── user_b.rb ←requireしたいファイルコードの説明
User.rbDir[File.expand_path("../user_type", __FILE__) << "/*.rb"].each do |file| require file end1.[File.expand_path("../user_type", FILE) << "/*.rb"]について
File.expand_path
は、相対パスを絶対パスに変換した文字列を返す。__FILE__
は実行ファイル名を返す。<< "/*.rb"
は、"/*.rb"の文字列を追加する。(参考)実行結果
# __FILE__なし [File.expand_path("../user_type") << "/*.rb"] => ["/上の階層のパス/user_type/*.rb"]# __FILE__あり [File.expand_path("../user_type", __FILE__) << "/*.rb"] => ["/上の階層のパス/sample/user_type/*.rb"]2.Dir[]について
リファレンスでこのように書いてあります。
ワイルドカードの展開を行い、パターンにマッチするファイル名を文字列の配列として返します。パターンにマッチするファイルがない場合は空の配列を返します。
ブロックが与えられたときはワイルドカードにマッチしたファイルを引数にそのブロックを 1 つずつ評価して nil を返します今回のディレクトリ構成だと、
"/上の階層のパス/sample/user_type/*.rb"
にマッチするuser_a.rb
とuser_b.rb
を文字列として配列に入れることになります。
その後は、この配列をeach文でそれぞれrequireする処理を実行しています。参考
- 投稿日:2021-01-24T22:46:17+09:00
【Rails】Rails5.2 リッチテキストエディタsummernote-railsのツールバーアイコンの表示・非表示を切り替える
gem 'summernote-rails'
https://github.com/summernote/summernote-railssummernote-init.coffee$(document).on 'turbolinks:load', -> $('[data-provider="summernote"]').each -> $(this).summernote lang: 'ja-JP' height: 300 toolbar: [ ['insert', ['picture', 'link']], ["table", ["table"]], ["style", ["style"]], ["fontsize", ["fontsize"]], ["color", ["color"]], ["style", ["bold", "italic", "underline", "clear"]], ["para", ["ul", "ol", "paragraph"]], ["height", ["height"]], ["help", ["help"]] ]jsのオプションに、toolbar:[]部分の記述を足して、表示したくないアイコンに対応したハッシュの値を消せばアイコンが消える。
- 投稿日:2021-01-24T20:22:52+09:00
listen tcp 0.0.0.0:xxx: bind: address already in us の対処法
目的
Ruby on Railsでポートフォリオを作成していた途中にdocker-compose upをした際に、listen tcp 0.0.0.0:xxx: bind: address already in usとエラーが出たのでその時の対処方法を忘れないように書きます。
エラー内容
$ docker-compose up -d ------------------------- ------------------------- Error starting userland proxy: listen tcp 0.0.0.0:3306: bind: address already in use ERROR: Encountered errors while bringing up the project.対処方法
最初にこのコマンドを打ちます。
$ sudo lsof -i -P | grep "LISTEN" name1 633 ~~~~~~ 10u IPv4 0xc46fc72c3028ba51 0t0 TCP localhost:49362 (LISTEN) name2 634 ~~~~~~ 53u IPv6 0xc46fc72c50688ca1 0t0 TCP *:3000 (LISTEN) name3 96145 ~~~~~~ 54u IPv6 0xc46fc72c506892c1 0t0 TCP *:3306 (LISTEN) name4 72412 ~~~~~~ 82u IPv4 0xc46fc72c4cf4e591 0t0 TCP localhost:62741 (LISTEN)エラー内容は3306ポートはすでにあるよと言われています。
なので3306ポートをkillします。$ sudo kill -9 96145これでdocker-compose upができます!!
- 投稿日:2021-01-24T18:00:53+09:00
【jQuery】Ajaxで追加した要素へ正しくイベントセットする方法
概要
自作ポートフォリオの買い物メモアプリで、Ajaxを用いてメモの投稿機能を非同期処理に書き換えたところ、bootstrap4(modal) & jQueryで実装していた、投稿に対するコメント投稿機能が動かなくなる不具合が発生しハマったため、備忘録のために投稿しました。
結論
私の場合、modalに値を渡す処理を書いていたjQueryのコードを修正したことで改善出来ました。
Ajaxで実装した動的な要素(投稿)に対して、適切なイベントセットが出来ていなかったことが原因です。app/assets/javascripts/comment_modal.js変更前 $(".js_comment_btn").on("click",function() { $("#comment_id").val(this.value); }); 変更後 $(document).on("click",".js_count_btn",function() { $("#comment_id").val(this.value); });app/views/notes/index.html.erb<!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> <%= csrf_meta_tags %> <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %> <%= javascript_include_tag "application", "data-turbolinks-track": "reload" %> </head> <body> <!-- 以下、投稿用の部分テンプレート --> <div id="index"> <%= render "shared/my_content", note: @own_notes %> </div> <!-- 以下、コメント投稿用のモーダル --> <%= render "shared/note_modal" %> <%= form_with model: @note do |f| %> <%= f.text_field :content,id:"note_create_form",class:"form-control" , placeholder: "商品名を入力" %> <%= f.submit "投稿", class: "btn btn-light form-btn" %></li> <% end %> <body> </html>app/views/notes/_my_content.html.erb<% note.each do |note| %> <ul> <li class="note_content"> <%= note.content %> <button type="button" class="btn js_comment_btn" data-toggle="modal" data-target="#comment_Modal" value="<%= note.id %>" id="js_count"> <i class="fa fa-sort" aria-hidden="true"></i></button> </li> </ul> <% end %>app/controllers/notes_controller.rbclass NotesController < ApplicationController def create note = current_user.notes.build(note_params) if note.save @own_notes = current_user.notes.includes(comments: :user) render "create.js.erb" else flash[:danger] = "投稿に失敗しました" render "notes/index" end end endapp/views/notes/create.js.erb$("#index").html("<%= j(render "shared/my_content", { note: @own_notes }) %>")具体的解説
エラー内容
①Ajax形式でPOSTリクエストを送り、notes_controllerで投稿オブジェクトを作成。
②jsファイル(create.js)にレンダリングし部分テンプレートの中身を更新
③作成されたオブジェクトのbuttonでモーダルを起動
④モーダルにテキスト入力してPOSTリクエストを送るとエラーが出る
(ActiveRecord::RecordNotFound (Couldn't find Note with 'id'=):)
⑤ページをリロードし再度、③④を実行すると動くdevtoolで調べた結果、modalに投稿のIDを渡す処理が走っていませんでした。
その後原因究明してわかったのは部分的にしか更新しないAjaxだと、DOM要素の指定に工夫が必要だということ
今回の場合、
jsファイルで部分的にページの更新
↓
modalを呼び出すbutton要素部分(部分テンプレート内)とmodal本体(部分テンプレート外)の間で
整合性が取れなくなり④のエラーが発生
↓
ページ全体のリロードによりDOMが再構築されると問題なく動くようになるそのため、下記のように親要素の下のクラスという指定をしてあげることで
改善することが出来ました。app/assets/javascripts/comment_modal.js$(document).on("click",".js_count_btn",function() { $("#comment_id").val(this.value); });実行環境
・Ruby v2.7.0
・Ruby on Rails v5.1.7
・jQuery-rails v4.4.0
・bootstrap v4.3.1
- 投稿日:2021-01-24T17:58:46+09:00
RailsにおけるSQLインジェクション対策について - Rails Tutorial リスト13.46
はじめに
Rails Tutorialを進めていく中で気になったところを記事にして残してます。
記事に間違いがある場合は教えてください
用語解説
SQLインジェクションとは
SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。
つまり、ログインフォームや投稿フォームなどでSQLのデータを処理する際に、不正(サーバーからみると正常)なデータが実行されてしまうことです。
これによってログイン偽装やデータの抜き取りが起こります。エスケープ処理とは
「'」「"」「NULL」「改行」などのSQL文において都合の悪い文字を使えなくすることです。
悪い例
不正に認証が通ってしまう
以下のコードでログイン処理をするとしましょう。
User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")一見問題はなさそうに見えますが、もし以下のパラメータが入っていたとします。
params = { name: "' OR '1'='1", password: "' OR '2'>'1" }これを実行すると以下のようなSQL文が呼ばれることになります。
SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1要約すると
1=1
,2>1
が成り立つ時usersから1人取り出してくださいということです。
ログインとしての機能を果たさなくなってしまいました。原因
こうなってしまう原因はエスケープ処理をしていないからです。
対策
対策としてSQL文を書くときには直接文字列を代入するのではなく、必ずエスケープ処理をするようにしましょう。
エスケープ処理には以下の方法があります。
- 配列、ハッシュとして渡す。(モデルのインスタンスのみ)
- sanitize_sql()を使う。(それ以外)
対策1
モデルの場合はこちらが楽です。
# 配列 Model.where("login = ? AND password = ?", entered_user_name, entered_password).first # もしくはハッシュ Model.where(login: entered_user_name, password: entered_password).first対策2
sanitize_sqlというメソッドでエスケープできます。
# 3つの例 sanitize_sql(["name=? and group_id=?", "foo'bar", 4]) sanitize_sql(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) sanitize_sql("name='foo''bar' and group_id='4'")参照
Rails セキュリティガイド - Railsガイド
ActiveRecord::Sanitization::ClassMethods - Rails API
- 投稿日:2021-01-24T17:37:49+09:00
Dockerのruby:2.7-alpineでruby2.7がないといわれてしまう
はじめに
これまでいくつかのDockerimageを構築してきました。OpenAPIとSinatraの組み合わせは、お手軽なのですが、これまで作成してきたイメージを作り直したところ問題が発生しました。
Dockerでbundle exec rackup ...を実行したタイミングでエラーメッセージが表示されてしまうようになりました。
エラーメッセージ$ sudo docker run -it --rm \ --env LC_CTYPE=ja_JP.UTF-8 \ --env RACK_ENV="deployment" \ -p 8080:8080 \ --name webapp mywebapp \ ... + bundle exec rackup --host 0.0.0.0 --port 8080 env: can't execute 'ruby2.7': No such file or directoryコンテナ内のファイルを確認して、最後のメッセージは ./lib/ruby/2.7.0/bin/rackupコマンドがruby2.7を呼び出しているからということは分かりました。
Dockerfileは次のようになっていて、multi-stage buildを利用しています。
Dockerfileの抜粋FROM ruby:2.7-alpine as rubydev RUN apk update && \ apk add --no-cache tzdata bash ca-certificates make gcc libc-dev COPY . /app WORKDIR /app RUN bundle config path lib RUN bundle install FROM ruby:2.7-alpine RUN apk update && \ apk add --no-cache tzdata bash ca-certificates COPY --from=rubydev /app /app WORKDIR /app ADD run.sh /run.sh RUN chmod +x /run.sh RUN addgroup sinatra RUN adduser -S -G sinatra sinatra USER sinatra RUN bundle config path lib ## generating the ~/.bundle/config file. ENTRYPOINT ["/run.sh"]理由
rackupを探してみると、2箇所に存在していて、なぜか lib/ruby/2.7.0/bin/rackup では ruby2.7バイナリを探していました。
+ find lib/ -name rackup lib/ruby/2.7.0/bin/rackup lib/ruby/2.7.0/gems/rack-2.2.3/bin/rackup + cat lib/ruby/2.7.0/bin/rackup #!/usr/bin/env ruby2.7 ... + cat lib/ruby/2.7.0/gems/rack-2.2.3/bin/rackup #!/usr/bin/env ruby ...ワークアラウンドで対応するのであれば、/usr/local/bin/ruby2.7 を準備する方法もあります。
Dockefileに追加した1行RUN ln -s ruby /usr/local/bin/ruby2.7ただ、こういった現象が発生するはずがないと思っていたので、少し原因の調査を進めました。
原因
結論からいうと、問題は Dockerfile の次の箇所にありました。
問題のあったDockerfileの抜粋COPY . /appここではDockerfileを含めて全てのファイルをコピーしています。
Dockerfile自体は秘密ではないため、この方法自体は横着しているなという程度のものですが、問題はこの時点でカレントディレクトリの./lib/ruby/2.7.0/ ディレクトリもコピーされてしまったことでした。コンテナを作成する前にコードをデバッグする目的で、ローカルのUbuntu環境でもrackupを起動しています。
そのため、bundle install コマンドを実行することで、./lib/ruby/2.7.0/bin/rackup が配置されていたのでした。Ubuntuでは /usr/bin/ruby が ruby2.7へのシンボリックリンクとなっているため、このrackupはruby2.7から起動されるようになっていました。
Ubuntuに配置されたrackupコマンド$ head -2 ./lib/ruby/2.7.0/bin/rackup #!/usr/bin/env ruby2.7 #他のDockerfileを処理するMakefileの中で、./lib/ruby ディレクトリを削除していたのですが、このイメージを作成する作業用リポジトリのMakefileには、このコードが含まれていませんでした。
Makefileのdockerイメージをbuildする箇所の処理抜粋## local debug用のコマンド bundle-install: bundle install --path lib ## dockerイメージをbuild docker-build: rm Gemfile.lock || true sudo docker build . --tag $(DOCKER_IMAGE)Gemfile.lockファイル以外に、./lib/ruby/, ./.bundle/ ディレクトリを削除するコードを追加することで恒久的な対策となりました。
結果は勘違いから勝手に悩んでいるだけだったのですが、手持ちのコンテナをbuildするプロセスを再度チェックしたいと思います。
他のイメージでは ./.bundle/ や、./lib/ruby を削除していたので、思い込みがあったのも問題解決がすぐに出来なかった原因でした。以上
- 投稿日:2021-01-24T17:24:35+09:00
Rubyの継承関係をまとめてみた(投稿テスト)
はじめに。
Rubyの継承関係を、Github pagesでマークダウンを書く練習がてらまとめてみたのですが、動作確認で上げたページの閲覧が意外とあったので、慌ててちゃんとしたページにしました;;(∩´~`∩);;
(ほんと今週末試験なのに何やってるんだろう、こやつは?)
継承関係まとめ
きっと私なんぞより上手くまとめたページは山ほどある。間違いない。(余談)先生が言っていた、「特異クラスは"singleton class"(1枚札)より相応しい呼び方があるんだけど、何だったかなぁ?」の答えはきっと
"eigenclass"(固有の)だろうな( ˊᵕˋ ;)以下、Github pagesと同文ですのでご承知おきくださいませ。
Rubyの継承関係で知っておくべきこと
silverの範囲
- すべてのクラスは、Classクラスから継承されている。
- Classクラス(トップレベル)は、生成機能をもっている。
- スーパークラスを明示せずクラス定義すると、暗黙的にObjectクラスを継承する。
- クラス名は定数であることを覚えておくと、Goldの範囲に入っていきやすくなる。
- 但し、関数型言語を扱う際は、クラス名=定数の概念はよろしくないとのことで注意が必要。
goldの範囲
- ClassクラスはModuleクラスを継承しており、ModuleクラスのsuperclassはObjectクラスである。(図に描くとループする。)
- モジュールのMix-inは、インタプリタがモジュールに対応した無名クラスを生成して継承関係に組込んでいる。
- 特異クラス(singleton class)は特異メソッド定義、或いは特異クラス式評価を確認したときに生成される。
上記を前提条件として、クラスの継承関係をまとめていきます。
Ruby技術者認定試験のGoldは、クラス/モジュールまわりだけでも半分近く回答出来るので頑張っていきましょう。イディオムといえる記法でクラス定義する
定数Fooへの代入の部分は、恐らく"初見殺し"と感じる方も多いのではないでしょうか?
# ここは明示せずともObjectクラスを継承してBarクラスを生成 class Bar def initialize(greet) @greet = greet end end # Fooという定数に、引数のBarクラスをスーパークラスとして # Classクラスからクラス生成 Foo = Class.new(Bar) do def initialize(greet, name) @name = name super(greet) end def f_method(msg) @greet + @name + msg end end # インスタンスメソッドなので、クラスのインスタンスを生成しておくこと foo = Foo.new("hello", " kuma") # 生成インスタンスをレシーバに、メソッドを呼び出す p foo.f_method(" welcome!") #=> "hello kuma welcome!"この記法だと特にわかり易くなるのが「定数に、Classクラスのオブジェクト=インスタンスを割り当てている」ということですね。
これは「クラスはオブジェクトである」ことの根拠のひとつなのですが、インスタンス変数やメソッドが絡むので、別途まとめを作りたいと思います。
- ary = Array.new()をイメージすると、クラスをインスタンスとして生成していることが理解しやすくなります。
- do~endはお馴染みのブロックなので、処理のかたまりと認識すると、こちらも理解しやすくなるのではないでしょうか。
# とは言え、この継承の基本がないと始まらないので原点回帰用に書いておく class Foo < Bar ... endなお、書籍でよく紹介される「レシーバのオブジェクトからひとつ右へ、それからクラス継承チェーンを上へ辿る」図を描くと理解しやすくなるのでオススメです。
実はよく使っている特異クラス
元々、RubyはObjectクラスより上位の階層を
意識しなくても使えるようにデザインされているわけですが、
更に「意識させたくないクラス」が存在しています。そのひとつが「特異クラス」です。
特異クラスは「特定のオブジェクトにのみ適用されるクラス」と定義付けされており、オブジェクトを1つしか持てない特徴があります。# お馴染みインスタンス生成メソッドnew Class.new()よく見ると、クラスを固有オブジェクトとしてnewメソッドを呼び出しているのにお気付きでしょうか?
これは「クラスの特異メソッド=クラスメソッド」を定義しているので、クラス=オブジェクトとして振る舞い、メソッド呼び出しができるようになっています。クラスメソッドの定義方法
# 特異メソッド形式、単体に定義するのに向いている def self.class_method ... end # 特異クラス形式、複数に定義するのに向いている # 何よりメソッドにself.の記述をしなくても補完してくれるのが有難い class << self def class_method ... endでは、実際に動作させて確認してみましょう。
Baz = Class.new do def inst_method puts "I am instance_method" end # 特異メソッド形式で定義する def self.class_method puts "I am class_method" end end # インスタンスメソッド呼び出しなので、newで生成したインスタンスをレシーバにする Baz.new.inst_method #=> I am instance_method # クラスメソッド呼び出しなので、固有オブジェクトであるクラスをレシーバにする Baz.class_method #=> I am class_methodBaz = Class.new do def inst_method puts "I am instance_method" end # 特異クラス形式で定義する # self(今回はBazクラス)の特異クラスを開いて直接特異メソッドを定義 class << self def class_method puts "I am class_method" end end end # 定義は同じなので、呼び出しも実行結果も同じ Baz.new.inst_method #=> I am instance_method Baz.class_method #=> I am class_method前提条件として申し上げた通り「特異メソッドを定義すると特異クラスが生成される」ため、
通常の継承チェーン同様、特異クラスチェーンも生成されています。これは図で表すとき「クラス継承チェーンを上へ辿る」図の右側に「特異クラスのみの継承チェーン」をModuleクラスまで描くと理解しやすくなるのではないでしょうか。
また、るりまを御覧頂くと明記されております通り、newメソッドはClassクラスに定義されている特異メソッドのため、全ての特異クラスがnewメソッドを継承しているのですね。
Mix-inによる継承関係
よく使うMix-inですが、Module#ancestorsメソッドで確認すると、Kernelを含めたモジュール名が継承関係に組込まれていることに「不思議」と感じたことはありませんでしょうか?
module Hoge def hoge; puts "hoge"; end end module Fuga def fuga; puts "fuga"; end end class Foo include Hoge def foo; puts "foo"; end end class Bar < Foo def bar; puts "bar"; end prepend Fuga end Bar.ancestors #=> [Fuga, Bar, Foo, Hoge, Object, Kernel, BasicObject]モジュールの特徴として
- インスタンスを持つことが出来ない
- 継承関係を持つことが出来ないという2点が挙げられるのですが、これはどういうことなのでしょうか?
実はRubyの意識させたくないクラスのひとつ「無名クラス」を利用して実装されています。
内部では、Rubyのインタプリタが判断し、「モジュールに対応した無名クラス」を生成し、継承チェーンに割込み拡張しているのです。また、意識させたくないクラスであることから、この無名クラスはModule#ancestorsやClass#superclassといったメソッドでも参照出来ない仕組みになっているため、複雑に感じる部分でもあります。
とはいえ、Rubyの優秀なインタプリタが実装していることを踏まえて、動作させてみるのが理解への1番の近道だと感じます。
include
呼び出したクラスの上に、モジュールと対応した無名クラスが挿入されます。
module M1 def method1; puts "M1"; end end module M2 def method1; puts "M2"; end end class C1 def method1; puts "C1"; end end class C2 < C1 include M1 def method1; puts "C2"; end include M2 end C2.new.method1さて、この実行結果はどうなるでしょうか?
ポリテクの授業では、includeのみ説明がありましたが、皆さん「えっ?」となっていた部分ですので少しだけ解説致しますね。まず、実行結果は探索経路がカレントクラス優先となりますので、答えはC2のメソッド呼び出しが優先となります。
C2.new.method1 #=> "C2"
- モジュールの機能として、名前空間が提供されているため、同名メソッドであっても衝突は起こらない。よってErrorにはならない。
- includeしただけでは継承チェーンに組み込まれるだけなので、呼び出しメソッドがカレントクラスにあれば、そのメソッドが実行される。
# ancestorsで確認する C2.ancestors #=> [C2, M2, M1, C1, Object, Kernel, BasicObject]上記を覚えておくと、格段に理解しやすくなるのではないでしょうか。
次に、カレントクラスのメソッドが無い場合はどうなるでしようか?上記のC2クラスのメソッドをコメントアウトしてみてくださいませ。
C2.new.method1 #=> "M2"この場合は、後にincludeしたモジュールが実行されます。
- カレントクラスから見た探索経路で1番近い箇所で見つかったメソッドが実行される。
- 複数のインクルードを実行すると、後に記述したモジュールが呼び出しクラスの直ぐ上に割込み挿入される。上記を理解しておくと、複数のインクルードがなされてもほぼ解釈を間違える事は無くなりますね。
prepend
呼び出したクラスの下に、モジュールと対応した無名クラスが挿入されます。
module M1 def method1; puts "M1"; end end module M2 def method1; puts "M2"; end end module M3 def method1; puts "M3"; end end class C1 def method1; puts "C1"; end end class C2 < C1 prepend M1 def method1; puts "C2"; end prepend M2 include M3 end C2.new.method1includeの説明で使ったコードを少しだけ複雑に見せ掛けてみました。この実行結果はどうなるでしょうか?
C2.new.method1 #=> "M2"includeでどこに挿入されるかを理解しておくと、挿入方向が下になるだけなので、簡単に回答出来ることでしょう。
prependの場合、探索経路の1番手前に配置されます。
# ancestorsで確認する C2.ancestors #=> [M2, M1, C2, M3, C1, Object, Kernel, BasicObject]ancestorsの結果は割と試験でも出る内容ですので、是非沢山コードを動かして見てくださいませ。
extend
固有オブジェクトに定義した特異クラスに、includeされる。
# extendの使い方 # 特異クラスBazにPiyoモジュールをインクルードする baz = Baz.new baz.extend(Piyo) p baz.piyo_method # 上記で行われている内容として # Bazクラスのインスタンスbazを固有オブジェクトとして baz = Baz.new # 特異クラスを開いて class << baz # Piyoモジュールをインクルードする include Piyo end # 特異メソッドなので、固有オブジェクトをレシーバに呼び出す p baz.piyo_methodこの場合の継承関係は、特異クラスチェーンに「baz→Piyo→Baz→...」と図にすると確認し易いのではないでしょうか。
なお、extendはancestorsでは確認出来ない仕様になっております。Gold試験で問われる関連メソッド
Module#append_features
- includeの実体であるメソッド。
- インクルードされる前に呼び出される。
- 引数にインクルードされるモジュールが入る。
- また、モジュール/クラスにselfの機能を追加する。
- superの記述が無いと上書き時にインクルード出来ない仕様になっている。
Module#included
- インクルード後に呼び出される。
- インクルード後にやりたい処理を実装しておくと、フックメソッドにできる。
Module#ancestors
- レシーバがModule/Classクラスのインスタンスの場合のみ有効。
- クラス/モジュールの、superclassとインクルードしているモジュールの優先順を配列で返却。
最後に
ここまでの長文をお読みいただき、ありがとうございます。
これで、一通りの継承関係を解説致しました。詳細部分をもう少し噛み砕いたものをつくりたかったのですが、私自身が今週末に資格試験を控えておりますので、もう少し掘り下げた内容は受験後に改めて編集しようと考えております。少しでもお役にたてれば幸いです。
- 投稿日:2021-01-24T16:53:28+09:00
2点間の距離検索をgeokit-railsからelasticsearchに切り替えのため、検索手法を調査した
はじめに
Railsで緯度経度の2点間の距離を検索するために、geokit-railsを使っていたが、全ての検索をelasticsearchにしたいので、検索手法を調査した
geokit-railsによる距離検索
geokit-railsによる距離検索はsphere_distance_sqlとflat_distance_sqlの2種類ある
def distance_sql(origin, units=default_units, formula=default_formula) case formula when :sphere sql = sphere_distance_sql(origin, units) when :flat sql = flat_distance_sql(origin, units) end sql endデフォルトはGeokit::default_formulaが使われている
self.default_formula = options[:default_formula] || Geokit::default_formulaGeokit::default_formulaの値は:sphereが設定されている
Geokit::default_formula = :sphereでは、sphere_distance_sqlとflat_distance_sqlの違いは何か??
大円距離(sphere_distance_sql)
https://ja.wikipedia.org/wiki/%E5%A4%A7%E5%86%86%E8%B7%9D%E9%9B%A2
sphere_distance_sqlは大円距離という球面上の2点間の長さが最短となる距離で計算している
geokit-railsでの計算箇所
def sphere_distance_sql(lat, lng, multiplier) %| (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier}) | endピタゴラスの定理(flat_distance_sql)
ピタゴラスの定理は、直角三角形の3辺の長さの関係を表す
2点を斜辺とする直角三角形から距離を計算しているgeokit-railsでの計算箇所
def flat_distance_sql(origin, lat_degree_units, lng_degree_units) %| SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) | end次に、elasticsearchはどの計算手法を使っているのか??
elasticsearchによる距離検索
distance_typeでarcかplaneを指定することができる
デフォルトではarcで計算されるdistance_type
How to compute the distance. Can either be arc (default), or plane (faster, but > inaccurate on long distances and close to the poles).https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-distance-query.html
まとめ
- geokit-railsはデフォルトでは大円距離の計算手法
- Elasticsearchはデフォルトでarc
余談
もう少し、elasticsearchの計算分岐箇所を追ってみた
public double calculate(double srcLat, double srcLon, double dstLat, double dstLon, DistanceUnit unit) { if (this == PLANE) { return DistanceUnit.convert(GeoUtils.planeDistance(srcLat, srcLon, dstLat, dstLon), DistanceUnit.METERS, unit); } return DistanceUnit.convert(GeoUtils.arcDistance(srcLat, srcLon, dstLat, dstLon), DistanceUnit.METERS, unit); }大円距離の方も見ていくと
DistanceUnit.convert(GeoUtils.arcDistance(srcLat, srcLon, dstLat, dstLon), DistanceUnit.METERS, unit)/** Return the distance (in meters) between 2 lat,lon geo points using the haversine method implemented by lucene */ public static double arcDistance(double lat1, double lon1, double lat2, double lon2) { return SloppyMath.haversinMeters(lat1, lon1, lat2, lon2); }public static double haversinMeters(double lat1, double lon1, double lat2, double lon2) { return haversinMeters(haversinSortKey(lat1, lon1, lat2, lon2)); }public static double haversinMeters(double sortKey) { return TO_METERS * 2 * asin(Math.min(1, Math.sqrt(sortKey * 0.5))); }ぱっと見、geokitのsphereの計算式と違うように見える、、時間ある時に調べよ、、
- 投稿日:2021-01-24T16:50:53+09:00
bundle exec
- 投稿日:2021-01-24T13:04:29+09:00
投稿テストついでにRubyの継承関係まとめ
Rubyの継承関係を、Github pagesでマークダウンを書く練習がてらまとめてみた。(今週末試験なのに何やってるんだろう?)
リンクがうまく貼れているかドキドキしながら、スマホから投稿してみる( ˙ω˙)و グッ!
継承関係まとめ先生が言っていた、「特異クラスは"singleton class"(1枚札)より相応しい呼び方があるんだけど、何だったかなぁ?」の答えはきっと
"eigenclass"(固有の)だろうな、という書き方をしてしまった( ˊᵕˋ ;)
- 投稿日:2021-01-24T12:35:29+09:00
【Ruby】ハッシュの生成
ハッシュの記述方法がよくわからなくなってしまうので、復習を兼ねて記事にしました。
ハッシュとは
データ」とそれに対応する「名前」のセットを要素として持つ値。
ハッシュにおいては、データをバリュー、それに対応する名前をキーと呼ぶ。ハッシュの生成方法
文字列をキーにする場合とシンボルをキーにする場合がある。
シンボルをキーにすると、キーを数値として扱ってくれるので処理速度が早い。文字列をキーにした書き方
height1 = { "Taro" => 175 , "Jiro" => 170 , "Kohei" => 165}シンボルをキーにした書き方
以下どちらの記述でもOK
height2 = { :Taro => 175 , :Jiro => 170 , :Kohei => 165} height3 = { Taro: 175 , Jiro: 170 , Kohei: 165}ハッシュを編集するとき(値の追加、変更)
ハッシュに値を追加する場合
ハッシュに値を追加したいときは以下のように記述する。
height3[:Hiro] = 183 puts height3 #{:Taro=>1175, :Jiro=>170, :Kohei=>165, :Hiro=>183}ハッシュの値を変更する
ハッシュの値を変更したいときは以下のように記述する。
height3[:Taro] = 185 puts height3 #{:Taro=>185, :Jiro=>170, :Kohei=>165, :Hiro=>183}ハッシュの中身を取り出す
キーを取り出す
キーを取り出すときはkeysメソッドを使用する。
keysメソッドを使用すると、ハッシュ内のキーを配列として取り出すことができる。全てのキーを取り出すとき
#height3 = {:Taro=>185, :Jiro=>170, :Kohei=>165, :Hiro=>183} puts height3.keysTaro Jiro Kohei Hiro
一つのキーを取り出すとき
#height3 = {:Taro=>185, :Jiro=>170, :Kohei=>165, :Hiro=>183} puts height3.keys[0]Taro
バリューを取り出す
バリューを取り出すときはvaluesメソッドを使用する。
valuesメソッドを使用すると、ハッシュ内のキーを配列として取り出すことができる。全てのバリューを取り出すとき
#height3 = {:Taro=>185, :Jiro=>170, :Kohei=>165, :Hiro=>183} puts height3.values185 170 165 183
一つのバリューを取り出すとき
#height3 = {:Taro=>185, :Jiro=>170, :Kohei=>165, :Hiro=>183} puts height3.values[0]185
- 投稿日:2021-01-24T12:32:06+09:00
Rspecでテストを作成する時、毎回 require 'rails_helper' と書くのを省略する
環境
Mac OS X
Ruby: 2.7.1
Rails: 6.0.3.4課題
RSpecでテストを作成する時、何も設定しないと以下のように記述すると思う
model_spec.rbrequire 'rails_helper' Rspec.describe Model, type: :model do ~以下テスト~しかしrequire 'rails_helper'と毎回記述するのは面倒くさいので省略できないか調査した
解決法
.rspecに以下のコードを追加する
.rspec--require rails_helper結果
以下のようにしてもテストが動き、コードを削減することができた
model_spec.rbRspec.describe Model, type: :model do ~以下テスト~参考記事
https://qiita.com/yuta-ushijima/items/ffb34823b8bba2180c94
最後に
お読みいただきありがとうございました。
- 投稿日:2021-01-24T12:32:06+09:00
Rspecでテストを作成する時、各ファイル毎に require 'rails_helper' と書くのを省略する
環境
Mac OS X
Ruby: 2.7.1
Rails: 6.0.3.4課題
RSpecでテストを作成する時、何も設定しないと以下のように記述すると思う
model_spec.rbrequire 'rails_helper' Rspec.describe Model, type: :model do ~以下テスト~しかしrequire 'rails_helper'と毎回記述するのは面倒くさいので省略できないか調査した
解決法
.rspecに以下のコードを追加する
.rspec--require rails_helper結果
以下のようにしてもテストが動き、コードを削減することができた
model_spec.rbRspec.describe Model, type: :model do ~以下テスト~参考記事
https://qiita.com/yuta-ushijima/items/ffb34823b8bba2180c94
最後に
お読みいただきありがとうございました。
- 投稿日:2021-01-24T11:56:37+09:00
attr_accessor(アクセサ)の使い方(Ruby初心者)
アクセサとは
定義:インスタンス変数にアクセスするために必要なもの。
前提:Rubyではそのままだとインスタンス変数にアクセスできない。
→外部からの変数書き換え、呼び出しができない。class User @name = "" end user = User.new user.name = "taro" p user.name #=>rb:6:in `': undefined method `name=' for # (NoMethodError)
→インスタンス変数からの呼び出しを可能にするのがattr_reader(ゲッター)
class Human attr_reader :name #(ゲッター) def initialize(name) @name = name end end human = Human.new("taro") puts human.name実行結果
taro
→インスタンス変数を外部から書き換え可能にするのがattr_writer(セッター)
class Human attr_writer :name #(セッター) attr_reader :name def initialize(name) @name = name end end human = Human.new("taro") puts human.name human.name = "siro" puts human.name実行結果
taro siro
→attr_reader(ゲッター)とattr_writer(セッター)の機能を足したのがattr_accessor(アクセサ)
class Human attr_accessor :name #アクセサ def initialize(name) @name = name end end human = Human.new("taro") puts human.name human.name = "siro" puts human.name実行結果
taro siro
- 投稿日:2021-01-24T11:28:38+09:00
【Rails】js.erb で文字列の途中で改行する方法
背景
JSにRubyを埋め込みしていたのですが、長ったらしくなってしまったので、途中で改行したくなりました。
方法
行の最後に「\」をつける。
document.getElementById('messages').insertAdjacentHTML('afterbegin', '<p id="<%= @message.id %>">\ <span><%= @message.content %></span>\ <span><%= @message.user.name %></span>\ <span><%= @message.created_at %></span>\ </p>');「\」の入力方法は、macなら「option」+ 「¥」
- 投稿日:2021-01-24T11:27:19+09:00
[Rails + jQuery]初学者向けAjaxを取り敢えず飛ばしてみたい
課題
初学者が感覚掴むために取り敢えずAjax飛ばしてみたい。
例として以下フォームで入力された文字をサーバに送ってみる。
form.erb<%= text_field_tag :myname %>output<input type="text" name="myname" id="myname">結論
以下コードで取り敢えず飛ぶ。
ajax.js$("#myname").change(function(){ let name = $(this).val(); $.ajax({ type: 'GET', url: "/api/v1/users", data: { name: name }, dataType: 'json' }) .done(function (response) { console.log(response) } .fail(function (){ }); })受け口のRouteやControllerは一般的なRailsの範囲なので割愛します。
参考情報
Ruby on RailsのAjax処理のおさらい
https://qiita.com/ka215/items/dfa602f1ccc652cf2888
- 投稿日:2021-01-24T11:01:07+09:00
【Ruby】小数点以下を表示する方法 .to_f
- 投稿日:2021-01-24T10:29:17+09:00
Rubyにおける「クラス」と「オブジェクト」の関係について分かりやすく解説してみる
Rubyの勉強を始めて、すぐに出てくる重要キーワード。それは「クラス」と「オブジェクト」です。
Rubyにおいて重要な概念だと分かってるけど、なんとなくの理解で学習が進んでいってしまうことも多いのではないでしょうか。
クラスはよく使うけど理解が浅い
「クラス」と「オブジェクト」の関係がいまいち掴めない
どうやって「オブジェクト」を作れば良いかわからない
今日はこのような方に向けて、記事を書いていこうと思います。「クラス」と「オブジェクト」の関係について
クラスとは「もの」を作るときの「設計図」です。
オブジェクトとは設計図をもとに作られる「もの」です。
クラスとオブジェクトの関係を図に表すと以下になります。
クラスが「人間」
オブジェクトが「たろう」「花子」「Michael」です。
クラスである「人間」には(名前,出身)という設計図が描かれています。
この設計図を使って、「名前:たろう,出身:東京」のオブジェクトを作っています。
つまり、
設計図である「クラス」をもとに、ものである「オブジェクト」を作ることができます。これが「クラス」と「オブジェクト」の関係です。
ここで、たろうに性別や年齢を加えたい場合は、人間クラスを(名前,出身,性別,年齢)という設計図にすればいいわけです。
クラスの作り方
ここからは、実際にコード書いてクラスとオブジェクトの理解をさらに深めていきましょう。
まずクラスの作り方についてです。試しにUserクラスを作ってみましょう。user.rbというファイルを作成して、以下のように記述します。
class User endこれだけでUserクラスの完成です。簡単!
オブジェクトの作り方
irb(書いたプログラムを実行できるコマンド)を開いて、先ほどのUserクラスを使って、オブジェクトを作っていきます。
> require './user.rb' #user.rbのコードを読み込むコマンド >Michael = User.newUser.newを使って、Michaelというオブジェクトを作ることができました。これまた簡単!
オブジェクトから、原型となるクラスを確認する方法
全てのオブジェクトはその元となるクラスを持っています。つまりMichaelはUserクラスを持っているはず。それが本当かどうか確認します。
>Michael.class =>Userclassというメソッドを使って、オブジェクトが持つクラスを確認することができます。エラーが出たとき「このオブジェクト、何のクラスを持ってるんだっけ?」となりがちなので覚えておきましょう。
(補足)いまはUserクラスに設計図(名前,出身など)が何も書かれていません。今回の記事は「クラス」と「オブジェクト」の関係について理解する記事なので、設計図の書き方は省略です。また記事にしたいと思います。
まとめ
「クラス」と「オブジェクト」の関係について理解が深まったでしょうか。なんとなくの理解でプログラミング学習を進めてしまいがちですが、少しずつ理解を深めながら、Ruby勉強を進めていきましょう。
この記事の説明がわかりやすかった!ここ間違ってるよ!次こんな記事を書いて欲しい!などあればコメント、DMよろしくお願いします。LGTMもぜひ。
Twitterもやってますので、フォローしていただけたらうれしいです。
卓球、心理学、哲学、Webサービス、好きな音楽、カメラ、登山、ランニング、読んだ本などなんでもつぶやいてます。
- 投稿日:2021-01-24T10:01:10+09:00
(備忘録)Everyday Rails 第2章「RSpecのセットアップ」
「Everyday Rails - RSpecによるRailsテスト入門」
Railsを使用したアプリケーションを作成したものの、テストの記述が全然分からない、という状態になったので、RSpecの勉強をできる教材を探していたところ、なんとプロを目指す人のためのRuby入門 (チェリー本)の著者が翻訳を手がけた本を発見しました。
こちらのサイトから購入できます!
販売方法はなんと、購入金額を自由に設定できるという、画期的なシステムでした(最低購入価格は$19からですが)。
先日、なんとか1周は終わらせたのですが、全然理解できていない部分が多いので、復習がてらアウトプットしていきたいと思います!
第1章 「イントロダクション」
第1章はイントロダクションということで、この本のアウトラインや著者の想いなどが書かれています。
著者の思い
その中で、著者の考える基本的な信条というものがあり、
・テストは信頼できるものであること
・テストは簡単に書けること
・テストは簡単に理解できること(今日も将来も)と書かれており、また
・とはいえ結局、一番大事なことはテストが存在することです。信頼性が高く、理解しやすいテストが書いてあることが大事な出発点になります。
とありました。
テストはどちらかと言うと、とっつきにくいイメージがあったので、学習することから逃げていた節もあったのですが、心を入れ替えてしっかりと取り入れていきたいと思います!サンプルコードについて
GitHubからサンプルコードを入手できます。
第2章 「RSpecのセットアップ」
早速、本に従って進めていきたいところですが、いきなり、「テストスイート」という聞いたこともない単語に出くわしました。
スルーしてもいいのですが、最初が肝心なので、調べてみました。
テストスイートとは、ソフトウェアテストの目的や対象ごとに複数のテストケースをまとめたもの。自動化テストにおいては、テストの実行単位となる。
ソフトウェアテスト(動的テスト)の最小単位はテストケースといえるが、実際にテスト作業ではいくつものテストケースを組み合わせることによって不具合をあぶり出したり、確率的に十分なテストを行ったという結論を出したりする。このようなテスト目的やテスト対象に応じて、多数のテストケースを束ねたものをテストスイートという。らしいです。
僕は知りませんでしたので、勉強になりました。Gemfileのインストール
まずはRSpecをインストールする必要があります。
Gemfilegroup :development, :test do gem 'rspec-rails', '~> 3.6.0' end開発環境とテスト環境の両方で、rspec-railsを読み込みますが、本番環境では読み込みません。
テストデータベースの作成
config/database.ymltest: <<: *default database: db/test.sqlite3接続可能なデータベースの作成を行います。
$ bin/rails db:create:all
RSpecの設定
続いて、RSpecのインストールを行います。
$ bin/rails generate rspec:install
インストール時に作成された
.rspec
ファイルを以下のように変更すると、RSpecの出力がドキュメント形式になるので、非常に読みやすくなります。.rspec--require spec_helper --format documentationrspec binstubで起動時間の短縮を図る
Gemfilegroup :development do gem 'spring-commands-rspec' end
bundle install
後、新しいbinstubを作成します。
$ bundle exec spring binstub rspec
RSpecを実行してみる
まだテストファイルは一個も作成していませんが、RSpecのインストールを確認する上でも、以下のコードで一度RSpecを起動してみるのがいいと思います。
$ bin/rspec
ジェネレータの設定
今後、アプリを作成していく中で、
$ rails g
コマンドを使用してコードを追加する際に、RSpec用のテストファイルも同時作成できるように設定を変更します。config/application.rbrequire_relative 'boot' require 'rails/all' Bundler.require(*Rails.groups) module Projects class Application < Rails::Application config.load_defaults 5.1 config.generators do |g| g.test_framework :rspec, fixtures: false, view_specs: false, helper_specs: false, routing_specs: false end end end
fixtures: false
テストデータベースにレコードを作成するファイルの作成をスキップ
view_specs: false
ビュースペックを作成しないことを指定
helper_specs: false
ヘルパーファイル用のスペックを作成しないことを指定
routing_specs: false
config/routes.rb
用のスペックファイルの作成を省略まとめ
これでRSpecのテスト実行するためのセットアップが整ったことになります。
色々と設定があって、大変ですね。
本によると、既存のtestディレクトリがあった場合は、rails test
コマンドでテストの有無を確認し、必要ならRSpecへの移行を検討するべき、とのことでした。
あくまで準備段階が終わっただけですが、一苦労でした。
- 投稿日:2021-01-24T02:10:47+09:00
ActiveRecordのwhere notの挙動について
はじめに
普段、RailsはActiveRecord使っているんですが、条件を複数書いたときに、
where.not()の挙動が直感と反しており、なかなかバグに気づかなかったので、共有します。環境
Ruby : 2.7.1
Rails : 6.0.2
Posgresql : 13.1where.notの挙動
まず、where.not()の単数条件で名字が山田以外のuserを取得したい時を考えてみます。
発行されるSQLは以下のようになります。User.where.not(last_name: '山田') => SELECT "users".* FROM "users" WHERE "users"."last_name" != $1 [["last_name", "山田"]]これは期待通りに
last name
が山田でないuserを取得してくれます。では次に「名字が山田」かつ「名前が太郎」以外のuserを取得するとしましょう。
僕の直感では以下のような記述で取得できると思ってましたが...User.where.not(last_name: '山田', first_name: '太郎') => SELECT "users".* FROM "users" WHERE "users"."last_name" != $1 AND "users"."first_name" != $2 [["last_name", "山田"], ["first_name", "太郎"]]上記、SQLを見てみると、「山田ではない」かつ「太郎ではない」というSQLが発行されています。でも欲しかったのは
「名字が山田」かつ「名前が太郎」
ではない userです。集合記号を使うと以下のようにかけます。\overline{ 山田 \cap 太郎} \neq \overline {山田} \cap \overline {太郎}
取得したいuserは赤と青の共通部分(山田太郎)以外の箇所です。
しかし、発行されるSQLで取得しているのは、赤と青以外、すなわち白の部分のuserです。
悲しいかな、「山田孝之」や「麻生太郎」は取得してくれません。解決
では、山田孝之や麻生太郎を取得するにはどうすれば良いかというと、以下のようになります。
User.where.not(last_name: '山田').or(User.where.not(first_name:'太郎')) => SELECT "users".* FROM "users" WHERE ("users"."last_name" != $1 OR "users"."first_name" != $2) [["name", "山田"], ["public_id", "太郎"]]上記は、書き方的には「山田ではない」または「太郎ではない」になります。これはドモルガンの法則を用いて
\overline{A \cap B} = \overline A \cup \overline Bとなることを使っています。A={山田},B={太郎}です。
よって、山田孝之は「山田ではある」ものの「太郎ではない」ので取得することができます。もっとスマートに書きたいんじゃ!と思っておりますので、良い方法あったら教えてくだされー!
- 投稿日:2021-01-24T01:56:40+09:00
【Rails】ルーティングの基礎的な書き方【Ruby】
ルーティングの役割
railsガイド( https://railsguides.jp/routing.html )によると
Railsのルーターは受け取ったURLを認識し、適切なコントローラ内アクションやRackアプリケーションに割り当てます。
とのこと。
「route」を直訳すると「経路」ですから、リクエストの道案内役といったところでしょうか。
基礎的な書き方について記してみたいと思います。
基本の書き方
rootの指定
resourcesを用いた書き方
基本の書き方
HTTPリクエスト 'URIパターン', to: 'コントローラー名#アクション名'
もしくは
HTTPリクエスト 'URIパターン', controller: 'コントローラ名', action: 'アクション名'
Rails.application.routes.draw do get 'profile', to: 'users#show' post 'tweets', controller: 'tweets', action: 'create' endrootの指定
root(ルート)とは、Railsアプリを実行する上で基本となる場所(パス)のことです。
例えばローカル環境にてアプリを実行した時に、「 http://localhost:3000 」のURLを指定した際に遷移するページを設定することができます。
root to: 'コントローラー名#アクション名'
もしくは
root 'pages#main'
Rails.application.routes.draw do root to: 'pages#main' root 'pages#main' endresourcesを用いた書き方
resourcesメソッドを使用すると、
index, new, create, edit, update, show, delete
の七つのアクションへの割り当てを自動的に行うことができます。Rails.application.routes.draw do resources :articles endこちらは、以下の7行のルーティングと同義になります。
get '/articles', to: 'articles#index' get '/articles/:id', to: 'articles#show' get '/articles/new', to: 'articles#new get '/articles/:id/edit', to: 'articles#edit post '/articles', to: 'articles#create patch '/articles/:id', to: 'articles#update' delete '/articles/:id', to: 'articles#destroy'参考文献
https://railsguides.jp/routing.html
https://web-camp.io/magazine/archives/16815
- 投稿日:2021-01-24T01:22:16+09:00
bundle installしてもエラーが続き沼にハマる。(備忘録として)
bundle installしてもエラーが続き沼にハマる。(備忘録として)
rails newしてbundle installすると
Make sure that `gem install #### -v '"#.#.#' --source https:・・・・`のようなエラーが延々と続く。
gemを指定してinstallしてもエラーは解消されず。
ググった情報を頼りにカタカタするも、エラーは解消されず。結局解決方法は以下でした。
1rbenvでrubyを管理する
2xcodeのupdateをする1rbenvでrubyを管理する。
デフォルトのrubyを使っていたが、toolのアップデートかなにかで齟齬が出てしまった?のかもしれない。
標準rubyは一つしか入っていないためrbenvでrubyのバージョンを管理する。
基本的に開発ではrbenvを用いてrubyを管理する。
参考:https://qiita.com/hujuu/items/3d600f2b2384c145ad122xcodeのupdateをする。
これが今回最も遠いようで近いエラーの原因でした。
エラー分には
you have to ....のような文字があったがなんのことやらと無視していた。
しかしそれはtoolをupdateしてくれと言うメッセージでした。
xcodeをupdateしないとだめだった。
bundle installでめっちゃ変更が多くなった。。
3k+!?gitignoreに
/vendor/bundle/*/
vendor配下のbundleを追加してあげることで解決
- 投稿日:2021-01-24T01:06:29+09:00
パーシャルで繰り返し処理
教えていただいたことの備忘録として残します。
bar.html.erb<%= render partial: 'foo', hoges: @hoges %>_foo.html.erb<% hoges.each do |hoge| %> <% hoge.name %> <% hoge.price %> <% end %>これでも動作確認はできましたが、以下のようにします。
bar.html.erb<%= render partial: 'foo', collection: @hoges, as: 'hoge' %>_foo.html.erb<% hoge.name %> <% hoge.price %>こうすることで読み込み速度が向上します。
collectionオプションを使うと1回しか読み込まれないみたいです。
- 投稿日:2021-01-24T00:13:18+09:00
railsで簡単ログイン(ゲストユーザーログイン機能)を実装する方法
今回はポートフォリオを見てもらう確率を上げるために必須な、簡単ログインの実装方法を書いていきます。
開発環境
Mac OS Catalina 10.15.7
ruby 2.6系
rails 6.0系前提
devise導入済み
通常の新規登録、ログイン、ログアウト機能に関しては実装済み
投稿機能ができるアプリを作っており、postsモデルとpostsコントローラーが登場しますが、ご自身の例に置き換えてご理解ください。ルーティング設定
まずはルーティングを設定します。
# HTTPメソッドはpostで、'/posts/guest_sign_in'というURLでpostsコントローラーのnew_guestアクションを参照する post '/posts/guest_sign_in', to: 'posts#new_guest'routes.rbRails.application.routes.draw do devise_for :users root to: "posts#index" # 次の1行を追加 post '/posts/guest_sign_in', to: 'posts#new_guest' resources :posts do resources :comments, only: [:create, :destroy] collection do get 'search' end end resources :users, only: :show post 'like/:id' => 'likes#create', as: 'create_like' delete 'like/:id' => 'likes#destroy', as: 'destroy_like' endコントローラーにnew_guestアクションを記述
それでは次にpostsコントローラーにアクションを定義していきます。
deviseのメソッド find_or_create_by や sign_in を使っていますので、気になる方は調べてみてください。
また、パスワードは SecureRandom.alphanumeric を用いてランダム生成しつつも、全て英字、全て数字のパスワードが生成されないように工夫しています。参考:https://qiita.com/take95/items/61b181449b38d4415fc3
posts_controller.rbdef new_guest # emailでユーザーが見つからなければ作ってくれるという便利なメソッド user = User.find_or_create_by(email: 'guest@example.com') do |user| # 自分はユーザー登録時にニックネームを必須にしているのでこの記述が必要 user.nickname = "ゲスト" # 英数字混合を必須にしているので、ランダムパスワードに、英字と数字を追加してバリデーションに引っかからないようにしています。 user.password = SecureRandom.alphanumeric(10) + [*'a'..'z'].sample(1).join + [*'0'..'9'].sample(1).join end # sign_inはログイン状態にするデバイスのメソッド、userは3行目の変数userです。 sign_in user # ログイン後root_pathに飛ぶようにしました。 redirect_to root_path endまた、deviseでメール確認機能を実装済みの場合、user.confirmed_at = Time.nowを追加する必要があります。
posts.controller.rbdef new_guest user = User.find_or_create_by(email: 'guest@example.com') do |user| user.nickname = "ゲスト" user.password = SecureRandom.alphanumeric(10) + [*'a'..'z'].sample(1).join + [*'0'..'9'].sample(1).join # 以下一文を追加 user.confirmed_at = Time.now end sign_in user redirect_to root_path endビューを追加
簡単ログインのリンクをヘッダーに追加します。
自分の場合は部分テンプレートに切り出しているので、その部分テンプレート内の記述を編集します。コンソールでrails routesを打ってパスを確認しましょう。その後ヘッダーに次の1文を追加します。
<li><%= link_to 'ゲストログイン(閲覧用)',posts_guest_sign_in_path, class: "guest-login", method: :post %></li>この時、HTTPメソッドをつけ忘れないように注意してください
_header.html.erb<nav> <ul class='lists-right'> <% if user_signed_in? %> <li><%= link_to current_user.nickname, user_path(current_user.id), class: "user-nickname" %></li> <li><%= link_to 'ログアウト', destroy_user_session_path, method: :delete, class: "logout" %></li> <% else %> # 以下1文を追加 <li><%= link_to 'ゲストログイン(閲覧用)',posts_guest_sign_in_path, class: "guest-login", method: :post %></li> <li><%= link_to 'ログイン',new_user_session_path, class: "header-login" %></li> <li><%= link_to '新規登録',new_user_registration_path, class: "sign-up" %></li> <% end %> </ul> </nav>以上で簡単ログイン(ゲストユーザーログイン機能)を実装できました。
参考になれば幸いです。