WeBlog

Webに関する情報を中心に発信してるブログ

ActiveRecordのinverse_ofについてメモ

inverse_ofとは

inverse_ofを指定した関連づいている2つのモデルでは、お互いに同じインスタンス(オブジェクト)を参照できるようになる。

両方のモデルがメモリ上で同一のインスタンス(オブジェクト)として扱われる。

逆に、inverse_ofの設定が無いと別物として扱われるので、一方のなんらかの変更がもう一方から参照したときに反映されていない事になる。

実際にやってみた

inverse_ofの記載なし

# Article で inverse_ofの記載なし
class Article < ApplicationRecord
  has_many :article_blocks

# ArticleBlockモデル
class ArticleBlock < ApplicationRecord
  belongs_to :article

# コンソール

> article = Article.find_by!(title: 'タイトル_01')
   
=> #<Article:0x00007fb82d7eba20
 id: 1,
 title: "タイトル_01",
 description: "概要_01",
 created_at: Sun, 06 Jun 2021 16:03:35 JST +09:00,
 updated_at: Sun, 06 Jun 2021 17:50:42 JST +09:00>

> article_block = article.article_blocks.first
=> #<ArticleBlock:0x00007fb82c3af7f0
 id: 1,
 article_id: 1,
 blockable_type: "Medium",
 created_at: Sun, 06 Jun 2021 17:46:12 JST +09:00,
 updated_at: Sun, 06 Jun 2021 17:46:12 JST +09:00>

> article.title == article_block.article.title  # 注目
=> true

> article.title = 'タイトル変更'
=> "タイトル変更"

> article.title == article_block.article.title  # 注目
=> false

上記のようにArticleモデルArticleBlockモデルがあります。

一つのArticleモデルは、複数のArticleBlockモデルと紐づいているので、has_many :article_blocksと記載しています。

一つのArticleBlockモデルは、一つのArticleモデルと紐づいているので、belongs_to :articleと記載しています。

まずは、inverse_of:がない場合で実験してみます。

そうすると、1回目のarticle.title == article_block.article.titleの結果はtrueになっています。

しかし、article.title = 'タイトル変更'でtitleを変更してみました。

そして、2回目のarticle.title == article_block.article.titleを実行するとfalseと判定されています。

Articleのフィールドtitleの変更が、ArticleBlockから関連づけでたどった場合に変更が参照できないです。

メモリ上で別インスタンスとして扱われているということです。

inverse_ofの記載あり

# Article で inverse_ofの記載あり
class Article < ApplicationRecord
  has_many :article_blocks, inverse_of: :article

# ArticleBlockモデル
class ArticleBlock < ApplicationRecord
  belongs_to :article

# コンソール
> article = Article.find_by!(title: 'タイトル_01')
=> #<Article:0x00007ff330976d98
 id: 1,
 title: "タイトル_01",
 description: "概要_01",
 created_at: Sun, 06 Jun 2021 16:03:35 JST +09:00,
 updated_at: Sun, 06 Jun 2021 17:50:42 JST +09:00>

> article_block = article.article_blocks.first
=> #<ArticleBlock:0x00007ff331a6c170
 id: 1,
 article_id: 1,
 blockable_type: "Medium",
 created_at: Sun, 06 Jun 2021 17:46:12 JST +09:00,
 updated_at: Sun, 06 Jun 2021 17:46:12 JST +09:00>

> article.title == article_block.article.title  
=> true

> article.title = 'タイトル変更'
=> "タイトル変更"

> article.title == article_block.article.title  # 注目
=> true

Articleモデルにinverse_of: :articleと記載しました。

inverse_ofはhas_manyの方に記載します。

inverse_ofを記載すると、article.title = 'タイトル変更'で変更を加えた後の

article.title == article_block.article.titleの比較がtrueに変わっています。

これは、メモリ上で同一インスタンスとして扱われるようになり、article.titleもしくはarticle_block.article.titleからtitleを変更しても、article.title == article_block.article.titleがtrueを返すようになります。

https://railsguides.jp/association_basics.html#双方向関連付け

https://shoken.hatenablog.com/entry/2015/07/14/095211

gem seed-fu を使って、ダミーデータを作成する

gem seed-fu を使うことでダミーデータを入れることができます。

railsにはデフォルトで rails db:seed というコマンドを使って、db/seeds.rbを実行することでダミーデータを入れることができます。

