😎

ハンズオン形式でRails newからCRUD操作の基礎を学習しよう

2023/12/25に公開

本記事の対象:Ruby on Railsの初学者

現在、「アプレンティス」の2期生として、プログラミングの学習しています。
Railsで関連テーブルの扱い方に苦労したので、学習も兼ねて記事としてまとめました。

本記事のゴール

sampleDB
例として、このようなテーブル構成の簡易ブログ投稿サイトを1から作成します。
機能としては、以下を含みます。

  • 記事の投稿、編集、削除機能
  • ユーザー登録、ログイン機能

本記事では、その第一弾として、記事の投稿、編集、削除機能を実装していきます。

タイトルのCRUD操作とは、(Create(作成)、Read(参照)、Update(更新)、Delete(削除))を指しており、データベースの基本操作の総称です。

まえがき

本記事では、Ruby on Railsチュートリアル (以下、Railsチュートリアル)のようなハンズオン形式で構成されています。
Railsチュートリアルよりも、内容を簡潔にし難易度を下げていますが、以下のような知識がなければ理解が難しいかもしれません。

  • Rubyの基本的な構文
  • MVCモデルとは?
  • Sessionとは?

それでも、書いてある順番にコードを書いていけば完成するようにはなっています。
また、自分が理解に苦しんだ部分は、解説を入れたり、コードの反映を確認しながら進めていきます。

本記事が、これからRailsを学ぶ方の助けになれば幸いです。

STEP1 環境構築

すでにrailsがインストールされていることを前提としています。

  1. railsアプリを作成
rails _7.0.4.3_ new many_tables --database=mysql

newの後に来るのはアプリ名です。
好きな名前で大丈夫です。

  1. 作成したディレクトリへ移動
cd many_tables
  1. データベースを作成
rails db:create
  1. railsサーバーを起動
ralils server
  1. http://localhost:3000 にアクセスして、以下のように表示されたらOK

many_tables

STEP2-1 Articleモデルの作成

rails generate model Article title:string body:text

db/migrateに以下のようなマイグレーションファイルが作成されます。(ファイル名は作成日時で変わります)

db/migrate/20231223081013_create_articles.rb
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に以下のようなファイルが作成されます。

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が追加されます。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
  end

  def show
  end

  def new
  end

  def edit
  end
end

また、routes.rbに以下のような記述が追加されます。

config/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 にアクセスしてみると、以下のように表示されるはずです。

many_tables

STEP2-3 Article関連のページを作成

一度、config/routes.rbを編集していきます。

現在、以下のようになっています。

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 にアクセスしてみましょう。
そうすると、このような画面が表示されるはずです。
many_tables

下に長く表示されていると思いますが、今見たいのは一番上の4行(画像に示している部分)です。

例えば一番上のindexに対するルーティングでは、Pathの/article/indexにアクセスすると、controllerのindexメソッドが呼び出されて、app/views/article/index.html.erbの内容が、表示されるという仕組みになっています。

config/routes.rbに戻って、
これは、

config/routes.rb
get 'articles/index'

これを省略した書き方です。

config/routes.rb
get 'articles/index', to: 'articles#index'

articles/indexにGETリクエストが来たら、articlesコントローラーのindexメソッドを呼び出すという意味です。

ここで、以下のように記述してみてください。

config/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"
+  resources :articles
end

再び、http://localhost:3000/rails/info/routes にアクセスしてみましょう。
many_tables

そうすると、先程と少し記述が変わっています。

この一行だけで、基本的なルーティングをすべて作成してくれる便利な記述方法です。

ついでに、以下の記述もしておきましょう。

config/routes.rb
Rails.application.routes.draw do
+ root 'articles#index'
  resources :articles
end

こうすることで、http://localhost:3000 にアクセスしたときに、articlesコントローラーのindexメソッドが呼ばれるようになります。
最初に表示されていたrailsのデフォルト画面ではなくなっていることを確認してみてください。

続いて、viewファイルを書いていきます。
今回はHTML、CSSの書き方は重要ではないので、以下の内容をコピペでOKです。

css
app/stylesheets/application.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
app/views/articles/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
app/views/articles/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
app/views/articles/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
app/views/articles/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>

次にヘッダーを作成します。

app/views/layouts/_header.html.erb
<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に以下のように記述することで、すべてのページでヘッダーが表示されます。

app/views/layouts/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 にアクセスして、以下のように表示されていれば成功です。
many_tables

最後に、ルーティング設定を行います。

config/routes.rb
Rails.application.routes.draw do
  root 'articles#index'
+ get '/post', to: 'articles#new'
  resources :articles
end

STEP2-4 記事一覧表示

まずは、データベースにテストデータを入れます。

rails dbconsole

