0
0

More than 1 year has passed since last update.

Rails初心者の僕がRails6でActive Storageを使用し、漫画投稿アプリを開発してデプロイした過程(設計編)

Posted at

※この記事は以前別アカウントで投稿した記事の移行版です。別アカウントを削除するにあたって移行しました。

設計編に入ります

みなさんこんにちは、Rails初心者のほまりんです。前回の環境構築編はお読みになられたでしょうか。もし未読かつ興味のある方は、是非ご一読なさってください。それではこれより、設計編に移らせていただきます。

今回のアプリの全体像

実際の設計に入る前に、まずは今回のアプリの説明をさせていただきます。記事のタイトルでは漫画投稿アプリと題しておりますが、今回私が作りたかったのは「小説・イラスト・漫画」の三種類が投稿できるサイトです。つまりは「pix◯v」みたいなものですね。更に、ユーザー間でメッセージのやり取りができるようにもしたかったです(なるべく早くデプロイに移りたかったので、現在、メッセージ機能部分は未実装)。どうしてそのようなサイトを作成したかったのかは、説明が長くなるのでまた今度話させていただこうと思います。

いざ設計へ

設計開始です。

最初は必要な機能を考える

設計をするうえで初めにすべきことは、何と言っても必要な機能の洗い出しです。「後で思いついたら機能を追加すればいいや!」みたいな甘い考えは捨てましょう。もちろん、開発途中で見えてくるものはありますが、最初の段階でどれだけ正確に機能の洗い出しをできているか、どれだけ確かなビジョンを描けているかで、開発の大変さががらりと変わってきます。結論から述べると僕は見通しが甘かったです。アウトライン(あらすじ、全体像)を作ることの重要さや作り方は下の動画で詳しく語られています。

【9割の人ができない】「仕事が遅い人」がやっていないたった一つのこと

動画を見ることが面倒な方のために纏めてしまうと、良いアウトラインの作り方は、①ゴールを決めて、②現状の課題を洗い出し、③解決策を探っていく、というものです。シンプルに見えて意外と奥が深かったりします。

必要な機能(ゴール)

では改めて、今回のアプリに必要な機能はなんでしょうか。メッセージ機能の部分は現在未実装かつ、本題から逸れてしまうので一旦無視し、「作品(小説・イラスト・漫画)の投稿」にだけ着目して挙げていきたいと思います。

  1. ユーザー機能 ・・・ これがないと始まらない。
  2. 下書き機能  ・・・ 作品を作成し、保存しておく機能。
  3. 投稿機能   ・・・ 下書きとして保存した作品を投稿する機能。
  4. 画像投稿機能 ・・・ イラストや漫画を作成するために画像をあげられるように。
  5. ジャンル機能 ・・・ 小説や漫画のジャンル分け(例: 恋愛、コメディーなど)はほぼ必須。
  6. シリーズ機能 ・・・ 作品のシリーズ化に対応する(連載小説、連載漫画という風に)。
  7. 検索機能   ・・・ 作品の検索をできるようにする。

以上が、私が最低限必要だと考えた機能です。ランキング機能やタグ機能も検討したのですが、先述通りデプロイまでの速度を重視していたので、実装はとりあえず見送ることにし、この七つを完成させることをゴールに据えました。

現状の課題

ゴールが決まれば、次は課題の洗い出しです。根気のいる作業ですが、序盤で課題のリスト化が終わっていれば後がとても楽になります。面倒でも頑張りましょう・・・!

1. ユーザー機能

Ⅰ 新規登録、ログイン機能
Ⅱ プロフィール設定

2. 下書き機能

Ⅲ 作品の作成、編集(create、edit)
Ⅳ 下書き、という状態を持たせる(つまり作成してもすぐには公開されない)

3. 投稿機能

Ⅴ 作品の投稿(公開)
Ⅵ 投稿された作品の非公開化(間違って投稿された場合や、その他のケースを考慮)

