この講座は、Web業界未経験の吉野 桜と、その先輩である堀切 あやめのかわいさをお楽しみいただくため、邪魔にならない程度の差し障りのない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の設定ファイルを書いていこっか」
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 server
はrails s
でも実行できるよ」
桜「なるほど、わかりました!」
$ rails s
桜「お馴染みのどこかの民族の人たちが見れました! 成功ですね!」
あやめ「Rails6系だから、このトップページだよね。7系はRailsのロゴが表示されるシンプルなものになったよね」
桜「私は6系のこのトップページが好きです。なんか楽しそうじゃないですか?」
あやめ「たしかに。Rubyはプログラミングの楽しさを重視してるから、そういう意味でRails6系のトップページはRubyっぽかったかもしれないね」
桜「さぁ、ここからどうしましょう?」
あやめ「そうだね、まずはテスト、入れたくない?」
桜「え、テスト書くんですか? サンプルアプリなのにめんどくさくないですか?」
あやめ「桜ちゃん、それ@t_w〇daさんの前でも同じこと言える?」
桜「言えないです……」
あやめ「じゃあ、入れよっか」
桜「あやめ先輩、笑顔が怖いです……」
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
から諸々設定できるからしておこっか」
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
あやめ「じゃあ、とりあえずルーティングちょっと変えてブラウザからリクエスト送ってみよっか」
Rails.application.routes.draw do
root "tweets#index"
get "/tweets", to: "tweets#index"
end
あやめ「rails serverに表示されてるIPとポートにリクエストを送るよ。私たちの場合は127.0.0.1:3000
だね」
桜「わー、古き良き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
あやめ「これでテーブルだけのマイグレーションファイルができたから、スキーマを追記していこっか」
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
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に書くものと同じだね」
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
group :test do
gem 'rails-controller-testing'
end
# RSpecファイルでFactoryBotを省略できるようにする
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
あやめ「これでテストの設定は終わり。次にテストコードを書いていくよ」
FactoryBot.define do
factory :user do
name { 'test' }
screen_name { '@test' }
end
end
FactoryBot.define do
factory :tweet do
association :user, factory: :user
body { 'test tweet' }
end
end
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になるんですね!?」
あやめ「そうだね。じゃあ、テストを通していこっか」
class TweetsController < ApplicationController
def index
@tweets = Tweet.all
end
end
$ bundle exec rspec ./spec/requests/tweets_spec.rb
桜「おっ、テスト通りました!」
あやめ「#indexのビューファイルもあるし、コントローラーが@tweetsを返してるからテストが正常に通ったね。じゃあ、ビューから@tweetsの内容が見れるようにしていこっか。まず、ビューの前にモデルのアソシエーションを書いていくよ」
class User < ApplicationRecord
has_many :tweets
end
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の引数は単数形。この規約を守らないと正常に動かないから注意してね」
桜「コントローラーは複数形で、モデルは単数形みたいなやつですね。わかりました!」
あやめ「じゃあ、ビューを書いていこっか」
<% @tweets.each do |tweet| %>
<div>
<h3>
username: <%= tweet.user.name %>
</h3>
<p>
body: <%= tweet.body %>
</p>
</div>
<% end %>
桜「やっとビューで見れましたね!」
あやめ「よし、じゃあ、練習がてらツイートに対してCRUD操作を増やしていこっか」
桜「CRUDってなんでしたっけ?」
あやめ「Create, Read, Update, Deleteの略だね。データに対する基本的な操作の総称だね。今回はCreateで#create
、Readで#show
、Updateで#update
、Deleteで#destroy
のアクションをそれぞれ作っていこう」
桜「わかりました! まずはテストからですかね?」
あやめ「そうだね。コントローラーのテストから書いていこっか」
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が受け取れないから、ルーティングも追記していきましょうね」
# 追記
def create
Tweet.create!(user_id: params[:user_id], body: params[:body])
end
Rails.application.routes.draw do
root "tweets#index"
resources :tweets
end
桜「ルーティングのresources
って何ですか? HTTPリクエストメソッドでもないですよね?」
あやめ「リソースフルルーティングだね。これでGET
、POST
、PATCH
、DELETE
を一括でルーティング登録することができるの」
桜「便利ですね! じゃあ、これでテストも通るんですね!」
あやめ「うん。通ると思うから回してみてね」
あやめ「じゃあ、ブラウザからPOSTリクエストを送れるようにしていこっか。まずはビューにフォームを作っていこう」
<% @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と違うので失敗しますよね?」
あやめ「そうだね。だから、コントローラーと、あとメソッドをモデルに書いていこう」
<!-- 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 %>
# 変更
def create
user = User.find_by_name(params[:name])
if user
Tweet.create!(user_id: user.id, body: params[:body])
else
raise "ユーザが存在しません"
end
end
class User < ApplicationRecord
has_many :tweets
class << self
def find_by_name(name)
User.find_by(name: name)
end
end
end
# 変更
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
桜「おー! 私のツイートが作成されました~。私、三大欲求関連のツイートしかしない人みたいじゃないですか笑」
あやめ「桜ちゃんのTwitterは思考ダンプどころか脊髄反射だからね笑」
桜「そんな~泣」
あやめ「まあまあ、次は#updateを作っていこっか」
桜「はい! じゃあ、テストを書いていきましょう!」
# 追記
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アクションを追加していこっか」
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
を作っていこう」
<% @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 %>
<%= form_with model: @tweet do |form| %>
<div>
<%= form.label :body, "ツイート: " %>
<%= form.text_field :body %>
</div>
<div>
<%= form.submit "ツイートを更新する" %>
</div>
<% end %>
桜「これでツイートの更新ができましたね! じゃあ、同じように最後に削除も追加していきましょう! まずはテスト書いていきますね」
# 追記
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になることを確認してみてね」
桜「確認できました! じゃあ、コントローラーにアクションを書いていきましょう!」
# 変更
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です!」
あやめ「よし、じゃあ、最後にビューから削除できるようにしていこっか」
<% @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 %>
桜「おー、削除できました!」
あやめ「お疲れ様! これで最低限は実装できたね。あとはビューのデザインを変えたいし、ユーザ機能作ってないから作らないといけないし、返信とかいいねとかリツイートとか、通知とかリストとか作っていきたいよね~」
桜「あ、それ、絶対1ヶ月以上かかるやつじゃないですか……」
あやめ「まあ、ちゃんと作るとそうかもね」
桜「桜、急用を思い出したので今日はこの辺で帰らさせていただきます!」
あやめ「あ! ちょっと桜ちゃん!?」
あやめ「もぅ、あの子ったら……」
~了~