学習記録

アウトプット用に作りました

STI(単一テーブル継承)について

STI(単一テーブル継承)

単一テーブル継承は、オブジェクトの継承関係を1つのテーブルで表現します。テーブルにはサブクラスを判断するためのカラム(type)を持たせます。

もしも同じカラムをもつテーブルが複数ある場合に、一つのテーブルにまとめて管理することで、余分なテーブルを作成せずに済みます。


STIの例

CoffeeクラスとTeaクラスのカラムが全部同じならば、一つCafesというテーブルを作って継承することでテーブル数を減らします。

f:id:kimura34:20210426180845p:plain

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になっています。

参考 : 【Rails】単一テーブル継承(STI)について - Qiita

Active Recordのattributeを使った更新メソッド

attributeとは

属性のことです。必要に応じて、既存の属性の型をオーバーライドすることができます。


attribute更新メソッド

これからさまざまな更新メソッドを見ていきたいと思います。

単一のattributeを更新する

指定した一つのattributeのみを変更したいときは、attribute=を使います。オブジェクトの変更をするだけで、DBに保存はしません。
※保存が必要なときは別途saveupdateを行います。

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をまとめて更新かつ保存する

updatesaveupdate_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" })

参考 : ActiveRecord の attribute 更新方法まとめ - Qiita

応用編課題8で参照したサイト

Twitterの公式、埋め込みに関して

ウェブサイトやブログにツイートを埋め込む方法

Twitter Publish

Twitterの埋め込み

TwitterのAPIを使わずに任意のツイートを埋め込む方法 - Sakura scope

【Twitter】埋め込み処理をAPIに投げずにローカルで行う - mizuff_diary

YouTubeの公式、埋め込みに関して

動画と再生リストを埋め込む - YouTube ヘルプ

YouTube 埋め込みプレーヤーとプレーヤーのパラメータ  |  YouTube IFrame Player API

Youtubeの埋め込み

railsアプリに投稿されたYouTubeURLを自動的に埋め込み表示させる方法~無理やり編~ - Qiita

回答の解説を読んで

fontawesomeのhtmlの書き方。

フォントアイコンの使い方 (FontAwesome)

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でページ内に同じ要素が何個かある時に指定する。

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita

一時間ごとにRakeタスクを実行する

今回行いたいことは、公開待ちになっているブログの記事を公開日時になったら自動で公開してくれるような実装をすることです。
ここで使う機能として3つあります。この3つを使って実装していくので少しややこしく感じますが、一つずつ説明していきたいと思います。

  1. Rakeタスク

  2. whenever

  3. cron


Rakeタスクとは

Rakeは、Ruby および Rails アプリケーションで人気のあるタスクランナーです。例: Rails は、データベースの作成、移行の実行、テストの実行のための事前定義された Rake タスクを提供します。カスタムタスクを作成して特定のアクションを自動化することもできます。コード分析ツールの実行、データベースのバックアップなど。

Rake タスクの実行 | RubyMine

そしてこの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

f:id:kimura34:20210424180006p:plain このようにコマンド上で確認することができます。

これで「公開日時になっている公開待ちの記事を公開に変更する」を定義できました。次は一時間ごとに自動で「公開待ちを公開にする」設定を行っていきたいと思います。


wheneverを導入する

wheneverはcrontab管理ライブラリです。 Gemfileにwheneverのgemを追加してbundle installを行います。

gem 'whenever', require: false

require: falseRailsの内部で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

参考 : crontabコマンドの使い方: UNIX/Linuxの部屋

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: " &rsaquo; " %>

" &rsaquo; "特殊文字で>を意味しています。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について

Active Storage の概要 - Railsガイド

横幅のカラムに対してのバリデーションについて

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

Active Storage の概要 - Railsガイド

simple_form

【Rails】simple_form使い方基礎 - Qiita

GitHub - heartcombo/simple_form: Forms made easy for Rails! It's tied to a simple DSL, with no opinion on markup.

enum_help

GitHub - zmbacker/enum_help: Help ActiveRecord::Enum feature to work fine with I18n and simple_form.