Rails Diary

プログラミングの学習記録です。

Ruby学習まとめ

自販機をシミュレートしたコードを問題点を完全しながら、オブジェクト指向についての考え方を学ぶ。

名前は省略しない

引数にはiとkind_of_drinkが記載されており、このままだとiが何を指すのか分からない。

def buy(i, kind_of_drink)
  if i != 100 && i != 500
  @change += i
  return nil
end

名前は省略せずに、誰が見ても分かるようにする

def buy(payment, kind_of_drink)
  if payment != 100 && payment != 500
  @change += payment
  return nil
end

ValueObject

intやstringと言ったプリミティブ型(基本的な型)をラップする。

意味のある値に対して型を定義すること。

改善前のコードでは自販機クラスbuyメソッドに、支払額として整数の数値をそのままと、Drinkクラスに直接記載されたドリンクの種類を渡していた(引数)。これを、それぞれCoinクラスとDrinkTypeクラスに分けて、enum型として定義しなおす。

一見面倒にしか思えなけれど、下記のメリットがある。

  • コードの可読性を高める
  • 変更に強い
  • 拡張しやすい

ValueObjectという考え方 - Qiita

https://codezine.jp/article/detail/10184

ファーストクラスコレクション

Rubyで作るファーストクラスコレクション - Qiita

配列をラップしたクラスのことで、対象の配列に対する処理を全て集約したクラスになります。別名コレクションオブジェクトとも呼ばれるそうです。

メリット
  • 配列の複雑さを専用の小さなクラスに集約できる
  • これにより対象の配列に関する処理がプログラムのあちこちに散らばることなく管理ができる
  • メンテナンス性の向上やプログラム自体の分かりやすさにつながる

GetterとSetter、プロパティを使用しない

GetterとSetter

【Ruby】「ゲッター」と「セッター」を理解する - Qiita

インスタンス変数そのものはクラス内からしか取得できない。これを外部でも取得できるように定義したのがゲッター。セッターも同様、更新はクラス内からしかできないため、外部から更新できるようにするためクラス内部に更新用のメソッドを定義する必要がある。

アクセスメソッド

ゲッター、セッターを定義しなくてもインスタンス変数の参照・更新を可能にする方法。

  • attr_reader: 変数名…参照を可能にする。ゲッター。
  • attr_writer: 変数名…更新を可能にする。セッター。
  • attr_accessor: 変数名…参照・更新の両方を可能にする。セッターとゲッターを一気に使える。

  • オブジェクト内部状態を使用した処理を実現したい場合は、オブジェクト内で処理し、その結果だけを返すようにする。

まとめ

  • これらはオブジェクト指向カプセル化を壊す不適切なメソッドになりやすい
  • オブジェクト内部状態を使用した処理を実現したい場合は、オブジェクト内で処理し、その結果だけを返すようにする。(処理結果を返すメソッドをクラス内に定義すると言うこと)
  • これにより、オブジェクトが管理する内容が変わっても、他のクラスやメソッドに影響することなく、クラスを変更することができる!

一つのクラスにつき、インスタンス変数は二つまで

※ あくまでもオブジェクト指向エクササイズの規約内容ではインスタンス変数を二つに絞るように書かれているが、同じ役割を一つのクラスにまとめることを考えること。

反対に、一つのクラスが複数の役割を担っている場合は、枠割ごとにクラスを分割することも検討する。

else句を使用しない

現状、自販機クラスのbuyメソッド内には「ジュース一本100円の時、投入された硬貨が100円ならば、お釣り管理オブジェクトに支払額を入れる。投入額が500円ならばお釣り管理から400円分のお釣りを取り出す処理をして、お釣り管理の在庫内を更新する。」みたいな処理が一緒くたに記載されている。

非効率で分かりづらいコード。

お釣り管理(coinmech)に対して、お支払額(payment)を渡すことで、後の内部処理はお釣り管理でするして、その結果のみ返すようにすれば、もっと見通しよく簡潔なコードが書ける。

「else句を使用しない」と言うのはそう言うこと。

関連が強いもの同士を同じパッケージにまとめる

お金関連ならmoneyディレクトリに、在庫関連なら在庫ディレクトリにまとめてファイルを見通し良く使おう。

アルゴリズムメモ

組み込み変数ARGV(argument vector)

【Rubyの基礎】コマンドラインからの実行で利用するARGVの基本 | ポテパンスタイル

Rubyプログラムを実行する際に、引数として指定したオプションをプログラムの中で利用するための仕組み。argumentは引数のことで、ARGVはプログラムを実行した際に指定された引数を格納する配列。

# sample.rb
arg = ARGV[0].to_i
$ sample.rb 9
  • sample.rbファイルをターミナルで実行。その際、引数として適当な数字を渡す
  • 渡される引数はstring型になっているので、もし数値そして使いたいのであればto_iメソッドで変換する
times, upto, downtoメソッド

timesメソッド、uptoメソッド、downtoメソッド - 繰り返し - Ruby入門

