20210124のRubyに関する記事は28件です。

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
  }
end

serialize_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
end

https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activejob/lib/active_job/arguments.rb#L99

ActiveRecordのインスタンスの場合は、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
end

https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activejob/lib/active_job/arguments.rb#L126

def deserialize_global_id(hash)
  GlobalID::Locator.locate hash[GLOBALID_KEY]
end

https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activejob/lib/active_job/arguments.rb#L151

def locate(gid, options = {})
  if gid = GlobalID.parse(gid)
    locator_for(gid).locate gid if find_allowed?(gid.model_class, options[:only])
  end
end

https://github.com/rails/globalid/blob/master/lib/global_id/locator.rb#L15

ちょっと省略するが、結局下記のコードにいきついて、findしている

def locate(gid)
  gid.model_class.find gid.model_id
end

https://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
  1. send_message!を実行
  2. complete!メソッドでmessageステータスをcompleteステータスに変更(この時点ではDBに変更が反映されていない)
  3. update_message_log!でログテーブルを更新
  4. MessagePushJob.perform_laterで非同期ジョブをキューする
  5. 非同期ジョブが実行される(この時点でも、まだDBに変更が反映されていない
  6. give_point!で送信者のポイントを更新
  7. トランザクションが完了したので、ここで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を渡すように変更してもいいのかもしれない

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
という解釈になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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?メソッドも紹介されています。

参考:https://techacademy.jp/magazine/20214

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActionMailerを使用した問い合わせ機能の実装

実装機能

問い合わせフォームからメールを送信、それを指定したメールアドレスで受信する。
(アプリの利用者から管理者に向けてのメール機能として実装)

ActionMailerでは他にもメルマガ配信のように運営側からユーザーに対してメール送信を行ったり、問い合わせに対するメールの返信機能なども実装が可能とのことだが、今回は一番シンプルなこちらの機能を実装。
またGmailアカウントでのメールの受信方法の為、前提としてGmailのアカウントを持っている体で話を進めていく。

完成イメージ

スクリーンショット 2021-01-24 18.46.45.png

実装内容

1.コントローラの作成

使用するビューファイルはnewのみな上にルーティングの修正が面倒な為、アクションの指定はせずにコントローラファイル単体で作成。

$ rails g controller inquiries

2.ルーティングの作成

今回使用するアクションは以下の2つ
・問い合わせ作成画面に使用するnewアクション
・問い合わせのデータを作成する為のcreateアクションのみ

config/routes.rb
resources :inquiries, only[:new, :create] 

3.モデルの作成

最低限必要な情報としてユーザーの名前とメッセージ内容を保存するカラムを用意するが、今回は送信したユーザーが問い合わせに関して返答を要求する場面を想定し、emailのカラムも作成しておく。

$ rails g model Inquiry name:string email:string message:text

マイグレーションファイルに特に変更がなければ bundle installを実行してテーブルを作成。

その後必要なカラムにバリデーションをかけておく。

app/models/inquiry
class Inquiry < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
  validates :message, presence: true
end

4.Mailerの作成

ActionMailerという機能がRailsに標準搭載されており、下記のコマンドで作成が出来る

terminal
$ rails g mailer inquiry 

以下のファイルが作成されるのを確認

terminal
create  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.rb
class InquiryMailer < ApplicationMailer

  def send_mail(inquiry)
    @inquiry = inquiry
    mail to: 'メールアドレス', subject: '【サイト名】お問い合わせ通知'
  end
end

上記のmailメソッドで指定されているプロパティの内容は以下の通り
to = 送信先(メールアドレス)の指定
subject = メールの件名

要はここで指定したアドレスから指定した件名でメールが届くということなる。

6.コントローラにメソッドを定義

app/controllers/inquiries_controller.rb
class 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.rb
  config.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形式)

スクリーンショット 2021-01-24 21.22.33.png

10.環境変数を使用

Githubにpushしたり、本番環境でアプリを使用する際はこれまでに使用したメールアドレスやパスワードはセキュリティを考慮し環境変数化しておく必要がある。
gemのdotenv-railsをinstallしている前提での説明な為、gemをインストールしていない場合は導入する。

