🦓

[Rails]CRUDアプリを作ってみる 5/20

2023/06/21に公開

はじめに

Railsの公式ドキュメントを参考にしながらRailsのCRUDアプリを作っていきます。

環境

Ruby: 3.0.0
Rails: 6.1.7

CRUDアプリとは

CRUDアプリは、データベースやデータストレージとやり取りする基本的な操作である「作成 (Create)」「読み取り (Read)」「更新 (Update)」「削除 (Delete)」を実行するアプリケーションのことを指します。

流れ

1. モデルを作成する
2. ルーティングを作成する
3. コントローラーを作成する
4. ビューを作成する

Articleモデルを作成する

bin/rails generate model Article title:string body:text
      invoke  active_record
      create    db/migrate/20230621083727_create_articles.rb
      create    app/models/article.rb

null制約をかける

db/migrate/xxx_create_articles.rb
class CreateArticles < ActiveRecord::Migration[6.1]
  def change
    create_table :articles do |t|
      t.string :title, null: false
      t.text :body

      t.timestamps
    end
  end
end
bin/rails db:migrate
Running via Spring preloader in process 54356
== 20230621083727 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0019s
== 20230621083727 CreateArticles: migrated (0.0021s) ==========================

rails consoleで確認する

rails consoleでarticleを作成されることを確認します。

$ bin/rails console
Running via Spring preloader in process 54414
Loading development environment (Rails 6.1.7.3)
irb(main):001:0> article = Article.new(title:"hello rails", body:"i am on rails")
   (0.8ms)  SELECT sqlite_version(*)
=> #<Article id: nil, title: "hello rails", body: "i am on rails", created_at: nil, updated_at: nil>
irb(main):002:0> article.save
  TRANSACTION (0.1ms)  begin transaction
  Article Create (0.8ms)  INSERT INTO "articles" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "hello rails"], ["body", "i am on rails"], ["created_at", "2023-06-21 08:44:41.981986"], ["updated_at", "2023-06-21 08:44:41.981986"]]
  TRANSACTION (0.5ms)  commit transaction
=> true

#  titleなしでarticleを作るとエラーが発生する
irb(main):003:0> article = Article.new(body:"i am on rails")
   (0.8ms)  SELECT sqlite_version(*)
=> #<Article id: nil, title: nil, body: "i am on rails", created_at: nil, updated_at: nil>
irb(main):004:0> article.save
  TRANSACTION (0.1ms)  begin transaction
  Article Create (0.7ms)  INSERT INTO "articles" ("body", "created_at", "updated_at") VALUES (?, ?, ?)  [["body", "i am on rails"], ["created_at", "2023-06-21 08:45:55.253175"], ["updated_at", "2023-06-21 08:45:55.253175"]]
  TRANSACTION (0.1ms)  rollback transaction
Traceback (most recent call last):
ActiveRecord::NotNullViolation (SQLite3::ConstraintException: NOT NULL constraint failed: articles.title)
=> false

# エラーメッセージ
irb(main):001:0> article = Article.new
   (0.9ms)  SELECT sqlite_version(*)
=> #<Article id: nil, title: nil, body: nil, created_at: nil, updated_at: nil>
irb(main):002:0> article.save
=> false
irb(main):003:0> article.valid?
=> false
irb(main):004:0> article.errors.messages
=> {:title=>["can't be blank"]}

concoleでは確認できましたので、次はブラウザでも確認していきます。

article.rbにバリデーションをかける

空のタイトルだとDBに保存されないようにバリデーションが必要です。

app/models/article.rb
class Article < ApplicationRecord
    validates :title, presence: true
end

article一覧用のルーティングを作成する

config/routes.rb
Rails.application.routes.draw do
  get "/articles", to: "articles#index"
end

article一覧用のコントローラーを作成する

$ bin/rails generate controller Articles index
Running via Spring preloader in process 54708
      create  app/controllers/articles_controller.rb
       route  get 'articles/index'
      invoke  erb
      create    app/views/articles
      create    app/views/articles/index.html.erb

indexアクションを編集する

app/controllers/article_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

article一覧用のビューを作成する

article一覧を表示させます。

