Closed45

RubyとRailsに入門する

alkshmiralkshmir

配列をputsするとそれぞれの要素が改行されて出力される

alkshmiralkshmir

クラスの属性は@(変数名)みたいな感じで指定

オブジェクトの属性にはデフォルトでアクセスできない。
attr_accessorという何かを属性に指定するとアクセスできるようになる。

alkshmiralkshmir

型を期待してメソッドを書く感じではないらしい。
duck typingと呼ばれている概念らしい。

say_byeメソッドはeachを使いません。その代わり、@namesがjoinメソッドを 処理できるかをチェックしています。もし処理できることがわかれば、それを使います。 そうでなければ、変数の値を文字列として出力します。 このメソッドは実際の変数の型を意識せず、サポートしているメソッドに頼っています。 これは“Duck Typing”という名前で知られている、「もしアヒルのように歩き、 アヒルのように鳴くものは……」というものです。この方法の良いところは、 対応する変数の型に不要な制約を課さずにすむことです。

引数がメソッドをサポートしているかどうかを調べて処理を変えるらしい。

interfaceとかgenericsとか使いたいな~

alkshmiralkshmir

配列の各要素に対する操作

a.each {|val| puts val}

pythonでいうenumerateみたいなやつ

a.each_with_index {|val, i| puts "#{val}, #{i}"}
alkshmiralkshmir

配列の末尾に要素を追加

a.push "Python"

配列の末尾の要素を出す

a.pop
=> "Python"
a.unshift "C++"  #配列の先頭に要素を追加
a.shift          #配列の先頭の要素を抽出し元配列から削除
=> "C++"
a.delete("Java") #値を指定して削除
a.delete_at(1)   #インデックスを指定して削除
a.delete_if{|lang| lang.size >= 5} #条件指定して削除
a.clear          #配列の要素をすべて削除

deleteは同じ値があるとすべて削除する。

alkshmiralkshmir

配列にはsortメソッドがある

sortのルールを変えるには、sortメソッドにブロックと呼ばれる関数みたいなやつを引数として渡す?
ブロックは波カッコで囲んで指定、またはdoendで囲んで指定

a = [2, 33, 1, 3, 11, 22]
a.sort{|x,y| x.to_s <=> y.to_s}

次のような書き方も可能

a.sort do |x, y|
  x <=> y
end

関数型っぽい

alkshmiralkshmir
irb(main):001:0> a = ["C++", "Ruby", "Python", "Java"]
=> ["C++", "Ruby", "Python", "Java"]
irb(main):002:0> a.size                     #配列の要素数
=> 4
irb(main):003:0> a.reverse                  #配列を逆転
=> ["Java", "Python", "Ruby", "C++"]
irb(main):004:0> a.find{|l| l.length == 4}  #最初に条件に合うものを検索
=> "Ruby"
irb(main):005:0> a.find_all{|l| l.length == 4} #条件に合うもの全て検索
=> ["Ruby", "Java"]
irb(main):006:0> a.map{|lang| lang * 2}     #配列の各要素の置き換え
=> ["C++C++", "RubyRuby", "PythonPython", "JavaJava"]
irb(main):007:0> b = ["Lisp", "Haskell"]
=> ["Lisp", "Haskell"]
irb(main):008:0> a + b                      #配列の連結
=> ["C++", "Ruby", "Python", "Java", "Lisp", "Haskell"]
irb(main):009:0> ["Ruby", "Java", "Ruby", "C++"].uniq #重複要素の除去
=> ["Ruby", "Java", "C++"]
irb(main):010:0> [nil, "Java", "Ruby", nil].compact  #nil要素の除去
=> ["Java", "Ruby"]
alkshmiralkshmir

ハッシュ

ハッシュはpythonでいう辞書
key valueのペアは=>で指定する

 h = {"John"=>40, "Paul"=>42}
 h = {John: 40, Paul: 42} #シンボルをキーとして生成
h.to_a      #キーと値のリスト
  • keyに対するvalueがないときはnilを返す

シンボルは:から始まるデータ型で、

  • 不変
  • 一意性
  • 高速な比較