いずれもIntegerクラスで用意されている

upto

# sample2.rb
arg = ARGV[0].to_i

1.upto(arg) do |num|
  p num
end
$ ruby sample2.rb 4

#=>
1
2
3
4
  • uptoメソッドはx.upto(y)のように使用した場合に、xからyまでの数値を順にブロック内に代入していることがわかる

MySQLメモ

M1 Mac MySQLのインストール

課題に際してbundle installしようとするとエラーが出て上手くいかなかったため、調べた自分用メモです。間違った記載が多くあると思われるので、あまり参考にしない方が良いかも知れないです。

※ 記事を参考にコマンド実行する前に記事の鮮度・信頼性、本当に自分と同じ環境なのか考えてから実行するようにすること。

bundle install時に下記エラーが出ました。

34 warnings generated.
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

Gem files will remain installed in
/Users/ユーザー名.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/mysql2-0.5.3 for
inspection.
Results logged to
/Users/ユーザー名/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/extensions/-darwin-20/2.6.0/mysql2-0.5.3/gem_make.out

An error occurred while installing mysql2 (0.5.3), and Bundler cannot
continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'`
succeeds before bundling.

An error occurred while installing mysql2 (0.5.3), M1 Mac

↑こちらのブログを参考にインストールすることができました。

どうやら、デフォルトで使われているSSLに互換性がないためgem installで探し出すことができないとかなんとか・・

試したこと

  1. 互換性のあるopensslというものに変更
  2. シェルにopensslのパスを通して、反映させる
  3. bundle configのパスを変更
  4. ライブラリのパスを変更

  5. 使用されているsslを確認

$ openssl version
LibreSSL 2.8.3
  • opensslのパス確認
$ brew --prefix openssl

bundle installがmysql2でコケるときに試すやつ

$ brew install openssl
$ echo 'export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc
$ bundle config --local build.mysql2 "--with-ldflags=-L/opt/homebrew/opt/openssl@3/lib"
$ export LIBRARY_PATH=$LIBRARY_PATH:/opt/homebrew/Cellar/zstd/1.5.2/lib:/opt/homebrew/Cellar/openssl@3/1.1.1m/lib/

これでbundle installできるようにはなったけど、正直良く分かってない。。🥲

OpenSSLとは

インターネット上で標準的に利用される暗号通信プロトコルであるSSLおよびTLSの機能を実装したプログラムの一つ。他のソフトウェアに組み込んで使用するライブラリ(プログラム部品)となっており、オープンソースソフトウェアとして公開されている。OpenSSLとは - IT用語辞典 e-Words

まずSSL(Secure Sockets Layer...『安全な受け口』層)とは

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典より

通信プロトコル(通信する際に双方で足並みを揃えるための約束事)の一つ。
インターネット上でやり取りする情報を暗号化して送受信するための仕組み。

SSLは、公開鍵暗号方式で共通鍵の受け渡しを行い、あとは受け渡した共通鍵を使って共通鍵暗号方式(秘密鍵暗号方式)でやり取りすることで、通信の安全性を高める仕組みです。

公開鍵暗号方式ってどんなもの?

  1. クライアントがサーバに挨拶する。
    🙋🏻‍♂️ < Hi! 🤖

  2. サーバはクライアントにサーバ証明書(自己紹介)を送る。このサーバ証明書の中にはサーバの公開鍵(「これを使って暗号化して下さい」の鍵)が入っている。
    💁🏻‍♂️ サーバ証明書📝🗝送る >🤖

  3. クライアントはサーバ証明書を送ってきたサーバが本物か確かめるために、認証局に問い合わせる。🏢< 本物です。 アレは本物ですか? >🤷🏻‍♂️ 🤖

  4. サーバが本物だった場合、サーバと秘密の会話をするためのルールである共通鍵を作成し、先ほど受け取ったサーバの公開鍵に従って共通鍵を暗号化する。
    📄+🗝🙆🏻‍♂️(暗号化) 🔐✨

  5. 暗号化された共通鍵(暗号化された共通ルール)をサーバに送る。
    💁🏻‍♂️< 共通鍵渡すね 🔐🤖どうも

  6. サーバは、暗号化を解除する鍵であるサーバの秘密鍵を使って、暗号化された共通鍵を復号する。 🤖🔑 🔓✨(復号)

これにより、クライアントもサーバもやり取りするための共通鍵(ルール)を手に入れた。あとはこのルール(共通鍵)を使ってやり取りすることで第三者にやり取りを盗み見られたとしても、暗号化されているので何が書いてあるのか分からない。これが共通暗号方式(秘密鍵暗号方式)でやり取りをするということ。
🙆🏻‍♂️🔐✨⇄🤖🔐✨ ❌👿(くそぉ...)

TLSとは

SSLが1.0→2.0→3.0までバージョンアップし、次のバージョンアップでSSL4.0になると思いきや「TLS1.0」になったらしい。SSLの後継。(厳密には違うらしい)

このあとrails db:createしようとするも上手くいかない

