ヨージとプログラミング

プログラミング勉強の記録

【Rails】あるモデルの1属性だけを変更するコントローラーの命名方法

Web系自社開発企業に入社してから初めての投稿です〜

TL;DR

  • 特殊なコントローラーの命名方法についてです
  • うまくまとめれないので全文読んで
  • コントローラーの命名に迷ったら見てください

(失敗談)Customerモデルのnote属性のみを変更するコントラーを作った時

業務において直面した実話。
顧客の情報を管理するCustomerモデルがあるとします。
当然、顧客は増えたり減ったりするわけなので、customers_controllerがあり、そこにcreate, destroyなどのアクションがあるわけですが、今回はありませんでした。
なぜなら、Customerにいくつかのサブクラスがあり、そのサブクラスからCRUDするからです。
初学者の方には難しいかもしれませんが、とにかくCustomerモデルにはコントローラーがありません。 しかし私は、Customerモデルの持つ、note属性(顧客の備考を書く欄)を、書き込んだり、変更したりするフォームを作る必要がありました。
Customerモデルには他にも、name, email, sexなどの属性がありましたが、これらは変更することはありません。(個人情報保護の観点)
そうか。
ならば、王道で行こう。
いろいろな実装方法がありましたが、customers_controllerが無いからってトリッキーな方法でいくよりも、Customerモデルの属性を変更するのだから、customers_controllerでやるべきだと考えました。
そこで新米の僕は、customers_controller.rbを作り、そこにupdateアクションを定義し、note属性をいろいろ書き換えれるようにしました。
さぁ完璧だ。
PR送信っと
……

(メンター)ねぇねぇyojiくん?

ひゃ!?

customers_controllerという命名は誤解を与える

これ以上長ったらしい小話は聞くに堪えないと思うので正解を言います。
今回の正解は、

note_controller.rb 

でした。 ただ、ディレクトリを工夫して、
controllers / customers / note_controller.rb
にするといいです。
要は、命名ってのは、名前を見ただけで中身がわかることが大事なわけです。
/ customers / note_controller.rb にしておけば、Customerモデルのnote属性に関するコントローラーであることは正しく伝わるわけです。
ちなみにこれは、DHHという有名な方が提唱されている割と一般的なソリューションだそうです。

DHH流のルーティングで得られるメリットと、取り入れる上でのポイント - KitchHike Tech Blog

【Rails】ransackで複数モデルから検索したい時

TL;DR

  • search_from_forヘルパーは使わない
  • viewでは検索ワードのみ取得
  • controllerで検索する

環境

Rails 5.2.4.1
Ruby 2.6.5
Ransack 2.3.2

ransackの通常の使い方の延長では無理だった

前提
channelモデルとpostモデルから、検索ワード一致のものをすべて検索したいとします。
1つのモデルから検索する方法はwebにもよく載っていると思います。

# よくあるransackの使い方
# controller
def index
  @search = Channel.ransack(params[:q])
  @results = @search.result
end
# view
# よくあるransackの使い方
= search_form_for @search do |f|
 .form-group
   = f.label :title_cont, "Title"
   = f.search_field :title_cont
 .actions
   = f.submit "Search"

ここらは説明不要ですね。
さて、それでは複数モデルから検索するにはどうしましょう?いろいろ検証してみました。

複数モデルからの検索でダメだった方法

検証からちょっと日がたったので若干うろ覚え…
[ダメ1] includesを使う方法

# controller
def index
  @search = Channel.includes(:post).ransack(params[:q])
end

[ダメ2] formにモデルを並列する方法

# controller
def index
  @channel = Channel.ransack(params[:q])
  @post = Post.ransack(params[:p])
end

# view
= search_form_for @channel, @posts do |f|
 .form-group
   = f.label :title_and_content_cont, "Title"
   = f.search_field :title_and_content_cont
 .actions
   = f.submit "Search"

解決方法

search_form_forヘルパーを使わない。通常のform_withでコントローラーに検索ワードを送る

# view
.search-container
  = form_with url: search_path, local: true, method: :get do |f|
    = f.text_field :q
    = f.submit "検索"

コントローラーで必要なモデル分の検索を行う

class ChannelsController < ApplicationController
  def search
    @q = params[:q]

    @channels = Channel.ransack(title_cont: @q).result
    @posts = Post.ransack(content_cont: @q).result
  end
  ・・・
end

あとはレンダリングすればOKです。
(参考)
Ransackで複数のモデルを一度に検索 - VoidCC

【Rails】ransackで複数モデルから検索したい時

TL;DR

  • search_from_forヘルパーは使わない
  • viewでは検索ワードのみ取得
  • controllerで検索する

環境

Rails 5.2.4.1 Ruby 2.6.5 Ransack 2.3.2

ransackの通常の使い方の延長では無理だった

前提
channelモデルとpostモデルから、検索ワード一致のものをすべて検索したいとします。
1つのモデルから検索する方法はwebにもよく載っていると思います。