4. 画像投稿機能(正しくは画像アップロード機能)

Ⅶ 画像を保存できる仕組みを作る
Ⅷ 画像が各作品と結びつくようにする
Ⅸ ドラッグ&ドロップでも投稿できるようにする
Ⅹ 画像ファイル選択時のプレビュー機能を作る

5. ジャンル機能

Ⅺ 作品と対応するジャンルを用意する

6. シリーズ機能

Ⅻ シリーズ(連載小説、連載漫画)の枠組みを作成し、シリーズの中には1話、2話というように単体の作品が多数存在する

7. 検索機能

XIII キーワードでタイトルを検索できるように


見ていただければ分かる通り、こちらも最低限の課題だけリストアップしています。検索機能はジャンルでも検索できる方が良いですし、なんならタグ機能を追加してタグ検索などもできるようにすべきでしょう。が、初心者の個人開発でいきなり機能を増やしすぎても痛い目を見るだけですので、このぐらいの規模で抑えておきます。

解決策

まず、ざっと書いていきます。説明の追記が必要なものや、初心者だと知らない可能性のあるものは後から説明しますのでご安心ください。

Ⅰ 新規登録、ログイン機能
gem 'devise'で対応(モデル名はUser

Ⅱ プロフィール設定
→ Userにprofileのカラムを追加するだけ

Ⅲ 作品の作成、編集(create、edit)
Novel(小説)、Illustration(イラスト)、Comic(漫画)のモデルを作るだけ

Ⅳ 下書き、という状態を持たせる(つまり作成してもすぐには公開されない)
Ⅴ 作品の投稿(公開)
Ⅵ 投稿された作品の非公開化(間違って投稿された場合や、その他のケースを考慮)
→ Novel、Illustration、Comicの各モデルにStatusカラムを持たせてenumで管理

Ⅶ 画像を保存できる仕組みを作る
Ⅷ 画像が各作品と結びつくようにする
carrierwaveでなくActive Storageを使って実装してみる。更にmini_magickではなくimage_processingを使う。

Ⅸ ドラッグ&ドロップでも投稿できるようにする
→ Javascriptのondropを使えば簡単そう

Ⅹ 画像ファイル選択時のプレビュー機能を作る
→ jQueryで頑張る(もちろん漫画のために複数表示できるようにする)

Ⅺ 作品と対応するジャンルを用意する
→ ジャンルは静的なデータとしてこちらで用意するため、gem 'active_hash'で実装する

Ⅻ シリーズ(連載小説、連載漫画)の枠組みを作成し、シリーズの中には1話、2話というように単体の作品が多数存在する
Seriesモデルを作成(命名はSerialとすべきでしたが、設計時は致命的な英語力によりSerialという単語を知りませんでした……)

XIII キーワードでタイトルを検索できるように
→ 普通にWhereで検索すれば良さそう(ransackは見送り)


こんな感じになりました。ここからは補足説明をしていきます。

gem 'devise'について

Railsでよく使われる、新規登録やログインに関する機能をいとも簡単に作成してくれるとんでもない奴です。Google認証やFacebook認証等にも対応しているので、かなりおすすめのgemとなります。が、初心者の方は、一度ぐらいはユーザー登録、ログイン機能を自作してみると、色々と勉強になります。今回は速度重視なので採用しない理由はないですね。

deviseとは
rails devise完全入門!結局deviseって何ができるの?

deviseの使い方
[Rails] deviseの使い方(rails6版)

P.S. ユーザー登録、ログイン機能を学びたい方へ

ユーザー登録及びログインの処理はかなり基本的な内容になりますので、この段階から学習したいという方には、私はProgateをオススメしています。お金はかかりますが、有料会員費は月額1000円とかなり安いです。1000円さえ払えば1ヶ月間、Progate内のレッスンが受け放題で、レッスン内容はとても分かりやすくかつ丁寧です。特にRubyとRailsのレッスンは充実しており、かつHTML、CSS、Javascript、Gitなども勉強できるので、Railsを勉強したい方はまずProgateに登録しましょう。1000円をケチるよりは、1時間働いて1000円稼ぎ、そのお金でProgateに登録する方が効率的です。Progateの学習が終われば、是非Railsチュートリアルにも挑戦してみてください。その過程を踏めば、Railsエンジニアとしての力だけならT◯C◯::EXPERT受講生にも負けません(但し◯E◯H::EXPERTでは、チーム開発の経験を積むことができ、後述するactive_hashのように便利なGemや本番環境にアプリをあげる方法なども教えてもらえます)。RailsチュートリアルもTECH::E◯◯◯◯Tのカリキュラムも全て履修した私が保証します。

Novel、Illustration、Comicモデルについて

設計段階で僕は、novels、illustrations、comicsのテーブルをそれぞれ一つずつ作るか、worksテーブルを一つ作り、WorkモデルからSTI(後述)を利用し、上記三つのモデルを作るかの二通り案を出しました。結果的に、それぞれの仕様は異なる部分が多いので、テーブルを一つずつ作成し、共通処理はModuleにまとめてincludeする方針に決めました。

enumについて

enumは一言で言えば、状態を管理してくれるとても便利な奴です。スクルトぐらい便利です。

例えば今回、各作品は「下書き」という状態と「投稿された」という状態を持つことになりますね。投稿された作品の非公開化も含めると「下書き」、「投稿され公開されている」、「投稿されたが非公開になっている」の3パターンの状態があります。これを簡単に実装してくれるのがこのenumです。enumの実装は、

  1. integer型のカラムを用意する(もしくはboolean型)
  2. モデルにenumの設定を書く

の二点をするだけで可能です。と言っても少し分かりづらいと思うので、まずはinteger型の場合について、コードを見ながら説明していきます。

何らかのmigrationファイル
add_column :novels, :status, :integer, default: 0

add_columnはその名の通り、テーブルにカラムを追加してくれるコマンドです。今回であれば、novelsテーブルstatusという名前のカラムをinteger型で追加してくれます。そしてdefault: 0をつければ、レコード作成時のカラムの初期値を0にしてくれます。どうして初期値を設定するのかは後述しますので、続いてenumの設定の書き方を見ていきます。enumの設定方法には二種類あり、

novel.rb
enum status: { draft: 0, public_posted: 1, private_posted: 2 }

と、カラムに対してハッシュで定義するか

novel.rb
enum status: [:draft, :public_posted, :private_posted]

と、配列で定義するかに分かれます。これによりNovelモデルは

0 draft(下書き)
1 public_posted(投稿され公開されている)
2 private_posted(投稿されたが非公開になっている)

の、3つの状態を持つことができます。そしてハッシュでの定義を見ていただくと分かりやすいのですが、この3つの状態をenumは数字で管理しており、カラムの値が0ならdraft1ならpublic_posted2ならprivate_postedとして扱われます。だからカラムの型はinteger型にする必要があったんですね。そしてdefaultの値を設定した理由は、レコード作成時に初期値(初期状態)を持たせるためです。

また配列で定義した場合はindexで判定されるので、同じく0ならdraft1ならpublic_posted2ならprivate_postedとなります。但し状態の種類が増えた場合、見づらくなってしまうので、私個人はハッシュで定義する方をオススメしています。

なお、boolean型の場合は

novel.rb
enum status: { draft: false, published: true }

というように記載します。

以上が定義の方法です。続いて実際の使い方を纏めましたので、ご覧ください。

コンソール
novel = Novel.create(status: :draft)
# これでstatusがdraftのnovelが作成される。
# 今回は、数字の0をdraftとして扱っているため、
# novel = Novel.create(status: 0)
# でも可(あまり望ましくない)。

novel.status # => draft
# novel.カラム名で、状態が取ってこれる。

novel.draft? # => true
novel.public_posted? # => false
novel.private_posted? # => false
# 状態名に?をつければ、真か偽かを返してくれる
# 今回はstatusをdraftにして作成したのでdraft?でtrue

novel.public_posted!
# 状態名に!をつけることでstatusをpublic_postedに変更
# novel.public_posted? => true

Novel.statuses # => { draft: 0, public_posted: 1, private_posted: 2 }
# 定義を返してくれる

Novel.draft
# これでstatusがdraftのレコードを検索できる
# Novel.where(status: :draft) と等しい

これで大体、enumとは何なのか把握していただけたと思います。

carrierwaveについて

Railsでよく使われる超お手軽な画像アップローダーです。

Gemfile
gem 'carrierwave'

と、Gemfileに書いてbundle installすれば使えます。知名度も高く、更に今回は使わなかったため説明を省略させていただきますが、ご存じない方はこの辺りの記事を参考にどうぞ!

CarrierWaveを使って、ユーザー画像を設定する。
【Rails入門】CarrierWaveを使って画像のアップロードに挑戦!

Active Storage

本題です。記事のタイトルにも記載しているので、Active Storageの情報を目当てにこの記事をお読みくださっている方も多いのかなと思います。この設計編ではActive Storageの概要だけ説明して、実際にどのように利用したかはまた今度書かせていただきます。

Active Storageとは

Rails5.2より追加された、ファイルアップロードを行うための機能です。上記のcarrierwaveよりも、更にお手軽に画像投稿機能を実現できます。お手軽すぎてカンガルーになりました。

導入方法
ターミナル
$ rails active_storage:install
$ rails db:migrate

を実行してください。これだけでActive Storageが使えるようになります。Active Storageをインストールすると、active_storage_blobsactive_storage_attachmentsという名前のテーブルが2つ生成され、彼らが画像保存の役割等を担ってくれます。

そして使いたいモデルに

illustration.rb
class Illustration < ApplicationRecord
  has_one_attached :image
end

もしくは

comic.rb
class Comic < ApplicationRecord
  has_many_attached :images
end

というように記載するだけで完了です。嘘みたいですがこれだけで画像のアップロードが可能になります。黒魔術、ここに極まれりといったところでしょうか。画像保存用のカラムさえいりません。

また、has_one_attachedとした場合は一対一の関係で、has_many_attachedとした場合は一対多の関係で画像を保存できます。

画像の保存の仕方は一旦保留しておき、保存した画像は、

illustrations_controller.rb
def show
  @illustration = Illustration.find(params[:id])
end
illustrations/show.html.erb
<%= image_tag @illustration.image %>
comic_controller.rb
def show
  @comic = Comic.find(params[:id])
end
comics/show.html.erb
<% @comic.images.each do |image| %>
  <%= image_tag image %>
<% end %>

で、表示できます。

そしてこれは補足ですが、has_one_attached :image:imageはファイルの呼び名に過ぎず、これは私たちの裁量次第で自由に決められますので、例えば

user.rb
class User < ApplicationRecord
  has_one_attached :icon
end

として、

users_controller.rb
def show
  @user = User.find(params[:id])
end
users/show.html.erb
<%= image_tag @user.icon %>

というように取ってくることもできます。便利すぎてスクルトどころかバイキルトです。

画像の保存の仕方

これもとても簡単です。

illustrations_controller.rb
def new
  @illustration = Illustration.new
end

def create
  @illustration = Illustration.create(illustration_params)
end

private

def illustration_params
  params.require(:illustration).permit(:image)
end
illustrations/new.html.erb
<%= form_with @illustration, local: true do |f| %>
  <%= f.file_field :image %>
  <%= f.submit '投稿' %>
<% end %>

と、このようにするだけです。まるでimageというカラムがIllustrationに存在しているかのようですね。
comicのようにhas_many_attachedしている場合は、

comics_controller.rb
def new
  @comic = Comic.new
end

def create
  @comic = Comic.create(comic_params)
end

private

def comic_params
  params.require(:comic).permit(images: [])
end
illustrations/new.html.erb
<%= form_with @comic, local: true do |f| %>
  <%= f.file_field :images, multiple: true %>
  <%= f.submit '投稿' %>
<% end %>

と、変わるだけです。

一応、初心者に向けた補足説明を軽くすると、

form_with
→ フォームの作成。デフォルトの設定では、submitすると非同期通信されるようになっており、local: trueを指定することでform_tagform_forのように同期通信を行えます。詳しくは【Rails 5】(新) form_with と (旧) form_tag, form_for の違いを参考にしてみてください。同期通信や非同期通信をそもそも知らない方は、各自ググってください。

comic_paramsのpermit(images: [])の部分
→ データを配列形式で受け取るようにする場合は、このように記載します。今はこれだけの説明に留めておきますので、こちらも知らない方はググっていただけたらと思います。

この記事内でのActive Storageの説明は以上になります。

mini_magickについて

Gemfile
gem 'mini_magick'

画像を指定したサイズにリサイズ(拡大、縮小、切り抜きなど)してくれます。こちらもcarrierwaveと同じく採用しなかったため、詳細な説明は省かせていただきます。知りたい方はこちらの記事を参考にしてください。

Rails gem MiniMagick を利用して画像ファイルをリサイズする

追記: RMagickについて
Gemfile
gem 'rmagick'

画像のリサイズに関する情報を調べていると、こちらもヒットすることがありますが、rmagickは使わなくて大丈夫です。rmagickを使うのであればmini_magickを使いましょう。

image_processingについて

画像のリサイズをmini_magickよりもお手軽に扱えるgemです

Gemfile
gem 'image_processing', '~> 1.2'

で、bundle installしましょう。'~> 1.2'の記述が必要かは不明ですが、とりあえず私はこう書いていたのでそれをそのままコピペしてきました。

image_processingを導入すれば、

illustrations/show.html.erb
<%= image_tag @illustration.image.variant(resize: "100x100") %>

とするだけで、画像のサイズ変換ができてしまいます。他にも書き方は色々あるので、記事を書き進めていく中で逐一説明していくつもりです。

Javascriptのondropについて

後日、画像投稿実装編にて説明します。

active_hashについて

Gemfile
gem 'active_hash'

active_hashは、Railsで静的なデータを扱うときに役立つGemです。

静的なデータとは、例えば都道府県のデータなどのことです。都道府県のデータを扱う際、仮にprefecturesテーブルを作成し、そこに計47件分のレコードを保存したとします。しかしこのレコードは今後、まず変化することがありません。仮に日本から東京が消滅するような未曾有の事態が起きても、運営側が東京都のレコードを一つ削除してしまえばそれで終わりです。小説の投稿やTweetのように、ユーザーからのリクエストを受け取ってレコードが増えたり変化したり、ということは間違いなくありません。
このような静的データのために新しくテーブルを作成するのは非効率な気がしますね。そこで役立つのがactive_hashという訳です。active_hashはハッシュデータをActiveRecordと同じ感覚で使えるようにしてくれます。

では、実際にactive_hashを使用してみましょう。まずはapp/models以下にgenre.rbを作成します。そして作成したファイルに以下のような記述をしました。

genre.rb
class Genre < ActiveHash::Base
  self.data = [
    {id: 1, name: '恋愛(現実世界)', path_name: 'love_in_real'}, {id: 2, name: '恋愛(異世界)', path_name: 'love_in_another'},
    {id: 3, name: 'ファンタジー', path_name: 'fantasy'}, {id: 4, name: 'コメディー', path_name: 'comedy'},
    {id: 5, name: 'ラブコメ', path_name: 'love_comedy'}, {id: 6, name: '日常', path_name: 'life'},
    {id: 7, name: '青春', path_name: 'springtime_of_life'}, {id: 8, name: 'ヒューマンドラマ', path_name: 'drama'},
    {id: 9, name: 'ミステリー', path_name: 'mystery'}, {id: 10, name: 'VRゲーム', path_name: 'vr_game'},
    {id: 11, name: 'SF', path_name: 'space_fantasy'}, {id: 12, name: 'アクション', path_name: 'action'},
    {id: 13, name: '純文学', path_name: 'classic_literature'}, {id: 14, name: '戦記', path_name: 'saga'},
    {id: 15, name: 'ホラー', path_name: 'horror'}, {id: 16, name: '童話', path_name: 'fairy_tale'},
    {id: 17, name: '詩', path_name: 'poem'}, {id: 18, name: 'TRPGリプレイ', path_name: 'trpg'},
    {id: 19, name: 'エッセイ', path_name: 'essay'}, {id: 20, name: 'その他', path_name: 'others'},
  ]
end

まず、Genre < ActiveHash::Baseとありますが、こちらは見たままで、GenreモデルはActiveHashで扱うモデルだということを表しています。次に、self.data = []に注目してください。この[]の中に格納したものが、保存する静的なデータとなります。格納するのはハッシュデータで、ここでは{id: ○, name: 〇〇, path_name: 〇〇}としていますね。これで、id、name、path_nameというカラムを持ったGenreモデルを擬似的に再現することができます(path_nameが何なのかは次の記事で説明します)。

一応、self.data = []にハッシュをぶちこむだけで静的データを持つことには成功するのですが、このままでは致命的なことが一つあります。それは、関連付け(Association)ができないということです。belongs_tohas_manyもできません。困りましたね。なので、少し追記してやりましょう。

genre.rb
class Genre < ActiveHash::Base
  include ActiveHash::Associations # 追記
  self.data = [
    {id: 1, name: '恋愛(現実世界)', path_name: 'love_in_real'}, {id: 2, name: '恋愛(異世界)', path_name: 'love_in_another'},
    {id: 3, name: 'ファンタジー', path_name: 'fantasy'}, {id: 4, name: 'コメディー', path_name: 'comedy'},
    {id: 5, name: 'ラブコメ', path_name: 'love_comedy'}, {id: 6, name: '日常', path_name: 'life'},
    {id: 7, name: '青春', path_name: 'springtime_of_life'}, {id: 8, name: 'ヒューマンドラマ', path_name: 'drama'},
    {id: 9, name: 'ミステリー', path_name: 'mystery'}, {id: 10, name: 'VRゲーム', path_name: 'vr_game'},
    {id: 11, name: 'SF', path_name: 'space_fantasy'}, {id: 12, name: 'アクション', path_name: 'action'},
    {id: 13, name: '純文学', path_name: 'classic_literature'}, {id: 14, name: '戦記', path_name: 'saga'},
    {id: 15, name: 'ホラー', path_name: 'horror'}, {id: 16, name: '童話', path_name: 'fairy_tale'},
    {id: 17, name: '詩', path_name: 'poem'}, {id: 18, name: 'TRPGリプレイ', path_name: 'trpg'},
    {id: 19, name: 'エッセイ', path_name: 'essay'}, {id: 20, name: 'その他', path_name: 'others'},
  ]

  # 追記
  has_many :novels
  has_many :comics
end

こちらも見たままですが、ActiveHash::Associationsをincludeしてやれば、関連付けが可能になります。視覚的に何をしているか分かりやすいのがactive_hashの良いところですね。が、実はこれだけでもまだ不十分なんです。Associationの基本として、NovelやComicの方にもbelongs_to :genreを書かなければいけません。しかし、

novel.rb
belongs_to :genre

このようにそのまま書いても、なんとジャンルに所有されることができないのです。ではどうするのかと言うと、

novel.rb
extend ActiveHash::Associations::ActiveRecordExtensions
belongs_to_active_hash :genre

このように書き換えてあげます。これが何をしているのか、私は調べたことがないので詳しくは知りませんが、これでNovel側からGenreの情報を取得することが可能になります。

ここからは余談です

active_hashにも一つ欠点があります。それはActiveHash::Baseを継承しているモデル(今回はGenre)を、他のモデルがhas_one及びhas_manyできない点です。has_many :genresとすることもhas_many_active_hash :genresとすることもかないません。そのようにしたければ、おそらくActiveHashを拡張してあげる必要性が出てくるのですが、私はまだそれができるほどの高度なスキルを持っておりません。とはいえ、案外静的なデータをhas_manyしたい場合ってないんですよね・・・。タグやカテゴリーを持つ時ぐらいでしょうか。しかしタグはハッシュタグ機能が盛んな現代、静的なデータにしたくはないですし、カテゴリーは多階層構造にできるようancestryを使うことも多いと思います。

・・・has_manyできる機能が実装されていないのは、それが理由なのでしょうか?

ransackについて

Gemfile
gem 'ransack'

レコードの検索機能を簡単に実装できるようにしてくれるgemです。こちらも使用しないため説明は省略させていただきますね。ransackの記事はこの辺りが参考になります。

[Rails]ransackを利用した色々な検索フォーム作成方法まとめ
Rubyon Rails で検索機能を作ろう(ransack)


以上で補足説明は終わりです。思っていたよりも長くなりました。

設計編では最後にSTIについて説明し、残りはテーブルの一覧を載せていこうと思います。

STI

STIとは、Single Table Inheritance(単一テーブル継承)の略です。これを活用すると、ほとんど同じ機能を持ったテーブルの重複を避けることができます。例を挙げると、今回私はSeriesモデルを作成することを解決策の項目で述べました。しかし、シリーズと一言に言っても、小説のシリーズ(連載小説)、イラストのシリーズ(イラスト集)、漫画のシリーズ(連載漫画)はそれぞれ異なります。

連載小説は小説を多数所有しますがイラストや漫画は持たないですし、イラスト集や連載漫画についても同様です。であるならば、

series.rb
class Series < ApplicationRecord
  has_many :novels
  has_many :illustrations
  has_many :comics
end

と一つのモデルに全てを所有させてしまうのは微妙に違和感があります。もしかしたら、連載小説、イラスト集、連載漫画で、それぞれ実装したい機能も異なってくるかもしれません。そんな時に役立つのがSTIです。STIは単一テーブル継承の略でしたね。つまり、seriesテーブルを継承して、新たにnovel_series、illustration_series、comic_seriesを用意してしまえば良いのです。新たに用意と言っても、実際にテーブルを作る訳ではありません。全てのデータはseriesテーブルに保存されます。

ここからは、実際にどのように実装するのかお見せします。現時点でどういうことか分からないという方も、とりあえずお付き合いいただけると幸いです。実装の流れを見ると、おそらく掴めてきます。

typeの追加

STIを実装するには、typeというカラムをstring型で継承元のテーブルに追加する必要があります。

何らかのmigrationファイル
add_column :series, :type, :string

このtypeカラムには、今回であればNovelSeriesIllustrationSeriesComicSeriesのいずれかが入ります。つまり、typeカラムにNovelSeriesが入っていたら連載小説を、ComicSeriesが入っていたら連載漫画を表すということになります。

続いて、app/models以下に、novel_series.rbillustration_series.rbcomic_series.rbを作成します。次に、

novel_series.rb
class NovelSeries < Series
end
illustration_series.rb
class IllustrationSeries < Series
end
comic_series.rb
class ComicSeries < Series
end

としてあげてください。注意すべきは、継承元がApplicationRecordではなく、Seriesになっている点です。実は、これだけでSTIの実装は終わりです。試しにrails cでコンソールを開き、

コンソール
irb(main)> NovelSeries

とでも打ってみると、NovelSeriesがSeriesと全く同じカラムを持っていることが分かるはずです。つまりこの時点で、

NovelSeries.create(title: "テスト")
# => NovelSeiresのレコード作成
NovelSeries.first.title
# => "テスト"

というように、通常通り動作します。NovelSeries.createの時点でtypeカラムにはNovelSeriesが入っており、

Series.first.title
# => "テスト"
Series.first.type
# => "NovelSeries"

となっているはずです。これで単一テーブル継承ができましたので、

novel_series.rb
class NovelSeries < Series
  has_many :novels
end
illustration_series.rb
class IllustrationSeries < Series
  has_many :illustrations
end
comic_series.rb
class ComicSeries < Series
  has_many :comics
end

とアソシエーションを書いてあげれば、期待通りの動作をしてくれるはずです。

以上がSTIの簡単な説明になります。こちら、全てを説明しようとするとかなり長くなりますので、是非お時間のある方は下の二記事を読んでいただきたいです。

[Rails] STI(単一テーブル継承)とメタプログラミングでDRY
みんなRailsのSTIを誤解してないか!?

テーブル一覧

先述通り、最後にテーブル一覧を載せてこの記事は終わらせていただきます。ここまで長々とお付き合いいただき、ありがとうございました。各テーブルのカラムの意味や追加理由などは一言程度で説明していきます。

User

deviseで作成
以下、deviseの自動生成には含まれない追加カラムのみ記載

カラム名 オプションや補足
nickname string null: false
profile text

Association

  • has_many :novel_series
  • has_many :illustration_series
  • has_many :comic_series
  • has_many :novels
  • has_many :illustrations
  • has_many :comics

Active Storage

  • has_one_attached :icon

Series

カラム名 オプションや補足
title string null: false, index: true
outline text 概要, null: false
user references
type string NovelSeries, IllustrationSeries, ComicSeries
genre_id integer
status integer public_posted, private_posted
works_count integer 話数
posted_at datetime 投稿日時

Association

  • belongs_to_active_hash :genre
  • belongs_to :user

Active Storage

  • has_one_attached :thumbnail

NovelSeries < Series

Association

  • has_many :novels, dependent: :destroy

IllustrationSeries < Series

Association

  • has_many :illustrations, dependent: :destroy

ComicSeries < Series

Association

  • has_many :comics, dependent: :destroy

Novel

カラム名 オプションや補足
title string null: false
outline text あらすじ
content text 本文
preface text 前書き
postscript text 後書き
user references
novel_series_id integer
genre_id integer
status integer draft, public_posted, private_posted
posted_at datetime 投稿日時

Association

  • belongs_to :user
  • belongs_to :novel_series, optional: true
  • belongs_to_active_hash :genre

Illustration

カラム名 オプションや補足
title string null: false
author_comment text 作者コメント
user references
illustration_series_id integer
status integer draft, public_posted, private_posted
posted_at datetime 投稿日時

Association

  • belongs_to :user
  • belongs_to :illustration_series, optional: true
  • has_one_attached :image

Comicモデル

カラム名 オプションや補足
title string null: false
outline text あらすじ
author_comment text 作者コメント
user references
comic_series_id integer
genre_id integer
status integer draft, public_posted, private_posted
posted_at datetime 投稿日時

Association

  • belongs_to :user
  • belongs_to :comic_series, optional: true
  • belongs_to_active_hash :genre
  • has_many_attached :images

Genre

active_hashで作成

self.data = [
{ id: 1, name: string, path_name: string },
{ id: 2, name: string, path_name: string },
{ id: 3, name: string, path_name: string },
]

Association

  • has_many :series
  • has_many :novel_series
  • has_many :comic_series
  • has_many :novels
  • has_many :comics

以上で設計編は終わりです!

※ 開発編は執筆をやめました、ご了承ください。
GithubのURLを貼っておくので、中身はこちらから確認していただければと思います。
https://github.com/homarinn/creaters_sample

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0