blog機能の実装(総復習)

はじめに

これからblog機能を実装していきます。blog機能はみなさんがプログラミングの学習した内容を第三者に共有したりご自身の備忘録としてアウトプットする場所の想定です。

これまでcareer機能において実装してきたことをblogの機能として名前を変えて同じことをするのがほとんどです。

はじめに要件を記載しますので、それをみて実装してみてもらえると嬉しいです。
章ごとにご自身で実装したコードと私のコードを見比べてみて、採用したい実装があれば採用するなどといった手順で進めてもらえるとよいかと思います。

それでは、早速はじめていきましょう

ゴールの確認

アプリケーションの一つの機能(ストーリー)を全て実装しきれる

ここでの差分はgithubにあげています。困った際には参考にしてみてください

要件

テーブル設計

blogsテーブルを作成してください
blogsではblogのタイトルとサマリーを保存できるようにしてください。
blogは「notionable」です。notionレコードを子に持ちます。

ER図

railsの管理画面

blogの一覧、詳細、作成、更新のページとその機能を作成してください。

画面のレイアウトは特に指定はありませんが、サンプルとして私の作成したものを載せておきます。

*一覧に使われている画像はbackend/assets/imagesにpng画像を直接配置してそれを読み込んでいます。

エンドポイント

http://localhost:3000/api/v1/blogs
からblogの一覧を取得できるようにしてください。
取得が成功したときのレスポンスの例は下記の通りです

[{
  "title": "blog-title",
  "summary": "blog-summary",
  "notionKey": "blog-notion-key"
}]

フロントエンド

ヘッダーのblogsをクリックしてブログの一覧ページに遷移できるようにしてください

ブログ一覧ページのブログの要素をクリックして/contents/${notionKey}のページへ遷移してNotionコンテンツが表示されるようにしてください

ブログの一覧ページの見た目は特に指定はありませんが、私は次のようにしました。

要件は以上です!
それでは実装していきましょう!

テーブル・モデル作成

それではまずはテーブルとモデルを作成していきます。

rails g model Blog
class CreateBlogs < ActiveRecord::Migration[7.0]
  def change
    create_table :blogs do |t|
      t.references :user
      t.string :title, null: false, default: ""
      t.text :summary, null: false

      t.timestamps
    end
  end
end
class Blog < ApplicationRecord
  has_one :notion, as: :notionable, dependent: :destroy
  validates :title, presence: true
  validates :summary, presence: true, length: { maximum: 500 }
end

Notionモデルに対しての実装はないです。
notionabel_typeに対する制限がないため、has_one :notionのあるモデルからガンガン作成できてしまいます。これを防ぐためにnotionable_typeをenum型にして制御をかけることもできるようです。

動作確認

blog = Blog.new(title: "blog1", summary: "blog1 summary")
blog.save!
notion = blog.build_notion(key: "blog-notion")
notion.save!
blog.notion
notion.notionable

テーブル・モデル作成の全体の差分はこちらです

BlogのCRUDの実装

routingを作成します。

# frozen_string_literal: true

Rails.application.routes.draw do
  draw(:api)

  resource :user, only: %i[show update create]
  resources :careers
  resources :blogs
end

動作確認します

rails routes | grep blog

まずはindexページを作成します。
コントローラとテンプレートを作成してとりあえず何かしらが表示されるようにしましょう

class BlogsController < ApplicationController
  def index
  end
end
<div>blog index page</div>

表示ができました。

それでは見た目を整えていきます。
次にこれを動的にします。

indexができたので作成機能を作成していきましょう
これも同様な手順で実装してきます。
見た目を作成するcreateアクションを実装する
createアクションはこの段階ではnotion_keyを一旦無視して作成しました。
いっぺんにあれこれ実装しようとすると上手くいかないときに原因の特定に時間がかかってしまうためです。
blogの単体で保存ができるようになったので、formオブジェクトを使った実装をいれます。

次に更新機能を作成していきましょう
見た目の作成(formのコードをもう一度書くのは面倒なのでこの時点でpartialに落とし込み、newページもリファクタします。)→updateアクションを実装

indexのcard要素をpartialにするのを忘れていたので、今やることにしました。

削除機能を作成していきます。
こちらは画面はないので、ボタンをeditにおいて、アクションを実装します。

最後に詳細ページを実装します。

今回はindex→new→create→edit→update→delete→showの順でアクションを実装しました。
実装の順番はなんでもいいのですが、他の機能との依存度の少ないもの・簡単なものから実装するのがよいです。
特にindexは他のアクションのベースになるので先に実装します。

管理画面は以上です。

エンドポイントの実装

routes, controllerと実装していきます。

  def blogs
    @blogs ||= user.blogs.includes(:notion).order(created_at: :desc)
  end

N+1問題に気づいたのでincludesして回避しています。

回避前