# よくあるransackの使い方
# controller
def index
  @search = Channel.ransack(params[:q])
  @results = @search.result
end
# view
# よくあるransackの使い方
= search_form_for @search do |f|
 .form-group
   = f.label :title_cont, "Title"
   = f.search_field :title_cont
 .actions
   = f.submit "Search"

ここらは説明不要ですね。
さて、それでは複数モデルから検索するにはどうしましょう?いろいろ検証してみました。

ダメだった方法

検証からちょっと日がたったので若干うろ覚え…
[ダメ1] includesを使う方法

# controller
def index
  @search = Channel.includes(:post).ransack(params[:q])
end

[ダメ2] formにモデルを並列する方法

# controller
def index
  @channel = Channel.ransack(params[:q])
  @post = Post.ransack(params[:p])
end

# view
= search_form_for @channel, @posts do |f|
 .form-group
   = f.label :title_and_content_cont, "Title"
   = f.search_field :title_and_content_cont
 .actions
   = f.submit "Search"

解決方法

search_form_forヘルパーを使わない。通常のform_withでコントローラーに検索ワードを送る

# view
.search-container
  = form_with url: search_path, local: true, method: :get do |f|
    = f.text_field :q
    = f.submit "検索"

コントローラーで必要なモデル分の検索を行う

class ChannelsController < ApplicationController
  def search
    @q = params[:q]

    @channels = Channel.ransack(title_cont: @q).result
    @posts = Post.ransack(content_cont: @q).result
  end
  ・・・
end

あとはレンダリングすればOKです。

【Rials】cronoのREADME通りに設定するとうまく行かないよという話

TL;DR

  • cronoでrakeタスクをスケジュールするときRake::Task['タスク名'].invokeではなくRake::Task['タスク名'].executeで呼び出そう

cronoの公式通りにやると、最初の1回目しかrakeタスクが実行されない

cronoはrailsのgemで、ジョブスケジューラーです。ジョブには自身で定義したrakeタスクを渡すこともできます。rakeタスクはもちろんlib/tasks配下に作成しますよね。
一方で、cronoのスケジュール設定ファイルはconfig/cronotab.rbです。ここからrakeタスクを呼び出すのですが、公式の説明には次のようにありました。

# config/cronotab.rb
require 'rake'

Rails.app_class.load_tasks

class Test
  def perform
    Rake::Task['crono:hello'].invoke
  end
end

Crono.perform(Test).every 5.seconds

(参考) GitHub - plashchynski/crono: A time-based background job scheduler daemon (just like Cron) for Rails
注目してほしいのはRake::Task['crono:hello'].invokeのところ。
crono.rakeというファイルのhelloというタスクを呼び出しているのですが、そのときにinvokeで呼び出しています。invokeの意味は後述しますが、愚直に公式のとおりにやったところ、rakeタスクは最初の一回のみ実行され、2回目以降は実行されませんでした(実際には実行されていたがnilになるようになっていた)。

考察:ジョブスケジュール中はタスクが実行中とみなされているため、invokeの2回目以降がnilになるのではないか

rakeタスクの呼び出し方にはinvokeの他にもexecuteがあります。この2つにどういう違いがあるかという、『invokeは実行中は1度しか同じタスクを実行しない』というのがあります。
(参考) rakeタスク内で別のタスクを呼び出す - Qiita
ほほう、とうことでrails consoleで試してみました。

# Rake::Task['weather:get']: 天気予報をDBに保存するrakeタスク

# Rakeタスク1回目実行
[2] pry(main)> Rake::Task['weather:get'].invoke
  Weather Create ・・略・・
  (7.2ms)  COMMIT
