if it does't challenge you, it can't change you

RubyのWebエンジニアやってます。主にRuby on Railsのことで勉強になったこと書いていきます。

railsコマンドでファイル名を指定してseedデータを入れる方法

バージョン

テストデータやマスタデータの挿入

Railsにはデフォルトでdb/seeds.rbというファイルが生成されていて、
ここにインサート文などを書いておくと、
rails db:seedでデータをDBに挿入してくれます。

指定したseedファイルだけを挿入

ただ、特定のデータだけ挿入したいというときには、
毎回、seeds.rbを書き換えないといけません。
1回だけならまだいいですが、間隔をおいて何回も挿入しなければいけないときは、だいぶめんどくさいです。
そんなときのために、このような

rails db:seed_from_file SEED_FILENAME='seeds/ファイル名'

railsコマンドの引数でファイル名を渡して、seedデータを挿入する方法を書いていきます。
なお、ディレクトリの構成はこんな感じです。

root/
  lib/
    tasks/
      db.rake
  db/
    seeds/
      01_sample_seeds.rb
      02_sample_seeds.rb
      03_sample_seeds.rb

挿入したいテストデータをdb/seeds配下のファイルに記載している想定です。

タスクファイルを作成する

rails g task ファイル名でタスクファイルを作成することができます。
今回は、rails g task dbとコマンドを叩き、lib/tasks/db.rakeを作成しました。

どのseedファイルを実行するか記載

タスクファイルが作成できたら、中身を書いていきます。
今回叩きたいコマンドrails db:seed_from_file SEED_FILENAME='seeds/03_sample_seeds.rb'に合わせて書いていきます。
今回はこのような感じで書いてみました。

namespace :db do
  desc 'Load the seed data from SEED_FILENAME'
  task :seed_from_file => 'db:abort_if_pending_migrations' do
    seed_file = File.join(Rails.root, 'db', ENV['SEED_FILENAME'])
    if File.exist?(seed_file)
      puts "seeding -- #{ENV['SEED_FILENAME']}"
      load(seed_file)
    else
      puts "the seed file does not exist."
    end
  end
end

以下から各メソッドの解説です。

・namespace

apiのルーティングなどでよく使われるnamespaceメソッドですが、
ここではコマンドを叩くときのコマンド名みたいなものです。
これをネストしていくとコマンドもネストされます。

namespace :apple do 
  namespace :banana do
    namespace :orange do 
      task :eat do
        #省略

であればrails apple:banana:orange:eat という風なコマンドになります。

・desc

メソッドの説明文になります。
rails -Tでタスクを表示したときに一緒に説明文が表示されます。

・task

実行するメソッド名です。
成功していないマイグレーションがあるとデータを入れるのはよくないので、先に'db:abort_if_pending_migrations'を実行するようにしています。

・File.join

seedファイルまでのパスを生成するメソッドです。
コマンドの引数であるSEED_FILENAMEからファイル名を受け取り、実行するファイルを特定します。
Rails.root, 'db'と記載しておくことで、コマンドからの引数であるファイルまでのパスを省略できます。
load(seed_file)で実行しています。

まとめ

僕のカップル専用家計簿アプリでは、
月毎に合計値や前月などを計算しています。
そのため、月が変わると計算する値がなく、
真っ白になってしまうページがあり、
実装するときに不便になるので、
今回のようなテストデータを入れられるタスクを作りました。
(まだ毎月挿入するシードデータを作ってないですがw)
いろんなメソッドを使いますが、どれも単純なものなので、
試してみてください。
改善点あれば是非ご指摘ください。

【Ruby】もっと早く知っていればよかったmapメソッド

mapメソッド

mapメソッドとは、配列やハッシュに対して使えるメソッドで、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。
↑と、Rubyリファレンスには書かれております。
このメソッドを知った時は、このメソッドを知らないのは罪だと思いましたwww
なぜなら、このメソッドを知らない自分はこんなことをやったらからです。

mapメソッドを知らないとき

