LoginSignup
1
0

More than 1 year has passed since last update.

【Rails基本編】美少女と学んだ気になれる講座

Posted at

この講座は、Web業界未経験の吉野 桜と、その先輩である堀切 あやめのかわいさをお楽しみいただくため、邪魔にならない程度の差し障りのないWeb技術の話を、お楽しみいただく番組です

キャラクター

20220920_resize.png
吉野 桜: Web業界未経験

20200920_resize.png
堀切 あやめ: Web業界三年目

本編

桜「うちのバックエンドってRuby on Railsですよね?」
あやめ「うん、そうだね」
桜「桜、実はRuby on Railsからっきしで……」
あやめ「そうなの、具体的にどの辺りがわからないの?」
桜「いや、どこがわからないのかもわからなくって。『〇nly my railgun』なら歌えるんですけど……」
あやめ「いや、それ語感の互換性も大して高くないからね。そっか、じゃあ、今日は概要だけ説明するね」
桜「ありがとうございます、あやめ先輩! 大好きです!」
あやめ「軽々しく好きとか言わないの」
桜「はーい」

***

あやめ「Railsは言わずもがなRubyで書かれたWebアプリケーションフレームワークだよね。Githubリポジトリはこれね。Star数は2022年9月時点で51.4kあるね。PHPのLaravelが71kで、PythonのDjangoが66.3kだからそれよりは少ないけど、Rubyが日本発祥の言語だから日本語のドキュメントや関連記事が豊富で初心者にフレンドリーと言えるかもしれないね」
桜「へー、RailsがバックエンドのWebアプリケーションフレームワークで一強かと思ってたんで、意外です」
あやめ「日本のベンチャー企業で利用しているところが多いからそう感じるのかな。Githubのスター数は世界基準だからね」

あやめ「じゃあ、概要だけど。まず、Railsの根底にある二つの哲学について説明するね。RailsはDRYとCoCっていう理念を持っているの。知ってる?」
桜「DRYってDRY原則のDRYですか? 繰り返しを避けようってやつですよね?」
あやめ「その通り。DRYっていうのはDon't Repeat Yourselfの略で、同じコードを繰り返し書くことを徹底的に避けることで、コードが保守しやすくなって、拡張も容易でバグも減るっていう思想だね」
桜「DRYは初心者でもわかりやすいですね。CoCっていうのは何ですか? 一昔前に流行ったFPSですか?」
あやめ「一昔前に流行ったFPSって、あぁ、『CoD(Call 〇f Duty)』のことか。いや、そうじゃなくて。CoCっていうのはConvention Over Configurationの略で、Webアプリケーションの各種設定に対して、最善のデフォルト値を設定することで、開発者が大量の設定ファイルを作成せずに済むっていうものだね。Railsにはモデルやコントローラー、ルーティングに様々な規約を設けていて、それに従うことで開発者が明示的に記述するコード量を減らすことができるんだよ」
桜「うえぇ、いきなり難しくなりました……」
あやめ「そうだね。今、咀嚼する必要はないからとりあえず用語だけ覚えておいて」

***

あやめ「さて、習うより慣れよだから、なにか動くモノを作りながら説明していこっか。何、作ろうかなぁ。桜ちゃん好きなWebサービスはある?」
桜「Twi〇ter!」
あやめ「あぁ、桜ちゃんツイ廃だもんね」
桜「いやぁ、褒めてもなんにも出ないですよぉ?」
あやめ「え、私、一ミリも褒めてないよ」
桜「ひどー--い」
あやめ「じゃあ、Twi〇ter的なものでサンプルを作っていこうかな」
桜「ラジャー!」

環境

  • Ruby: 2.7.4
  • Rails: 6.1.7
  • MySQL: 5.7.39
  • Yarn: 1.22.19

※上記は予めインストール済のものとします

$ gem install rails -v 6.1.7
$ rails new twitter-clone -d mysql -T

桜「あれ? rails newはお馴染みですけど、その後のオプションは何ですか?」
あやめ「データベースを標準のSQLite3からMySQLに変更したり、テストファイルの作成を省略したりしてるんだよ。他にもRails標準のライブラリのインストールをスキップできたりするの。helpコマンドでオプションを確認できるから、一度確認してみてね」

$ rails new --help

# 以下が出力されます
Usage:
  rails new APP_PATH [options]