=> [#<Proc:0x00005...]

# Rakeタスク2回目実行
[3] pry(main)> Rake::Task['weather:get'].invoke
=> nil

たしかに1回目はきちんとDBにCommitできていますが、2回目はnilが返ってしまっています。ということは、やはりジョブスケジュールを実行している時はrakeタスクが完了しても完結してしまうわけでは無いみたいですね。(この表現があってるとは思ってない)
一方で、executeを使うと何回でもrakeタスクを実行できます。

# 1回目
[1] pry(main)> Rake::Task['weather:get'].execute
  Weather Create ・・略・・
   (14.5ms)  COMMIT
=> [#<Proc:0x00005...]

# 2回目
[2] pry(main)> Rake::Task['weather:get'].execute
  Weather Create ・・略・・
   (9.2ms)  COMMIT
=> [#<Proc:0x00005...]

なので私はexecuteを使っています。
ただ、invokeを使う場合でも、invokeを使った後に毎回Rake::Task["task_name"].reenableすると再度使えるようになるみたいです。

# 1回目
[1] pry(main)> Rake::Task['weather:get'].invoke
  Weather Create ・・略・・
   (7.2ms)  COMMIT
=> [#<Proc:0x00005...]

# 2回目はnilになってしまう
[2] pry(main)> Rake::Task['weather:get'].invoke
=> nil

# 復活の呪文
[3] pry(main)> Rake::Task['weather:get'].reenable

# 再生!
[4] pry(main)> Rake::Task['weather:get'].invoke
  Weather Create ・・略・・
   (19.1ms)  COMMIT
=> [#<Proc:0x00005...]

ただ、executeで問題ないなら、ソッチのほうがシンプルなのでとりあえずexecuteでいってます。invokeやexecuteの本質を理解する必要はありそうです。

【Rails】Dockerなジョブスケジューラーはwheneverよりcronoがいい

ほとんど検証してない上に独断と偏見ばっかです

TL;DR

  • Dockerにcronをインストールしてやる方法はうまく動かなかった(調査中)
  • そもそもcronに依存するスケジューラーは使い勝手悪い
  • cronに依存しないジョブスケジューラーを選定すべき(cronoとか)

cronに依存するwheneverはdockerでは使いにくい

rails ジョブスケジューラー」で検索するとwheneverというgemを使う方法がいの一番に来ていますが、dockerではなぜかうまく行かなかった。
具体的にはdocker-composeでcronをスタートできなかった

# docker-compose一部抜粋
command: bash -c "crond -f && bundle exec rails s -b 0.0.0.0"

docker-compose upで上記のコマンドが動いてほしいわけですが、この時cornd -fでcronは起動しますが、rails sの方は動きません(というかhttp://localhost:3000が表示されないのでそう判断してます)。
いろいろ解決方法を探りましたが、cron専用のコンテナを作ったり、delayed_job_active_recordというバックグラウンドジョブを扱うgemを入れたりがあるようですが、、、個人的にはこれらは複雑になりすぎてる気がしました。
(参考)
Add background jobs and cron to your dockerized ruby on rails app

cronoはcron操作がなく、バックグラウンドで動かせる

cronoというgemはジョブスケジューラーですが、wheneverではあった、whenever --update-corntabのような更新作業はありません。
daemonsというgemを入れることでバックグラウンドで動かせます。 cronをdockerにインストールする必要もありません。(多分。alpine環境なので未検証)
最終的にdocker-compose upで作動するコマンドは次のようになりました

command: bash -c "bundle exec crono start && bundle exec rails s -b 0.0.0.0"

これでcronoとrails serverの両方を起動することができました。
(参考)
Ruby On Railsで、cronoを使って最小手順でバッチ処理を作る

補足

cronに依存しないジョブスケジューラーはまだまだあるようで、こちらのブログでいろいろ紹介してくださっております。自分に合うものを見つけよう tech.medpeer.co.jp

【Rails6】rails newでちょっとしかファイルができない時の対処

rails newが途中で止まっていた

新規プロジェクトでrails newした時です。明らかに生成されるファイルの数が少ない。configフォルダは?appフォルダは?modleは!?

>>> docker-compose run --rm backend rails new . --force --database=mysql
       ()
       exist  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
   identical  Gemfile
         run  git init from "."

なんとcreateされてるのは5つのみ
どうやら、git init from "."でストップしたようです。

gitをスキップするオプションを付ける -G

>>> docker-compose run --rm backend rails new . -G --force --database=mysql

rails newの後に-Gをつけることでgitをスキップでき、通常通り動きました。
しかしなぜこうなったか。いつも、rails newしたあとにgit initしてたと思ったけど違うのか。

(参考)

Ruby on Rails 5 - rails newコマンドで生成されるフォルダ構成が異なります|teratail

【heroku】heroku環境では画像は必ずS3に保存しよう

herokuには画像を保存しておけない

しりませんでした。
Instagramのクローンアプリをherokuにデプロイした後、seedデータ(ユーザー、画像)を投入。ルートページにアクセスして確認する、うまくいってるなニッコリ
翌日、ルートページを確認すると画像が表示されなくなってる。みんな経験あるはず
これにはheroku特有の事情があります。 herokuは1日に最低1回、Dynoの再起動があるようです。
Dynoってなんだ?って思って調べると。。。そもそもherokuの実体ってEC2のようでして、そのEC2上で起動しているコンテナがDynoらしい
そのコンテナが再起動し、その時local filesystemがリセットされると公式にも書かれております

f:id:yoji4910:20200201161206p:plain

Dynos and the Dyno Manager | Heroku Dev Center

アプリ自体は消えてしまわないのー?って思うかもしれませんが、git pushに含まれる分は消えないようです。
多分ですけど、herokuのリポジトリから再起動ごとにpullしてくれてるんでしょう。
ただ、ユーザーデータ、リレーションデータ等のseedは残りますね。なんでだろう、間違った理解をしているのかなぁ

S3(外部ストレージ)を使おう

S3じゃなくてもいいけど。自分へのリマインダーです。