これに関してはusernameとpasswordが記載されていないことが原因でした。

各自の環境に合わせてusernameとpasswordを修正するように指示があったので、初期設定のrootログインをしてみることに。

MySQLのデフォルトパスワードを確認する - Qiita

$ mysql -u root
パスワードを設定する
mysql> set password for root@localhost='新しいパスワード';
Query OK, 0 rows affected (0.00 sec)
MySQLにログインする
$ mysql -u root -p

よく使うMySQLコマンド&構文集 - Qiita

メール機能のRSpecメモ

テスト環境でもメイラーのメソッドが使える?

require "rails_helper"

RSpec.describe ArticleMailer, type: :mailer do
  let(:article_publish_wait_tomorrow) { create(:article, :article_publish_wait_tomorrow) }
  let(:article_published_yesterday) { create(:article, :article_published_yesterday) }
  let(:mail) { ArticleMailer.report_summary.deliver_now }
  let(:check_sent_mail) {
    expect(mail.present?).to be_truthy, 'メールが送信されていません'
    expect(mail.to).to eq(['admin@example.com']), 'メールの送信先が正しくありません'
    expect(mail.subject).to eq('公開済記事の集計結果'), 'メールのタイトルが正しくありません'
  }
  • テスト環境でもArticleMailer.r{作成したメール送信メソッド}.deliver_nowでメール送信ができるので、letに入れておく
  • check_sent_mail内で上で定義したmailが呼ばれるので、「メールが送信されたかどうか」「宛先」「タイトル」の検証ができる
check_sent_mail内
expect(mail.present?).to be_truthy
expect(mail.to).to eq(['admin@example.com'])
expect(mail.subject).to eq('公開済記事の集計結果')
  • 戻り値がtrue/falseのメソッドはbe_truthy/be_falseyで検証できる
  • mail.toで送信先
  • mail.subjectでタイトル
  • mail.toのeqは[]で囲まないとエラーになる

スペック内容

describe '公開済記事の集計結果通知メールの送信' do
  context '昨日までに公開された記事が存在しない場合' do
    it '昨日までに公開された記事がない旨の結果が送られること' do
      article_publish_wait_tomorrow
      check_sent_mail
      expect(mail.body).to match('0'), '公開済記事数の件数取得結果が正しくありません'
      expect(mail.body).to match('公開済の記事数: 0件'), '公開済記事数の送信フォーマットが正しくありません'
      expect(mail.body).to match('昨日公開された記事はありません'), '昨日公開された記事の件数取得結果が正しくありません'
      expect(mail.body).not_to match('タイトル: ' + article_publish_wait_tomorrow.title), '公開されていない記事のタイトルを取得しています'
      end
    end
  • 昨日公開記事がない場合のテストがしたいため、article_publish_wait_tomorrow(明日公開待ちの記事) をテストデータとして作成・投入する
  • 最初に定義したcheck_sent_mailを呼び出して、メール送信と送信先・件名を検証する
expect(mail.body).to match('0'), '公開済記事数の件数取得結果が正しくありません'
expect(mail.body).to match('公開済の記事数: 0件'), '公開済記事数の送信フォーマットが正しくあり
  • 最初この二行の意味が分からなかったけれど、一行目で公開済記事数として取得した件数が合っているかを検証、二行目でそれを表示するフォーマットが正しいかをチェックしている

参考にしたサイト

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita

RSpecでActionMailerのテスト - 技術メモ

mailer spec - Mailer specs - RSpec Rails - RSpec - Relish

Rakeタスクとwhenever ※ごちゃごちゃメモ

RubyのMakeコマンド = Rake

■ Makeコマンドとは

あるソースファイルから目的のファイルを生成するためのコマンド。

使い方はmakefileというファイルに、ソースファイル名と目的のファイルの生成方法を記述しておき、$ makeとターミナルに入力するだけで、ファイル内のコードが実行され、目的のファイルが作成できるというもの。それのRuby版がRake。