db/seeds.rbは何も考えずに使うと、rails db:seed というコマンドを実行するたびに同じデータが登録されてしまいます。

gem seed-fuは、すでにデータベース(カラム)に存在しているが、変更したいレコードだけ更新したり、ファイル単位で実行できたり、簡単に書けるような便利さがあります。

https://github.com/mbleigh/seed-fu

使い方

インストール

# Gemfile
gem 'seed-fu'

Gemfileに記述して、bundle install します。

ディレクトリを作成

seedファイルを置くディレクトリを作成します。

f:id:weblog_tec:20210610094453p:plain

% mkdir db/fixtures
% mkdir db/fixtures/development
% mkdir db/fixtures/production
% mkdir db/fixtures/test

db/fixturesは絶対に必要なディレクトリです。

このディレクトに下にseedファイルを置きます。

db/fixtures配下に環境ごとにシーダーファイルを作りたい場合は、「development」「production」とディレクトリを作成します。

データを作成してみる

# db/fixtures/delopment/users.rb
User.seed do |s|
  s.id    = 1
  s.name = "田中"
  s.crypted_password = "password"
end

User.seed do |s|
  s.id    = 2
  s.name = "佐藤"
  s.crypted_password = "password"
end

Userモデルが存在していたとして、gem seed-fuではデータを上記のように書けます。

データを作成するコードは上記のようにdo ~ end で記載できます。

User.seed(
  :id,
  { id: 1, name: '田中', crypted_password: User.encrypt('password') },
  { id: 2, name: '佐藤', crypted_password: User.encrypt('password') },
)

しかし、多くのレコードを記載する場合、do ~ endブロックの構文は長くなる可能性があります。

代わりに上記のように書けます。

そして、rails db:seed_fuコマンドを使って、seedファイルからデータを投入できます。

https://qiita.com/ko2ic/items/be96e450a33d631e0059

https://qiita.com/punkshiraishi/items/18c80475fcd50d708dc4

https://bagelee.com/programming/make-data-with-seed-fu/

テストでseedデータを挿入する

Rspecを使ってテストを実行する前にデータベースにデータを入れておきたいことがあります。

seed-fuを使用して、事前にデータベースにデータを入れておく方法を書きます。

# db/fixtures/test/sites.rb

Site.seed(
    :id,
    id: 1,
    name: 'タイトル',
    subtitle: 'サブタイトル'
)

例えば上記のようにあるサイトの「タイトル」と「サブタイトル」をテスト前にデータベースに保存しておきたいと思います。

このファイルはdb/fixtures/test配下に用意しておきます。

# spec/spec_helper.rb

RSpec.configure do |config|
    # テスト開始前にDBにデータを入れる。db/fixtures/test/sites.rb ファイルの seed-fu を使う
  config.before :suite do
    SeedFu.seed
  end

spec/spec_helper.rbに上記のように記載しておくと、テスト実行前にデータを挿入できます。

デフォルトでは、シード ファイルは

で検索されます。

なので、テストの時は/db/fixtures/test配下のseedファイルを実行してくれます。

https://qiita.com/naoki_mochizuki/items/1d3026a32786642fc762

Slimの使い方

Slim とは?

Slim は Rails3 以降 に対応した高速, 軽量なテンプレートエンジンです。

  • HTMLの< >や閉じタグなどを無くして、最低限必要なものだけを残したシンプルなテンプレート言語
  • 軽量

といった特徴を持つ、Ruby製のテンプレートエンジンです。

つまり、このslimの記法を用いることによって、HTMLがより簡潔に記述できるという訳ですね。

使い方

slim-railsをインストールする

Railsにはデフォルトでerbというテンプレートエンジンが導入されています。

これをslimに変更したい場合、

# Gemfile
gem 'slim-rails'

とGemfileに追記してあげた上で、bundle intallを実行して、viewファイルの拡張子をhtml.slimとしてあげればOKです。

slim-railsインストールした後に、ジェネレーターで生成されるviewファイルの拡張子は、.html.erbから.html.slimに変更されます。

既にerbで記載したコードがある場合の変更

# Gemfile
gem 'html2slim'

erbをslimに変更したい場合は、html2slimなどのgemを用いて記述を書き換えてしまうのが一般的です。

# -dを付けるとslimに変換した後にerbを削除する
bundle exec erb2slim app/views app/views -d