app/views/articles/index.html.erb
<h1>Articles</h1>
<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
    <li>
      <%= article.body %>
    </li>
  <% end %>
</ul>

先consoleで作成した記事を表示されました。

ブラウザに記事を表示されるまでの流れ

1. ブラウザはリクエストを行います:GET http://localhost:3000/articles
2. Railsアプリケーションがこのリクエストを受信します。
3. Railsルーターは、ルートからArticlesControllerのindexアクションにマップします。
4. indexアクションは、Articleモデルを使用して、データベース内のすべての記事を取得します。
5. Railsは自動的にapp/views/articles/index.html.erbビューをレンダリングします。
6. ビューのERBコードをHTMLに変換されます。
7. サーバーはHTMLを含む応答をブラウザーに送信します。

article一覧ができました。
タイトルをクリックするとarticle詳細ページに飛ぶように作っていきます。

article詳細ページ用ルーティングを作成する

config/routes.rb
Rails.application.routes.draw do
  get "/articles", to: "articles#index"
    get "articles/:id", to: "articles#show"
end

コントローラーにshowアクションを追加する

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def show
    @article = Article.find_by(id: [params[:id]])
    if @article.nil?
      flash[:danger] = "記事が見つかりませんでした。"
      redirect_to root_path
    end
  end
end

app/views/articles/index.html.erbにリンクを追加する

app/views/articles/index.html.erb
<h1>Articles</h1>
<ul>
  <% @articles.each do |article| %>
    <li>
     <%= link_to article.title, article_path(article) %>
    </li>
    <li>
      <%= article.body %>
    </li>
  <% end %>
</ul>

app/views/articles/show.html.erbを作成する

app/views/articles/show.html.erb
<h1><%= @article.title %></h1>
<p><%= @article.body %></p>

これではCRUDのR(Read)が完了しました。
オペレーションごとにroutes,controller,viewsを作成する必要があります。
resourcesメソッドを使用してルーティングを定義すると、一般的なCRUDアクションに対するルートが自動的に生成されます。

articleのresourcesを作成する

routes.rbを編集します。

config/routes.rb
Rails.application.routes.draw do
  resources :articles
end

CRUDのルートが作成されました!

$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
        root GET    /                            articles#index
    articles GET    /articles(.:format)          articles#index
 new_article GET    /articles/new(.:format)      articles#new
     article GET    /articles/:id(.:format)      articles#show
             POST   /articles(.:format)          articles#create
edit_article GET    /articles/:id/edit(.:format) articles#edit
             PATCH  /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

app/views/articles/index.html.erbのリンクを編集する

app/views/articles/index.html.erbのリンクをもっと短く書けることが分かりました。

Prefix Verb   URI Pattern                  Controller#Action
article GET   /articles/:id(.:format)      articles#show
app/views/articles/index.html.erb
<%= link_to article.title, article_path(article) %>
# 変える
<%= link_to article.title, article %>

パスを確認する時にbin/rails routesが便利です。
次はCRUDのC(Create)、新規投稿の作成を作っていきます。

articleコントローラーにnewとcreateアクションを追加する

newアクションではフォームをレンダリングします、createアクションではバリデーションやDBへの保存を行います。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
     flash[:success] = "記事が作成できました。"
      redirect_to @article
    else
      flash[:danger] = "記事の作成が失敗しました。もう一度試してください。"
      render :new
    end
  end

  private

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

Strong Parameters(ストロングパラメータ)は、Ruby on Rails フレームワークにおけるセキュアなパラメータの取り扱い方法です。一般的に、フォームからのユーザー入力データを受け取り、それをデータベースに保存する際に使用されます。

ストロングパラメータは、不正なデータの送信やセキュリティ上のリスクを防ぐため、特定の属性のみを許可する仕組みを提供します。これにより、ユーザーが意図せずにデータベースの他の属性を更新することや、マスアサインメント攻撃(Mass Assignment Attack)を防ぐことができます。

createアクション内でparams.require(:article).permit(:title, :body)を使用して、articleパラメータの中で許可する属性を指定しています。これにより、:title:bodyのみが許可され、他の属性は無視されます。

redirect_torenderの違い