Options:
      [--skip-namespace], [--no-skip-namespace]              # Skip namespace (affects only isolated engines)
      [--skip-collision-check], [--no-skip-collision-check]  # Skip collision check
  -r, [--ruby=PATH]                                          # Path to the Ruby binary of your choice
                                                             # Default: /home/kamihitoe/.rbenv/versions/2.7.4/bin/ruby
  -m, [--template=TEMPLATE]                                  # Path to some application template (can be a filesystem path or URL)
  -d, [--database=DATABASE]                                  # Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)
                                                             # Default: sqlite3
      [--skip-gemfile], [--no-skip-gemfile]                  # Don't create a Gemfile
  -G, [--skip-git], [--no-skip-git]                          # Skip .gitignore file
      [--skip-keeps], [--no-skip-keeps]                      # Skip source control .keep files
  -M, [--skip-action-mailer], [--no-skip-action-mailer]      # Skip Action Mailer files
      [--skip-action-mailbox], [--no-skip-action-mailbox]    # Skip Action Mailbox gem
      [--skip-action-text], [--no-skip-action-text]          # Skip Action Text gem
  -O, [--skip-active-record], [--no-skip-active-record]      # Skip Active Record files
      [--skip-active-job], [--no-skip-active-job]            # Skip Active Job
      [--skip-active-storage], [--no-skip-active-storage]    # Skip Active Storage files
  -P, [--skip-puma], [--no-skip-puma]                        # Skip Puma related files
  -C, [--skip-action-cable], [--no-skip-action-cable]        # Skip Action Cable files
  -S, [--skip-sprockets], [--no-skip-sprockets]              # Skip Sprockets files
      [--skip-spring], [--no-skip-spring]                    # Don't install Spring application preloader
      [--skip-listen], [--no-skip-listen]                    # Don't generate configuration that depends on the listen gem
  -J, [--skip-javascript], [--no-skip-javascript]            # Skip JavaScript files
      [--skip-turbolinks], [--no-skip-turbolinks]            # Skip turbolinks gem
      [--skip-jbuilder], [--no-skip-jbuilder]                # Skip jbuilder gem
  -T, [--skip-test], [--no-skip-test]                        # Skip test files
      [--skip-system-test], [--no-skip-system-test]          # Skip system test files
      [--skip-bootsnap], [--no-skip-bootsnap]                # Skip bootsnap gem
      [--dev], [--no-dev]                                    # Set up the application with Gemfile pointing to your Rails checkout
      [--edge], [--no-edge]                                  # Set up the application with Gemfile pointing to Rails repository
      [--master], [--no-master]                              # Set up the application with Gemfile pointing to Rails repository main branch
      [--rc=RC]                                              # Path to file containing extra configuration options for rails command
      [--no-rc], [--no-no-rc]                                # Skip loading of extra configuration options from .railsrc file
      [--api], [--no-api]                                    # Preconfigure smaller stack for API only apps
      [--minimal], [--no-minimal]                            # Preconfigure a minimal rails app
  -B, [--skip-bundle], [--no-skip-bundle]                    # Don't run bundle install
  --webpacker, [--webpack=WEBPACK]                           # Preconfigure Webpack with a particular framework (options: react, vue, angular, elm, stimulus)
      [--skip-webpack-install], [--no-skip-webpack-install]  # Don't run Webpack install

Runtime options:
  -f, [--force]                    # Overwrite files that already exist
  -p, [--pretend], [--no-pretend]  # Run but do not make any changes
  -q, [--quiet], [--no-quiet]      # Suppress status output
  -s, [--skip], [--no-skip]        # Skip files that already exist

Rails options:
  -h, [--help], [--no-help]        # Show this help message and quit
  -v, [--version], [--no-version]  # Show Rails version number and quit

Description:
    The 'rails new' command creates a new Rails application with a default
    directory structure and configuration at the path you specify.

    You can specify extra command-line arguments to be used every time
    'rails new' runs in the .railsrc configuration file in your home directory,
    or in $XDG_CONFIG_HOME/rails/railsrc if XDG_CONFIG_HOME is set.

    Note that the arguments specified in the .railsrc file don't affect the
    defaults values shown above in this help message.

Example:
    rails new ~/Code/Ruby/weblog

    This generates a skeletal Rails installation in ~/Code/Ruby/weblog.

