ハンズオン形式でRails newからCRUD操作の基礎を学習しよう
本記事の対象:Ruby on Railsの初学者
現在、「アプレンティス」の2期生として、プログラミングの学習しています。
Railsで関連テーブルの扱い方に苦労したので、学習も兼ねて記事としてまとめました。
本記事のゴール
例として、このようなテーブル構成の簡易ブログ投稿サイトを1から作成します。
機能としては、以下を含みます。
- 記事の投稿、編集、削除機能
- ユーザー登録、ログイン機能
本記事では、その第一弾として、記事の投稿、編集、削除機能を実装していきます。
タイトルのCRUD操作とは、(Create(作成)、Read(参照)、Update(更新)、Delete(削除))を指しており、データベースの基本操作の総称です。
まえがき
本記事では、Ruby on Railsチュートリアル (以下、Railsチュートリアル)のようなハンズオン形式で構成されています。
Railsチュートリアルよりも、内容を簡潔にし難易度を下げていますが、以下のような知識がなければ理解が難しいかもしれません。
- Rubyの基本的な構文
- MVCモデルとは?
- Sessionとは?
それでも、書いてある順番にコードを書いていけば完成するようにはなっています。
また、自分が理解に苦しんだ部分は、解説を入れたり、コードの反映を確認しながら進めていきます。
本記事が、これからRailsを学ぶ方の助けになれば幸いです。
STEP1 環境構築
すでにrailsがインストールされていることを前提としています。
- railsアプリを作成
rails _7.0.4.3_ new many_tables --database=mysql
newの後に来るのはアプリ名です。
好きな名前で大丈夫です。
- 作成したディレクトリへ移動
cd many_tables
- データベースを作成
rails db:create
- railsサーバーを起動
ralils server
- http://localhost:3000 にアクセスして、以下のように表示されたらOK
STEP2-1 Articleモデルの作成
rails generate model Article title:string body:text
db/migrate
に以下のようなマイグレーションファイルが作成されます。(ファイル名は作成日時で変わります)
class CreateArticles < ActiveRecord::Migration[7.0]
def change
create_table :articles do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
データベースに反映させます。
rails db:migrate
db/schema.rb
に以下のようなファイルが作成されます。
ActiveRecord::Schema[7.0].define(version: 2023_12_23_081013) do
create_table "articles", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "title"
t.string "body"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
以下のコードで、データベースを覗いてみます。
rails dbconsole
データは作成していないので、カラム情報を見てみます。
mysql > show columns from articles;
以下のように表示されていれば成功です。
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| title | varchar(255) | YES | | NULL | |
| body | text | YES | | NULL | |
| created_at | datetime(6) | NO | | NULL | |
| updated_at | datetime(6) | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
STEP2-2 Articleコントローラーの作成
コンソールで以下のコマンドを実行します。
rails generate controller articles index show new edit
すると、app/controllers/
にarticles_controller.rbが追加されます。
class ArticlesController < ApplicationController
def index
end
def show
end
def new
end
def edit
end
end
また、routes.rbに以下のような記述が追加されます。
Rails.application.routes.draw do
get 'articles/index'
get 'articles/show'
get 'articles/new'
get 'articles/edit'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
end
また、app/views/article/
に、index.html.erb
,show.html.erb
,new.html.erb
,edit.html.erb
の4つのビューファイルが作成されます。
ここで、http://localhost:3000/article/index にアクセスしてみると、以下のように表示されるはずです。
STEP2-3 Article関連のページを作成
一度、config/routes.rb
を編集していきます。
現在、以下のようになっています。
Rails.application.routes.draw do
get 'article/index'
get 'article/show'
get 'article/new'
get 'article/edit'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
end
ルーティングの設定がどうなっているか確認するときは、ターミナルで、
rails routes
# または
rails routes | grep articles
としてもいいですが、ブラウザで確認するほうが見やすいので、
http://localhost:3000/rails/info/routes にアクセスしてみましょう。
そうすると、このような画面が表示されるはずです。
下に長く表示されていると思いますが、今見たいのは一番上の4行(画像に示している部分)です。
例えば一番上のindex
に対するルーティングでは、Pathの/article/index
にアクセスすると、controllerのindex
メソッドが呼び出されて、app/views/article/index.html.erb
の内容が、表示されるという仕組みになっています。
config/routes.rb
に戻って、
これは、
get 'articles/index'
これを省略した書き方です。
get 'articles/index', to: 'articles#index'
articles/index
にGETリクエストが来たら、articlesコントローラーのindexメソッドを呼び出すという意味です。
ここで、以下のように記述してみてください。
Rails.application.routes.draw do
- get 'articles/index'
- get 'articles/show'
- get 'articles/new'
- get 'articles/edit'
- # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
- # Defines the root path route ("/")
- # root "articles#index"
+ resources :articles
end
再び、http://localhost:3000/rails/info/routes にアクセスしてみましょう。
そうすると、先程と少し記述が変わっています。
この一行だけで、基本的なルーティングをすべて作成してくれる便利な記述方法です。
ついでに、以下の記述もしておきましょう。
Rails.application.routes.draw do
+ root 'articles#index'
resources :articles
end
こうすることで、http://localhost:3000 にアクセスしたときに、articlesコントローラーのindexメソッドが呼ばれるようになります。
最初に表示されていたrailsのデフォルト画面ではなくなっていることを確認してみてください。
続いて、viewファイルを書いていきます。
今回はHTML、CSSの書き方は重要ではないので、以下の内容をコピペでOKです。
css
* {
margin: 0;
padding: 0;
color: #243F53;
}
body {
max-width: 1080px;
margin: 0 auto;
}
h1 {
text-align: center;
letter-spacing: 0.2em;
margin: 50px 0;
}
li {
list-style: none;
}
a {
text-decoration: none;
}
header {
padding: 20px;
display: flex;
justify-content: space-between;
}
.header-left {
font-size: 20px;
font-weight: bold;
}
.header-right {
font-size: 16px;
}
header ul {
display: flex;
}
header ul li {
margin-right: 20px;
}
header ul li:last-child {
margin-right: 0;
}
.article {
padding: 30px;
border-top: 1px solid #ccc;
}
.article-bottom {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.article-title {
font-size: 28px;
margin-bottom: 10px;
}
.author-name {
font-size: 16px;
font-weight: bold;
}
.posted-date {
font-size: 10px;
color: #aaa;
}
.tags {
display: flex;
height: 25px;
}
.tag {
font-size: 10px;
padding: 5px 10px;
margin-right: 10px;
background: #243F53;
color: #fff;
border-radius: 20px;
}
.article-body {
font-size: 28px;
margin: 30px 0;
}
.button {
display: flex;
}
.edit-button, .delete-button {
text-align: center;
border-radius: 10px;
padding: 5px;
width: 60px;
}
.edit-button {
margin-right: 20px;
border: 2px solid #3BB371;
color: #3BB371;
}
.delete-button {
border: 2px solid #DC143B;
color: #DC143B;
}
/* form */
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
form {
display: flex;
flex-direction: column;
align-items: center;
}
.form-container {
padding: 0 20px;
}
.form-item {
width: 100%;
}
.form-label {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
padding: 0 10px;
}
.form-input {
font-size: 16px;
padding: 10px;
border: 2px solid #243F53;
border-radius: 5px;
}
input:focus, textarea:focus {
outline: 1px solid #18364b;
}
.submit-button {
border: none;
border-radius: 10px;
font-size: 18px;
font-weight: bold;
background: #243F53;
color: #fff;
padding: 10px;
width: 150px;
cursor: pointer;
}
button {
background: #fff;
font-size: 16px;
height: 100%;
}
index.html.erb
<h1>投稿記事一覧</h1>
<div class="article-container">
<div class="article">
<h2 class="article-title"><a href="/articles/1">タイトル</a></h2>
<div class="article-bottom">
<div class="author">
<p class="author-name">test user</p>
<p class="posted-date">2023/12/23</p>
</div>
<div class="tags">
<div class="tag">test1</div>
<div class="tag">test2</div>
</div>
</div>
</div>
</div>
show.html.erb
<h1>タイトル</h1>
<div class="article-container">
<div class="article">
<div class="author">
<p class="author-name">test user</p>
<p class="posted-date">2023/12/23</p>
</div>
<p class="article-body">
Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<div class="article-bottom">
<div class="tags">
<div class="tag">test1</div>
<div class="tag">test2</div>
</div>
<div class="button">
<a href="/articles/1/edit" class="edit-button">Edit</a>
<a href="" class="delete-button">Delete</a>
</div>
</div>
</div>
</div>
new.html.erb
<h1>新規投稿</h1>
<div class="form-container">
<form action="/articles" method="post">
<div class="form-item">
<label for="title" class="form-label">タイトル</label>
<input type="text" name="title" placeholder="タイトル" class="form-input">
</div>
<div class="form-item">
<label for="body" class="form-label">本文</label>
<textarea name="body" rows="10" placeholder="記事を入力" class="form-input"></textarea>
</div>
<input type="submit" value="投稿" class="submit-button">
</form>
</div>
edit.html.erb
<h1>記事編集</h1>
<div class="form-container">
<form action="/articles" method="post">
<div class="form-item">
<label for="title" class="form-label">タイトル</label>
<input type="text" name="title" placeholder="タイトル" class="form-input">
</div>
<div class="form-item">
<label for="body" class="form-label">本文</label>
<textarea name="body" rows="10" placeholder="記事を入力" class="form-input"></textarea>
</div>
<input type="submit" value="投稿" class="submit-button">
</form>
</div>
次にヘッダーを作成します。
<header>
<div class="header-left"><a href="/">Sample App</a></div>
<div class="header-right">
<nav>
<ul>
<li><a href="/post">Post</a></li>
</ul>
</nav>
</div>
</header>
application.html.erb
に以下のように記述することで、すべてのページでヘッダーが表示されます。
<!DOCTYPE html>
<html>
<head>
<title>ManyTables</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
+ <%= render 'layouts/header' %>
<%= yield %>
</body>
</html>
このとき、ファイル名に注意してください。
_header.html.erb
のようにファイル名の先頭に_(アンダースコア)
をつけることで部分テンプレートと認識してくれます。
http://localhost:3000 にアクセスして、以下のように表示されていれば成功です。
最後に、ルーティング設定を行います。
Rails.application.routes.draw do
root 'articles#index'
+ get '/post', to: 'articles#new'
resources :articles
end
STEP2-4 記事一覧表示
まずは、データベースにテストデータを入れます。
rails dbconsole
このコマンドで、データベースにアクセスできますが、Railsにはテストデータを一括で作成できる機能があるので、それを使っていきましょう。
そのために、faker
というgemをインストールします。
~~~
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mingw x64_mingw ]
+ gem "faker"
end
~~~
group :development, :test do
という記述部分を見つけて、こののように記述してください。
コンソールでこのコマンドを実行します。
bundle install
こうすることで、gemのインストールができます。
このgemを使用すると、ランダムな名前、住所、文章などを用意することができます。
詳しく知りたい方は以下の公式ドキュメントを参考にしてみてください。
https://github.com/faker-ruby/faker
それでは、テストデータの作成をはじめましょう。
seed.rbというファイルに記述します。
20.times do
Article.create!(
title: Faker::Lorem.sentence,
body: Faker::Lorem.paragraph(sentence_count: 10)
)
end
次にコンソールで、
rails db:seed
このコマンドを実行します。
エラーが表示されなければ、テストデータの作成に成功しているはずです。
続いて、実際の保存されたデータをviewでも表示させてみましょう。
まず、コントローラーのindexメソッドにこのように記述します。
def index
+ @articles = Article.all
end
こうすることで、app/views/articles/index.html.erb
に、Article.all(Articleテーブルのすべてのデータ)
を@articles
という変数で渡すことができます。
続いて、index.html.erb
を修正します。
<h1>投稿記事一覧</h1>
<div class="article-container">
+ <% @articles.each do |article| %>
<div class="article">
<h2 class="article-title">
- <a href="/articles/1">タイトル</a>
+ <%= link_to article.title, article %>
</h2>
<div class="article-bottom">
<div class="author">
<p class="author-name">test user</p>
- <p class="posted-date">2023/12/23</p> %>
+ <%= tag.p "#{article.created_at}", class:"posted-date" %>
</div>
<div class="tags">
<div class="tag">test1</div>
<div class="tag">test2</div>
</div>
</div>
</div>
+ <% end %>
</div>
これで、https://localhost:3000 にアクセスしてみると、以下のように表示されるはずです。
articleテーブルの一覧が表示され、タイトルと投稿日時は、実際のデータになっています。
先ほども書いたように、@articles
には、articleテーブルの全データが配列として渡されていますので、eachの繰り返し処理の中で、個別のデータをarticle
として、取り出します。
<%= link_to article.title, article %>
これは、railsの書き方で、最終的には以下のようなhtmlに変換されます。
<a href="/articles/1">Qui tempore expedita aut.</a>
link_to
はメソッド名で、その後の記述は引数です。
rubyではメソッドの呼び出しで()を省略できるため、このような書き方になります。
つまり、
<%= link_to(article.title, article) %>
と書くのと同じ意味になります。
続いて、引数について解説します。
link_toメソッドの引数は、
- 第1引数→aタグ内のテキスト
- 第2引数→href属性
となります。
第1引数のarticle.title
はそのまま、titleカラムの値を取得しています。
その後のarticle
については、理解しづらいと思いますが、
article_path(article)
を省略した書き方です。
http://localhost:3000/rails/info/routes でも確認できますが、article_path
とは、articlesコントローラーのshowメソッドに対して、GETリクエストを行うパス名を呼び出すための変数になります。(routes.rbでresources
と書いたことで使えるようになったものです)
これはhtmlになったときに、/articles/1
のように変換されます。1
はここではarticleテーブルのidを表します。
今はまだ、articlesコントローラーのshowメソッドの中身と、showのビューファイルを記述していないので、表示できませんが、最終的にこのリンクをクリックして、/articles/1
にアクセスすることで、showメソッドが呼ばれて、show.html.erbが出力され、idが1のarticleのデータが画面に表示されるようになります。
railsでは、article_path
みたいな書き方でパスを指定することが多いので、覚えて起きましょう。
続いて、以下の部分です。
<%= tag.p article.created_at, class:"posted-date" %>
tag.p
とすることで、htmlのpタグに変換されます。使い方は、先程のlink_to
とほぼ同じで、引数に表示させるテキストarticle.created_at
を渡したり、class名を渡したりできます。
変換されて、
<p class="posted-date">2023-12-24 06:49:58 UTC</p>
のように表示されます。
ここで、日付が2023-12-24 06:49:58 UTC
と表示されていますが、実際には2023/12/24
と表示して欲しいです。
そこで、articleモデル内に、フォーマットを変換するための記述を行います。
class Article < ApplicationRecord
+ def formatted_created_at
+ created_at.strftime("%Y/%m/%d")
+ end
end
このように書くことで、articleモデルに対して、formatted_created_at
というメソッドを使うことができます。
続いて、ビューファイルの該当箇所を修正します。
- <%= tag.p article.created_at, class:"posted-date" %>
+ <%= tag.p article.formatted_created_at, class:"posted-date" %>
これで、http://localhost:3000にアクセスして、日付の表示形式が変更されていることを確認しましょう。
STEP2-5 記事詳細
続いて、showメソッドを記述して、記事の個別ページへアクセスし、内容が表示されるようにします。
まずは、コントローラーのshowメソッドを書きます。
def show
+ @article = Article.find(params[:id])
end
findメソッドは、引数にidを指定することで、そのデータを取得することができます。
params[:id]
とすることで、URLの/articles/1
の1をidとして取得してくれます。
次にビューを修正します。
- <h1>タイトル</h1>
+ <%= tag.h1 @article.title %>
<div class="article-container">
<div class="article">
<div class="author">
<p class="author-name">test user</p>
- <p class="posted-date">2023/12/23</p>
+ <%= tag.p @article.formatted_created_at, class:"posted-date" %>
</div>
- <p class="article-body">
- Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
- </p>
+ <%= tag.p @article.body, class:"article-body" %>
<div class="article-bottom">
<div class="tags">
<div class="tag">test1</div>
<div class="tag">test2</div>
</div>
<div class="button">
<a href="/articles/1/edit" class="edit-button">Edit</a>
<a href="" class="delete-button">Delete</a>
</div>
</div>
</div>
</div>
今回は、特に解説は不要かと思います。
showメソッドで定義した@article
を受け取って、titleやbodyを表示しています。
STEP2-6 投稿機能
コントローラーを定義します。
def new
+ @article = Article.new
end
これはArticleモデルのインスタンスを作成しています。
このインスタンスにformで入力されたデータを入れて、最終的に保存することになります。
続いて、ビューファイルを修正します。
<h1>新規投稿</h1>
<div class="form-container">
- <form action="/articles" method="post">
+ <%= form_with(model: @article) do |f| %>
<div class="form-item">
- <label for="title" class="form-label">タイトル</label>
+ <%= f.label :title, "タイトル", class: "form-label" %>
- <input type="text" name="title" placeholder="タイトル" class="form-input">
+ <%= f.text_field :title, placeholder: "タイトル", class: "form-input" %>
</div>
<div class="form-item">
- <label for="body" class="form-label">本文</label>
+ <%= f.label :body, "本文", class: "form-label" %>
- <textarea name="body" rows="10" placeholder="記事を入力" class="form-input"></textarea>
+ <%= f.text_area :body, rows: 10, placeholder: "記事を入力", class: "form-input" %>
</div>
- <input type="submit" value="投稿" class="submit-button">
+ <%= f.submit "投稿", class:"submit-button" %>
- </form>
+ <% end %>
</div>
まずform_with
メソッドを使用すると、formタグに変換されます。
ここで、model: @article
という引数を渡していますが、これは先程newメソッドに書いた@articleを指します。
@articleはArticleモデルのインスタンスですが、中身は空っぽです。
railsの仕様として、中身のないモデルがform_withに渡されたとき、formの内容をコントローラーへ送信時に、自動でcreateメソッド(後で定義します)へ振り分けてくれるようになっています。
f.label
はlabelタグを、f.text_field
はinputタグを、f.text_area
はtextareaタグを生成してくれます。
labelは「タイトル」「本文」と書かれている部分ですね。
text_field
とtext_area
の第1引数に渡されている、:title
と:body
は、htmlに変換されたときにname
属性となります。
例えば、titleのinput欄に「aaa」、bodyのtextareaに「bbb」と書いて投稿ボタンを押すと、コントローラーへ、
Parameters: {"authenticity_token"=>"[FILTERED]", "article"=>{"title"=>"aaa", "body"=>"bbb"}, "commit"=>"投稿"}
このようなパラメーターが渡されます。authenticity_tokenはrailsが勝手に入れているパラメーターで、安全性を高めるためだと思っておけば良いでしょう。
:title、:bodyはそれぞれ、
- :title→「"title"=>"aaa"」
- :body→「"body"=>"bbb"」
このようにハッシュのキーとして使われます。
一度、ブラウザの検証ツールで、どのようなhtmlに変換されているか確認してみると、わかりやすいかもしれません。
続いて、コントローラーにcreate
メソッドを定義します。
先ほども、解説しましたが、form_withに対して、中身が空のインスタンスを渡すと、自動でcreateメソッドで送信してくれるんでしたね。
def new
@article = Article.new
end
+ def create
+ @article = Article.new(article_params)
+ if @article.save
+ redirect_to @article
+ end
+ end
def edit
end
+ private
+ def article_params
+ params.require(:article).permit(:title, :body)
+ end
少しだけ処理が複雑になっていますが、一つずつ解説します。
まず、createメソッドを追加し、
@article = Article.new(article_params)
としました。引数になっているarticle_params
は、コントローラーの下のほうに定義しているプライベートメソッドです。
article_params
では、params.require(:article).permit(:title, :body)
としていますが、これは先程のパラメーター、
Parameters: {"authenticity_token"=>"[FILTERED]", "article"=>{"title"=>"aaa", "body"=>"bbb"}, "commit"=>"投稿"}
これの、"article"=>{"title"=>"aaa", "body"=>"bbb"}
のうち、
titleとbodyの値しか受け取りませんよ。という意味になります。
createメソッドに話を戻しますが、指定されたパラメーターを受け取って、saveメソッドで保存します。
saveメソッドは成功していればtrueを返すので、そのままif文の条件式に入れています。
保存に成功した場合、redirect_toメソッドで、指定したパスへ画面を遷移させます。
redirect_toの引数@article
は、前に解説したように、article_path(@article)
と同じ意味です。(保存された記事の詳細ページへリダイレクトされます)
これで新規投稿機能が完成しました。
実際に、http://localhost:3000/postへアクセスして、投稿してみましょう。
STEP2-7 編集機能
続いて、記事の編集機能を実装します。
まずコントローラーを編集します。
def edit
+ @article = Article.find(params[:id])
end
showメソッドと全く同じ書き方です。
editのパスもshowと同じくidが含まれるためです。
次にビューファイルを修正します。
<h1>記事編集</h1>
<div class="form-container">
- <form action="/articles" method="post">
- <div class="form-item">
- <label for="title" class="form-label">タイトル</label>
- <input type="text" name="title" placeholder="タイトル" class="form-input">
- </div>
- <div class="form-item">
- <label for="body" class="form-label">本文</label>
- <textarea name="body" rows="10" placeholder="記事を入力" class="form-input"></textarea>
- </div>
- <input type="submit" value="投稿" class="submit-button">
- </form>
+ <%= form_with(model: @article) do |f| %>
+ <div class="form-item">
+ <%= f.label :title, "タイトル", class: "form-label" %>
+ <%= f.text_field :title, placeholder: "タイトル", class: "form-input" %>
+ </div>
+ <div class="form-item">
+ <%= f.label :body, "本文", class: "form-label" %>
+ <%= f.text_area :body, rows: 10, placeholder: "記事を入力", class: "form-input" %>
+ </div>
+ <%= f.submit "投稿", class:"submit-button" %>
+ <% end %>
</div>
これは、先程のnew.html.erb
とほとんど同じです。
editメソッドでは、Article.find(params[:id])
としているため、すでにarticleの情報が含まれています。
このとき、一致するname属性のinputに、現在の値を入力してくれます。
http://localhost:3000/articles/1/edit
にアクセスすると、idが1の記事の情報が入力された、formが表示されるはずです。
また、form_withメソッドに中身の入ったモデルのインスタンスを渡すことで、送信時にupdateメソッドへ振り分けてくれるんでしたね。
そこで、updateメソッドをコントローラーに定義して、更新ができるようにしましょう。
def edit
@article = Article.find(params[:id])
end
+ def update
+ @article = Article.find(params[:id])
+ if @article.update(article_params)
+ redirect_to @article
+ end
+ end
createメソッドと似ていますが、まず更新する対象のarticleを、Article.find(params[:id])として、@articleに代入します。
formから送られてたarticle_params
を使ってupdateメソッドを実行します。
あとは、createメソッドと同じで、成功したら記事の個別ページへリダイレクトされます。
また、ここで記事の詳細ページを少し修正します。
<%= tag.h1 @article.title %>
<div class="article-container">
<div class="article">
<div class="author">
<p class="author-name">test user</p>
<%= tag.p @article.formatted_created_at, class:"posted-date" %>
</div>
<%= tag.p @article.body, class:"article-body" %>
<div class="article-bottom">
<div class="tags">
<div class="tag">test1</div>
<div class="tag">test2</div>
</div>
<div class="button">
- <a href="/articles/1/edit" class="edit-button">Edit</a>
+ <%= link_to "Edit", edit_article_path(@article), class:"edit-button" %>
<a href="" class="delete-button">Delete</a>
</div>
</div>
</div>
</div>
これで、記事の個別ページから編集フォームへ飛ぶことができます。
試しに、どれでもいいので記事を編集して、変更されていることを確認してみてください。
formの共通化
new.html.erb
とedit.html.erb
は、以下の部分が全く同じです。
<div class="form-container">
<%= form_with(model: @article) do |f| %>
<div class="form-item">
<%= f.label :title, "タイトル", class: "form-label" %>
<%= f.text_field :title, placeholder: "タイトル", class: "form-input" %>
</div>
<div class="form-item">
<%= f.label :body, "本文", class: "form-label" %>
<%= f.text_area :body, rows: 10, placeholder: "記事を入力", class: "form-input" %>
</div>
<%= f.submit "投稿", class:"submit-button" %>
<% end %>
</div>
こういうときは、一つのファイルにまとめることができます。
app/views/articles/
に、_form.html.erb
というファイルを作成し、中身に共通部分を貼り付けましょう。
<div class="form-container">
<%= form_with(model: @article) do |f| %>
<div class="form-item">
<%= f.label :title, "タイトル", class: "form-label" %>
<%= f.text_field :title, placeholder: "タイトル", class: "form-input" %>
</div>
<div class="form-item">
<%= f.label :body, "本文", class: "form-label" %>
<%= f.text_area :body, rows: 10, placeholder: "記事を入力", class: "form-input" %>
</div>
<%= f.submit "投稿", class:"submit-button" %>
<% end %>
</div>
そして、new.html.erb
とedit.html.erb
から共通部分を削除し、代わりに以下のように変更します。
<h1>新規投稿</h1>
+ <%= render 'form' %>
<h1>記事編集</h1>
+ <%= render 'form' %>
とてもシンプルになりました。
この記述は、headerを共通化したときと同じですね。
STEP2-8 削除機能
それでは最後に記事の削除機能を実装していきます。
まずはコントローラーにdestroyメソッドを定義します。
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
end
end
+ def destroy
+ Article.find(params[:id]).destroy
+ redirect_to root_url, status: :see_other
+ end
private
def article_params
params.require(:article).permit(:title, :body)
end
この記述により、DELETEリクエストが飛んできたときに、該当記事をdestroyメソッドでデータベースから削除できます。
root_url
はroutes.rbに、root 'articles#index'
と記述したことで使えるようになったもので、http://localhost:3000
を表しています。
その後の、status: :see_other
については、ビューの修正をした後でまとめて解説します。
- <a href="" class="delete-button">Delete</a>
+ <%= link_to "Delete", @article, data: { "turbo-method": :delete, turbo_confirm: "本当に削除しますか?" }, class: "delete-button" %>
deleteリクエストを送るには、特殊なlink_toの書き方をします。
Rails7からの仕様で、data: { "turbo-method": :delete}
としなければ、DELETEリクエストになりません。
turbo_confirm: "本当に削除しますか?"
とすることで、画面にalert表示できます。
あってもなくても動作しますが、間違ってボタンを押してしまったときにすぐ消えてしまわないように、付けておいたほうが良いでしょう。
link_toはhtmlに変換されたとき、以下のようになります。
<%= link_to "Delete", @article, data: { "turbo-method": :delete, turbo_confirm: "本当に削除しますか?" }, class: "delete-button" %>
<a data-turbo-method="delete" data-turbo-confirm="本当に削除しますか?" class="delete-button" href="/articles/1">Delete</a>
似たようなメソッドに、button_to
というものがあります。これはbuttonタグに変換されるのですが、link_toとは書き方がことなります。
<%= button_to "Delete", @article, method: :delete, data: { turbo_confirm: "本当に削除しますか?" }, class: "delete-button" %>
<form class="button_to" method="post" action="/articles/1"><input type="hidden" name="_method" value="delete" autocomplete="off">
<button data-turbo-confirm="本当に削除しますか?" class="delete-button" type="submit">Delete</button>
<input type="hidden" name="authenticity_token" value="VuzvPRv7TXF3PkLhKK..." autocomplete="off">
</form>
htmlへの変換も全く違っていて、POSTリクエストを行うformが生成されます。
結果的はどちらも同じになります。
コントローラーのstatus: :see_other
に戻りますが、まずstatusとつけることで、httpステータスコードを指定することができます。
DELETEリクエストの後にリダイレクトを行うとき、一部のブラウザでは元のリクエスト(ここではDELETEリクエスト)を使用してリダイレクトが行われます。
つまり、
DELETEリクエスト → リダイレクト → リダイレクト先へDELETEリクエストを送信
となってしまい、意図していないDELETEが起こってしまう可能性があるのです。
status: :see_other
を指定すると、GETリクエストになるので安全というわけです。
以上で、基本的なCRUD操作ができるアプリが完成しました。
今回は、基本の使い方のみ解説する都合で、テストコードや、バリデーションを無視しました。
さらに踏み込んだ学習がしたい場合は、Railsチュートリアルをやるのが一番良いと思います。
まとめ
今回は、簡易ブログサイトを例に、基本的なCRUD処理の書き方を解説しました。
しかし、ここまでの実装では以下のような問題点を残しています。
- 他人が投稿した記事についても編集、削除ができてしまう
- ユーザー登録機能を実装していないため、誰が投稿したものか分からない
続編として、ユーザーの認証機能や、関連テーブルを一括で保存する処理、多対多のモデル定義などを考えています。
完成しましたら公開していきますので、引き続きよろしくお願いいたします。
Discussion