.envファイルに実際のメールアドレスやパスワードを記述

.env
SEND_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認証のエラーが出た時にした事。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

特定のディレクトリ内のファイルを一括で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.rb
Dir[File.expand_path("../user_type", __FILE__) << "/*.rb"].each do |file|
require file
end

1.[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.rbuser_b.rbを文字列として配列に入れることになります。
その後は、この配列をeach文でそれぞれrequireする処理を実行しています。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Rails5.2 リッチテキストエディタsummernote-railsのツールバーアイコンの表示・非表示を切り替える

gem 'summernote-rails'
https://github.com/summernote/summernote-rails

summernote-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"]]
             ]

4bddbabf5f7dd3fbd8dff2fb73afe709.png

jsのオプションに、toolbar:[]部分の記述を足して、表示したくないアイコンに対応したハッシュの値を消せばアイコンが消える。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ができます!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.rb
  class 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
  end
app/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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsにおけるSQLインジェクション対策について - Rails Tutorial リスト13.46

はじめに

Rails Tutorialを進めていく中で気になったところを記事にして残してます。

記事に間違いがある場合は教えてください:grin:

用語解説

SQLインジェクションとは

SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。

Rails セキュリティガイド - Railsガイド

つまり、ログインフォームや投稿フォームなどで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文を書くときには直接文字列を代入するのではなく、必ずエスケープ処理をするようにしましょう。

エスケープ処理には以下の方法があります。

  1. 配列、ハッシュとして渡す。(モデルのインスタンスのみ)
  2. 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 を削除していたので、思い込みがあったのも問題解決がすぐに出来なかった原因でした。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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_method
Baz = 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.method1

includeの説明で使ったコードを少しだけ複雑に見せ掛けてみました。この実行結果はどうなるでしょうか?

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とインクルードしているモジュールの優先順を配列で返却。

最後に

ここまでの長文をお読みいただき、ありがとうございます。
これで、一通りの継承関係を解説致しました。

詳細部分をもう少し噛み砕いたものをつくりたかったのですが、私自身が今週末に資格試験を控えておりますので、もう少し掘り下げた内容は受験後に改めて編集しようと考えております。少しでもお役にたてれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

https://github.com/geokit/geokit-rails/blob/cc5fd43ab4e69878fb31ebd1fc22918e2952b560/lib/geokit-rails/acts_as_mappable.rb#L213

デフォルトはGeokit::default_formulaが使われている

self.default_formula = options[:default_formula] || Geokit::default_formula

https://github.com/geokit/geokit-rails/blob/cc5fd43ab4e69878fb31ebd1fc22918e2952b560/lib/geokit-rails/acts_as_mappable.rb#L37

Geokit::default_formulaの値は:sphereが設定されている

Geokit::default_formula = :sphere

https://github.com/geokit/geokit-rails/blob/93b7153ce73a21ec139cca056c7250e0211f28f2/lib/generators/templates/geokit_config.rb#L3

では、sphere_distance_sqlとflat_distance_sqlの違いは何か??

大円距離(sphere_distance_sql)

スクリーンショット 2020-08-17 15.56.14.png

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

https://github.com/geokit/geokit-rails/blob/93b7153ce73a21ec139cca056c7250e0211f28f2/lib/geokit-rails/adapters/mysql.rb#L5

ピタゴラスの定理(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

https://github.com/geokit/geokit-rails/blob/93b7153ce73a21ec139cca056c7250e0211f28f2/lib/geokit-rails/adapters/mysql.rb#L13

次に、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);
}

https://github.com/elastic/elasticsearch/blob/237650e9c054149fd08213b38a81a3666c1868e5/server/src/main/java/org/elasticsearch/common/geo/GeoDistance.java#L73

大円距離の方も見ていくと

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);
}

https://github.com/elastic/elasticsearch/blob/a01d2bd24b0e849af034b88b563897a692a87bd8/server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java#L603

public static double haversinMeters(double lat1, double lon1, double lat2, double lon2) {
  return haversinMeters(haversinSortKey(lat1, lon1, lat2, lon2));
}

https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/util/SloppyMath.java#L50