という特徴があるらしい

ブロック

hashもarrayと同様にブロックを使える

h.each{|k, v| puts "#{k}, #{v}"} #各キーと値の処理
h.each_key{|k| puts k}   #ハッシュの各キーに対する処理
h.each_value{|v| puts v} #ハッシュの各値に対する処理

存在チェック

h.has_key? "George"      #キーがあるか?
h.has_value? 40          #値があるか?

削除

arrayとだいたい同じ

その他

irb(main):001:0> h = {"John" => 40, "Paul" => 42, "Geroge" => 43}
=> {"John"=>40, "Paul"=>42, "Geroge"=>43}
irb(main):002:0> h.size       #ハッシュの要素数
=> 3
irb(main):003:0> h.empty?     #ハッシュが空かどうか?
=> false
irb(main):004:0> h.invert     #値からキーへのハッシュを返す
=> {40=>"John", 42=>"Paul", 43=>"Geroge"}
irb(main):005:0> h.sort       #ハッシュのキーでソートし配列で返す
=> [["Geroge", 43], ["John", 40], ["Paul", 42]]
irb(main):006:0> h2 = {"Ringo" => 40}
=> {"Ringo"=>40}
irb(main):007:0> h.merge h2   #ハッシュの内容をマージする(非破壊的)
=> {"John"=>40, "Paul"=>42, "Geroge"=>43, "Ringo"=>40}
irb(main):008:0> p h
{"John"=>40, "Paul"=>42, "Geroge"=>43}
=> {"John"=>40, "Paul"=>42, "Geroge"=>43}
irb(main):009:0> h.update h2  #ハッシュの内容をマージする(破壊的)
=> {"John"=>40, "Paul"=>42, "Geroge"=>43, "Ringo"=>40}
irb(main):010:0> p h
{"John"=>40, "Paul"=>42, "Geroge"=>43, "Ringo"=>40}
=> {"John"=>40, "Paul"=>42, "Geroge"=>43, "Ringo"=>40}

alkshmiralkshmir

時刻

Timeクラスを使う

irb(main):001:0> t = Time.now                  #現在時刻を取得
=> 2012-01-23 01:23:45 +0900
irb(main):002:0> t.strftime("%Y/%m/%d %H:%M:%S") #時刻の書式化
=> "2012/01/23 01:23:45"
irb(main):003:0> t1 = Time.local(2012,1,1,0,0) #地方時で任意時刻を設定
=> 2012-01-01 00:00:00 +0900
irb(main):004:0> t2 = t1 + 3600                #3600秒後をt2に設定
=> 2012-01-01 01:00:00 +0900
irb(main):005:0> puts t2                       #t2はt1の1時間後に
2012-01-01 01:00:00 +0900
=> nil

数字を足し算すると秒扱いなのか

alkshmiralkshmir

正規表現

https://www.ruby.or.jp/ja/tech/development/ruby/tutorial/040_regexp.html

正規表現オブジェクトは以下で生成

  • /で囲む
  • Regexpクラスから生成
  • %r||の間に書く

マッチングするかどうかは=~演算子で判定する。マッチすると、マッチした文字列の先頭インデックスを返す。

  • マッチした文字列は $&で取得する
  • マッチしない場合はnilを返す

最新の結果が予約された変数に入るのか……

文字列メソッドと組み合わせることもできます

irb(main):001:0> s = "apple   banana     orange"
=> "apple   banana     orange"
irb(main):002:0> s.scan(/\w+/)      #英数字の並びを抽出し配列に
=> ["apple", "banana", "orange"]
irb(main):003:0> s.split(/\s+/)     #スペースの並びを区切り文字とし配列に
=> ["apple", "banana", "orange"]
irb(main):004:0> s.gsub(/\s+/, ",") #スペースの並びをカンマに変換
=> "apple,banana,orange"
alkshmiralkshmir

条件分岐

  • if 条件式 thenendで囲む

  • else ifはelsif

  • switch文的なやつはcase

  • 三項演算子

