Open9

Rubyチュートリアルまとめ

ゆずゆず

3章ふりかえり

やったこと

  • 3度目の新規Railsアプリケーションを作成。

  • 必要なgemのインストール

  • リモートリポジトリへのプッシュ

  • production環境設定

  • コントローラを新規作成

    • railsコマンド "rails generate controller ControllerName アクション名"(省略可)
  • 新しいルーティングはconfig/routes.rbファイルで定義

  • Viewでは、静的HTMLの他にERB(埋め込みRuby: Embedded Ruby)が使用できる

  • 新機能開発は自動化テストを使って進める

    • 自信を持ってリファクタリングできるようになる
    • 回帰バグも素早くキャッチできるようになる
    • テスト駆動開発は「 red ・ green ・REFACTOR」サイクルを繰り返す
  • Railsのレイアウトでは、アプリケーションのページの共通部分をテンプレートに置くことでコードの重複を解決することができる(DRYの原則)

  • ルートディレクトリは、root 'static_pages#home'と書く事で設定できる(root_urlメソッドがテストで使用可能になる)

  • assert_selectを使うことで表示されているHTMLタグのチェックが出来る、'title', "Home | #{@base_title}"

  • setupという特別なメソッド(各テストが実行される直前で実行されるメソッド)を使ってテストで共有できるものを変数などに収納できる

  • push前の自動テストを癖付ける

ゆずゆず

4章

対話的操作のできるツール rails console

rails console

上記コマンドを使って、起動することが出来る。
ビルドなどをせずにrailsを動かすことが出来る、チュートリアルや簡単な基本文法の確認などはこれで行ってもいいかも。


application_helper.rb

app/helpers/application_helper.rb

上記のヘルパーモジュールは、自動的に読み込んでくれるので、include行を使って読み込ませなくても良い

メソッドの種類

# Rubyの公式ドキュメント「Rubyリファレンスマニュアル」の表記 . で表記
String.new … クラスメソッド 

 # Rubyの公式ドキュメント「Rubyリファレンスマニュアル」の表記 # で表記
"foobar".length … インスタンスメソッド
ゆずゆず

5章ふりかえり

ルートURLをconfig/routes.rbで定義することで、以下のメソッドを使うことが出来るようになる

root_path -> '/'
root_url  -> 'https://www.example.com/'

基本的には_pathでいいが完全なURLが求められる場面では_urlを使用する

名前付きルーティングの定義

// config/routes.rb
get "static_pages/help"

以下のように変換する

get  "/help", to: "static_pages#help"

これにより以下のメソッドも使用可能となる

help_path -> '/help'
help_url  -> 'https://www.example.com/help'

また、名前付きルーティングは、as:オプションを使って呼び方を変更できる

Rails.application.routes.draw do
  root "static_pages#home"
  get  "/help",    to: "static_pages#help", as: 'helf'
 test 'should get help' do
    get helf_path
    assert_response :success
    assert_select 'title', "Help | #{@base_title}"
  end

これはメソッド名などの呼び方が変わるだけでpathはhelpのままとなる。

リンクが正しく動いているかどうかチェックするテストの作成

テストのテンプレートを作成

 rails generate integration_test site_layout
      invoke  test_unit
      create    test/integration/site_layout_test.rb

テスト内容

  • ルートURL(Homeページ)にGETリクエストを送る。
  • 正しいページテンプレートが描画されているかどうか確かめる。
  • Home、Help、About、Contactの各ページへのリンクが正しく動くか確かめる。
ゆずゆず

ユーザー登録

Usersコントローラ作成

rails generate controller Users new  

ユーザーモデル作成

rails generate model User name:string email:string

上記コマンドを実行すると、以下のテーブル作成する関数(マイグレーション)も作られる

// table”の頭文字を取ってt
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps // created_atとupdated_atという2つの「マジックカラム(Magic Columns)を作成
    end
  end
end

マイグレーションは以下のコマンドで実行できる「マイグレーションの適用(migrating up)」と呼ぶ

rails db:migrate

ロールバックは以下のコマンドで実行できる

rails db:rollback

サンドボックスモードでコンソールを起動(ここで行ったすべての変更は終了時にロールバックされます)

 rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit

データベースにUserオブジェクトを保存するには、userオブジェクトのsaveメソッドを呼び出す必要があります

user.save