public static double haversinMeters(double sortKey) {
  return TO_METERS * 2 * asin(Math.min(1, Math.sqrt(sortKey * 0.5)));
}

https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/util/SloppyMath.java#L59

ぱっと見、geokitのsphereの計算式と違うように見える、、時間ある時に調べよ、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bundle exec

bundle exec をつける時

bundle execをつけずにrailsコマンドを叩くと、デフォルトのrailsが動く
bundle execをつけると、そのプロジェクトで管理している(rbenvなどのバージョンを管理してくれるもの)rails が動く

bundlerとは
そのプロジェクトのgemを管理しているよ
bundle install をするとgemfileの中身が読み込まれる

参考:https://qiita.com/d0ne1s/items/fa2dafcee02e963fe997

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

投稿テストついでにRubyの継承関係まとめ

Rubyの継承関係を、Github pagesでマークダウンを書く練習がてらまとめてみた。(今週末試験なのに何やってるんだろう?)
リンクがうまく貼れているかドキドキしながら、スマホから投稿してみる( ˙ω˙)و グッ!
継承関係まとめ

先生が言っていた、「特異クラスは"singleton class"(1枚札)より相応しい呼び方があるんだけど、何だったかなぁ?」の答えはきっと
"eigenclass"(固有の)だろうな、という書き方をしてしまった( ˊᵕˋ ;)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.keys
Taro
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.values
185
170
165
183

一つのバリューを取り出すとき

#height3 = {:Taro=>185, :Jiro=>170, :Kohei=>165, :Hiro=>183}
puts height3.values[0]
185
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rspecでテストを作成する時、毎回 require 'rails_helper' と書くのを省略する

環境

Mac OS X
Ruby: 2.7.1
Rails: 6.0.3.4

課題

RSpecでテストを作成する時、何も設定しないと以下のように記述すると思う

model_spec.rb
require 'rails_helper'
Rspec.describe Model, type: :model do

~以下テスト~

しかしrequire 'rails_helper'と毎回記述するのは面倒くさいので省略できないか調査した

解決法

.rspecに以下のコードを追加する

.rspec
--require rails_helper

結果

以下のようにしてもテストが動き、コードを削減することができた

model_spec.rb
Rspec.describe Model, type: :model do

~以下テスト~

参考記事

https://qiita.com/yuta-ushijima/items/ffb34823b8bba2180c94

最後に

お読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rspecでテストを作成する時、各ファイル毎に require 'rails_helper' と書くのを省略する

環境

Mac OS X
Ruby: 2.7.1
Rails: 6.0.3.4

課題

RSpecでテストを作成する時、何も設定しないと以下のように記述すると思う

model_spec.rb
require 'rails_helper'
Rspec.describe Model, type: :model do

~以下テスト~

しかしrequire 'rails_helper'と毎回記述するのは面倒くさいので省略できないか調査した

解決法

.rspecに以下のコードを追加する

.rspec
--require rails_helper

結果

以下のようにしてもテストが動き、コードを削減することができた

model_spec.rb
Rspec.describe Model, type: :model do

~以下テスト~

参考記事

https://qiita.com/yuta-ushijima/items/ffb34823b8bba2180c94

最後に

お読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参照サイト
https://www.sejuku.net/blog/14168

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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」+ 「¥」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails + jQuery]初学者向けAjaxを取り敢えず飛ばしてみたい

課題

初学者が感覚掴むために取り敢えずAjax飛ばしてみたい。

例として以下フォームで入力された文字をサーバに送ってみる。

form.erb
<%= text_field_tag :myname %>
output
<input type="text" name="myname" id="myname">

スクリーンショット 2021-01-24 11.18.12.png

結論

以下コードで取り敢えず飛ぶ。

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】小数点以下を表示する方法 .to_f

背景

平均値を小数以下まで表示させたいシュチュエーションがあったので、調べてみました。

10÷3を小数点第3位まで出したい時

a = 10
b = 3
c = a.to_f / b.to_f

puts c.round(3)

=> 3.333

.to_f

文字列を10進数と解釈して、浮動小数点数Floatに変換するメソッドです。

.round()

引数に少数点の桁数を指定することができ、四捨五入して表示されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyにおける「クラス」と「オブジェクト」の関係について分かりやすく解説してみる