桜「じゃあとりあえず、MySQLサーバを立ち上げて、RailsのPumaサーバに接続してみますね!」

$ sudo service mysql start
$ rails server

桜「あれ? 
ActiveRecord::ConnectionNotEstablished
Access denied for user 'root'@'localhost' (using password: NO)
って出ました……」
あやめ「うん。DBの設定をしないと、Railsのトップページまで見れないからね。じゃあ、DBの設定ファイルを書いていこっか」

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: USERNAME
  password: PASSWORD
  host: localhost

development:
  <<: *default
  database: twitter_clone_development

test:
  <<: *default
  database: twitter_clone_test

あやめ「USERNAMEとPASSWORDは自分のを設定してね。できたら、ローカルのMySQLに入ってDBを作ろう」

$ mysql -u root -p
mysql> CREATE DATABASE twitter_clone_development;
mysql> CREATE DATABASE twitter_clone_test;
mysql> exit;

桜「DBの設定が必要だったんですねー、できました!」
あやめ「じゃあ、もう一回pumaサーバを起動してみよっか。ちなみに、rails serverrails sでも実行できるよ」
桜「なるほど、わかりました!」

$ rails s

tmp01.png

桜「お馴染みのどこかの民族の人たちが見れました! 成功ですね!」
あやめ「Rails6系だから、このトップページだよね。7系はRailsのロゴが表示されるシンプルなものになったよね」
桜「私は6系のこのトップページが好きです。なんか楽しそうじゃないですか?」
あやめ「たしかに。Rubyはプログラミングの楽しさを重視してるから、そういう意味でRails6系のトップページはRubyっぽかったかもしれないね」

桜「さぁ、ここからどうしましょう?」
あやめ「そうだね、まずはテスト、入れたくない?」
桜「え、テスト書くんですか? サンプルアプリなのにめんどくさくないですか?」
あやめ「桜ちゃん、それ@t_w〇daさんの前でも同じこと言える?」
桜「言えないです……」
あやめ「じゃあ、入れよっか」
桜「あやめ先輩、笑顔が怖いです……」

Gemfile
group :development, :test do
  gem 'factory_bot_rails'
  gem 'rspec-rails'
end
$ bundle install

あやめ「さて、じゃあ簡単な投稿機能を作っていこっか。まずはじめにツイートのコントローラを作っていこう。RailsはCoCに則ってコマンドで必要なファイルを簡単に作れるようになってるから、コマンドを叩いていくね」

$ rails generate controller Tweets

# 以下が出力されます
create  app/controllers/tweets_controller.rb
  route  get 'tweets/index'
invoke  erb
create    app/ビューs/tweets
create    app/ビューs/tweets/index.html.erb
invoke  rspec
create    spec/requests/tweets_spec.rb
create    spec/ビューs/tweets
create    spec/ビューs/tweets/index.html.erb_spec.rb
invoke  helper
create    app/helpers/tweets_helper.rb
invoke    rspec
create      spec/helpers/tweets_helper_spec.rb
invoke  assets
invoke    scss
create      app/assets/stylesheets/tweets.scss

桜「わわっ、なんかいっぱい出てきましたね!」
あやめ「コントローラーとビュー、ルーティング、テスト、ヘルパーが自動生成されたんだね。便利だよね」
桜「でも、これって全部必要なんですか?」
あやめ「良い質問だね。Railsは至れり尽くせりだけど、必要だと思わないものは自分の判断で使わないことだってできるよ。config/application.rbから諸々設定できるからしておこっか」

config/application.rb
module TwitterClone
  class Application < Rails::Application
    config.generators do |g|
      g.orm :active_record
      g.test_framework :rspec, fixture: true, ビュー_specs: false, helper_specs: false
      g.fixture_replacement :factory_bot, dir: "spec/factories"
      g.stylesheets false
      g.javascripts false
      g.assets false
      g.helper false
    end
  end
end

あやめ「これで不要なテストファイルとかcss, javascript、ヘルパーは作られないようになったよ。試しに新しいファイルを作ってみるね」

$ rails generate controller Tests index

# 以下が出力されます
create  app/controllers/tests_controller.rb
  route  get 'tests/index'
invoke  erb
create    app/ビューs/tests
create    app/ビューs/tests/index.html.erb
invoke  rspec
create    spec/requests/tests_spec.rb