コマンド「make」初心者向けメモ(Hishidama's make-command Memo)

■ Rakeコマンド

Rakeにおいても、まずRailsで定期的に実行したい処理Rakeタスクとして定義しておき、必要な時にコマンド実行することでファイル内に記載した処理が実行される。

■ 自動化

wheneverというgemを使うことで、わざわざ手動でコマンド実行しなくても指定した時間間隔で自動的にRakeコマンドを実行させることができる。

使用例

  • あるブログ制作サイトの投稿ステータス(下書き、公開予定、公開中)があったとする
  • この投稿ステータスはアップロード時間によって決定される
  • アップロード時間が設定されていなければ下書き状態、アップロード時間が現在または過去であれば公開中、未来であれば公開予定といった形
  • アップロード時刻が現在になったタイミングで投稿ステータスを公開中に切り替えるというRakeタスクを作成しておく
  • wheneverを用いて自動化させる

→これにより、製作者が手動で操作しなくても投稿ステータスを適宜変更してくれる機能が作れる

作成方法

$ rails g task article

# lib/tasks/article.rake
namespace :article do
  # 目的の処理を記述
  desc '記事のステータスを変更する'
  task :change_status
    # 処理内容
end
  • lib/tasksディレクトリ配下にarticle.rakeが生成される
  • desc(description)はどういうタスクかの説明文
  • taskにはタスク名と具体的な処理を記載
  • Rakeタスクのファイル名(namespaceで指定される名前空間)に命名規則はないものの、ブロック内のタスクを包括する名詞が望ましい(メールならmail、記事ならarticleみたいに)
    → その方がコマンド実行する際、直感的で分かりやすいから

今回の例で言えば、namespaceがarticleというタスクのinsert_recordというタスクを実行するので

$ rails article:change_status

という形になる。

rails db:migrateも同じような形をしているが実はこれもRakeタスク。

↑の内容と全く関係ないけれどRakeタスクの例

例)youtubeの埋め込み設定で、IDをフォームに入力して貰う仕様だったが、URLをそのまま入力できるように変更。その際、リニューアル前にすでに保存されているidentifierカラム内のデータを変更しなければデータの整合性が保てない。データを一括修正するタスクを作成する。

namespace :fix_embed_youtube_identifier do
  desc 'IDを入力していたidentifierカラムの過去データを一括で修正'
  task update_old_identifier_for_youtube_embed: :environment do
    Embed.youtube.each do |embed|
      embed.update(identifier: "https://youtube/#{embed.identifier}")
    end
  end
end
  • taskの末尾に記載されている:environmentはDBに接続し変更を加える場合に記載が必要
  • 今回はindentifierカラムの内容を書き換えるタスクのため、記載が必要
  • タスクの概略として、元々末尾の相対パス部分だけ保存していたがURL完全体として保存し直すために変数展開で補完して更新している

wheneverとは

crontab管理ライブラリ。wheneverを使うことでcronを動かすことができる。

cronとは(詳細は省く)

【入門】cron(クロン)設定・書き方の基本 - カゴヤのサーバー研究室

  • プログラムを定期的に実行したい時に使う
  • UNIX系のOSに標準で備わっている
  • cronは機能を示す名称で、crontabとはコマンド名またはファイル名

wheneverはcronの設定をrubyの簡単な文法で扱えるようにしたライブラリ。

wheneverの使い方

① インストール

gem 'whenever', require: false
  • wheneverはバックグラウンドで処理されるものであり、Railsとは関係のないプロセス
  • require: falseRailsの実行時に読み込まないようにするための記載
  • まとめるとwheneverはRailsと関係のないプロセスのため、Railsの実行時に読み込まないようにrequire: falseを記載しているよということ
$ bundle exec wheneverise
  • このコマンドにより、config配下にschedule.rbが作成される

wheneverの設定

細かい点で不明なことが多いけれど、ひとまずこういうものと思っておく。

# config/schedule.rb

# Rails.root(Railsメソッド)を使用するために必要
require File.expand_path(File.dirname(__FILE__) + "/environment")

# cronを実行する環境変数
# 環境変数ENV['RAILS_ENV']にセットされている変数または:developmentを指定
# 自分の環境でENV['RAILS_ENV']にすでにdevelopmentがセットされていた
rails_env = ENV['RAILS_ENV'] || :development

# cronを実行する環境変数をセット
set :environment, rails_env

# cron.logの出力先を指定している
# Rails.rootはこのアプリのルート階層が返される
set :output, "#{Rails.root}/log/cron.log"

# 以下、wheneverが上手く機能しなかったので追加

# シェルコマンド設定
# デフォルトはbash -l -c 'command...'で実行される
# 自分の環境は.zshなので設定しておく
set job_template, "/bin/zsh -l -c ':job'"

# .zshrcとrbenvのパスを指定してrakeを定義
# cronは.zshとrbenvの環境で動いてくれないためPATHを通す
job_type :rake, "source /Users/[ユーザー名]/.zshrc; export PATH=\"$HOME/.rbenv/bin:$PATH\"; eval \"$(rbenv init -)\"; cd :path && RAILS_ENV=:environment bundle exec rake :task :output"

job_templateとjob_typeはここら辺参考に

一番上のrequireは何を読み込んでいる?

# config/schedule.rb

# Rails.root(Railsメソッド)を使用するために必要な記載
require File.expand_path(File.dirname(__FILE__) + "/environment")

結論: config/environmentをrequireしている

config/environmentを読み込む理由

インストールの部分と重複するが、wheneverはバックグラウンドで処理するものであり、普段rails sで起動しているプロセスとは別プロセスで実行されるもの。これはRailsとは切り離されたもので、単なるRubyファイルに過ぎない。Railsとは関係のないRubyファイルの中でRailsのメソッドを使いたいので、一番上の行でconfig/environmentを読み込んでいる。

  • __FILE__
  • File.dirname
  • File.expand_path

上記三つの意味をそれぞれ深堀してみることで、File.expand_path(File.dirname(__FILE__) + "/environment")が指し示す意味が分かる◎