score = 80
result = score > 70 ? "Pass" : "Failed"  #右辺の評価結果をresultに代入
puts result
  • if文の逆(条件を満たさないときに実行する)unlessもある

    • いらんくね…?
  • if修飾子

debug = true
num = 10
 
puts "num = #{num}" if debug  #debugがtrueのときのみ実行

繰り返し

for文はtimesメソッドを使う

数値.times {|変数|
  #処理
}

関数型っぽくていいね

その他、uptoメソッド、downtoメソッド、stepメソッドもある

puts "upto:"
#numが3から5に1つずつ増加
3.upto(5) {|num|
  puts "num = #{num}"
}
 
puts "downto:"
#numが8から6に1つずつ減少
8.downto(6) {|num|
  puts "num = #{num}"
}
 
puts "step:"
#numが12.3から14.1を超える前まで0.5ずつ増加
12.3.step(14.1, 0.5) {|num|
  puts "num = #{num}"
}

for文もあるがpythonのfor文っぽい感じ(オブジェクトを一個ずつ取り出す)

lang = ["Ruby", "Java", "Python"]
 
for s in lang do
  puts s
end
 
for i in 1..3 do
  puts i
end

いまさらだけど 1..3はRangeオブジェクトという種類らしい

while文もある

while 条件式 do
  #処理
end
  • 条件が成立しない間繰り返すuntil文もある(いらんくね)

  • 無限ループをするためのloop文もある(いらんくね)

  • continueに相当するnext文

  • 同じ繰り返しをするredo文
    もある

例外

begin
  #例外が発生するかもしれない処理
rescue 例外クラス => 例外オブジェクトを格納する変数 then
  #例外が発生したときの処理
else
  #例外が発生しなかったときの処理
ensure
  #例外の有無に関わらず最後に実行される処理
end
  • raise関数で明示的にエラーを発生させることも可能。
alkshmiralkshmir

入出力

https://www.ruby.or.jp/ja/tech/development/ruby/tutorial/060_in_output.html

標準入出力は定数に格納されている。

  • 標準入力からデータを受け取る
while str = STDIN.gets
  break if str.chomp == "exit"
  print "input text:", str
end

getsは改行文字を取り除かないので注意

  • STDERR.putsメソッドで標準エラー出力できる。

ファイル入出力

openしたら閉じる

io = File.open(ファイル名, モード)
#ファイル処理
io.close

ブロックを渡すと終わったら自動で閉じてくれる

File.open(ファイル名, モード) do |io|
#ファイル処理
end
File.open("data.txt", "r") do |io|
  puts "---gets"
  p io.gets      #1行読み込んで表示
 
  puts "---readline"
  p io.readline  #1行読み込んで表示
 
  puts "---read"
  io.rewind      #読み込み位置を先頭に戻す
  p io.read(5)   #5バイト分読み込んで表示
  p io.read      #現在位置よりファイル全体を読み込んで表示
 
  puts "---readlines"
  io.rewind      #読み込み位置を先頭に戻す
  p io.readlines #ファイルの各行を要素とする配列を作成して表示
end
File.open("out.txt", "w") do |io|
  p io.puts  "AAAAA"  #文末に改行を入れて書き込む
  p io.print "BBBBB"  #文末の改行は付けずに書き込む
  p io.write "CCCCC"  #文末の改行は付けない。書き込んだバイト数を返す
end

コマンドライン引数

ARGV変数に配列として入っている

ファイル操作、ディレクトリ操作

FileクラスとDirクラスに各種メソッドがある

特にほかの言語と変わることはない

joinとかはおもしろいかも?

File.join("C:/ruby", "data.txt")  #ファイルパスを連結
=> "C:/ruby/data.txt"

Dir.globメソッド

Dir.glob("C:/ruby/*.txt")  #ファイル一覧の取得
=> ["C:/ruby/out.txt", "C:/ruby/test.txt"]
alkshmiralkshmir

更新サボっていたがRuby on Railsチュートリアル進めている。
8章から気になったことをメモしていく。

cookies と session

cookiesメソッド(9.1)とは対照的に、sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了します

使い方(抽象)はともかく、実態(具象)が何なのかよくわからないなあ…