Rubyの勉強を始めて、すぐに出てくる重要キーワード。それは「クラス」「オブジェクト」です。

Rubyにおいて重要な概念だと分かってるけど、なんとなくの理解で学習が進んでいってしまうことも多いのではないでしょうか。


  • クラスはよく使うけど理解が浅い

  • 「クラス」と「オブジェクト」の関係がいまいち掴めない

  • どうやって「オブジェクト」を作れば良いかわからない



今日はこのような方に向けて、記事を書いていこうと思います。

「クラス」と「オブジェクト」の関係について

クラスとは「もの」を作るときの「設計図」です。

オブジェクトとは設計図をもとに作られる「もの」です。

クラスとオブジェクトの関係を図に表すと以下になります。

Image from Gyazo

クラスが「人間」
オブジェクトが「たろう」「花子」「Michael」です。

クラスである「人間」には(名前,出身)という設計図が描かれています。

この設計図を使って、「名前:たろう,出身:東京」のオブジェクトを作っています。


つまり、
設計図である「クラス」をもとに、ものである「オブジェクト」を作ることができます。これが「クラス」と「オブジェクト」の関係です。

ここで、たろうに性別や年齢を加えたい場合は、人間クラスを(名前,出身,性別,年齢)という設計図にすればいいわけです。

クラスの作り方

ここからは、実際にコード書いてクラスとオブジェクトの理解をさらに深めていきましょう。


まずクラスの作り方についてです。試しにUserクラスを作ってみましょう。user.rbというファイルを作成して、以下のように記述します。

class User 
end

これだけでUserクラスの完成です。簡単!

オブジェクトの作り方

irb(書いたプログラムを実行できるコマンド)を開いて、先ほどのUserクラスを使って、オブジェクトを作っていきます。

> require './user.rb' #user.rbのコードを読み込むコマンド
>Michael = User.new

User.newを使って、Michaelというオブジェクトを作ることができました。これまた簡単!

オブジェクトから、原型となるクラスを確認する方法

全てのオブジェクトはその元となるクラスを持っています。つまりMichaelはUserクラスを持っているはず。それが本当かどうか確認します。

>Michael.class
=>User

classというメソッドを使って、オブジェクトが持つクラスを確認することができます。エラーが出たとき「このオブジェクト、何のクラスを持ってるんだっけ?」となりがちなので覚えておきましょう。

(補足)いまはUserクラスに設計図(名前,出身など)が何も書かれていません。今回の記事は「クラス」と「オブジェクト」の関係について理解する記事なので、設計図の書き方は省略です。また記事にしたいと思います。

まとめ

「クラス」と「オブジェクト」の関係について理解が深まったでしょうか。なんとなくの理解でプログラミング学習を進めてしまいがちですが、少しずつ理解を深めながら、Ruby勉強を進めていきましょう。

この記事の説明がわかりやすかった!ここ間違ってるよ!次こんな記事を書いて欲しい!などあればコメント、DMよろしくお願いします。LGTMもぜひ。


Twitterもやってますので、フォローしていただけたらうれしいです。
卓球、心理学、哲学、Webサービス、好きな音楽、カメラ、登山、ランニング、読んだ本などなんでもつぶやいてます。