半年ほど前から、カップル専用の家計簿アプリを作っているのですが、家計簿には当然カテゴリ分けがあります。
実装していく中で、特定の条件のカテゴリのidを集めたいと思いました。
まず、何回も使わなければいけないと思ったので、モデルにscopeを追加しました。

extract_category, -> {unscope(:order).select(:category_id).distinct.pluck(:category_id)}

すでにこのスコープを作るのにも一苦労しました。
orderが既にかかったものにselectなどがかけられなかったのです。
次に、このメソッドを呼び出すときです。

category_ids = (current_user_expenses.extract_category  + partner_expenses.extract_category)

このように、scopeを使ったとしても、何回もそのscopeを呼び出していました。

mapメソッドを知った後

それがmapメソッドを使うとこんなにも簡単に書けます。

category_ids = (current_user_expenses + partner_expenses).map{|i| i.category_id}

scopeなどは必要なかったんです。
mapを使えばすごく簡単に書けました。
エイリアスメソッドとして、collectメソッドもあります。
どちらも同じ挙動ですが単語のニュアンスとして、collectのほうが読んでいてしっくり来るときがあります。
実は上記の場合は、collectのほうが僕は好きですね。

上記とは変わっていますが、実際の改修したコード こちらになります。

mapメソッドのようなブロックを使った便利メソッドは他にもいくつかあるので、しっかり勉強しようと思いました。

【Ruby on Rails】結局メソッドってどこに書けばいいの?

Railsではモデルなんて使わなくてもコントローラやビューにメソッドを書きまくっても動かすことができます。
しかし、そんなことをしてたらMVC構造の意味がないですし、きちんと考えて書くことによって、そのコード量を劇的に減らすことができます。 メソッドをどこに定義するかは、そのメソッドをどこで呼び出したいかに依存すると思います。
今回はビューで呼び出したいメソッドを定義する場所について書いていきます。

コントローラは最低限

最近はカップルで使える家計簿アプリを作っているのですが、
同じビューを2つのコントローラで使っていました。
何も考えていなかった頃は、コントローラで計算してインスタンス変数に代入してビューで使うということをしていました。 こんな感じですね。

# expenses_controller.rb
 def index
  @current_user_expenses_of_both = Expense.ones_expenses_of_both(current_user)
  @partner_expenses_of_both = Expense.ones_expenses_of_both(partner)
  @sum = @current_user_expenses.sum(:amount)
  @both_sum = Expense.must_pay_this_month(current_user, partner)
  @category_sums = Expense.category_sums(@current_user_expenses, @current_user_expenses_of_both, @partner_expenses_of_both)
  @category_badgets = current_user.badgets
end

# shift_months_controller.rb
def past_and_future(cnum)
  @current_user_expenses = ShiftMonth.ones_expenses(current_user, cnum)
  @current_user_expenses_of_both = ShiftMonth.ones_expenses_of_both(current_user, cnum)
  @partner_expenses_of_both = ShiftMonth.ones_expenses_of_both(partner, cnum)
  @sum = @current_user_expenses.sum(:amount)
  @both_sum = ShiftMonth.must_pay_one_month(current_user, partner, cnum)
  @category_sums = Expense.category_sums(@current_user_expenses, @current_user_expenses_of_both, @partner_expenses_of_both)
  @category_badgets = current_user.badgets
end 

この二つのコントローラは全く同じ、ビューにレンダリングされます。

これは後々のことを考えると、何か修正したりするときにかなり手間がかかります。

ビューはインスタンス変数に依存するんです。 ビューのインスタンス変数の数だけコントローラで定義しなければいけないんです。 コントローラは薄くというのは鉄板らしいですね。

コントローラを薄くするには

これまでは単純にモデルにクラスメソッドとして計算式を書いて、それをコントローラで呼び出してインスタンス変数に入れてビューに渡していました。ビューではその変数をただ表示するだけでした。
これでは修正がかなり大変になります。
そこでコントローラでは必要なレコードをまとめて一気にとりだし、必要な計算や表示順はモデルやヘルパーに定義し、ビューから呼び出すようにしました。
例えば、上のものはこうなります。

#expenses_controller.rb
 def index
  @cnum = 0
  @current_user_expenses = current_user.expenses.this_month
  @partner_expenses = partner.expenses.this_month.both_t