桜「おー、だいぶすっきりしましたね!」
あやめ「そうでしょ。ちなみに、generateのかわりにdestroyで作ったファイルを削除できるから、今作ったものは削除しておくね」

$ rails destroy controller Tests index

# 以下が出力されます
remove  app/controllers/tests_controller.rb
  route  get 'tests/index'
invoke  erb
remove    app/ビューs/tests
remove    app/ビューs/tests/index.html.erb
invoke  rspec
remove    spec/requests/tests_spec.rb

あやめ「じゃあ、とりあえずルーティングちょっと変えてブラウザからリクエスト送ってみよっか」

config/routes.rb
Rails.application.routes.draw do
  root "tweets#index"

  get "/tweets", to: "tweets#index"
end

あやめ「rails serverに表示されてるIPとポートにリクエストを送るよ。私たちの場合は127.0.0.1:3000だね」

tmp02.png

桜「わー、古き良きHTMLがレンダリングされてますね!」
あやめ「サーバから静的HTMLファイルが返されてるねー。じゃあ、次はツイートのモデルを作って取得してみよっか」

$ rails generate model Tweet

# 以下が出力されます
invoke  active_record
create    db/migrate/20220923034247_create_tweets.rb
create    app/models/tweet.rb
invoke    rspec
create      spec/models/tweet_spec.rb
invoke      factory_bot
create        spec/factories/tweets.rb

桜「これは何ができたんですか?」
あやめ「マイグレーションファイルとモデル、モデルのテストファイルとfactory_botが生成されたね。TweetテーブルはUserテーブルとアソシエーションを持つから、Userテーブルも作っておこう」

$ rails generate model User

あやめ「これでテーブルだけのマイグレーションファイルができたから、スキーマを追記していこっか」

db/migrate/_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[6.1]
  def change
    create_table :tweets do |t|
      t.integer :user_id, null: false
      t.text    :body

      t.timestamps
    end
  end
end
db/migrate/_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :name,        null: false
      t.string :screen_name, null: false

      t.timestamps
    end
  end
end

あやめ「このマイグレーションファイルを元に、スキーマファイルを生成しよう」

$ rails db:migrate

桜「おっ! db/schema.rbにスキーマファイルが生成されましたね!」
あやめ「rails db:migrateを実行したら、DBにも同期されるから、MySQLに入ってもテーブルが作成されたことが確認できるね」
あやめ「じゃあ、実際にデータを作っていこう。ビューを作ってないから、rails consoleからレコードを作ってもいいんだけど、再利用するからもしれないから、db/seeds.rbに書いていこうか」
桜「seeds.rbに書いたらあとでレコードとして読み込めるんですか?」
あやめ「うん。rails db:seedでDBにレコードを反映させることができるんだよ」
桜「なるほどですねー」
あやめ「じゃあ、さっそくデモデータを投入していこっか。書くコードはrails consoleに書くものと同じだね」

db/seeds.rb
User.create!(name: 'sakura', screen_name: '@sakura')
User.create!(name: 'ayame', screen_name: '@ayame')

Tweet.create!(user_id: User.find_by(name: 'sakura').id, body: 'お腹すいた')
Tweet.create!(user_id: User.find_by(name: 'ayame').id, body: '週休三日が良い')
$ rails db:seed

あやめ「ちゃんとレコードが保存されてるかどうかrails consoleを使って確認しよう。rails consoleはrails serverみたいにrails cで省略して実行できるよ」

$ rails c

irb(main):001:0> Tweet.all
=> #<ActiveRecord::Relation [#<Tweet id: 2, user_id: 4, body: "お腹すいた", created_at: "2022-09-23 07:16:56.712641000 +0000", updated_at: "2022-09-23 07:16:56.712641000 +0000">, #<Tweet id: 3, user_id: 5, body: "週休三日が良い", created_at: "2022-09-23 07:16:56.721109000 +0000", updated_at: "2022-09-23 07:16:56.721109000 +0000">]>

桜「ちゃんとレコードが登録されてましたね! あやめ先輩週休三日が良いんですか?」
あやめ「え? うん。週休三日だと嬉しいし、ほぼリモートで月一だけの物理出社だったらなお嬉しいかな」
桜「引きこもりじゃないですか~」
あやめ「うん。お家最高だからね」