[https://twitter.com/atsushi101011]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(備忘録)Everyday Rails 第2章「RSpecのセットアップ」

「Everyday Rails - RSpecによるRailsテスト入門」

Railsを使用したアプリケーションを作成したものの、テストの記述が全然分からない、という状態になったので、RSpecの勉強をできる教材を探していたところ、なんとプロを目指す人のためのRuby入門 (チェリー本)の著者が翻訳を手がけた本を発見しました。
こちらのサイトから購入できます!
販売方法はなんと、購入金額を自由に設定できるという、画期的なシステムでした(最低購入価格は$19からですが)。
先日、なんとか1周は終わらせたのですが、全然理解できていない部分が多いので、復習がてらアウトプットしていきたいと思います!
image.png

第1章 「イントロダクション」

第1章はイントロダクションということで、この本のアウトラインや著者の想いなどが書かれています。

著者の思い

その中で、著者の考える基本的な信条というものがあり、

・テストは信頼できるものであること
・テストは簡単に書けること
・テストは簡単に理解できること(今日も将来も)

と書かれており、また

・とはいえ結局、一番大事なことはテストが存在することです。信頼性が高く、理解しやすいテストが書いてあることが大事な出発点になります。

とありました。
テストはどちらかと言うと、とっつきにくいイメージがあったので、学習することから逃げていた節もあったのですが、心を入れ替えてしっかりと取り入れていきたいと思います!

サンプルコードについて

GitHubからサンプルコードを入手できます。

第2章 「RSpecのセットアップ」

早速、本に従って進めていきたいところですが、いきなり、「テストスイート」という聞いたこともない単語に出くわしました。
スルーしてもいいのですが、最初が肝心なので、調べてみました。
テストスイートとは、

ソフトウェアテストの目的や対象ごとに複数のテストケースをまとめたもの。自動化テストにおいては、テストの実行単位となる。
 ソフトウェアテスト(動的テスト)の最小単位はテストケースといえるが、実際にテスト作業ではいくつものテストケースを組み合わせることによって不具合をあぶり出したり、確率的に十分なテストを行ったという結論を出したりする。このようなテスト目的やテスト対象に応じて、多数のテストケースを束ねたものをテストスイートという。

らしいです。
僕は知りませんでしたので、勉強になりました。

Gemfileのインストール

まずはRSpecをインストールする必要があります。

Gemfile
group :development, :test do
  gem 'rspec-rails', '~> 3.6.0'
end

開発環境とテスト環境の両方で、rspec-railsを読み込みますが、本番環境では読み込みません。

テストデータベースの作成

config/database.yml
test:
  <<: *default
  database: db/test.sqlite3

接続可能なデータベースの作成を行います。
$ bin/rails db:create:all

RSpecの設定

続いて、RSpecのインストールを行います。
$ bin/rails generate rspec:install

インストール時に作成された.rspecファイルを以下のように変更すると、RSpecの出力がドキュメント形式になるので、非常に読みやすくなります。

.rspec
--require spec_helper
--format documentation

rspec binstubで起動時間の短縮を図る

Gemfile
group :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.rb
require_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への移行を検討するべき、とのことでした。
あくまで準備段階が終わっただけですが、一苦労でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecordのwhere notの挙動について

はじめに

普段、RailsはActiveRecord使っているんですが、条件を複数書いたときに、
where.not()の挙動が直感と反しており、なかなかバグに気づかなかったので、共有します。

環境

Ruby : 2.7.1
Rails : 6.0.2
Posgresql : 13.1

where.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 {太郎}

スクリーンショット 2021-01-24 1.37.55.png
取得したい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={太郎}です。
よって、山田孝之は「山田ではある」ものの「太郎ではない」ので取得することができます。

もっとスマートに書きたいんじゃ!と思っておりますので、良い方法あったら教えてくだされー!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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'
end

rootの指定

root(ルート)とは、Railsアプリを実行する上で基本となる場所(パス)のことです。
例えばローカル環境にてアプリを実行した時に、「 http://localhost:3000 」のURLを指定した際に遷移するページを設定することができます。
root to: 'コントローラー名#アクション名'
もしくは
root 'pages#main'

Rails.application.routes.draw do
  root to: 'pages#main'
  root 'pages#main'
end

resourcesを用いた書き方

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/3d600f2b2384c145ad12

2xcodeのupdateをする。

これが今回最も遠いようで近いエラーの原因でした。
エラー分には
you have to ....のような文字があったがなんのことやらと無視していた。
しかしそれはtoolをupdateしてくれと言うメッセージでした。
xcodeをupdateしないとだめだった。


bundle installでめっちゃ変更が多くなった。。
3k+!?

gitignoreに
/vendor/bundle/*/
vendor配下のbundleを追加してあげることで解決

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パーシャルで繰り返し処理

教えていただいたことの備忘録として残します。

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回しか読み込まれないみたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.rb
Rails.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.rb
def 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.rb
def 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>

以上で簡単ログイン(ゲストユーザーログイン機能)を実装できました。
参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む