Rails Model で has_many 定義した要素数を validate する方法
実装例
class Kuso < ActiveRecord::Base has_many :unchis validate: buriburi_unchis def buriburi_unchis errors.add(:unchis, "oops!") if unchis.size > 1 end end
ポイントは unchis.size。
以前記事にした
Railsで階層化された複数モデルに対応するフォームの作り方 - Shred IT!!!!
のようにフォームからのパラメーターを利用したい場合は size メソッドを使うこと。
count メソッドにしてしまうと、DB に接続してレコード数をカウントしてしまう。
逆に DB のレコード数が欲しければ count でいい。
まとめ
Mac から踏み台サーバ経由(SSHトンネル)で git する方法
踏み台サーバ経由で git する方法をメモしておく。
Linux でも同じ、.ssh/config を編集して踏み台サーバ経由(SSHトンネル)する方。
あくまで Mac(ローカルマシン)から、git コマンドを直接叩きたいときの設定。
踏み台サーバ上で作業する場合の設定ではない。
踏み台サーバ経由(SSHトンネル)する ~/.ssh/config の設定
前提条件
bastion(踏み台サーバ)
git_repository(git サーバ)
設定ファイルの編集
# Mac(ローカルマシン)の ~/.ssh/config Host bastion # ホスト名(任意) User tsuyacchi # ユーザ名 HostName xxx.xxx.xxx.xxx # IP or ホスト名 Port 22 # ポート番号 IdentityFile ~/.ssh/tsuyacchi_id_rsa # 秘密鍵ファイルパス Host git_repository # ホスト名(任意) User git # ユーザ名 HostName yyy.yyy.yyy.yyy # IP or ホスト名 IdentityFile ~/.ssh/git_tsuyacchi_id_rsa # 秘密鍵ファイルパス ProxyCommand ssh bastion -W %h:%p # SSHトンネル(bastion) # ProxyCommand ssh bastion nc %h %p # バージョンによって nc と -W 使い分ける
コマンド
git clone git_repository:hoge.git
git_repository は ~/.ssh/config の Host で設定した名前を利用している。
gitolite 以外でも適時設定を書き換えれば、大体こんな感じの設定で問題ないはず。
注意点があるとしたら、git_repository へのログインアカウントが共有アカウントなのか否か。
おまけ
SSH したい場合
仮に上記で設定した踏み台サーバ経由で WEB サーバに接続したい場合。
# Mac(ローカルマシン)の ~/.ssh/config Host web_server # ホスト名(任意) User tsuyacchi # ユーザ名 HostName zzz.zzz.zzz.zzz # IP or ホスト名 IdentityFile ~/.ssh/web_tsuyacchi_id_rsa # 秘密鍵ファイルパス ProxyCommand ssh bastion -W %h:%p # SSHトンネル(bastion 経由)
ユーザ名・IP・秘密鍵等の設定を適時変更して、下記コマンドを叩くだけ。
ssh web_server
ちなみにこんな風にも書ける。
ssh -oProxyCommand='ssh -W %h:%p bastion' tsuyacchi@zzz.zzz.zzz.zzz -i ~/.ssh/web_tsuyacchi_id_rsa
多段SSH(踏み台が複数ある場合)
# Mac(ローカルマシン)の ~/.ssh/config Host bastion1 # ホスト名(任意) User tsuyacchi # ユーザ名 HostName xxx.xxx.xxx.xxx # IP or ホスト名 IdentityFile ~/.ssh/bastion1_id_rsa # 秘密鍵ファイルパス Host bastion2 # ホスト名(任意) User tsuyacchi # ユーザ名 HostName yyy.yyy.yyy.yyy # IP or ホスト名 IdentityFile ~/.ssh/bastion2_id_rsa # 秘密鍵ファイルパス ProxyCommand ssh bastion1 -W %h:%p # SSHトンネル(bastion1) Host goal # ホスト名 User tsuyacchi # ユーザ名 HostName zzz.zzz.zzz.zzz # IP or ホスト名 IdentityFile ~/.ssh/goal_id_rsa # 秘密鍵ファイルパス ProxyCommand ssh bastion2 -W %h:%p # SSHトンネル(bastion2)
コマンドは下記。
ssh goal
これで bastion1 -> bastion2 -> goal の多段 SSH が行われる。
トンネル(多段)する数が増えても、ProxyCommand でリレーする順番や指定を間違わなければ問題ない。
関連記事
Railsで階層化された複数モデルに対応するフォームの作り方【JavaScript/CoffeeScriptによる動的処理追加】
今回はフォームに「追加・削除」ボタンを設置し、それらをJavascript/CoffeeScriptと連携して動的にする実装をする。
前回の静的な実装までは↓から。
jetglass.hatenablog.jp
タスク
- 追加に関すること
- ネストされた要素内に追加ボタンを表示
- 追加ボタンを押下したら画面上に空のフォームが追加される
- 追加されたフォームに値を入れて新規作成および更新でレコードが追加される
- 削除に関すること
- ネストされた要素内に削除ボタンを表示
- 削除ボタンを押下したら対象のフォームが画面上から削除される
- 更新では削除対象のレコードが削除される
- 新規作成では削除したフォームのレコードが登録されない
実装
モデル
フォームから送信されるパラメーター hoge_attributes
内の削除対象としたい要素に、
_destroy=true
というを値を含めて処理させると、対象の要素が削除されるという機能がある。
詳細は下記。
ActiveRecord::NestedAttributes::ClassMethods
この便利機能を利用するためにモデルでallow_destroy
という定義をしておく必要がある。
class Event < ActiveRecord::Base has_many :event_details accepts_nested_attributes_for :event_details, allow_destroy: true #追加 end class EventDetail < ActiveRecord::Base ... accepts_nested_attributes_for :topics, allow_destroy: true #追加 end
コントローラー
パラメーターのフィルター処理で_destroy
を受け取れるようにする。
def event_params params.require(:event).permit( :id, :name, event_details_attributes: [ :id, :detail, :_destroy, #追加 topics_attributes: [:id, :name, :_destroy] #追加 ] ) end
コントローラーに追加されているこのフィルター処理、strong_parameters と呼ばれているみたい。 github.com
ヘルパー
追加ボタンと削除ボタンを設置する際のヘルパーを準備する。
module EventsHelper # 追加ボタン def link_to_add_field(name, f, association, options={}) # association で渡されたシンボルから、対象のモデルを作る # 前回コントローラーで実装したモデルの build にあたる処理 new_object = f.object.class.reflect_on_association(association).klass.new # Javascript 側で配列のインデックス値とする # 追加しまくると、インデックス値がかぶりまくるので、 # 後に Javascript 側でこのインデックス値は現在時刻をミリ秒にした値で置き換えていく id = new_object.object_id # f はビューから渡されたフォームオブジェクト # fields_for で f の子要素を作る fields = f.fields_for(association, new_object, child_index: id) do |builder| render(association.to_s.singularize + "_form", f: builder) end # ボタンの設置。classを指定してJavascriptと連動、fields を渡しておいて、 # ボタン押下時にこの要素(fields)をJavascript側で増やすようにする link_to(name, '#', class: "add_field", data: {id: id, fields: fields.gsub("\n","")}) # Rails3系だと下記のように書けるが、4系で link_to_function は葬られた #link_to_function(name, raw("add_field(this, \"#{association}\", \"#{escape_javascript(fields)}\")"), options) end #削除ボタン def link_to_remove_field(name, f, options={}) # _destroy の hiddenフィールドと削除ボタンを設置 f.hidden_field(:_destroy) + link_to(name, '#', class: "remove_field") end end
ビュー
要素を追加するボタンと削除するボタンをヘルパーを使って設置。
#_form.html.erb 一部抜粋 <div class="field"> ... <%= f.fields_for :event_details do |df| %> <%= render partial: "event_detail_form", locals: {f: df } %> <% end %> <%= link_to_add_field("detail add", f, :event_details, {}) #追加 %> </div> #_event_detail.html.erb 一部抜粋 <div class="field"> ... <%= f.fields_for :topics do |tf| %> <%= render partial: "topic_form", locals: {f: tf } %> <% end %> <%= link_to_add_field("topic add", f, :topics, {}) #追加 %> <br> <%= link_to_remove_field("detail remove", f, {}) #追加 %> </div> #_topic_form.html.erb 一部抜粋 <div class="field"> ... <%= f.hidden_field :id %> <br> <%= link_to_remove_field("topic remove", f, {}) #追加 %> </div>
# coffeescript # Rails4では Turbolinks が動作していて、 # この書き方でないと ready イベントが発火しない $(document).on 'ready page:load', -> # 追加ボタンを押されたとき $('form').on 'click', '.add_field', (event) -> # 現在時刻をミリ秒形式で取得 time = new Date().getTime() # ヘルパーで作ったインデックス値を↑と置換 regexp = new RegExp($(this).data('id'), 'g') # ヘルパーから渡した fields(HTML) を挿入 $(this).before($(this).data('fields').replace(regexp, time)) event.preventDefault() # 削除ボタンを押されたとき $('form').on 'click', '.remove_field', (event) -> # 削除ボタンを押したフィールドの _destroy = true にする $(this).prev('input[name*=_destroy]').val('true') # 削除ボタンが押されたフィールドを隠す $(this).closest('div').hide() event.preventDefault()
CSS
画面表示を見やすくするためにフィールドや要素を枠で囲む
.field { border: 1px solid #000; padding: 10px; margin: 10px; }
動作確認
わかりづらいが、gif 動画を残してみる。
追加ボタン(ボタンっていうかリンク)を押すと、対象のフィールドが追加され、 削除ボタンを押すと、対象のフィールドが削除される。
上記の動画では確認できないが、追加・削除したフォームの状態はそっくりそのまま保存される。
パラメーターは下記の通り送信される。
[1] pry(#<EventsController>)> event_params => { "id"=>"3", "name"=>"ab", "event_details_attributes"=> # "0" の中身は登録済みデータを削除ボタンで削除した場合 {"0"=> {"id"=>"3", "detail"=>"ab", "_destroy"=>"true", # 削除ボタンによりtrue "topics_attributes"=> {"0"=> {"id"=>"3", "name"=>"ab", "_destroy"=>"true" # 削除ボタンによりtrue} } }, # 追加ボタンで追加した場合、キーが現在日時のミリ秒 "1429265236985"=> {"id"=>"", # 追加なのでidは空 "detail"=>"cd", "_destroy"=>"false", # 追加なので _destroy はfalse "topics_attributes"=> {"1429265242729"=> {"id"=>"", "name"=>"ef", "_destroy"=>"false" # 追加なので id は空、 _destroy はfalse}, # ボタン押下の現在日時をミリ秒にしているので、キーの数字が↑と異なる "1429265245905"=> {"id"=>"", "name"=>"gh", "_destroy"=>"false" # 追加なので id は空、 _destroy はfalse} }}}}
これら event_params は 新規作成でも更新でもよしなに処理される。
参考
Railsで階層化された複数モデルに対応するフォームの作り方
Rails4.1.8 での自分用メモとして記事にしておく。
業務でモデルの階層が深いけど、1画面でフォームを作らなければならなかったので、簡略化した形で記事にまとめる。
記事は2つに分かれていて、 最初に書いたこの記事は Rails 側で行うべき基本的なことをまとめている。
2つ目の記事では、Javascirpt/Coffeescript と連携して動的に追加する部分を記事にまとめている。
概要
今回、業務で実際にあったモデルの階層は下記のような感じ。
イベント 1:N イベント詳細 1:N 中間テーブル N:N トピック
要件としてはざっくり下記とします。
- 1画面で関連する全モデル要素の編集フォームが表示されること
- 新規作成・編集・削除ができること
- 1:N の Nは動的にフォームで追加できること
実装
Javascript で動的にフォームを追加できるようにする予定だが、 今回の実装では accepts_nested_attributes_for, fields_for を利用して、 階層化されたモデルを静的にフォームで利用できるところまで。
準備
とりあえず、必要最低限の準備。
Rails の環境を bundler で作ってある前提で、scaffold や ジェネレーター的なもので作りたい環境を準備する。
bundle exec rails g scaffold Event name bundle exec rails g model EventDetail detail event_id:integer bundle exec rails g model Topic name bundle exec rails g model EventDetailTopic event_detail_id:integer topic_id:integer
テーブル構成は以下の通り。
Event | EventDetail | Topic | EventDetailTopic |
---|---|---|---|
id | id | id | id |
name | event_id | name | event_detail_id |
- | detail | - | topic_id |
マイグレートして、DB環境を整える。
bundle exec rake db:migrate
モデルに階層の関連付け(accepts_nested_attributes_for)
フォーム上で 1:N で紐づくモデルも編集するのに、モデルで accepts_nested_attributes_for の定義をしておくと、ビューで fields_for を利用して意図した動作にすることができる。
説明は後にして、一旦、定義の方法。
class Event < ActiveRecord::Base has_many :event_details accepts_nested_attributes_for :event_details end class EventDetail < ActiveRecord::Base belongs_to :event has_many :event_detail_topics has_many :topics, through: :event_detail_topics accepts_nested_attributes_for :topics end class EventDetailTopic < ActiveRecord::Base belongs_to :event_detail belongs_to :topic end class Topic < ActiveRecord::Base has_many :event_detail_topics end
N:N の扱いではポイントがある。
EventDetail で
has_many :topics, through: :event_detail_topics
と定義しているところ。
through 定義により、中間テーブル(EventDetailTopicテーブル)を意識せず Topic テーブルというか Topic モデルを扱えるようにしている。
その上で、 accepts_nested_attributes_for の対象をTopicテーブルにすることで、フォーム上でも中間テーブルを意識しなくて済む。
コントローラーの実装( javascript の動的追加などを一切考慮しない)
build で階層化されたモデルの空オブジェクトを作成するロジックを追加。
理由としてはビューで出てくる fields_for が関連していて、空のフォームを作るのに必要なため。
# newメソッドの部分のみを記載 # GET /events/new def new @event = Event.new @event.event_details.build #追加 @event.event_details.first.topics.build #追加 end
ビューの実装( fields_for )
fields_for で階層化したモデル(子要素)を指定することで、フォームにそのモデルの要素を作ることができる。
新規作成時の動作としては、コントローラーで build した空のモデルが渡されて、空の入力フォームが作られる。
編集時の動作では、親モデルに紐づいた子モデルが渡されて、フォームに値が設定された状態になる。
_form.html.erb #divタグの中身のみを記載 <div class="field"> <%= f.label :name %><br> <%= f.text_field :name %> <%= f.hidden_field :id %> <%= f.fields_for :event_details do |df| %> <%= render partial: "event_detail_form", locals: {df: df } %> <% end %> </div>
_event_detail_form.html.erb <div class="field"> <%= df.label :detail %><br> <%= df.text_field :detail %> <%= df.hidden_field :id %> <%= df.fields_for :topics do |tf| %> <%= render partial: "topic_form", locals: {tf: tf } %> <% end %> </div>
_topic_form.html.erb <div class="field"> <%= tf.label :topicname %><br> <%= tf.text_field :name %> <%= tf.hidden_field :id %> </div>
画面表示
現時点での新規作成画面を表示。
仮に適当な値を入れて、「Create Event」ボタンを押したときに受け取るパラメータを確認。
[1] pry(#<EventsController>)> params => {"utf8"=>"✓", "authenticity_token"=>"CXuJ8dX6dILlw5SyTvipjFY6MvHPZooG1DF4rhnEbLc=", "event"=>{ "name"=>"n", "id"=>"", "event_details_attributes"=>{ "0"=>{ "detail"=>"d", "id"=>"", "topics_attributes"=>{ "0"=>{ "name"=>"t", "id"=>"" } } } } }, "commit"=>"Create Event", "action"=>"create", "controller"=>"events"}
accepts_nested_attributes_for, fields_for で定義したところが、hoge_attributes としてパラメーターが飛んでくる様子が確認できました!
hoge_attributes の中に "0" というキーが指定されていることに注目。
子要素のフォームが増えてもその値がインクリメントされて、複数の子要素に対応できる。
つまり、1:N の Nに対応している。
コントローラーの修正
コントローラーでパラメーターのホワイトリスト処理的(event_paramsメソッド)なものがあって、
hoge_attributes が受け取れない状態なので修正。
このままだと hoge_attributes は無視されるので、Eventテーブルにレコードが新規追加されるだけになる。
# 修正前 def event_params params.require(:event).permit(:name) end # 修正後 def event_params params.require(:event).permit( :id, :name, event_details_attributes: [ :id, :detail, topics_attributes: [:id, :name] ] ) end
新規作成画面の挙動
上記修正後、新規作成画面のフォームに適当に値を入力して「Create Event」ボタンを押すと、各モデルのリレーションを保った状態で値がDBに保存される。
新規作成ではコントローラーの create メソッドが呼ばれるので、ソースを見てみる。
def create @event = Event.new(event_params) ... if @event.save ...
ここで注目すべきは、
event_params には hoge_attributes が含まれているにも関わらず、
Event.new でよしなに関連するモデルを生成してくれること(もちろんリレーションされた状態)。
save メソッドを呼んで保存する際には、関連するモデルも一緒に保存される。
一覧画面
いくつか新規登録した後の一覧画面。
編集画面の挙動
一覧画面から3つ目の Edit を押して、編集画面へ。
今度は編集画面で値を編集する。
適当に値を書き換えて「Update Event」ボタンを押下。
編集(更新)ではコントローラーの update メソッドが呼ばれるので、ソースを見てみる。
def update ... if @event.update(event_params) ...
update メソッドの引数として event_params(hoge_attributes を含む)を渡しても、 よしなに該当するレコードを更新してくれる。
なぜなら下記のように hidden で設定しておいた id が活用されるため。
pry(#<EventsController>)> event_params => { "id"=>"3", "name"=>"ab", "event_details_attributes"=>{ "0"=>{ "id"=>"3", "detail"=>"ab", "topics_attributes"=>{ "0"=>{ "id"=>"3", "name"=>"ab" } }}}}
今回はここまで。
accepts_nested_attributes_for, fields_for を利用して、静的に関連するモデルを編集できるフォームを作った。
次回は動的に子要素にあたるモデルの編集フォームを追加できるようにする。
↓続きはこちら
参考
Railsでaccepts_nested_attributes_forとfields_forを使ってhas_many関連の子レコードを作成/更新するフォームを作成 - Rails Webook
3. Rubyによるデザインパターン【Builder】
3回目のデザインパターンは Builder(ビルダー) パターンの記事を書いてみる。
Template Method と Strategy のいいところを使ったパターンだと思う。
おなじみ、下記ソウルを忘れないこと。
参考:programming - Rubyによるデザインパターン5原則 - Qiita
調査
Wikiで調べてみる。
参考:Builder パターン - Wikipedia
オブジェクトの生成過程を抽象化することによって、動的なオブジェクトの生成を可能にする。
Wikiの図の説明だが、Director は Builder を内包しており、buildPart メソッドで処理を Builder へ委譲している。
Builder インターフェースの実装を ConcreteBuilder で行い、ConcreteBuilder は buildPart メソッドによって Product を生み出し、getResult メソッドで Product を返却する。
簡単に言うと、Director が Builder に Product を作らせている。
Builder をすげかえることで、出来上がる Product を変えられる。
また Builder における生成過程の抽象化を Director が行っているため、Director をすげかえることでも、生成過程の異なる Product を生成させることができる。
Template Method や Strategy との違いは、Builer パターンはオブジェクトの生成を目的としている。
概念
なるべくイメージしやすいことに例えて、Builder パターンを考えてみる。
魔王(Director) 魔物を作る(悪い博士に委譲) 悪い博士に魔物を開発させる 悪い博士に魔物を武器を持たせる 悪い博士に魔物を連れてこさせる 悪い博士インターフェース(Builder) 魔物を開発する 魔物に武器を持たせる 魔物を連れて行く ゴブリン博士(ConcreteBuiler) < 悪い博士 魔物を開発する ゴブリンを開発する 魔物に武器を持たせる ゴブリンに武器を持たせる 魔物を連れて行く ゴブリンを連れて行く オーク博士 < 悪い博士 ... 魔物(Product) 名前 身長 体重 ... ゴブリン < 魔物 オーク < 魔物
悪の大王(Director)は魔物(Product)を作りたいが、魔物を作るのは複雑なため、悪の大王には魔物を作ることができない(インスタンス生成)。
そこで悪い博士(Builder)を連れて来て、魔物を作らせている。
悪の大王は悪い博士に指示を出し、魔物の生成は悪い博士が行っている。
ゴブリン博士ならゴブリンを作れ、オーク博士ならオーク博士を作れる。
悪の大王は魔物を直接作ることはできないが、悪い博士達に指示を出して、目的の魔物を作らせることができる。
実装
# -*- coding: utf-8 -*- class Maou def set_doctor(doctor) @doctor = doctor end def create_monster @doctor.create_monster @doctor.equip_weapon_to_monster @doctor.get_monster end end class Doctor def create_monster;end def equip_weapon_to_monster;end def get_monster;end end class GoblinDoctor < Doctor def create_monster @monster = Goblin.new end def equip_weapon_to_monster @monster.weapon = "knife" end def get_monster @monster end end class Monster attr_accessor :weapon end class Goblin < Monster;end maou = Maou.new maou.set_doctor(GoblinDoctor.new) goblin = maou.create_monster p goblin #<Goblin:0x007f95a210c540 @weapon="knife">
Maou クラスは set_doctor メソッドによって、 Doctor クラスを内包する。 Maou クラスは create_monster メソッドによって、Monster のインスタンス生成に関する過程を抽象化しつつ、Doctor へ Monster のインスタンス生成処理を委譲している。
上記の例で、GoblinDoctor クラスでは、Goblin のインスタンス生成過程が実装されているので、create_monster メソッドでインスタンス生成、equip_weapon_to_monster メソッドでパラメータ設定、get_monster メソッドでインスタンス返却を行っている。
Maou(Director)がDoctor(Builder)にMonster(Product)を作らせる、Builderパターンの実装になっている。
まとめ
オブジェクト生成に特化した Builer パターン。 正直、今までの業務でこのパターンで実装した記憶がない。
使いどころとしては、あるクラスのインスタンス生成や状態が複雑な場合に利用するといい。
ただ、業務上で利用する場合の具体例が出てこなくて悔しい・・・。
記事にしていて思ったのは Director の可能性。
Director を複数実装することでもメリットはあると思う。(Director を複数実装している記事をあまり見かけない)
魔王の例だと、魔王1は武器のみを持たせる方針だが、魔王2は武器と防具を持たせる方針、とか生成過程の差分を Director で吸収させる目的で Builder パターンを使うのはありだと思った。
例えば、完璧な車インスタンスが欲しい場合と、タイヤがない車インスタンスが欲しい場合、
完璧な車作る職人とタイヤがない車作る職人をそれぞれ Builder として実装するのはイケテナイ。
もし、この車がスポーツカー・トラック・オープンカーなど、種類が増えたときに Builder 側でそれらを吸収しようとすると、車の種類が追加される度に、2人の職人を追加していくことになってしまう・・・
この場合、タイヤがない車作る職人は必要なくて、完璧な車作る職人に出す指示、つまり、Director の生成過程でタイヤの取り付けをするかしないかで、実装を分けた方が DRY な実装になる。
おさらい
- Director が Builder に Product を作らせている。
- Director にも目を向けろ
次回
次は生成つながりで、 Factory Method パターンでも記事にしようと思う。
下記サイトを参考にさせて頂きました。
ビルダー Ruby 2.0.0 デザインパターン速攻習得[Builder][Design Pattern] - 酒と泪とRubyとRailsと