■ File.expand_path

pathを絶対パスに展開した文字列を返します。pathが相対パスであればdefault_dirを基準にします。File.expand_path (Ruby 3.1 リファレンスマニュアル)

ちなみにdefault_dirはデフォルトのディレクトリのこと。File.expand_path(path, default_dir = '.')渡されたpathが相対パスだった場合は第二引数のdefault_dirを基準にする(よく分かってない)

  • 絶対パス(フルパス)とは、最上位に位置するディレクトリ(ルートディレクトリ)から、対象のファイルまでの道順全てを記述する方法
  • 相対パスとは、自分の現在位置を基準として説明される対象ファイルの場所。(B町に住む佐藤さんから見て、田中くんはA町にある田中家の2階の部屋にいる。佐藤さんを基準とした田中くんの相対パス)

絶対パス、相対パスとは?使用例からメリット/デメリットまでをまるっと解説
相対パス | 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

__FILE__

現在実行しているファイル。現在のソースファイル名が格納された擬似変数 [Ruby] __FILE__や__dir__って何? - Just do IT

■ File.dirname("dir/file.ext")

filenameの一番後ろのスラッシュより前を文字列として返します。スラッシュを含まないファイル名に対しては"."(カレントディレクトリ)を返します。
クラス:File > クエリ:dirname | るりまサーチ

filenameの一番後ろのスラッシュより前を文字列として返す。小見出しの例で言えば一番後ろのスラッシュより前にある"dir"を返す

まとめると

# config/schedule.rb
require File.expand_path(File.dirname(__FILE__) + "/environment")
  • __FILE__は現在実行しているファイルであるconfig/schedule.rbを指す
  • File.dirname(__FILE__)によってconfig/schedule.rbの一番後ろのスラッシュより前の文字列を取得するため、この場合はconfigが返される
  • 最後、File.expand_pathに"config/environment"というパスが渡されるため、config/environmentの絶対パスが返されている
  • それをrequireしている

スケジュールの記述はgithubを参考に

every 3.hours do # 1.minute 1.day 1.week 1.month 1.year is also supported
  # the following tasks are run in parallel (not in sequence)
  runner "MyModel.some_process"
  rake "my:rake:task"
  command "/usr/bin/my_great_command"
end

every 1.day, at: '4:30 am' do
  runner "MyModel.task_to_run_at_four_thirty_in_the_morning"
end

every 1.day, at: ['4:30 am', '6:00 pm'] do
  runner "Mymodel.task_to_run_in_two_times_every_day"
end

every :hour do # Many shortcuts available: :hour, :day, :month, :year, :reboot
  runner "SomeModel.ladeeda"
end

every :sunday, at: '12pm' do # Use any day of the week or :weekend, :weekday
  runner "Task.do_something_great"
end

every '0 0 27-31 * *' do
  command "echo 'you can use raw cron syntax too'"
end

# run this task only on servers with the :app role in Capistrano
# see Capistrano roles section below
every :day, at: '12:20am', roles: [:app] do
  rake "app_server:task"
end

https://github.com/javan/whenever#example-schedulerb-file

wheneverが上手く機能しなかった

wheneverが機能しているか確かめるために、スケジュールを1分おきに設定

every 1.minutes do
  rake "{自作したRakeコマンド実行}"
end

上手く動かなかったので、下記の設定を追加

# シェルコマンド設定
# デフォルトはbash -l -c 'command...'で実行される
# 自分の環境は.zshなので設定しておく
set job_template, "/bin/zsh -l -c ':job'"

# .zshrcとrbenvのパスを指定してrakeを定義
# cronは.zshとrbenvの環境で動いてくれないためPATHを通す
job_type :rake, "source /Users/[ユーザー名]/.zshrc; export PATH=\"$HOME/.rbenv/bin:$PATH\"; eval \"$(rbenv init -)\"; cd :path && RAILS_ENV=:environment bundle exec rake :task :output"

https://github.com/javan/whenever#define-your-own-job-types

メモ:環境変数やPATHについて不理解のため調べる

参考にしたサイト

Wheneverは導入が超簡単なcrontab管理ライブラリGemです![Rails 4.2 x Ruby 2.3] | 酒と涙とRubyとRailsと

[Rails]Rails5 wheneverでRakeタスクを定期的に実行

【Rails】Rakeタスクの基本情報と作成・実行方法 - AUTOVICE

Railsでwheneverを使って定期的にタスクを実行する方法 | 人と情報

Wheneverとcronを使用した時にbundlerがおかしくなる

YouTubeやTwitterの埋め込み

フォームに入力されたURLを埋め込みに使いたい

youtube用埋め込みパーシャルのcontent_tagのsrc属性に、フォームから入力されたURLを入れ込む方法を取ろうとするも、ブラウザのyoutube埋め込み箇所には「www.youtube.com で接続が拒否されました。」という表示が出てしまい上手く行かなかった。