あやめ「じゃあ、作ったレコードをERBから見れるようにしていこう。まずはコントローラーのテスト書こっか。まずはRSpecが動作するように設定していこう」

$ rails generate rspec:install
Gemfile
group :test do
  gem 'rails-controller-testing'
end
spec/rails_helper.rb
# RSpecファイルでFactoryBotを省略できるようにする
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

あやめ「これでテストの設定は終わり。次にテストコードを書いていくよ」

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'test' }
    screen_name { '@test' }
  end
end
spec/factories/tweets.rb
FactoryBot.define do
  factory :tweet do
    association :user, factory: :user
    body { 'test tweet' }
  end
end
spec/requests/tweets_spec.rb
require 'rails_helper'

RSpec.describe TweetsController, type: :request do
  let!(:tweets) { create_list(:tweet, 2) }

  describe "GET #index" do
    it "assigns @tweets" do
      get tweets_path
      expect(response).to have_http_status(200)
      expect(assigns(:tweets)).to eq(tweets)
    end
  end
end

桜「ふえぇ。なにを書いてるのかまったくわからないですぅ」
あやめ「テストコードって、はじめはとっつきにくいよね。順番に説明していくね。まず最初の二つがfactory_botでテストデータを作ったの。次はコントローラーのテストで、RSpecのrequest specを書いたんだよ」
桜「へー。なんとなくですけど、factory do endのブロックの中でテスト用の変数を定義してる感じなんですか?」
あやめ「そうだね。作成した:userとか:tweetは、他のテストコードから暗黙的に参照することができるよ」
桜「Rails宣言的じゃなくて、ぜんぶ暗黙的だから怖いですぅ」
あやめ「オートロードだね。Railsは外部ライブラリではない自分が作成したファイルは暗黙的に読み込まれるようになっているからね」
桜「ふーん。じゃあ、テストコードのcreate_list(:tweet, 2):tweetはfactory_botで作成した変数を参照してるんですか?」
あやめ「そういうことになるね」
桜「最初にあるlet!(:tweets)って、なんですか? 変数を定義してるっぽいですけど。!が付いてるってことは破壊的メソッドなんですかね?」
あやめ「テスト用の変数定義まではあってるね。ブロックの中で書いてある処理を通して()の中の変数が定義されるの。!は今回の場合、破壊的メソッドではなくて、事前評価だね。letだと遅延評価になるから、expect().toの評価時に変数が定義されてなくてテストが通らないんだよ」
桜「itブロックの中の処理はどういう意味なんですか?」
あやめ「まずtweets_path、今回だったら/tweetsだね。ここにGETリクエストを送って、結果がresponseっていう変数に格納されるの。最初のexpect().toの行ではレスポンスのHTTPステータスコードが200であることをテストしてるんだね。次の行はassigns()の括弧内にあるのがコントローラーがビューに送るインスタンス変数で、今回なら@tweetsがビューに送られているかをテストしているんだね」
桜「なるほどー。じゃあ、今はコントローラーもビューも特に何も書いてないんでテストは落ちますよね?」
あやめ「うん。落ちるね。試しにテストが落ちてREDになることを確認してみよっか」

$ bundle exec rspec ./spec/requests/tweets_spec.rb