end

# shift_months_controller.rb
def past_and_future(cnum)
  @cnum = params[:id].to_i
  @current_user_expenses = ShiftMonth.ones_expenses(current_user, @cnum)
  @partner_expenses = ShiftMonth.ones_expenses(partner, @cnum).both_t
end 

たったこれだけになります。前者ではインスタンス変数をわけていたものを、後者ではまとめています。
必要な計算やクエリなどはモデルとヘルパーに書いていきます。

モデルか?ヘルパーか?

ビューにはできるだけロジックを書かないというのが鉄板です。
viewの役割であるブラウザで何を表示するかというのがわかりにくくなりますもんね。
そこでビューに関するロジックは大体はモデルかヘルパーに書いていきます。
メソッドを定義するときに重要な点は、selfメソッドが使えるかどうかです。
このselfというのは定義するところによって、インスタンス自身かクラス自身かが違ってきます。
selfが使えなさそうなら、ヘルパーに書きましょう。
使えるなら、モデルに定義しましょう。
上記の@current_user_expensesと@partner_expensesを例にしてみます。
@current_user_expensesは自分が払った全ての出費を呼び出しています。全ての出費というのは、
・自分のためだけの出費
・自分とパートナー二人のための出費
・パートナーのためだけの出費
@partner_expensesはuserが違うだけで@current_user_expensesと定義は同じです。
この両方を使って自分の支出合計値を計算したいとします。
この場合、モデルにこの両方を引数で渡して、呼びだすことができます。

# expense.rb
def self.one_total_expenditures(current_user_expenses, partner_expenses)
  current_user_expenses.both_f.sum(:amount) + current_user_expenses.both_t.sum(:mypay) + partner_expenses.sum(:partnerpay)
end

# view.html.haml
= Expense.one_total_expenditures(@current_user_expenses, @partner_expenses)

しかし、ヘルパーに書くとモデル名を省くことができます。

# expenses_helper.rb
def one_total_expenditures(current_user_expenses, partner_expenses)
  current_user_expenses.both_f.sum(:amount) + current_user_expenses.both_t.sum(:mypay) + partner_expenses.sum(:partnerpay)
end

# view.html.haml
= one_total_expenditures(@current_user_expenses, @partner_expenses)

このメソッドを他のモデルから呼び出したいとなると話は違いますが、
ビューでモデル名を書くのは嫌だと言う人もいますし、ただビューで計算したいという場合はヘルパーで十分です。

インスタンスメソッドかクラスメソッドか

これはオブジェクト指向に関することです。
一つのレコード(インスタンス)に対してのメソッドは、モデルにインスタンスメソッドとして書きましょう。
例えば、出費テーブル、カテゴリテーブル、そのカテゴリに対する予算テーブルがあるとします。
一つの出費のカテゴリーの予算を表示したいとすると、 アソシエーションを組んであると、 こういう風に書けます。

# view.html.haml
= expense.category.badget.amount

と言う風にも呼び出せますが、何回も書くと長いですよね。
これをインスタンスメソッドとして定義します。

# expense.rb
def category_badget
  self..category.badget.amount
end

# view.html.haml
- @current_user_expenses.each do |expense|
  = expense.category_badget

こうすることですっきりしますね。

複数のレコードに対してはクラスメソッドで定義します。
例えば、ある条件で表示する順番を入れ替えたいとします。

# view.html.haml
= @current_user_expenses.arrange(true).each do |expnese|

# expense.rb
def self.arrange(both_flg)
  expenses = both_flg ? self.both_t : self.both_f
  (並び替えの処理)
end

とすると、すっきりします。 これをヘルパーに書いてしまうと@current_user_expensesとboth_flgを引数で渡さなくはいけません。

まとめ

・ビューで扱うインスタンス変数は要注意
・ビューで呼び出したいメソッドを定義するときは、selfメソッドが使えるかどうかでモデルとヘルパーのどちらに書くか判断する。

complete i guess
refactored expense model and expense helper

Rails5.1にReactを導入

react-railsとwebpackerの場合