backend-1   | Started GET "/api/v1/careers" for 192.168.65.1 at 2024-06-30 17:20:18 +0900
backend-1   | Cannot render console from 192.168.65.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
backend-1   | Processing by Api::V1::CareersController#index as */*
backend-1   |   User Load (0.5ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
backend-1   |   ↳ app/controllers/api/v1/careers_controller.rb:9:in `user'
backend-1   |   Career Load (0.4ms)  SELECT `careers`.* FROM `careers` WHERE `careers`.`user_id` = 1 ORDER BY `careers`.`started_at` ASC
backend-1   |   ↳ app/controllers/api/v1/careers_controller.rb:3:in `index'
backend-1   | [active_model_serializers]   Notion Load (0.2ms)  SELECT `notions`.* FROM `notions` WHERE `notions`.`notionable_id` = 7 AND `notions`.`notionable_type` = 'Career' LIMIT 1
backend-1   | [active_model_serializers]   ↳ app/models/career.rb:8:in `notion_key'
backend-1   | [active_model_serializers]   Notion Load (0.2ms)  SELECT `notions`.* FROM `notions` WHERE `notions`.`notionable_id` = 15 AND `notions`.`notionable_type` = 'Career' LIMIT 1
backend-1   | [active_model_serializers]   ↳ app/models/career.rb:8:in `notion_key'
backend-1   | [active_model_serializers]   Notion Load (0.2ms)  SELECT `notions`.* FROM `notions` WHERE `notions`.`notionable_id` = 2 AND `notions`.`notionable_type` = 'Career' LIMIT 1
backend-1   | [active_model_serializers]   ↳ app/models/career.rb:8:in `notion_key'
backend-1   | [active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Attributes (6.05ms)

回避後

backend-1   | Started GET "/api/v1/careers" for 192.168.65.1 at 2024-06-30 17:20:38 +0900
backend-1   | Cannot render console from 192.168.65.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
backend-1   | Processing by Api::V1::CareersController#index as */*
backend-1   |   User Load (0.1ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
backend-1   |   ↳ app/controllers/api/v1/careers_controller.rb:9:in `user'
backend-1   |   Career Load (0.1ms)  SELECT `careers`.* FROM `careers` WHERE `careers`.`user_id` = 1 ORDER BY `careers`.`started_at` ASC
backend-1   |   ↳ app/controllers/api/v1/careers_controller.rb:3:in `index'
backend-1   |   Notion Load (0.1ms)  SELECT `notions`.* FROM `notions` WHERE `notions`.`notionable_type` = 'Career' AND `notions`.`notionable_id` IN (7, 15, 2)
backend-1   |   ↳ app/controllers/api/v1/careers_controller.rb:3:in `index'
backend-1   | [active_model_serializers] Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Attributes (0.27ms)
backend-1   | Completed 200 OK in 21ms (Views: 5.2ms | ActiveRecord: 4.3ms | Allocations: 13596)

SELECT `notions`.* FROM `notions
の分が回避前は返されるレコードの数だけ実行されていましたが、回避後は返すレコードの数が増えても一度のみになりました。

careers_controllerの同じ問題を抱えていたのでついでに修正しています。

N+1問題はデータベース周りのパフォーマンス悪化させる大きな要因になります。
基本的にサーバーがデータベースに問い合わせる数が増えればその分バフォーマンスは下がります。気づくのが遅くなってしまいましたが、実装途中の動作確認の段階でログを正常な動作をしていてもログを確認しておかしな挙動が生まれていないかもチェックしておくのが望ましいですね。

Frontendのblogページ

見た目から作成していきます。

careerはトップページで表示させるため、app直下のpage.tsxに実装しましたが、blogは/blogsのURL
にしたいため、app/blogs/page.tsxを作成してこちらに実装します。

見た目を作成し終えたら、次はエンドポイントを実行してblogsリソースをとりにいき、画面を流す作業です。
どの順番で実装してもいいのですが、私は手前から実装するようなイメージで作成していくと確実かなと考えています。具体的には
page.tsx→actions→blogRepository→Client・Model
です。

これで終了です。

最後にヘッダーのナビゲーションアイテムが現在のパスに応じてactiveを切り替えられるようにします。
navbar用のコンポーネントを用意してナビゲーションのテンプレートとロジックをHeaderコンポーネントから切り離します

おわりに

おつかれ様でした!
いかがでしたでしょうか?ご自身で実装を進めることはできましたでしょうか?

これでBlog機能が完成しました。notionを使ってどんどん学習した内容を書き込んでいきましょう!
未経験からのエンジニア転職において、あなたがどの程度プログラミングに関心があって何をどのくらい学習したのかはなかなか見えずらいものです。それをわかりやすく表現する手段がブログ記事になると考えています。たくさんアウトプットしてアピール兼あなたの備忘録に使っていってください!

今回の実装ではコミットをかなり細かくつけました。これはその時々での実装内容をご紹介するために細かくつけています。
実際に業務で同じような実装をする場合は私はもっと荒いです。(良い悪いはおいておいて。。。)
おそらく、1.テーブル・モデルの作成 2.BlogのCRUDの作成 3.エンドポイントの実装 4.Frontendの実装の5コミットくらいになると思われます。

コミットの粒度については個々の好みによってまちまちですが、チームによってはある程度統制が取られていることもあるようです。その場合はそのチームの運用に合わせられるようにしておきましょう。

コメント

タイトルとURLをコピーしました