redirect_toはブラウザを別のURLにリダイレクトするためのメソッドです。新しいリクエストが発生します。
renderは同じアクション内でレスポンスを生成し、指定したビューファイルを描画します。同じリクエスト内で処理が行われます。

app/views/articles/new.html.erbのリンクを作成する

投稿のタイトルと本文を入力できるようにフォームを作っていきます。

app/views/articles/new.html.erb
<h1>New Article</h1>
<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>
  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </div>
  <div>
    <%= form.submit %>
  </div>
<% end %>

ブラウザとデベロッパーツールを確認します。

https://api.rubyonrails.org/v7.0.5/classes/ActionView/Helpers/FormHelper.html#method-i-form_with

バリデーションに引っかかるかを確認します。

app/views/articles/index.html.erbに新規投稿のリンクを追加する

article一覧から新規投稿を作成できるように/newへのリンクを追加します。

app/views/articles/index.html.erb
<%= link_to "New Article", new_article_path %>

CRUDのC(Create)とR(Read)ができました。
CRUDのU(Update)、投稿の編集機能を作っていきます。

articleコントローラーにeditとupdateアクションを追加する

newcreateアクションに似たようで、editアクションが編集のリクエストを送って編集画面edit.html.erbを表示されます。updateアクションがフォームの検証とDBへの保存を行います。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def edit
    @article = Article.find_by(id: [params[:id]])
  end

  def update
    @article = Article.find_by(id: [params[:id]])
    if @article.update(article_params)
      flash[:success] = "投稿が更新されました。"
      redirect_to @article
    else
      flash[:danger] = "投稿の更新が失敗しました。もう一度試してください。"
      render :edit
    end
  end
end

app/views/articles/edit.html.erbを作成する

app/views/articles/new.html.erbと同じフォームを使うため、app/views/articles/_form.html.erbのパーシャルを作成します。

app/views/articles/_form.html.erb
<%= form_with model: @article do |form| %>
  <% article.errors.full_messages.each do |message| %>
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
      <%= message %>
      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    </div>
  <% end %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>
  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </div>
  <div>
    <%= form.submit %>
  </div>
<% end %>

app/views/articles/new.html.erbapp/views/articles/edit.html.erb_form.html.erbを読み込みます。

app/views/articles/new.html.erb
<h1>New Article</h1>
<%= render "form", article: @article %>
app/views/articles/edit.html.erb
<h1>Edit Article</h1>
<%= render "form", article: @article %>

上記のコードでは、formという名前の部分テンプレート_form.html.erbをレンダリングし、その部分テンプレート内でarticleというローカル変数に@articleの値を渡しています。

articleローカル変数を使用して、@articleオブジェクトの属性にアクセスしたり、表示したりすることができます。コントローラーに定義された@articleをビューにも使われるようになります。通常、このような方法を使用して、フォームの構造や共通したコードを再利用することができます。

app/views/articles/show.html.erbapp/views/articles/index.html.erb編集へのリンクを追加する

app/views/articles/show.html.erb
<h1><%= @article.title %></h1>
<p><%= @article.body %></p>
<li><%= link_to "Edit", edit_article_path(@article) %></li>
app/views/articles/index.html.erb
<h1>Articles</h1>
<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
    <li>
      <%= article.body %>
    </li>
    <li><%= link_to "Edit", edit_article_path(article) %></li>
  <% end %>
</ul>
<%= link_to "New Article", new_article_path %>

最後はCRUDのD(Delete)、投稿の削除を追加していきます。
resourcesメソッドがdestroyアクションを用意していますのでそちらを使います。

articleコントローラーにdestroyアクションを追加する

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def destroy
    @article = Article.find_by(id: [params[:id]])
    @article.destroy
    flash[:success] = "投稿が削除されました。"
    redirect_to articles_path
  end
end

app/views/articles/show.html.erbに削除ボタンを追加する

app/views/articles/show.html.erb
<li><%= link_to "Delete", article_path(@article),
    method: :delete, data: {confirm: "削除してよろしいでしょうか?"} %></li>

終わりに

公式ドキュメントを参考して簡単なCRUD機能を実装しました。
今度はユーザと紐つけるようにしていきたいと思います。
https://guides.rubyonrails.org/getting_started.html

Discussion