インストール

Gemfileに以下を追加して、bundle install

gem 'react-rails' 
gem 'webpacker'

installコマンド実行

rails webpacker:install  
rails webpacker:install:react   

これでReactに必要なフォルダやファイルが生成されてReactが入る。 webpackerによりapp/javascript/packsにhello_react.jsxが生成されているのがわかる。
これはReactを使って、「Hello React」を表示させる簡単なものである。 そして、webpackerにより使えるようになったを追加していく。

Reactを使って「Hello React」を表示

コントローラー、ビュー、ルーティングを生成
rails g controller sample index

app/views/layouts/application.html.erbに以下のようにjavascript_pack_tagを追加

<!DOCTYPE html>
<html>
<head>
  <title>CalculatorReact2</title>
  <%= csrf_meta_tags %>
  <%= stylesheet_link_tag 'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= javascript_pack_tag 'hello_react' %>
</head>
<body>
  <%= yield %>
</body>
</html>

この状態でrailsサーバーを起動すると、「Hello React」が表示される。

react-railsのReactコンポーネントの作成

react-railsではReactコンポーネントは以下のコマンドで作成できる。

rails g react:component コンポーネント名

そうするとapp/javascript/component/ の配下にコンポーネント名.jsが生成される。

例えば、rails g react:component calculator だと

app/javascript/component/calculator.jsが生成される。

あとは好きなviewのそのコンポーネントを使いたいところで
<% react_component 'calculator' %>というようなview helperを記載しておくと、app/javascript/component/calculator.jsを読み込んでくれる。

app/views/layouts/application.html.erbには<%= javascript_pack_tag 'application' %>を記載しておけば読み込んでくれる。

react-rails のapplication.js

webpackerと比べて以下が追加されている。

var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)

rails g react:componet で作成したコンポーネントファイルに以下を書かなくてもいい。

import ReactDOM from 'react-dom';
ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

 

webpackerのみ場合

react-railsがないとrailsサーバーを起動しても、Reactは動いてくれない。

railsサーバー起動後、別ターミナルで以下を叩きコンパイルする

./bin/webpack-dev-server

このままではreactを変更するたびにコンパイルしなければいけないので、

package.jsonに以下を追加し、

"scripts": {
"start": "rails s & bin/webpack-dev-server"
},

yarn startで同時に行ってくれる。

webpackerで読み込むコンポーネント

コンポーネントを入れるフォルダを作成

webpackerのみの場合、app/javascript/配下にはpacksフォルダしか生成されないので、ここで任意のフォルダを作成する。

app/javascript/calculator/

コンポーネントファイルを作成

app/javascript/calculator/calculator.js

import React, { Component } from 'react';

class Calculator extends Component {
 render() {
  return (
   <div>
    <h1>Calculator is working!!</h1>
   </div>
  )
 }
}
export default Calculator;

app/javascript/calculator/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Calculator from './calculator';

document.addEventListener('DOMContentLoaded', ()=> {
  const container = document.body.appendChild(document.createElement('div'));
  ReactDOM.render(<Calculator/>, container)
})

Viewで読み込む

app/javascript/packs/calculator.jsを作成し、その中に読み込むフォルダを記載

import 'calculator'

そうするとapp/javascript/calculator/ 配下のコンポーネントをimportする。

あとはapp/views/layouts/application.html.erbのHEADタグ内にreact-railsの時と同じようにjavascript_pack_tagを追加し、app/javascript/packs/calculator.jsを指定してあげるとviewで読み込んでくれる。

<!DOCTYPE html>
<html>
<head>
  <title>CalculatorReact2</title>
  <%= csrf_meta_tags %>
  <%= stylesheet_link_tag 'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= javascript_pack_tag 'calculator' %>
</head>
<body>
  <%= yield %>
</body>
</html>

複数のフォームがあり空のフォームはサブミットされないようにする

したいこと