次でsessionを生成できる

session[:user_id] = user.id

これはハッシュのように使えるがハッシュではない。sessionインスタンスを生成しているらしい。

要はcookieを生成してuser_idをkey、暗号化(ハッシュ化?)されたuser_idの値をvalueにセットして、HTTPのset-cookieヘッダに載せて返すってこと?

クッキーをdevtoolsで確認したら、_sample_app_sessionというNameでValueがよくわからん値のものが保存されていた。
ログインしたときのPOSTリクエストのレスポンスのSet-Cookieヘッダには_sample_app_session=<ランダム?文字列>というのがついてた。

中身はわからないけどこの中にuser_idが入っていて、railsはそれをデコードできるようになっているのだと思う

alkshmiralkshmir

or equals

次のコードは@current_userがnilならば後ろの式を評価して代入するという意味である。

@current_user = @current_user || User.find_by(id: session[:user_id])

ただし普通次のようにかく

@current_user ||= User.find_by(id: session[:user_id])
alkshmiralkshmir

fixture

test/fixturesの下にyamlで書く。中にERBを書ける。

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

passwordを使いたいが、DBにはそのようなカラムはないため、password属性を追加するとエラーになる。そこで、fixtureでは全員同じパスワードpasswordが使われる。

テスト中では以下のように参照可能。

user = users(:michael)
alkshmiralkshmir

safe navigation演算子(ぼっち演算子)

obj && obj.methodobj&.methodと書ける。

  • 1回の呼び出しのようで実は2回呼び出してるのがちょっと気持ち悪いかも。まあ実際困る場面は思いつかない。
alkshmiralkshmir

Cookies

sessionメソッドを使うとブラウザを閉じるとセットした情報は消えてしまう。ブラウザにセッション情報を永続化するにはcookiesメソッドを使えば良い

  • 実際、ブラウザへのインターフェースとしては何が異なるんだろう?
    • HTTPのヘッダが異なる?
      • ChatGPT先生によると、Set-Cookieヘッダに有効期限を指定しない場合、有効期限はブラウザが開いている間とブラウザは解釈するらしい。
      • Cookieに有効期限を設定するには、ExpiresまたはMax-Ageフィールドを使う。
      Set-Cookie: myCookie=myValue; Expires=Wed, 21 Oct 2023 07:28:00 GMT;
      
      Set-Cookie: myCookie=myValue; Max-Age=3600;
      
    • 要はcookiesメソッドを使うとこれらのフィールドをつけてくれるのだと理解した。

cookieに認証情報を載っけるので、これが漏洩するとまずい。主な攻撃方法として以下がある:

  • 平文でcookieが送られている時に、man-in-the-middleでcookieを取り出す。

    • TLSで防御可能。
  • DBへの侵入で、保存されているトークンを窃取する

    • ハッシュを保存しておくことで防げる。
  • XSSで窃取する。

    • RailsではView templateで入力された内容はエスケープされる
  • パソコンやスマホを物理的に窃取して取得する。

    • 根本的対策は難しい。
    • 別の端末などでログアウトしたらトークンも必ず更新する、などで二次的被害を最小限にすることは可能。
  • 一時セッションはセッションリプレイ攻撃に対しては脆弱。

    • Diveseでは対策済みらしい

次の方針で永続的セッションを作成する。

  1. ランダムな文字列を生成して、記憶トークンとして使う
  2. 記憶トークンは、ハッシュ化してデータベースに保存
  3. 記憶トークンをブラウザのcookiesに保存するときは、有効期限を設定する
  • 上で説明したように、ExpiresMax-Ageをつければ良い
  1. 記憶トークンをブラウザのcookiesに保存するときは、ユーザIDを暗号化する
  • どゆこと?ランダムな文字列じゃなかったん?どうやってユーザIDを入れる?
  1. 以降、もしブラウザからcookiesが送られてきて、暗号化されたユーザIDがあったら、復号したユーザIDでDBを検索して、DB内のハッシュ値と一致するか確認する
  • sessionsの時も思ったけど復号キーはどこで持っておく?
alkshmiralkshmir

有効期限の設定

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