このコマンドで、データベースにアクセスできますが、Railsにはテストデータを一括で作成できる機能があるので、それを使っていきましょう。

そのために、fakerというgemをインストールします。

/Gemfile
~~~
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というファイルに記述します。

db/seed.rb
20.times do
  Article.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraph(sentence_count: 10)
  )
end

次にコンソールで、

rails db:seed

このコマンドを実行します。
エラーが表示されなければ、テストデータの作成に成功しているはずです。

続いて、実際の保存されたデータをviewでも表示させてみましょう。

まず、コントローラーのindexメソッドにこのように記述します。

app/controllers/articles_controller.rb
  def index
+  @articles = Article.all
  end

こうすることで、app/views/articles/index.html.erbに、Article.all(Articleテーブルのすべてのデータ)@articlesという変数で渡すことができます。

続いて、index.html.erbを修正します。

app/views/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 にアクセスしてみると、以下のように表示されるはずです。

many_tables

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モデル内に、フォーマットを変換するための記述を行います。

app/models/article.rb
class Article < ApplicationRecord
+ def formatted_created_at
+   created_at.strftime("%Y/%m/%d")
+ end
end

このように書くことで、articleモデルに対して、formatted_created_atというメソッドを使うことができます。
続いて、ビューファイルの該当箇所を修正します。

app/views/articles/index.html.erb
- <%= tag.p article.created_at, class:"posted-date" %>
+ <%= tag.p article.formatted_created_at, class:"posted-date" %>

これで、http://localhost:3000にアクセスして、日付の表示形式が変更されていることを確認しましょう。

STEP2-5 記事詳細

続いて、showメソッドを記述して、記事の個別ページへアクセスし、内容が表示されるようにします。

まずは、コントローラーのshowメソッドを書きます。

app/controllers/articles_controller.rb
  def show
+  @article = Article.find(params[:id])
  end

findメソッドは、引数にidを指定することで、そのデータを取得することができます。

params[:id]とすることで、URLの/articles/1の1をidとして取得してくれます。

次にビューを修正します。

app/views/articles/show.html.erb
- <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 投稿機能

コントローラーを定義します。

app/controllers/articles_controller.rb
  def new
+  @article = Article.new
  end

これはArticleモデルのインスタンスを作成しています。

このインスタンスにformで入力されたデータを入れて、最終的に保存することになります。

続いて、ビューファイルを修正します。

app/views/new.html.erb
 <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_fieldtext_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メソッドで送信してくれるんでしたね。

app/contollers/articles_controller.rb
  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 編集機能

続いて、記事の編集機能を実装します。
まずコントローラーを編集します。

app/contollers/articles_controller.rb
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メソッドをコントローラーに定義して、更新ができるようにしましょう。

app/contollers/articles_controller.rb
  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メソッドと同じで、成功したら記事の個別ページへリダイレクトされます。

また、ここで記事の詳細ページを少し修正します。

app/views/show.html.erb
<%= 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.erbedit.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というファイルを作成し、中身に共通部分を貼り付けましょう。

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.erbedit.html.erbから共通部分を削除し、代わりに以下のように変更します。

app/views/articles/new.html.erb
  <h1>新規投稿</h1>
+ <%= render 'form' %>
app/views/articles/edit.html.erb
  <h1>記事編集</h1>
+ <%= render 'form' %>

とてもシンプルになりました。
この記述は、headerを共通化したときと同じですね。

STEP2-8 削除機能

それでは最後に記事の削除機能を実装していきます。
まずはコントローラーにdestroyメソッドを定義します。

app/controllers/article_controoler.rb
  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については、ビューの修正をした後でまとめて解説します。

app/views/articles/show.html.erb
- <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の場合
<%= link_to "Delete", @article, data: { "turbo-method": :delete, turbo_confirm: "本当に削除しますか?" }, class: "delete-button" %>
link_toをhtmlに変換
<a data-turbo-method="delete" data-turbo-confirm="本当に削除しますか?" class="delete-button" href="/articles/1">Delete</a>

似たようなメソッドに、button_toというものがあります。これはbuttonタグに変換されるのですが、link_toとは書き方がことなります。

button_toの場合
<%= button_to "Delete", @article, method: :delete, data: { turbo_confirm: "本当に削除しますか?" }, class: "delete-button" %>
button_toをhtmlに変換
<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処理の書き方を解説しました。

しかし、ここまでの実装では以下のような問題点を残しています。

  • 他人が投稿した記事についても編集、削除ができてしまう
  • ユーザー登録機能を実装していないため、誰が投稿したものか分からない

続編として、ユーザーの認証機能や、関連テーブルを一括で保存する処理、多対多のモデル定義などを考えています。

完成しましたら公開していきますので、引き続きよろしくお願いいたします。

GitHubで編集を提案

Discussion