gemをインストール後に上記のコマンドを打つことで、app/views配下のerbファイルやerbで記載したコードを、slimに変換してくれます。

erbをslimに変換するhtml2slim

https://github.com/slim-template/html2slim

ERB 2 SLIM

slimの記法

slimはテンプレートエンジンであるため、最終的にはHTMLとなって出力されます。

例えばerbの中でrubyのeach記法を使用したい場合、

<% @users.each do |user| %>
  <p><%= user.name %></p>
<% end %>

こんな形で書きます。

これをslim記法になると、

- @users.each do |user|
  p = user.name

このような記述となります。

  • <% %>というruby記法の使用宣言が、「-」となっている
  • <%= %> というruby記法の使用宣言 + 出力が、「=」となっていること
  • 閉じタグが不要なこと (段落によって閉じタグを判断している)
  • <p> ~~ </p>という記法が、pとなっている(HTMLタグの <> や は不要)

といったルールにより、この短いコードで記述することができました。

# slim
p 段落
a アンカー

# 出力
<p>段落</p>
<a>アンカー</a>

HTMLも<p></p>など必要ないです。

上記のように書けます。

dev
  p.title 段落

dev
  p#title 段落

# 出力
<dev>
  <p class="title">段落</hoge>
</dev>

<dev>
  <p id="title">段落</hoge>
</dev>

class属性やid属性はcssのように.を使って書きます。

p.title.text-center 段落

# 出力
<p class="title text-center">段落</hoge>

class属性やid属性を連続して書くときも上記のように繋げてかけます。

p
  | テキスト

# 出力
<p>テキスト</p>

テキストはパイプラインを使って書きます。

# = を使った場合
div
  - if policy(article).edit?
      =  link_to edit_admin_article_path(article.uuid), class: %w[btn btn-default btn-xs btn-flat]
       i.fa.fa-edit
       '
       | 編集

f:id:weblog_tec:20210610094259p:plain

# =>を使った場合
div
  - if policy(article).edit?
    => link_to edit_admin_article_path(article.uuid), class: %w[btn btn-default btn-xs btn-flat]
     i.fa.fa-edit
     '
     | 編集

f:id:weblog_tec:20210610094312p:plain

Rubyスクリプトを書く<%= %>=で書くことが多いですが、=> で書くこともできます。

=>は末尾のスペースを伴った出力をします。

なので画像のように=で書くと「編集」と「プレビュー」がくっついていますが、=>で書くとスペースが空いていることがわかります。

他にも色々ありますが、下記記事を参考にしてください。

slim

https://qiita.com/ngron/items/c03e68642c2ab77e7283

FactoryBotでtraitを使って重複をなくす方法

今回はFactoryBotでtraitを使って重複をなくす方法について書いていきます。

traitを使わない場合

FactoryBot.define do
  factory :todo_task do
    title { 'Task' }
    status { :todo } # enumを使っている
    association :project
  end

    factory :doing_task do
    title { 'Task' }
    status { :doing } # enumを使っている
    association :project
  end

    factory :done_task do
    title { 'Task' }
    status { :done } # enumを使っている
        completion_date { Time.current.yesterday }
    association :project
  end
end

Factory Bot では同じデータを作成するファクトリを複数定義することもできます。

上記のように todo_task、doing_taskdone_taskの3つのファクトリを作成しています。

このように複数定義できます。

3つの違いは

  • TODOとして登録しているtodo_task
  • 今取り組んでいるdoing_task
  • 終了したdone_task

の3つに分けています。

また、done_taskは終了したTaskなので、終了した日付のデータを持つようにcompletion_date { Time.current.yesterday }と記載しています。

FactoryBot.create(:todo_task)

FactoryBot.create(:doing_task)

FactoryBot.create(:done_task)

もしこのFactoryBotを呼び出そうと思ったら、上記のようにcreatebuildメソッドの引数にFactoryBotの名前を指定してあげることで、それぞれのFactoryBotを作成することができます。

require 'rails_helper'

RSpec.describe 'Task', type: :system do
    let(:todo_task) { FactoryBot.create(:todo_task) }
    let(:todo_task) { FactoryBot.create(:doing_task) }
    let(:todo_task) { FactoryBot.create(:done_task) }

テストファイルの中でletlet!を使って書く場合は、上記のような感じになります。

require 'rails_helper'