/ 変更前
.embed-youtube
  = content_tag 'iframe', nil, width: width, height: height, src: "https://www.youtube.com/embed/#{embed.identifier}", \
    frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true

/ 変更後
.embed-youtube
  = content_tag 'iframe', nil, width: width, height: height, src: embed.identifier, \
    frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true

埋め込むURLとブラウザで直接見るURLの違い

YouTube動画埋め込み時に「www.youtube.com で接続が拒否されました。」が表示された際に確認すること | アナライズギア開発ブログ

# 「共有」→「コピー」で取得できるURL
https://youtu.be/IwFO-FNnvrM

# 「共有」→「埋め込む」で取得
https://www.youtube.com/embed/IwFO-FNnvrM

埋め込んで使うタイプのURLにはホスト名とドメインディレクトリ名が加わっていた。src属性にブラウザで直接見るためのURLを入れ込んでも上手く表示できなかったのはこのため。

では、どうしたら良いのか

入力されたURLから末尾のパスのみを取得する

【Ruby】URL文字列から、相対パスを取得する手順 - スリ飯屋MaLankaのフリーエンジニアな日々

やり方は↑のサイトを参考にしました。

※ この項目を書いている時点で相対パスのことをURL末尾のパスと思っていたため、以降の内容にも正しくない表記があるかもしれません。念のため

相対パスとは、現在いるファイルを基準にして説明した対象ファイルの位置のことを指します。(ex: 私の右隣にAさんがいるので、私から見てAさんは私の右隣にいる。私は1階にいて兄は2階にいる。この時、私から見て兄は2階に位置している。)

コンソールで試してみる

※ 課題で元から作られていたアプリを使用しています。

[6] pry(main)> embed = Embed.first
  Embed Load (0.1ms)  SELECT  "embeds".* FROM "embeds" ORDER BY "embeds"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.1ms)  select sql from (select * from sqlite_master where type='table' union select * from sqlite_temp_master where type='table') where tbl_name = 'embeds'
=> #<Embed:0x00000001330a7ee8
 id: 1,
 embed_type: "youtube",
 identifier: nil,
 created_at: Mon, 25 Apr 2022 19:27:35 JST +09:00,
 updated_at: Mon, 25 Apr 2022 19:27:35 JST +09:00>
[7] pry(main)> embed.identifier
=> nil
[8] pry(main)> embed.identifier = 'https://youtu.be/IwFO-FNnvrM'
=> "https://youtu.be/IwFO-FNnvrM"
[9] pry(main)> URI.parse(embed.identifier).path
=> "/IwFO-FNnvrM"
[10] pry(main)> URI.parse(embed.identifier).path.delete('/')
=> "IwFO-FNnvrM"
# これでURLの末尾のパスのみ取得できる
URI.parse(URL文字列).path

# /(スラッシュ)はいらないの削除して相対パスを取得する
URI(URL文字列).path.delete('/')

これらを踏まえて

  1. モデルに相対パスを取得するギミックを作成
  2. ビューの埋め込み箇所、content_tagのsrc属性に任意の相対パスを入れ込む
def get_relative_path
   URI.parse(self.identifier).path.delete('/')
end
  • どこのモデルにギミックを記載すべきか分からないので、embed.rbに書いておいた
  • 何通りか試したものの、全部上手く行かずに下記の感じにおさまった(良し悪しはさておき)
【追記】

メソッド名のgetは不適切とチェックが入ったため削除
def get
relative_path

冗長なselfとlintチェックで警告されたため削除
URI.parse(self.identifier).path.delete('/')

↓付随して下記の呼び出し元のビューファイルの表記も修正

.embed-youtube
  = content_tag 'iframe', nil, width: width, height: height, src: "https://www.youtube.com/embed/#{embed.relative_path}", \
    frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true
  • ビュー側でembedに対し、作成したギミックを呼び出し

URLの相対パスを取得するロジック(別解)

def split_id_from_youtube_url
  identifier.split('/').last if youtube?
end
  • もしYouTubeならスラッシュの区切り文字で分解し、末尾を取得

ツイッターの埋め込み

同じようなツイッター用埋め込みパーシャルを作って、下記サイトに書いてある通りにしたらできた。

TwitterのAPIを使わずに任意のツイートを埋め込む方法 - Sakura scope

.embed-twitter
  blockquote.twitter-tweet
    a href =  "#{embed.identifier}" 
  script async="" charset="utf-8" src="https://platform.twitter.com/widgets.js" 

例外ハンドリング

github.com

エラーは基本的にRailsが勝手にやってくれる

Railsが自動的に作ってくれたエラーページはpublicディレクトリの中にあり、Railsのデフォルトの例外ハンドリングでは、例外の種類に関わらず500 Server Errorを表示するらしい。

開発環境でやっていると赤を基調としたエラー画面のイメージしかなかったけれど、本番環境では下記のようなエラーページが表示される。(本番環境のエラーを確認する方法は後述)

400番台(処理失敗)

404 (File)Not Found