二つのフォームがあり片方は何も書かれていない空の状態であれば、そのフォームをサブミットせずに記入されている片方のフォームだけ送信する方法。
この記事のように二つのフォームが同じモデルへ送信するのであれば、これは意味がないが、それぞれが別のモデルへ保存される時に便利。
今回はこの一つのフォームで複数のモデルへ保存するフォームに実装することを想定して作例する。 ただし、そこまで深入りしないが、今回はお互いのフォームはアソシエーションで紐づいていて、親子関係にあるため、親モデルのフォームが空であっても、親モデルのレコードは作成されるようにしている。

ポイント

  • サブミットするときの関数を設定する。
  • クリックすると、jqueryでフォームが空であるか調査し、空であればそのフォームにdisabled属性を加え、フォームを送信

View.html.erb

<%= form_with(model: review, local: true, id: "review_form") do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content, id: :review_content %>
  </div>
  <%= form.fields_for :phrases do |phrase_field| %>
    <div class="field">
      <%= phrase_field.text_area :content, id: :phrase_content %>
    </div>
  <% end %>
  <div class="actions">
    <button name="button" type="button" id="submit_btn">投稿</button>
  </div>
<% end %>

jquery

$(function(){
$('#review_form').on('submit', function(){
  if($("#phrase_content").val() == "") {
    $('#phrase_content').attr('disabled','true');
  };
});
});

参考

【jQuery】submit前に処理を行う方法 - Qiita

textareaの縦幅を入力行数によって自動変更させる方法

autosizeというライブラリを使います

autosize | RubyGems.org | your community gem host

デモはこちら

方法

  1. Gemfileに記入
    gem 'autosize', '~> 2.4'
    bundleinstallを実行
  2. app/assets/javascripts/application.jsに以下を追記 //= require autosize

本来ならこれでできるはずだけど、エラーが出てassets配下を探していたので、gem environmentを実行して、GEM PATHを頼りにgemがインストールされているところへ行き、app/assets/javascripts/へautosize.jsファイルをコピーした

jqueryコード

autosize($('textarea'));
たったこれだけで、縦幅が自動で変更されます。 javascriptのコードやいろんなオプションはこちらを参考に

一つのフォームで複数のモデルに保存する

したいこと

今回は映画のレビューを投稿するreview投稿フォーム画面に、 映画の好きなフレーズを投稿できるphrase投稿フォームも表示し、 一度送信ボタンへ押すだけでreviewはもちろん、そのreviewに紐づいたphraseも保存する。 なので、reviewモデルとそのreviewモデルに紐づいたphraseモデルが存在している。

重要なメソッド

  • field_forメソッド
  • accepts_nested_attributes_for

バージョン

rails 5.1.4

 カラム

reviewsテーブルにはcontentカラム
phrasesテーブルにもcontentカラムのみとする

アソシエーション定義

model/review.rb

  has_many :phrases, dependent: :destroy
  accepts_nested_attributes_for :phrases

model/phrase.rb

belongs_to :review, optional: true

Controller

def new
  review = Review.new
  2.times { @review.phrases.build }
end

def create
  @review = Review.new(review_params)
  @review.save
end

private
def review_params
  params.require(:review).permit(:content,  phrases_attributes: [:content])
end

View

<%= form_with(model: review, local: true) do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content, id: :review_content %>
  </div>
  <%= form.fields_for :phrases do |phrase_field| %>
    <div class="field">
      <%= phrase_field.text_area :content, id: :phrase_content %>
    </div>
  <% end %>
  <%= form.submit %>
<% end %>

学んだこと

  • field_forメソッドで異なるモデルへ保存するためのフォームを作成できる。controller#newでtimesメソッドを使ってインタンスを作成しておけば、viewではeach文を使って保存しなくても、(field_forだけではなく)form.fields_forでコントローラーで作成したインスタンス分のフォームを表示してくれる。
  • accepts_nested_attributes_forをモデルに追加することでStrongParameterに記載しているネストされたパラメーター(phrases_attributes: [:content])を読み取り、それぞれのモデルのレコードを更新できる。
  • rails5からはbelongs_to :review, optional: trueにしないと外部キー(〇〇_id)がもしnilの場合、保存ができなくなっているため、optional: trueをつけておくとそれを無効化しておく。ただ、今回はあってもなくても、どちらでも大丈夫。

参考