RSpec.describe 'Task', type: :system do
    let(:todo_task) { create(:todo_task) }
    let(:todo_task) { create(:doing_task) }
    let(:todo_task) { create(:done_task) }

さらにrails_helper.rbconfig.include FactoryBot::Syntax::Methodsという設定を書くことでFactoryBot.という記載は必要なくなるので、上記のように省略して書くことができます。

FactoryBot.define do
  factory :todo_task do
    title { 'Task' }
    status { :todo } # enumを使っている
    association :project
  end

    factory :doing_task do
    title { 'Task' }
    status { :doing } # enumを使っている
    association :project
  end

    factory :done_task do
    title { 'Task' }
    status { :done } # enumを使っている
        completion_date { Time.current.yesterday }
    association :project
  end
end

話を戻して、FactoryBotを見てみると、ファクトリにたくさん重複があります。

FactoryBot.define do
  factory :todo_task do
    title { 'Task' }
    status { :todo }
    association :project
  end

    factory :doing_task do
    title { 'Task' }
    status { :doing }
    association :project
  end

    factory :done_task do
    title { 'Task' }
    status { :done }
        completion_date { Time.current.yesterday }
    association :project
  end

    # 新しいファクトリ
    factory :〇〇_task do
    title { 'Task' }
    status { :△△△△ }
    association :project
  end
end

もし、新しいファクトリを定義しようとした場合、全属性を再定義しなければいけません。

上記のように〇〇_taskを作って、titlestatusなどを全部再定義しないといけません。

FactoryBot.define do
  factory :todo_task do
    title { 'Task' }
        # 追加
        content { 'Content' }
    status { :todo }
    association :project
  end

    factory :doing_task do
    title { 'Task' }
        # 追加
        content { 'Content' }
    status { :doing }
    association :project
  end

    factory :done_task do
    title { 'Task' }
        # 追加
        content { 'Content' }
    status { :done }
        completion_date { Time.current.yesterday }
    association :project
  end

    # 新しいファクトリ
    factory :〇〇_task do
    title { 'Task' }
        # 追加
        content { 'Content' }
    status { :△△△△ }
    association :project
  end
end

また、ファクトリを全部再定義しないといけないということは、逆にいうとTaskモデルの属性(カラム)を変更した場合に毎回全部のファクトリの定義を変更する必要が出てきます。

例えばTaskモデルの属性にcontentを追加して、その属性(カラム)がvalidates :content, presence: trueというバリデーションが付与されていたりした場合は、全てのファクトリに

content { 'Content' }にように追加してあげる必要があります。

こんな時に、Factory Bot には重複を減らすテクニックがあります。

一つ目は 「ファクトリの継承 」を使ってユニークな属性だけを変えることです。

二つ目が 「trait」を使う方法です。

今回は「trait」を使う方法を記載します。

traitを使って重複を減らす方法

FactoryBot.define do
  factory :task do
    title { 'Task' }
    status { :todo }
    association :project
        
        # status が done
    trait :done do
      status { :done }
      completion_date { Time.current.yesterday }
    end
        
        # status が doing
      trait :doing do
      status { :doing }
    end

  end
end

traitは上記のように書きます。

traitというのがキーワードになります。

traitを使うことで重複がなくなっていることがわかります。

デフォルトから変更したい部分だけをtraitで記載することで重複を減らすことができます。

# taskファクトリーを使う
FactoryBot.create(:task)

# taskファクトリー と trait :done を使う
FactoryBot.create(:task, :done)

# taskファクトリー と trait :doing を使う
FactoryBot.create(:task, :doing)

FactoryBot.create(:task)FactoryBotを呼び出してあげると、taskという名前がついているFactoryBotを使ってデータを作ることができます。

FactoryBot.create(:task, :done)FactoryBotを呼び出してあげると、taskという名前のついてFactoryBotとdoneという名前のついたtraitを使ってデータを作ることができます。

f:id:weblog_tec:20210603141444p:plain

実際に確認してみるとFactoryBot.create(:task)FactoryBotを呼んであげると、titleは「Task」でstatusは「todo」になっていることがわかります。

f:id:weblog_tec:20210603141458p:plain

FactoryBot.create(:task, :done)FactoryBotを呼んであげると、statusは「done」でcompletion_dateにも日付が入っていることがわかります。

require 'rails_helper'