インスタンス化したuserはドット記法を用いてその属性にアクセスすることができる。

user.name
=> "Michael Hartl"
user.email
=> "michael@example.com"
user.updated_at
=> Thu, 14 Dec 2023 16:40:33.159758000 UTC +00:00

上記のようにモデルの生成と保存を2つのステップに分けておくと何かと便利
.newを行った後、.save保存

 human2 = User.new(name: "Yuji Tani", email: "tani@example.com")
=> 
#<User:0x000000010fc85798

human2.save
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Create
  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) 
 [["name", "Yuji Tani"],
 ["email", "tani@example.com"],
 ["created_at", "2023-12-14 16:48:42.920696"],
 ["updated_at", "2023-12-14 16:48:42.920696"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1=> true

この2ステップを一気にやるコマンドもある(user.create)

User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2022-03-11 01:53:22", updated_at: "2022-03-11 01:53:22">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2022-03-11
01:54:03", updated_at: "2022-03-11 01:54:03">

6.1.4ユーザーオブジェクトを検索する

検索色々

{変数名}.find(0) // インデックス検索

{変数名}.find_by(name: 'hoge') // プロパティで検索

{変数名}.all // 全部取得(Arrayオブジェクトではないので注意)

データ更新

{変数名}.[プロパティ名] = 値
// 変更後、保存が必要
{変数名}.save

{変数名}.update([プロパティ名: 値, n...])

ゆずゆず

Railsのオブジェクトの調べ方

継承クラスを調べる事で祖先を調べられます

irb(main):012> yuji.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
irb(main):013> yuji.class.superclass
=> ApplicationRecord(abstract)
irb(main):014> yuji.class.superclass.superclass
=> ActiveRecord::Base
irb(main):015> yuji.class.superclass.superclass.superclass
=> Object
irb(main):016> yuji.class.superclass.superclass.superclass.superclass
=> BasicObject
irb(main):017> yuji.class.superclass.superclass.superclass.superclass.superclass
=> nil
ゆずゆず

作ったUserモデルの検証

setupメソッド内に書かれた処理は、各テストが走る直前に実行される。
@userはsetupメソッド内で宣言しておけば、すべてのテスト内でこのインスタンス変数が使えるようになる。

valid?メソッドを使うと、有効性を確認できる
オブジェクトが1つ以上のバリデーションに失敗したときはfalseを返す
すべてのバリデーションがパスしたときはtrueを返す

この段階でのテストは特に何も確認していないのでtrueとなる

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end
end

存在性を検証する

assert_not … 結果がtrueなら失敗falseなら成功となる

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do
    @user.name = "     "
    assert_not @user.valid?
  end
end

この段階のテストでは@user.nameの値が空白でも問題ないのでuser.valid?はtrueを返し、テストは失敗します

テストが成功するように、User.nameの値が空でないことを検証するように変更

空でないなら、true
validates :title, presence: true

class User < ApplicationRecord
  validates(:name, presence: true)
end

再度テスト

$ rails console --sandbox // 終了時にロールバックする、コンソールを起動
>> user = User.new(name: "", email: "michael@example.com")
>> user.valid?
=> false

失敗したときにはerrorsオブジェクトが作られる

user.errors.full_messages
=> ["Name can't be blank"]

エラーを吐いているオブジェクトは保存が失敗する

user.save
=> false

長さの検証

nameemailの長さを検証するテストを作成する

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "name should not be too long" do
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end

最大文字数を設定

length: { maximum: (最大文字数) }

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

フォーマットを検証する

email属性の場合 … 有効なメールアドレスかどうかを判定する

emailのフォーマット検証にはformatというオプションを使う

validates :email, format: { with: /<regular expression>/ }

このオプションは引数に正規表現(Regular Expression)とれる

正規表現の確認や、練習には対話的に確認のできる
Rubularを使うといいみたい

一意性を検証する

メールアドレスの一意性を強制するために、validatesメソッドの:uniquenessをオプションを使う
※ ただし重大な警告があります

通常、メールアドレスでは大文字小文字が区別されないため

一意性を検証するテスト(大文字小文字を区別しない)

  test "email addresses should be unique" do
    duplicate_user = @user.dup // dupは、同じ属性を持つデータを複製するメソッド
 duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end

問題

Active Recordはデータベースのレベルでは一意性を保証していないため、瞬間的に2つ登録されてしまったという場合、データベース内で重複して登録されてしまうという可能性がある

しかし、この問題はデータベースレベルでも一意性を強制するだけで解決する。
データベース上でemailのカラムにインデックス(index)を追加し、そのインデックスが一意であるようにすれば解決する。

コラム 6.2.データベースのインデックス

データベースでカラム作成時、そのカラムでレコードを検索する(find)必要が生じるかどうかを考えることは重要。

ログイン認証時、入力されたものと一致するメールアドレスのユーザーのレコードをデータベースの中から探す必要がある。

インデックス機能のないデータベースの場合、1件1件のレコーダーを確認するしかない、最後の1件だったとしても、全件確認する(データベースの世界では、全表スキャン(Full-table Scan)として知られており、極めて不都合とされる)

emailカラムにインデックスを追加すると、書籍の索引のように'文字列'などを含む索引だけを検索すれば良い

Railsでの対応方法

emailインデックスを追加するには、データモデリングの変更が必要。
Railsではマイグレーションでインデックスを追加する。

既に存在するモデルに構造を追加する場合、下記コマンドを実行する
User.emailカラムにindexを追加

rails generate migration add_index_to_users_email

ユーザー用のマイグレーションの時とは異なり、メールアドレスの一意性を追加するマイグレーションでは、自動で内容を記述してくれていないため、自分で書く必要があります。

db/migrate/[timestamp]_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email, unique: true
  end
end

上記マイグレーションではadd_indexというRailsのメソッドを使ってusersテーブルのemailカラムにインデックスを追加している。
インデックス自体は一意性は強制しない。
オプションにunique: trueを指定するとインデックスを追加し、一意性を矯正することになる

ゆずゆず

6章続き

セキュアなパスワード

Railsでの、セキュアなパスワード実装はhas_secure_passwordというRailsメソッドを呼び出すだけで、ほぼ完了する

class User < ApplicationRecord
  .
  .
  .
  has_secure_password // ユーザークラスで呼び出す
end

モデルにこのメソッドを追加すると、以下が使えるようになる

  • ハッシュ化したパスワードを、データベース内のpassword_digest属性に保存できるようになる。
  • passwordpassword_confirmationが使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
  • authenticateメソッドが使えるようになる(引数の文字列がパスワードと一致するとUserオブジェクトを返し、一致しない場合はfalseを返すメソッド)。

has_secure_password機能を使えるようにするには、モデル内にpassword_digest属性が含まれている必要がある

password_digestカラムを作成するマイグレーションを生成する

※ 末尾をto_usersにすると、usersテーブルにカラムを追加するマイグレーションが自動的に作成される

rails generate migration add_password_digest_to_users password_digest:string

上記を実行すると、下記が作成される

invoke  active_record
      create    db/migrate/20231219132847_add_password_digest_to_users.rb

# db/migrate/20231219132847_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :password_digest, :string
  end
end

マイグレーションを実行

rails db:migrate 

必要なライブラリをインストールする

has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptライブラリが必要となる

Gemfileに追記

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.2.2"

gem "rails",           "7.0.4.3"
gem "bcrypt",          "3.1.18" # ← new!

では、ついにhas_secure_passwordをUserクラスに追加してみます!

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
end

テストを実行する

rails test

テストは失敗する
失敗する理由はhas_secure_passwordは、password属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加されているから

しかし、テスト上にこの値が存在しないので失敗してしまう

テストのsetupに追記

password属性とpassword_confirmation属性を追加

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

パスワードの最小文字数設定

パスワードが空でないこと、最小文字数(6文字)以上であることの2つを設定する。

パスワードが空白じゃないか、長さは6文字以上かを検証するテスト

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "password should be present (nonblank)" do
 # パスワードとパスワード確認に対し、同時に代入を行う書き方
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end
ゆずゆず

7章 ユーザー登録

Railsの3つの環境について

Railsは3つの環境がある

  1. テスト環境(test)
  2. 開発環境(development)
  3. 本番環境(production)
    Rails consoleは、デフォルトでdevelopmentになる。
  $ rails console
  Loading development environment
  >> Rails.env
  => "development"
  >> Rails.env.development?
  => true
  >> Rails.env.test?
  => false

テスト環境のデバッグなど、他の環境でconsoleを実行する必要が生じた場合、下記のようにオプションを指定できる

  $ rails console --environment test
  Loading test environment
  >> Rails.env
  => "test"
  >> Rails.env.test?
  => true

Railsサーバーもデフォルトはdevelopmentが使われる

# 本番環境でサーバーを立てる
  $ rails server --environment production

マイグレーションを本番環境で実行して、本番データベースを作成する

  $ rails db:migrate RAILS_ENV=production

Renderは本番サイト用のプラットフォームなので、実行されるアプリケーションはすべて本番環境となります。

下記の1行を追加すると、ユーザーのURLを生成するための多数の名前付きルーティング(5.3.3)と共に、RESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになる

Rails.application.routes.draw do
  root "static_pages#home"
  get  "/help",    to: "static_pages#help"
  get  "/about",   to: "static_pages#about"
  get  "/contact", to: "static_pages#contact"
  get  "/signup",  to: "users#new"
  resources :users
end

登録したUser[1]を表示できるようにする

ルーティングを追加したので、ユーザーのビューとアクションを定義します。

ビュー

// app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>

アクション

// app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
  end
end

デバッグメソッド

class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
    debugger
  end

  def new
  end
end

が開きその時点の変数やparamsなどの値を検証することが出来る

サーバーのプロセスが落とせなくなったとき

下記でプロセスをキルできる

 lsof -i :3000                    
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ruby    48455 user   10u  IPv4 0x4495ffb590b11a85      0t0  TCP localhost:hbci (LISTEN)
ruby    48455 user   11u  IPv6 0x4495ffb592eeeaf5      0t0  TCP localhost:hbci (LISTEN)
ruby    48455 user   18u  IPv4 0x4495ffb590110b55      0t0  TCP localhost:hbci->localhost:55821 (CLOSED)
  kill -9 48455 48455 48455       

日が空いたので、復習と図解を書いていた
めもは最後に行って、それまでアナログにまとめるほうが頭に入りそう
7.3.4失敗時のテスト

ゆずゆず

Active Recordについて

https://railsguides.jp/active_record_basics.html

Active Recordとは、MVCで言うところのM、つまりモデルに相当するもの

O/Rマッピング(オブジェクト/リレーショナルマッピングとは

アプリケーションが持つオブジェクトをリレーショナルデータベース(RDBMS)のテーブルに接続すること
ORMを用いると、SQL文を直接書く代わりにわずかなアクセスコードを書くだけで、SQLと同じようなことが出来るようになる

ORMフレームワークとしてのActive Record

Active Recordにはさまざまな機能が搭載されており、その中でも以下のものが特に重要です。

  • モデルおよびモデル内のデータを表現する
  • モデル同士の関連付け(アソシエーション)を表現する
  • 関連付けられているモデル間の継承階層を表現する
  • データをデータベースで永続化する前にバリデーション(検証)を行なう
  • データベースをオブジェクト指向スタイルで操作する

Active Recordの命名ルール

ORM作成時に従うべきルールがいくつか存在する

  • モデルのクラス - 単数形、アッパーキャメルケースで書く
  • データベースのテーブル - (モデルの)複数形にする、スネークケースで書く

スキーマのルール

テーブルで使うカラム名にも利用目的に応じたルールがある

  • 外部キー(foreign key): テーブル名の単数形_idにする必要があります(例: item_id、order_id)
  • 主キー(primary key): デフォルトでは id という名前のintegerカラムがテーブルの主キーに使われる、自動的に作成される。

その他

  • created_at: レコード作成時に現在の日付時刻が自動的に設定される
  • updated_at: レコード作成時や更新時に現在の日付時刻が自動的に設定る
  • lock_version: モデルにoptimistic lockingを追加する
  • type: モデルでSingle Table Inheritanceを使う場合に指定する
  • 関連付け名_type: ポリモーフィック関連付けの種類を保存する
  • テーブル名_count: 関連付けた、オブジェクトの数をキャッシュするのに使う。
    Articleクラスにcomments_countというカラムがある場合、そこにCommentが多数あると、ポストごとにコメント数が追加される

Active Recordモデル作成

非常に簡単で、以下のようにApplicationRecordクラスのサブクラスを作成するだけで完了する

class Product < ApplicationRecord
end

テーブルの作成はマイグレーションなどで行う

他にもテーブル名の指定や、CRUD操作などの方法は、下記リンクで確認すること

https://railsguides.jp/active_record_basics.html#active-recordにおけるcoc(convention-over-configuration)