STI(単一テーブル継承)について
STI(単一テーブル継承)
単一テーブル継承は、オブジェクトの継承関係を1つのテーブルで表現します。テーブルにはサブクラスを判断するためのカラム(type)を持たせます。
もしも同じカラムをもつテーブルが複数ある場合に、一つのテーブルにまとめて管理することで、余分なテーブルを作成せずに済みます。
STIの例
CoffeeクラスとTeaクラスのカラムが全部同じならば、一つCafesというテーブルを作って継承することでテーブル数を減らします。
CoffeesとTeasはテーブルではないのでDBには存在しません。継承データは全てCafesテーブルに保存されていきます。Typeカラムにクラス名(CoffeeかTea)を入れてどちらのクラスのデータなのかを区別しています。
MenusテーブルとCofeeクラス、Teaクラスに直接アソシエーション関係を結びたいのでモデルの定義は以下のようになります。
※ MenuとCafeにはアソシエーションは組みません。
menu.rb class Menu < ApplicationRecord belongs_to :coffee belongs_to :tea end
cafe.rb class Cafe < ApplicationRecord validates :name, presence: true, uniqueness: true validates :price, presence: true end
coffee.rb class Coffee < Cafe has_many :menus end
tea.rb class Tea < Cafe has_many :menus end
CoffeeクラスとTeaクラスは継承元がApplicationRecordを継承したCafeになっています。
Active Recordのattributeを使った更新メソッド
attributeとは
属性のことです。必要に応じて、既存の属性の型をオーバーライドすることができます。
attribute更新メソッド
これからさまざまな更新メソッドを見ていきたいと思います。
単一のattributeを更新する
指定した一つのattributeのみを変更したいときは、attribute=
を使います。オブジェクトの変更をするだけで、DBに保存はしません。
※保存が必要なときは別途save
やupdate
を行います。
user.name = 'Hoge' user.save # DB更新のためにsaveを使う
複数のattributeをまとめて更新する
特定のいくつかのattributeを変更したいときは、attributes=
かassign_attributes
を使います。これもオブジェクトの変更をするだけなので、DBに保存はしません。
※複数変更したいので複数形のattributesとなっています。
user.assign_attributes({ name: "Hoge", email: "sample@example.com" }) user.save # DB更新のためにsaveを使う
実際に以下の例文を使って説明していきます。
def update @user = User.find(params[:id]) # ① @user.assign_attributes(params[:user]) # ② if @user.save # ③ redirect_to @user else render :edit end end
①idで指定したユーザーが@userに入っています。
②@userにパラメーターで送られてきたuser情報を上書きしています。
③DBへ@userの上書きされた情報が保存されます。
なぜここでsave
を使わずにassign_attributes
を使っているのかというと、assign_attributes
はDBへの保存をしないのでSQLが複数回実行されてしまうことを防ぐことができるからです。
複数のattributeをまとめて更新かつ保存する
update
、save
、update_attributes
を使います。オブジェクトの変更をしてくれるだけでなく、DBに保存までしてくれます。またエラーが出た場合はDBへの保存は行わずにfalse
を返します。
user.update({ name: "Hoge", email: "sample@example.com" })
複数のattributeをまとめて更新かつ保存、例外処理を行う
update!
、save!
、update_attributes!
を使います。オブジェクトの変更をしてくれるだけでなく、DBに保存までしてくれます。またエラーが出た場合はDBへの保存は行わずに例外処理(ActiveRecord::RecordInvalid)をします。
user.update!({ name: "Hoge", email: "sample@example.com" })
応用編課題8で参照したサイト
Twitterの公式、埋め込みに関して
Twitterの埋め込み
TwitterのAPIを使わずに任意のツイートを埋め込む方法 - Sakura scope
【Twitter】埋め込み処理をAPIに投げずにローカルで行う - mizuff_diary
YouTubeの公式、埋め込みに関して
YouTube 埋め込みプレーヤーとプレーヤーのパラメータ | YouTube IFrame Player API
Youtubeの埋め込み
railsアプリに投稿されたYouTubeURLを自動的に埋め込み表示させる方法~無理やり編~ - Qiita
回答の解説を読んで
fontawesomeのhtmlの書き方。
content_tagとtagとは
【Rails】content_tagを解説してみる。 - Qiita
Rails: 5.1以降タグヘルパーの#tagの新しい記法は#content_tagより便利|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
display: inlineについて、アイコンを横並びにする
【CSS】displayの使い方を総まとめ!inlineやblockの違いは?
d-inline-flexについて、アイコンを横並びにする
Bootstrap4に用意されているクラス【flex編】 | Webお役立ちネタ帳
splitメソッド
【Ruby入門】splitで文字列を分割しよう! | 侍エンジニアブログ
ポリモーフィック関連について
Active Record の関連付け - Railsガイド
Railsのポリモーフィック関連とはなんなのか - Qiita
単一テーブルとポリモーフィックの違い
【Rails】ActiveRecord:単一テーブル継承(sti)とポリモーフィック関連を未だにぱっと思い出せないのでまとめ。 - 訳も知らないで
ポリモーフィックと中間テーブル
Railsで中間テーブルダブルポリモーフィックやってみた。 - Qiita
Rails4で中間テーブルがポリモーフィックなテーブルとhas_many throughする - Qiita
ポリモーフィックと中間テーブルで使うsourceとsource_typeについて
Active Record の関連付け - Railsガイド
RSpec js: trueをつける理由のmodalウィンドウ
【Rails、JavaScript】モーダルウィンドウに写真詳細画面を表示させる | LaptrinhX
RSpecでページ内に同じ要素が何個かある時に指定する。
一時間ごとにRakeタスクを実行する
今回行いたいことは、公開待ちになっているブログの記事を公開日時になったら自動で公開してくれるような実装をすることです。
ここで使う機能として3つあります。この3つを使って実装していくので少しややこしく感じますが、一つずつ説明していきたいと思います。
Rakeタスク
whenever
cron
Rakeタスクとは
Rakeは、Ruby および Rails アプリケーションで人気のあるタスクランナーです。例: Rails は、データベースの作成、移行の実行、テストの実行のための事前定義された Rake タスクを提供します。カスタムタスクを作成して特定のアクションを自動化することもできます。コード分析ツールの実行、データベースのバックアップなど。
そしてこのRakeが実行する処理内容を「Rakeタスク」と呼び、定義する場所を「Rakefile」と呼びます。
Rakeタスクの使用方法
Rakeタスクに「公開日時になっている公開待ちの記事を公開に変更する」を定義していきます。enumで記事の状態を「公開(published)」「公開待ち(future_publish)」に分類しています。
models/article.rb enum state: { published: 0, future_publish: 1 }
まず最初にタスクファイルを生成します。
ファイル名は何を行うファイルなのかわかりやすい名前をつけると良いです。今回は「article_state」にしました。
$ rails g task ファイル名
lib/tasks/配下にarticle_state.rakeというファイルが生成されました。
ファイルの中身は以下のようになっていると思います。
namespace :article_state do end
作成されたファイルに実行したい処理を記入していきます。
lib/tasks/article_state.rake namespace :article_state do desc '公開待ちの中で、公開日時が過去になっているものがあれば、ステータスを「公開」に変更されるようにする。' task change_to_be_published: :environment do Article.future_publish.past_published.find_each(&:published!) end end
desc
には処理の説明を記入します。
task
にはタスク名をつけます。
:environment
はデータベースに接続する必要がある場合につけます。今回はstateカラムのデータが変わるので必要です。
do
以下に処理内容を記入します。
do
以下のコードは少し難しいので一つずつ読み解いていきます。
past_published
は下記のarticleモデルで定義したscopeのメソッドです。
事前に公開日時が現在からみて過去の記事を取得するためのscopeを準備します。
参考 : 部分一致とか日付の範囲で検索したい - Qiita
models/article.rb scope :past_published, -> { where('published_at <= ?', Time.current) }
find_each
は大量のデータを一度に取得してループするのではなく決まった単位(デフォルト1,000件)ごとに取得してループしてくれる機能です。データを分割して取得することで少ないメモリで処理することができます。
参考 : [Rails]find_eachが無限ループして本番環境のメモリを食いつぶした話 - Qiita
&:
は配列(find_eachやmap、select、pluckなど)の中身を受け取るようなメソッドで使用することができます。
この二つのコードは同じ意味です。 Article.find_each(&:published!) Article.find_each { |article| article.published! }
published!
はenumのメソッドです。以下でenumのメソッドについて説明しておきます。
article = Article.new(state: :future_publish) article.state => future_publish article.future_publish? => true article.published? => false article.published! => stateをpublishedに変更することができます。 article.state = 'published'と同じ意味ですが、コード量が多くなるのでNGです。 Article.published => publishedになっている記事をまとめて全件検索することができます。 Article.where(state: :published)と同じ意味になります。
ファイルの中身が書き終わったら作ったRakeファイルを確認します。
$ rails -T
このようにコマンド上で確認することができます。
これで「公開日時になっている公開待ちの記事を公開に変更する」を定義できました。次は一時間ごとに自動で「公開待ちを公開にする」設定を行っていきたいと思います。
wheneverを導入する
wheneverはcrontab管理ライブラリです。
Gemfileにwheneverのgemを追加してbundle install
を行います。
gem 'whenever', require: false
require: false
はRailsの内部でwheneverを使うわけではないため、Railsの実行時に読み込まないようにするためです。
wheneverの設定ファイルを作成します。
$ bundle exec wheneverize .
このコマンドによってconfig/schedule.rbファイルが生成されました。このファイルの中で、cronを設定していきます。
config/schedule.rb # Rails.rootを使用するために必要。なぜなら、wheneverは読み込まれるときにrailsを起動する必要がある require File.expand_path(File.dirname(__FILE__) + "/environment") # cronを実行する環境変数 rails_env = ENV['RAILS_ENV'] || :development # cronを実行する環境変数をセット set :environment, rails_env # cronのログの吐き出し場所。ここでエラー内容を確認する set :output, "#{Rails.root}/log/cron.log" # 1時間ごとにarticle_state.rakeのchange_to_be_publishedを実行する every 1.hour do rake "article_state:change_to_be_published" end
参考 : [初学者]whenever を使って定期的にバッチ処理を行う(公開設定編) - Qiita
crontabを実行する
cronとは「○時になったら○○のコマンドを実行」などといった具合に、定期的にコマンドを実行するためにメモリ場で常に命令を待機しているプロセスです。このcronに対して命令を行うには、crontabというコマンドを実行して登録・確認・管理しています。
# wheneverの設定更新 $ bundle exec whenever --update-crontab
このコマンドを使うことでcronにデータを反映して、schedule.rbファイルに記載したコードを実行することができます。
他にも下記のようなcrontabコマンドがあります。
# 設定内容にエラーがないか確認 $ bundle exec whenever # 設定されているcronを見る $ crontab -l # crontabの設定削除 $ bundle exec whenever --clear-crontab
gretelを使ってパンくずリストを実装
パンくずリストとは
ユーザーがサイトを使用する際、自分が今サイトのどの位置にいるのかというのを瞬時にわかるようにするための表示のことを「パンくずリスト」と言います。「パンくず」と省略して使われています。
Railsアプリケーションにおいてパンくずを導入したい場合は、gretelというgemが便利です。
gretelの使用方法
Gemfileにgretelのgemを追加してbundle install
します。
gem 'gretel'
generatorでgretelを起動させるコマンド実行します。
$ rails generate gretel:install
config配下にbreadcrumbs.rbが作成されたら準備完了です。このファイルの中にパンくずの定義をまとめて管理します。
config/breadcrumbs.rb crumb :root do link "Home", root_path end
ファイルにデフォルトで入っているコードは上記のようになっていて、現在はルートページのパンくずの設定のみ記述されています。
ユーザーの新規登録画面とユーザー詳細画面、ユーザー編集画面にもパンくずをつけるための設定を行っていきます。
config/breadcrumbs.rb #:rootはパンくずの設定上の名前なのでわかりやすいものにします。 crumb :root do #実際にパンくずリストに表示される部分の設定です。 link "Home", root_path end crumb :user_new do link "ユーザー登録", new_user_path #親のページ名(現在のページから見て一つ前のページ) parent :root end #ブロック変数を使って選択したユーザーを特定する crumb :user_show do |user| link "#{user.name}の詳細", user_path(user) parent :root end crumb :user_edit do |user| #編集機能の子ページはないのでpathはつけない link "ユーザー編集" #ブロック変数の値を使って選択したユーザーを特定する parent :user_show, user end
実際に設定したパンくずを表示していきます。
全てのページにパンくずリストを表示したいので、application.html.erbファイルに記述します。
view/layout/application.html.erb <%= breadcrumbs separator: " › " %>
" › "
は特殊文字で>を意味しています。HTMLで使うときに閉じタグと間違えてしまうのでこのように記載します。
そして、application.html.erbに表示されるパンくずをパンくずを使いたい各ビューファイルに指定する必要があります。
users/new.html.erb <% breadcrumb :user_new %> users/show.html.erb <% breadcrumb :user_show, @user %> users/edit.html.erb <% breadcrumb :user_edit, @user %>
詳細画面と編集画面にはユーザー情報を入れたいのでインスタンスを渡さなくてはいけません。
参考 : GitHub - kzkn/gretel: Flexible Ruby on Rails breadcrumbs plugin.
seed_fuを使って初期データを作成
seed_fuとは
すでに存在しているデータの中の変更したいレコードだけを更新することができたり、ファイル単位でデータを作成することができます。
データを作成するrailsの仕組みとしてseeds.rbファイルに記入する方法がありますが、この方法だとデータの変更があった時に、データを一度削除してから再度追加する必要があリます。seeds.rbファイルに関しては以前まとめた記事を参照してください。
一方で seed-fuを使うと、seed データの変更をしてもrails db:seed_fu
を再度行うだけで変更点だけをデータに反映させることができるという点において優れています。
seeds.rbとseed_fuの違い
seeds.rb
データの一部を変更した時、既存のデータを削除してから再度読み込まなくてはいけない。
seed_fu
変更した部分だけを読み込むことができます。
seed_fuの使用方法
まずGemfileにseed_fuのgemを追加してbundle install
します。
gem 'seed-fu'
seedファイルを置くためのディレクトリを作成します。
$ mkdir db/fixtures $ mkdir db/fixtures/development $ mkdir db/fixtures/production $ mkdir db/fixtures/test
seedファイルはdb/fixtures
配下に作成するのが慣例です。また今回のようにデータを投入する環境(開発、本番、テスト)をディレクトリごとに分けて指定することもできます。
開発環境のUserテーブルにデータを作成していきたいと思います。
まずdevelopment配下にusers.rbファイルを追加します。
$ touch db/fixtures/development/users.rb
ファイルの中身はこのように書きます。
db/fixtures/development/users.rb User.seed do |s| s.id = 1 s.name = 'admin' s.password = "password" s.role = 'admin' end User.seed do |s| s.id = 2 s.name = 'editor' s.password = "password" s.role = 'editor' end User.seed do |s| s.id = 3 s.name = 'writer' s.password = "password" s.role = 'writer' end
上記のコードをシンタックスシュガー(構文の省略した書き方)で記入することもできます。
db/fixtures/development/users.rb User.seed( :id, { id: 1, name: 'admin', password: 'password', role: :admin }, { id: 2, name: 'editor', password: 'password', role: :editor }, { id: 3, name: 'writer', password: 'password', role: :writer }, )
データベースにデータを入れるために下記のコマンドを実行します。
※ データを挿入する環境を指定することもできます。
$ rails db:seed_fu $ rails db:seed_fu RAILS_ENV=development #開発 $ rails db:seed_fu RAILS_ENV=production #本番 $ rails db:seed_fu RAILS_ENV=test #テスト
RSpecでseed_fuのデータを使用
RSpecのテストを実行する前に、ユーザーのデータが存在することを前提に自動テストを実行したいですよね。
そのためにspec/spec_helper.rbファイルに次の記述をしておきます。
参考 : Railsアプリのマスターデータ管理 Seed Fu ベタープラクティス - ナガモト の blog
spec/spec_helper.rb RSpec.configure do |config| config.before(:suite) do SeedFu.seed end end
応用編課題7で参照したサイト
has_one_attachedについて
横幅のカラムに対してのバリデーションについて
3. バリデーション(3) | TECHSCORE(テックスコア)
[Rails][validation]integerに対する最小値や最大値などの設定 - Qiita
画像の位置を変更する
【初心者向け】HTMLで画像を配置する様々な方法 | CodeCampus
divとsectionとarticle要素それぞれの特徴、使い分けと組み合わせ方 -HTML5のセクショニング コンテンツ | コリス
ラジオボタンの追加
Railsでラジオボタンを実装する方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン
ruby-on-rails - enumを使用するときにsimple_form入力に表示されるテキストを変更 - ITツールウェブ
RSpec
画像を挿入する
capybaraで画像アップロード機能の統合テストをしたい - その辺にいるWebエンジニアの備忘録
ラジオボタンの選択
RSpec + Capybara でラジオボタン/セレクト/チェックボックスのフォーム要素をテスト | EasyRamble
[RSpec] System Specでおしゃれラジオボタンをチェックできないときの対処法 - Qiita
回答例をみてから参考にしたサイト
Active Strageについて 【Rails 5.2】 Active Storageの使い方 - Qiita
simple_form
【Rails】simple_form使い方基礎 - Qiita
enum_help
GitHub - zmbacker/enum_help: Help ActiveRecord::Enum feature to work fine with I18n and simple_form.