RSpec.describe 'Task', type: :system do
    let(:task) { create(:task) }
    let(:done_task) { create(:task, :done) }
    let(:doing_task) { create(:task, :doing) }

letを使って上記のように書いておいて、呼び出すこともできます。

Railsの環境構築での疑問

今回はスクールの課題でforkした後に環境構築で疑問に思うことがあったので、その内容を書いていきます。

1、リモートリポジトリをforkする

今回は既存のリモートリポジトリをforkして課題を進める感じだったので、ますはforkしました。

2、リモートリポジトリをローカルにcloneする

% git clone URL

% cd rspec_app_exam

次にforkしたリモートリポジトリをローカルにcloneしました。

3、ブランチを切る

# ローカルリポジトリでブランチを切る
% git checkout -b test

% git branch
* test

まずはgit checkout -bコマンドで、開発用にブランチを切ってあげました。

git branchコマンドで、ブランチが作成されて、切り替わっていることを確認しました。

4、環境構築

% bundle install

まずは、ローカルにcloneしてきたので、Gemfileに記載されたgemをインストールするためにbundle installコマンドを実行しました。

すると、vender/bundle配下にインストールした、いろんなgemのディレクトリやファイルが作成されます。

ちなみにbundle install --path vendor/bundleなど--path vendor/bundleは必要ないらしいです。

https://qiita.com/jnchito/items/62adbea043abf72fa7cc

f:id:weblog_tec:20210602220012p:plain

f:id:weblog_tec:20210602220032p:plain

% rails db:create
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'

rails db:createconfig/database.ymlの設定に基づいて、データベースが作成される。

ちなみにdevelopmentとtestしか作成されないので、prodactionは作成されない。

なので、上記の画像をみてみると、development.sqlite3 や test.sqlite3 というファイルが作成されている。

また、GUIで確認してみると、DBは作成されているが、テーブルやカラムは作成されていないことがわかる。

f:id:weblog_tec:20210602220122p:plain

f:id:weblog_tec:20210602220134p:plain

% rails db:migrate

rails db:migrateコマンドでマイグレーションファイルを実行して、テーブルやカラムを作成する。

また、rails db:migrateコマンドを実行すると、rails db:schema:dumpコマンドも自動で実行されるので、スキーマファイル(schema.rb)を作られたり、更新されたりする。

rails db:migrateコマンドを実行した場合、デフォルトでは開発用に対してマイグレーションが実施される。

なのでGUIでみてみると、開発環境にはテーブルやカラムが作成されているが、test環境には作成されていない。

f:id:weblog_tec:20210602220158p:plain

# test環境にマイグレーションファイルを適用する
% rails db:migrate RAILS_ENV=test

なので、今回はtest環境にもマイグレーションファイルを適用してあげたいので上記のコマンドを実行。

https://qiita.com/mah666hhh/items/6e3bbdde67f9ccb59d0e

% rails db:seed

rails db:seedコマンドdb/seeds.rbファイルをを実行して、develop環境にダミーデータを作成する。

% bundle exec rails s

rails serverコマンドによるサーバーの起動。

http://localhost:3000でアクセスできたらOK

疑問に思ったこと

何に疑問を感じたのかですが、今回の課題では1個1個コマンドを打って開発環境を整えていきました。

# 今回は使わない
% rails db:setup

ただ、rails db:setupコマンドを実行すれば

  • データベースの作成
  • スキーマの読み込み(テーブル作成、カラム作成、マイグレーションファイルの実行)
  • シードデータを用いてデータベースの初期化

までをトータルで実行してくるんじゃないのかと疑問に思いました。

rails db:setupは何をしてくれるのか?

# データベースを作成する
% rails db:create

# db/schema.rbを利用してテーブルやカラムが作成される。db/migrate/**.rb は使われません。
% rails db:schema:load

# シーダーファイルを実行する
% rails db:seed

rails db:setupコマンドは上記3つを実行してくれるみたいです。

rails db:migrateマイグレーションファイルをもとに、テーブルやカラム、schema.rbファイルを作成したり、更新する。

rails db:schema:loadはschema.rbの内容を現在参照しているDBに適用して、テーブルやカラムを作成する。

データベースは削除され、作り直されるみたいです。

https://www.it-swarm-ja.com/ja/ruby-on-rails/rake-db:migrate-db:resetとdb:schema:loadの違い/1067577738/

https://qiita.com/hirohero/items/2f29334878b0cb525bda