有効期限は省略できる。この場合は、ブラウザの動作としてはsessionsメソッドと同じになり、セットしたcookieはブラウザ終了で削除される。

  • ブラウザ終了で削除されてよいcookieをどちらで保存するかは、好みの問題らしい。
  • セッションクッキーはsessionsを使い、それ以外の一時クッキーはcookiesでいいらしい

なお、上のコードと以下のコードは同じ動作となる。

cookies.permenent[:remember_token] = remember_token
  • permenentにすると期限が20年になる。

cookieの暗号化

ユーザIDを暗号化してクッキーにセットする

cookies.encrypted[:user_id] = user.id
  • 暗号化しないと、ユーザ自身がクッキーを書き換えて操作可能となってしまう。
    • 例えば、悪意のあるユーザーaliceがサイトにログインした結果、CookieStoreに{“user”:”alice”}という情報が保存されたとします。攻撃者は自分自身のクッキーを{“user”:”bob”}に書き換えるだけで、bobになりすましができてしまいます。このような攻撃を防ぐために、CookieStoreを暗号化(本質的に改ざん防止)するのです。

    • 参考: セッションを暗号化するのにはなんの意味がありますか?
alkshmiralkshmir

ユーザにログインを要求する

before_actionで保護したいページに認証を要求する

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]

  private
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url, status: :see_other
      end
    end
end
  • なぜSee Otherなのか気になる。401 Unauthorizedでは?
alkshmiralkshmir

sendメソッド

次の関数を〇〇_digestという属性一般で使えるようにしたい

def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

railsではメタプログラミングで可能

def authenticated?(attribute, token)
  digest = self.send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end

sendメソッドは文字列とかシンボルとかをとってその名前に対応する属性にアクセスすると思っておけば良い

こうすると、次のようにかける

user.authenticated?(:remember, remember_token)

所感

気持ち悪い…
というか、バグりそう。そのメソッドがあるかどうか、コーディングの段階で気付きたいのだけどそういう方法はあるのだろうか。

alkshmiralkshmir

Active Recordにおけるテーブル間の関係

AとBが1対多なら、Aのmodelにはhas_manyをつけてBにはbelongs_toをつける

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end
alkshmiralkshmir

user.micropostsにメソッドチェーン繋げる形で

  • create
  • create! (失敗時に例外を発生)
  • build
  • find_by(id: )

などを使える

  • createはDBに保存するが、buildはしない
alkshmiralkshmir

あれ、DBに制約つけるならばmigrateしなくていいのかな?

alkshmiralkshmir

関連データの削除

データ削除の際に関連しているデータを削除したければhas_many側にdependent: destroyをつければ良い

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  .
  .
  .
end

これはON DELETE CASCADE制約をつけているわけではなく、railsが代わりに削除のSQLを発行してくれるという機能らしい。

ChatGPTさんによると

このオプションはRailsのActiveRecordの機能であり、実際のデータベーステーブルにON DELETE CASCADE制約を直接的に追加するものではありません。代わりに、Railsはdependent: :destroyが指定された関連付けに関連するレコードを削除するためのコードを生成します。この場合、Userが削除されると、関連するMicropostsも一緒にdestroyされますが、それはRailsのActiveRecordのメソッドを使用して個々のレコードを削除することによって行われます。

例えば、PostgreSQLを使っている場合、マイグレーションファイルで外部キー制約を追加する際にon_delete: :cascadeオプションを指定することで、ON DELETE CASCADE制約を設定できます。以下は、マイグレーションファイルでの例です

class AddForeignKeyToMicroposts < ActiveRecord::Migration[6.0]
  def change
    add_foreign_key :microposts, :users, on_delete: :cascade
  end
end
alkshmiralkshmir

ChatGPTさんによると、modelでhas_manyをつけてもDBに外部キー制約はつかないらしい。
まあそれはそうか。

外部キー制約は参照する側が持つので、belongs_to側に持たせる。
チュートリアルでは、modelの生成の際に外部キー制約を付与したmigrationを生成していた。

alkshmiralkshmir

default_scope