リクエストしたアドレスのページ(ファイル)が見つからない、またはそのサーバが落ちている状態。

https://i.gyazo.com/210d97ee5a708eef526daade59d4a326.png

探していたページが存在しません。 アドレスを間違って入力したか、ページが移動した可能性があります。

■ 422.html

https://i.gyazo.com/1c11d0ee8e9e0417dd59f4258d7611db.png

必要な変更は拒否されました。 アクセスできないものを変更しようとしたのかもしれません。

500番台(サーバーエラー)

サーバー側がリクエストの処理に失敗した場合。

https://i.gyazo.com/62889c4b3e2fb8ce40def37de4f2e3cc.png

申し訳ございません。不具合が発生しました。

本番と同じエラー画面を確認する方法

★ 開発環境の設定ファイルをいじる
# config/environments/development.rb
Rails.application.configure do
# 中略
  # Show full error reports.
  config.consider_all_requests.local = true # ここを

  # falseに変更するだけ
  config.consider_all_requests.local = false
  1. show full error reports(完全なエラーレポートを表示する)の設定部分をfalseに変更する
  2. 変更後は必ずサーバーを再起動させる(設定の変更をサーバー側が検知してくれるわけではないので再起動して変更を反映させる)
  3. エラー画面の確認ができたら、falseに変更した箇所をtrueに戻しておく

Railsで用意されている以外にエラーページを作る

① public配下に手動でファイルを作成する

元々あるエラーページのファイルを参考に文言を変えたてみたりレイアウトを考えればいい。

② エラーの表示させ方については下記二通りを試した

↓↓↓↓

HTTPステータスに割り当てる例外を設定する

3.9.18 config.action_dispatch.rescue_responses | Railsガイド

例外とステータスの様々なペアを指定したハッシュを一つ指定ができる。デフォルトで設定されているものは上記のリンク参照。設定されていない例外は全て500 Internel Server Errorに割り当てられる。

# config/application.rb
class Application < Rails::Application
# 中略
  config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden

  # シンボルではなく数値でステータスコードを書いても動いた 
  config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = 403

今回はPunditを用いて権限管理をし、権限を持たないユーザーがページにアクセスした際に403.html(自作する)を表示させたかったので、Pundit::NotAuthorizedErrorエラー名と、HTTPステータス403のシンボル:forbiddenを記載しています。

rescue_fromを使ってエラーをキャッチ

rescue_from | Railsガイド

最初はこの方法を使ったやり方しか調べて辿り着けなかったのですが、こちらも特に問題なく動きます。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pundit
  
  protect_from_forgery with: :exception

  rescue_from Pundit::NotAuthorizedError, with: :render_403

  private

  def render_403
    render plain: "権限がありません(^^^^)", status: 403
  end
end
権限のないページへアクセス

↑指定した平文が表示されている

参考にしたサイト

14 rescue | Railsガイド

404ってどういう意味?HTTPステータスコードを理解しよう | SiTest (サイテスト) ブログ

本番と同じエラー画面を出したい【Rails】 - 箱のプログラミング日記。

[Rails]404/500などのエラーページって結局どうすればいいの? - CPX

[Rails]gem Punditによる権限管理 (認可)

Punditを使って権限を管理する - Qiita

Punditをなるべくやさしく解説する - Qiita

検索機能

備忘録というかコードを見ながら思ったことで、非常にぼやぼやしています🤤
検索=ransackみたいな頭の回路になっていた。。

検索フォーム

@search_articles_form = SearchArticlesForm.new(search_params)

コントローラに↑という記載があったが、SearchArticlesFormというモデルがあるわけでもない。VScodeの検索でそういうクラスを検索してみると、app/forms配下に検索フォーム用のクラスが作られていた。

これが例のActiveRecordを継承しないでActiveRecordのような書き方を使いたい例か..!

正直ActiveModelの理解度は3割行くか行かないかのぼんやり具合なので、あんまりピンと来てはいない。ただ事前に記述されているコードからおそらくここにこれを記載するんだろうな、ということだけは推測できた。

↓ActiveModel::Attributesに関して(すごく分かりやすい) ushinji.hatenablog.com

distinctのみで使うパターンとは・・

【Rails】 distinctメソッドでユニークなデータを取得する方法 | Pikawaka

def search
  relation = Article.distinct

distinctは取得した値の重複を取り除いて返してくれるメソッド

  • 現状記事のタイトルに重複を防ぐバリデーションが記載されている
  • distinctは通常selectで指定したカラムに重複があった場合に、重複を取り除いた配列を返してくれる
  • selectを使用せずに上記のようにdistinctをかけた場合、全てのカラムで値が重複する場合のみ機能する
では、この場合にdistinctを使用する意味とは(Article.allではダメなのか)
  • 結論: Article.distinctArticle.allのいずれであっても結果は変わらない

  • ただsearchメソッドの部分だけで重複を許容していないことが分かるように、あえてArticle.distinctが使われているとのこと
    →また、仕様変更でバリデーションが変更になったとしても、distinctがあるので重複を排除して検索が行われることが担保されるため

