【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という有名な方が提唱されている割と一般的なソリューションだそうです。
【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してたと思ったけど違うのか。
(参考)
【heroku】heroku環境では画像は必ずS3に保存しよう
herokuには画像を保存しておけない
しりませんでした。
Instagramのクローンアプリをherokuにデプロイした後、seedデータ(ユーザー、画像)を投入。ルートページにアクセスして確認する、うまくいってるなニッコリ
翌日、ルートページを確認すると画像が表示されなくなってる。みんな経験あるはず
これにはheroku特有の事情があります。
herokuは1日に最低1回、Dynoの再起動があるようです。
Dynoってなんだ?って思って調べると。。。そもそもherokuの実体ってEC2のようでして、そのEC2上で起動しているコンテナがDynoらしい
そのコンテナが再起動し、その時local filesystemがリセットされると公式にも書かれております
Dynos and the Dyno Manager | Heroku Dev Center
アプリ自体は消えてしまわないのー?って思うかもしれませんが、git pushに含まれる分は消えないようです。
多分ですけど、herokuのリポジトリから再起動ごとにpullしてくれてるんでしょう。
ただ、ユーザーデータ、リレーションデータ等のseedは残りますね。なんでだろう、間違った理解をしているのかなぁ
S3(外部ストレージ)を使おう
S3じゃなくてもいいけど。自分へのリマインダーです。