# ./spec/requests/tweets_spec.rbで以下が出力
Failures:

  1) TweetsController GET #index assigns @tweets
      Failure/Error: expect(assigns(:tweets)).to eq(tweets)

        expected: [#<Tweet id: 21, user_id: 21, body: "test tweet", created_at: "2022-09-23 14:48:05.014541000 +0000", ...eated_at: "2022-09-23 14:48:05.020593000 +0000", updated_at: "2022-09-23 14:48:05.020593000 +0000">]
            got: nil

        (compared using ==)
      # ./spec/requests/tweets_spec.rb:10:in `block (3 levels) in <main>'

桜「Tweetの配列をexpectしたのに、結果がnilだったので落ちてるんですね。じゃあ、コントローラーで@tweetsを返してあげればテストが通ってGREENになるんですね!?」
あやめ「そうだね。じゃあ、テストを通していこっか」

app\controllers\tweets_controller.rb
class TweetsController < ApplicationController
  def index
    @tweets = Tweet.all
  end
end
$ bundle exec rspec ./spec/requests/tweets_spec.rb

桜「おっ、テスト通りました!」
あやめ「#indexのビューファイルもあるし、コントローラーが@tweetsを返してるからテストが正常に通ったね。じゃあ、ビューから@tweetsの内容が見れるようにしていこっか。まず、ビューの前にモデルのアソシエーションを書いていくよ」

app/models/user.rb
class User < ApplicationRecord
  has_many :tweets
end
app/models/tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user
end

桜「has_manyとかbelongs_toとか書いてあるのがアソシエーションってやつですか?」
あやめ「そうだね。usersテーブルはtweetsテーブルに対して1対Nの関係だからhas_many、逆にN対1の関係の場合はbelongs_toって書いてあげると、userからtweetをメソッドとして呼べるようになるんだね。tweetからuserも同じ。あと、has_manyの引数は複数形でbelongs_toの引数は単数形。この規約を守らないと正常に動かないから注意してね」
桜「コントローラーは複数形で、モデルは単数形みたいなやつですね。わかりました!」
あやめ「じゃあ、ビューを書いていこっか」

app\views\tweets\index.html.erb
<% @tweets.each do |tweet| %>
  <div>
    <h3>
      username: <%= tweet.user.name %>
    </h3>
    <p>
      body: <%= tweet.body %>
    </p>
  </div>
<% end %>

tmp03.png

桜「やっとビューで見れましたね!」
あやめ「よし、じゃあ、練習がてらツイートに対してCRUD操作を増やしていこっか」
桜「CRUDってなんでしたっけ?」
あやめ「Create, Read, Update, Deleteの略だね。データに対する基本的な操作の総称だね。今回はCreateで#create、Readで#show、Updateで#update、Deleteで#destroyのアクションをそれぞれ作っていこう」
桜「わかりました! まずはテストからですかね?」
あやめ「そうだね。コントローラーのテストから書いていこっか」

spec\requests\tweets_spec.rb
require 'rails_helper'

RSpec.describe TweetsController, type: :request do
  let!(:user) { create(:user) }

  describe "GET #index" do
    let!(:tweets) { create_list(:tweet, 2) }

    it "assigns @tweets" do
      get tweets_path
      expect(response).to have_http_status(200)
      expect(assigns(:tweets)).to eq(tweets)
    end
  end

  describe "POST #create" do
    let!(:params) { {user_id: user.id, body: "test tweet"} }

    it "create new tweet" do
      expect {
        post tweets_path, params: params
      }.to change(Tweet, :count).by(1)
    end
  end
end

桜「#createのテストを追記したんですね。expectメソッドって単純に引数を取るだけじゃなくて、ブロックでも書けるんですね! changeメソッドは何をしてるんですか?」
あやめ「Tweetテーブルのレコード数の増減をテストしてるんだね。引数はブロックでも書けて{ Tweet.count }としても同義だね。byは値の相対的な増減を見ていて、POSTの後にレコードが1つ増えることを意味しているね」
桜「なるほどぉ。今は#createアクションを作ってないから、テストは落ちるんですね」
あやめ「その通り。じゃあ、REDになることを確認してみよう」

$ bundle exec rspec ./spec/requests/tweets_spec.rb

# 以下が出力されます
     Failure/Error: post tweets_path, params: params

     AbstractController::ActionNotFound:
       The action 'create' could not be found for TweetsController    
       Did you mean?  index

桜「予想通り#createがないって怒られてますね。さっそくコントローラーに追記していきましょう!」
あやめ「コントローラーだけだと、POSTが受け取れないから、ルーティングも追記していきましょうね」

app/controllers/tweets_controller.rb
# 追記
  def create
    Tweet.create!(user_id: params[:user_id], body: params[:body])
  end
config/routes.rb
Rails.application.routes.draw do
  root "tweets#index"

  resources :tweets
end

桜「ルーティングのresourcesって何ですか? HTTPリクエストメソッドでもないですよね?」
あやめ「リソースフルルーティングだね。これでGETPOSTPATCHDELETEを一括でルーティング登録することができるの」
桜「便利ですね! じゃあ、これでテストも通るんですね!」
あやめ「うん。通ると思うから回してみてね」
あやめ「じゃあ、ブラウザからPOSTリクエストを送れるようにしていこっか。まずはビューにフォームを作っていこう」

app/views/tweets/index.html.erb
<% @tweets.each do |tweet| %>
  <div>
    <h3>
      ユーザ名: <%= tweet.user.name %>
    </h3>
    <p>
      ツイート: <%= tweet.body %>
    </p>
  </div>
<% end %>

<form action="/tweets" method="post">
  <input type="hidden" name="authenticity_token" value="test_token">

  <div>
    <label for="tweet_user_name">ユーザ名: </label>
    <input type="text" id="tweet_user_name" name="name">
  </div>
  <div>
    <label for="tweet_body">ツイート: </label>
    <input type="text" id="tweet_body" name="body">
  </div>
  <div>
    <input type="submit" value="ツイートを投稿する" name="commit">
  </div>
</form>

あやめ「このformタグの部分はform_withヘルパーメソッドを使った方が、後々#updateとか作る時に便利だから、ちょっと書き換えるね」
桜「そんなものがあるんですね。あと、inputタグのnameがuser_idじゃなくてnameなんですか? これじゃあコントローラーで受け取るparamsと違うので失敗しますよね?」
あやめ「そうだね。だから、コントローラーと、あとメソッドをモデルに書いていこう」

app/views/tweets/index.html.erb
<!-- formタグの部分を以下に変更 -->
<%= form_with model: @tweet, url: tweets_path do |form| %>
  <div>
    <%= form.label :name, "ユーザ名: " %>
    <%= form.text_field :name %>
  </div>
  <div>
    <%= form.label :body, "ツイート: " %>
    <%= form.text_field :body %>
  </div>
  <div>
    <%= form.submit "ツイートを投稿する" %>
  </div>
<% end %>
app/controllers/tweets_controller.rb
# 変更
  def create
    user = User.find_by_name(params[:name])
    if user
      Tweet.create!(user_id: user.id, body: params[:body])
    else
      raise "ユーザが存在しません"
    end
  end
app/models/user.rb
class User < ApplicationRecord
  has_many :tweets

  class << self
    def find_by_name(name)
      User.find_by(name: name)
    end
  end
end
spec/requests/tweets_spec.rb
# 変更
  describe "POST #create" do
    let!(:params) { {name: user.name, body: "test tweet"} }

    it "create new tweet" do
      expect {
        post tweets_path, params: params
      }.to change(Tweet, :count).by(1)
    end
  end

tmp04.png

桜「おー! 私のツイートが作成されました~。私、三大欲求関連のツイートしかしない人みたいじゃないですか笑」
あやめ「桜ちゃんのTwitterは思考ダンプどころか脊髄反射だからね笑」
桜「そんな~泣」
あやめ「まあまあ、次は#updateを作っていこっか」
桜「はい! じゃあ、テストを書いていきましょう!」

spec/requests/tweets_spec.rb
# 追記
  describe "PATCH #update" do
    let!(:tweet) { create(:tweet) }
    let!(:params) { {name: user.name, tweet: {body: "change tweet"}} }

    it "update tweet" do
      patch tweet_path(tweet.id), params: params
      expect(response[:body]).to eq(params[:body])
    end
  end

桜「テストを回してREDになることが確認できました!」

$ bundle exec rspec ./spec/requests/tweets_spec.rb

# 以下が出力されます
Failures:
     Failure/Error: patch tweet_path(tweet.id), params: params

     AbstractController::ActionNotFound:
       The action 'update' could not be found for TweetsController
       Did you mean?  create
                      index

あやめ「じゃあ、エラー通り、まずはコントローラーに#updateアクションを追加していこっか」

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController

  before_action :set_tweet, only: %i(edit update)

  def index
    @tweets = Tweet.all
  end

  def create
    user = User.find_by_name(params[:name])
    if user
      Tweet.create!(user_id: user.id, body: params[:body])
      redirect_to tweets_path
    else
      raise "ユーザが存在しません"
    end
  end

  def edit
  end

  def update
    @tweet.update!(body: params[:tweet][:body])
    redirect_to tweets_path
  end

  private

  def set_tweet
    @tweet = Tweet.find(params[:id])
  end

end

桜「#edit#updateが増えたのはわかりますが、privateの下のset_tweetメソッドは何ですか?」
あやめ「これは#editと#updateの共通処理だね。どっちも@tweetを作成するから、それをまとめてるの。before_actionの引数でメソッドを指定してあげて、onlyオプションで該当するアクションと指定しているの」
桜「%i(edit update)は何ですか?」
あやめ「これはRubyの%記法と呼ばれるもので、%i()で括弧内の変数をシンボルの配列として返すの」
桜「なるほど。じゃあ%i(edit update) = [:edit, :update]なんですね」
あやめ「そういうことだね。じゃあ、ビューを書いていこっか。ビューは一覧表示用のindex.html.erbに加えて更新用のedit.html.erbを作っていこう」

app/views/tweets/index.html.erb
<% @tweets.each do |tweet| %>
    <h3>
      ユーザ名: <%= tweet.user.name %>
    </h3>
    <p>
      <%= link_to "ツイート: #{tweet.body}", edit_tweet_path(tweet) %>
    </p>
<% end %>

<%= form_with model: @tweet, url: tweets_path do |form| %>
  <div>
    <%= form.label :name, "ユーザ名: " %>
    <%= form.text_field :name %>
  </div>
  <div>
    <%= form.label :body, "ツイート: " %>
    <%= form.text_field :body %>
  </div>
  <div>
    <%= form.submit "ツイートを投稿する" %>
  </div>
<% end %>
app/views/tweets/edit.html.erb
<%= form_with model: @tweet do |form| %>
  <div>
    <%= form.label :body, "ツイート: " %>
    <%= form.text_field :body %>
  </div>
  <div>
    <%= form.submit "ツイートを更新する" %>
  </div>
<% end %>

tmp05.png

桜「これでツイートの更新ができましたね! じゃあ、同じように最後に削除も追加していきましょう! まずはテスト書いていきますね」

spec/requests/tweets_spec.rb
# 追記
  describe "DELETE #destroy" do
    let!(:tweet) { create(:tweet) }

    it "destroy tweet" do
      expect {
        delete tweet_path(tweet.id)
      }.to change(Tweet, :count).by(-1)
    end
  end

あやめ「#updateと同様に、ルーティングだけあって、コントローラーに#destroyアクションを追加してないからREDになることを確認してみてね」
桜「確認できました! じゃあ、コントローラーにアクションを書いていきましょう!」

app/controllers/tweets_controller.rb
# 変更
  before_action :set_tweet, only: %i(edit update destroy)

# 追記
  def destroy
    @tweet.destroy
    redirect_to tweets_path
  end

桜「さっき、set_tweetメソッドを書きましたから、とてもスッキリした実装になりましたね。これでテストも通りますかね?」
あやめ「通ると思うよ。確認してみてね」

$ bundle exec rspec ./spec/requests/tweets_spec.rb

# 以下が出力されます
....

Finished in 5.15 seconds (files took 38.77 seconds to load)
4 examples, 0 failures

桜「通りました! GREENです!」
あやめ「よし、じゃあ、最後にビューから削除できるようにしていこっか」

app/views/tweets/index.html.erb
<% @tweets.each do |tweet| %>
    <h3>
      ユーザ名: <%= tweet.user.name %>
    </h3>
    <p>
      <%= link_to "ツイート: #{tweet.body}", edit_tweet_path(tweet) %>
      <!-- 追記 -->
      <%= link_to "削除する", tweet_path(tweet), method: :delete,
        data: {confirm: "ツイートを削除してよろしいですか?"}
      %>
    </p>
<% end %>

<%= form_with model: @tweet, url: tweets_path do |form| %>
  <div>
    <%= form.label :name, "ユーザ名: " %>
    <%= form.text_field :name %>
  </div>
  <div>
    <%= form.label :body, "ツイート: " %>
    <%= form.text_field :body %>
  </div>
  <div>
    <%= form.submit "ツイートを投稿する" %>
  </div>
<% end %>

tmp06.png

桜「おー、削除できました!」
あやめ「お疲れ様! これで最低限は実装できたね。あとはビューのデザインを変えたいし、ユーザ機能作ってないから作らないといけないし、返信とかいいねとかリツイートとか、通知とかリストとか作っていきたいよね~」
桜「あ、それ、絶対1ヶ月以上かかるやつじゃないですか……」
あやめ「まあ、ちゃんと作るとそうかもね」
桜「桜、急用を思い出したので今日はこの辺で帰らさせていただきます!」
あやめ「あ! ちょっと桜ちゃん!?」

あやめ「もぅ、あの子ったら……」

~了~

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0