scopeとは? modelに記載している判定ロジックと何が違うのか

Rails の scope をあまり使わない方がいい理由 - ハトネコエ Web がくしゅうちょう

relation = relation.by_category(category_id) if category_id.present?
  • このby_categoryがどこから来たのか
  • 最初、ファットコントローラ対策でモデルに記載するロジック的なヤツだと思い、articleモデルを見に行くとscopeという括りで定義されていた。モデルに記載されたメソッドとscopeの違いはなんなのか
結論
  • scopeに書こうがメソッドとして定義しようが基本的に同じような動きをしているので、書き手の好みだそう
  • ただ、scopeは一行で書いているイメージがあり、無理くり一行でまとめるとかなり読みづらい
  • 複数行に分けた方が見やすいのであればdef~endを使った方がいいのかなぁという印象 らしい

qiita.com

whereメソッド

whereメソッドとは、テーブル内の条件に一致したレコードを配列の形で取得することができるメソッドです。 【Rails】 whereメソッドを使って欲しいデータの取得をしよう! | Pikawaka

find_byとwhereの違い

find_by
  • findメソッドで検索できるのがidのみであるのに対し、find_byではid以外の要素でも検索ができる
  • 複数のデータが検索結果に該当する場合は、検索に引っかかった最初のデータを一つだけ取得する
  • 検索に該当するものがなかった場合はnilを返す
where
  • id以外でも検索可能
  • 該当するデータが複数あった場合はその全てを配列の形で取得することができる
  • 検索に該当するものがなかった場合は空の配列を返す
# 文字列指定
User.where("name = '太郎'")

# シンボル指定 ★SQLインジェクション対策としてこちらを使う
User.where(name: '太郎')
SQLインジェクションとは

SQLインジェクションとは、アプリケーションの脆弱性により本来の意図ではない不当な「SQL」文が作成されてしまい、「注入(injection)」されることによって、データベースのデータを不正に操作される攻撃 のことです。 SQLインジェクションとは?仕組み・被害事例・対策をわかりやすく解説 | クラウド型WAF 攻撃遮断くん

例えば下記のようなユーザーを取得するコードがある

id = params[id]
User.where("id = #{id} ")
# ユーザーidが1の場合、下記のSQLが発行される
#=> SELECT `users`. * FROM `users` WHERE (id = 1)
このコードにおける脆弱性

もしこのidに1 OR 1 = 1という値が入ってしまったら

id = '1 OR 1 + 1'
User.where("id = #{id}")
#=> SELECT `users`. * FROM `users` WHERE (id = 1 OR 1 = 1)
  • SQLではWHEREにtrueが渡ると全てのレコードを取得する
  • このコードでは1 = 1 がtrueと判断され、全てのレコードが取得できてしまう

必要な値を下記のようにハッシュ形式で記述しておけば、発行されるSQLは以下のように変わり、予期せぬ値が渡ることを防ぐことができる。

User.where(id: id)
#=> SELECT `users`. * FROM `users` WHERE `users`.`id` = 1

scope内の指定方法について

※ 途中で分からなくなったため、一時保留

scope :body_contain, ->(body) { joins(:sentences).merge(where('sentences.body LIKE ?', "%#{body}%")) }
Article Load (0.2ms)  
SELECT  DISTINCT "articles".* 
FROM "articles" 
INNER JOIN "article_blocks" 
ON "article_blocks"."article_id" = "articles"."id" 
AND "article_blocks"."blockable_type" = ? 
INNER JOIN "sentences" 
ON "sentences"."id" = "article_blocks"."blockable_id" 
WHERE (sentences.body LIKE '%カレーライス%') 
ORDER BY "articles"."id" DESC 
LIMIT ? 
OFFSET ? 
 [["blockable_type", "Sentence"], ["LIMIT", 25], ["OFFSET", 0]]
  • articlesテーブルとarticle_blocksテーブルを内部結合させて、blockable_typeをsentenceと判断
  • sentencesテーブルを内部結合させて最終的にはsentencesテーブルのbodyカラムで曖昧検索している
    joinsメソッド

    関連するテーブル同士を内部結合(両テーブルで結合条件がマッチするレコードのみ取得する結合)してくれるメソッド。関連するテーブルと内部結合したデータを取得する際に便利。

下記のようなアソシエーションがあるとする

# owner.rb
class Author < ActiveRecord::Base
  has_many :books
end

# book.rb
class Book < ActiveRecord::Base
  belongs_to :author
end
# モデル名.joins(:関連名)
Author.joins(:books)
SELECT `authors`. * FROM `authors` INNER JOIN `books` ON `books`.`author_id` = `authors`.`id`

参考にしたサイト

【Rails】findとfind_byとwhereの使い分け - Qiita

【Rails】 whereメソッドを使って欲しいデータの取得をしよう! | Pikawaka

SQL like 前方一致、あいまい検索、エスケープ | ITSakura