default_scopeメソッドを使うと、ORMで取得する順番を指定できる

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
end

->はラムダ式で、ブロックを引数に取り、Procオブジェクトを返す。Procオブジェクトは、callメソッドが呼ばれたとき、ブロック内の処理を評価する。

この場合呼ばれるSQLはこんな感じになる

irb(main):001> a = Micropost.first
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> nil
alkshmiralkshmir

countメソッド

user.microposts.count

データベース上のマイクロポストを全部読みだしてから結果の配列に対してlengthを呼ぶ、といった無駄な処理はしていないという点です。そんなことをしたら、マイクロポストの数が増加するにつれて効率が低下してしまいます。そうではなく、データベース内で高度に最適化された計算をしてもらった後に、特定のuser_idに紐付いたマイクロポストの数をデータベースに問い合わせています

高度に最適化された計算…?

irb(main):002> User.count
  User Count (0.2ms)  SELECT COUNT(*) FROM "users"
=> 100
irb(main):003> User.first.microposts.count
  User Load (1.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Micropost Count (0.2ms)  SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ?  [["user_id", 1]]
=> 0

普通にCOUNT(*)メソッド呼んでいるだけだった。

alkshmiralkshmir

13章のリファクタリングについて…

テストでログインしているかどうかを使うためのメソッドlogged_in_userをUserモデルに実装していたが、Micropostクラスから参照できないので、共通の親であるApplicationControllerに移すということだが…

class ApplicationController < ActionController::Base
  include SessionsHelper

  private

    # ユーザーのログインを確認する
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url, status: :see_other
      end
    end
end

なんだろう、これ…
本当に?という気持ちになってしまうけど

うまく言えないけど共通の処理があるから安易に親に実装を書くというのに非常にためらいがある。SessionsHelperに書くのではダメなんだったけ?(まあ、それでも結局同じことではあるけど)

なんか、親にはインターフェースだけ書いて実装は書くなという刷り込みがかなりあるのですごく嫌な感じする。

じゃあどう分割するのがいいのか、ってのはわからないけど。

alkshmiralkshmir

そもそも名前が微妙な気がする
名前から、ログインしていない場合にログイン画面へリダイレクトするというのが読み取れない。

alkshmiralkshmir

includeだとprivateメソッドが呼び出せない(継承だと呼び出せる)ので仕方なくここに書いてると理解した。

alkshmiralkshmir

Active Recordと関係

フォロワーの関係を表すためにエンティティを作成する

class CreateRelationships < ActiveRecord::Migration[7.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end
  • なぜ、外部キー制約をつけないのだろう?

シンボルとクラス名が一致しないので、ユーザ側で指定:

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  .
  .
  .
end
  • 外部キー制約つけるのは参照側だからUser側につけるのはなんだかこんがらがるな
  • 多分このあとfollowedにもつけるんだろう

生のSQLであれば参照される側のテーブル定義には参照している側の定義は全く関与しないが、ORMを使う以上参照される側に参照する側との関係をちゃんと書かないと、属性として使用できない、というのは理解した。

alkshmiralkshmir

14.1.2の演習で、NoMethodErrorになったけど

先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。

irb(main):006> user.active_relationships
  Relationship Load (0.4ms)  SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 
[#<Relationship:0x0000000108bfa280
  id: 1,
  follower_id: 1,
  followed_id: 3,
  created_at: Sun, 19 Nov 2023 04:31:18.942491000 UTC +00:00,
  updated_at: Sun, 19 Nov 2023 04:31:18.942491000 UTC +00:00>]
irb(main):007> user.active_relationships.follower
/Users/shira/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.0.4.3/lib/active_record/relation/delegation.rb:110:in `method_missing': undefined method `follower' for #<ActiveRecord::Associations::CollectionProxy [#<Relationship id: 1, follower_id: 1, followed_id: 3, created_at: "2023-11-19 04:31:18.942491000 +0000", updated_at: "2023-11-19 04:31:18.942491000 +0000">]> (NoMethodError)
irb(main):008> user.active_relationships.followed
/Users/shira/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.0.4.3/lib/active_record/relation/delegation.rb:110:in `method_missing': undefined method `followed' for #<ActiveRecord::Associations::CollectionProxy [#<Relationship id: 1, follower_id: 1, followed_id: 3, created_at: "2023-11-19 04:31:18.942491000 +0000", updated_at: "2023-11-19 04:31:18.942491000 +0000">]> (NoMethodError)
alkshmiralkshmir

あー、user.active_relationships経由でフォロワー、フォロイー取得するのかと思ってたけど違うのか。

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :following, through: :active_relationships, source: :followed
  .
  .
  .
end
  • has_many throughをつけるとエイリアス?みたいな感じでアクセスできると理解
    • DBの構造を隠蔽しつつORMを使いやすくする措置だと理解。
alkshmiralkshmir

すごい。inner joinしてくれている。

irb(main):001> user = User.first
  User Load (0.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 
#<User:0x000000010870ca20
...
irb(main):002> user.following
  User Load (0.1ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
alkshmiralkshmir

そのあともう一回user.followingにアクセスしたけど、SQLが表示されない。
キャッシュしている?

alkshmiralkshmir

ルーティングとobject schema

memberメソッドは1つのインスタンスに紐づく別のインスタンスへのルーティングを提供する。

Rails.application.routes.draw do
  # 略
  resources :users do
    member do
      get :following, :followers
    end
  end
  # 略
end
  • 上の例では、users/1/followingとかusers/1/followersというパスへのgetクエリに対するルーティングを提供する。

idに紐づけず、全てのリソースへのルーティングはcollectionメソッドを使うらしい

resources :users do
  collection do
    get :tigers
  end
end
alkshmiralkshmir

アクションはそのまま、followingfollowersになる

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end
alkshmiralkshmir

非同期通信

  • Hotwire
  • Turbo
  • Turbo Streams

何これ〜〜?

alkshmiralkshmir

とりあえず、以下のコードは、createアクションが起動されたときに、

  • follow_formというCSS IDを持つパーシャルをusers/unfollowに置き換える
  • followersというCSS IDを持つパーシャルを@user.followers.countに置き換える
    ということをしているのは理解した。
<%= turbo_stream.update "follow_form" do %>
  <%= render partial: "users/unfollow" %>
<% end %>
<%= turbo_stream.update "followers" do %>
  <%= @user.followers.count %>
<% end %>
  • 対応するアクションは例によってNaming Conventionで指定する。
    • <アクション名>.trubo_stream.erbという命名にする
alkshmiralkshmir
  • [Follow]ボタンまたは[Unfollow]ボタンをクリックすると、Turbo StreamリクエストがRailsサーバーに送信される
  • Turbo Streamに応答するコードが存在しない場合は、Turbo Streamリクエストを通常のHTMLリクエストとして扱う
  • Turbo Streamに応答するコードが存在する場合は、同じ名前を持つTurboテンプレートを、リクエストに対応するアクション(ここではcreateまたはdestroy)として自動的に評価する
  • Turboテンプレートでは、turbo_stream.updateメソッドを用いて特定のHTML要素のコンテンツを、テンプレートに渡されたERBブロックを評価した結果で置き換える
alkshmiralkshmir

eager loading

Active Recordでは.includesをつけたら表を結合してくれるらしい。これは便利。

Micropost.where("user_id IN (#{following_ids})
                 OR user_id = :user_id", user_id: id)
         .includes(:user, image_attachment: :blob)
alkshmiralkshmir

Rails tutorial完走した

完走した感想は

  • 必要な機能が大体全て最初から使えるようになっていて非常に便利
  • Active Record便利
  • ただし、Naming conventionが多すぎて、DHHの手のひらの上で踊らされている感覚になる。
  • 開発初期はRailsでガッと作って後から別の技術で置き換えることも含めてリファクタリングするのが多いよ、みたいな話にそれなりの実感が伴った。
  • テストを簡単に書けるのもよかったが、フロントのテストが統合テストしかないのがつらかった。
  • HotwireでSPA作るのはReactとかと比べてどんなメリットがあるのか気になった。
このスクラップは2023/11/19にクローズされました