QA@IT

RailsのModelでnew_or_findの様な処理を書きたい。

3342 PV

現在、Rails4で開発しています。

今回はタグをnewした場合に、もし既存のタグがあるならその既存のタグを返す。無い場合はそのままnewをするという様な設計をしたいのですが、どうすればいいか分からず質問させていただきました。

  • 状況説明

具体的に現在の状況と問題を説明させていただきます。
現在ブログシステムを作っています。このブログにはタグをつける事が可能です。
このタグの機能を実装するために、ブログとタグは多対多の関係にあります。

このブログを作成するときなのですが、ブログのnew.html.erb内でブログの内容も、タグの登録も同時に行います。
この機能を簡単に実現するためにaccepts_nested_attributes_forを使って、new一回でブログとタグを同時に登録できるようにしています。
ブログのモデルと、コントローラのコードはこんな感じです。

class Blog < ActiveRecord::Base
     has_many :blog_tags
     has_many :tags,:through => :blog_tags

     accepts_nested_attributes_for :tags
end
class BlogController < ActiveRecord::Base
     def create
         @blog = Blog.new(blog_params)##blog_paramsにはフォームからのブログデータとタグデータが返ってくる。
         @blog.save!
     end
end

またタグのmigrateファイルはこれです。

class CreateTags < ActiveRecord::Migration
  def change
    create_table :tags do |t|
      t.string :text

      t.timestamps
    end
    add_index :tags, :text, :unique => true
  end
end
  • 問題点

ただここでこまったことがあります。
それは、newをする際に同じ名前のタグを登録できてしまうということです。
例えば"hello"というタグが既にDBにある状態で、フォームから"hello"というタグを登録しても、新しい"hello"というタグがnewされてしまうのです。
もちろん":unique => true"なので、saveするときには例外が発生するのですが、それはそれで処理に困ります。

  • 質問内容

なので理想としては、
Blog.new(blog_params)した段階で、既に"hello"というタグがあればそのインスタンスを返す。もしないなら、新しくnewをする。
というような処理を書きたいのですが、どのように書けばいいか分かりません。

そこで質問なのですがどのようにすればこの問題が解決できるでしょうか?またnew_or_findと書いていますが、この方法以外でも解決方法があればご教授いただけると幸いです。

回答

以下のどれかでうまくゆかないでしょうか。

  1. BlogController#create で @blog = Blog.new(blog_params) する前に blog_params の内容で tag がすでにあるかを確認する処理を書く

  2. Tag の before_create か何かのコールバックで判定処理を書く

  3. Blog.new をオーバーライドして、今回のような条件の場合は tag がすでにあるかを判定する処理を追加する

  4. :update_only オプションを付ける(多対多で使えるのかは不明)

http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for

:update_only

    For a one-to-one association, this option allows you to specify how nested attributes are to be used when an associated record already exists. In general, an existing record may either be updated with the new set of attribute values or be replaced by a wholly new record containing those values. By default the :update_only option is false and the nested attributes are used to update the existing record only if they include the record’s :id value. Otherwise a new record will be instantiated and used to replace the existing one. However if the :update_only option is true, the nested attributes are used to update the record’s attributes always, regardless of whether the :id is present. The option is ignored for collection associations.

編集 履歴 (4)
  • お返事ありがとうございます。
    Blog.new をオーバーライドする方法でうまくいきました。
    ご回答ありがとうございました。
    -
  • あ、そうですか。私もいま accepts_nested_attributes_for を使っていて、つい先程良さそうな資料を見つけました。
    :reject_if を使うと良さそうにも思えました。
    -
  • 参考リンク
    * http://kray.jp/blog/has-many-through-nested-object-forms/
    * http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
    -
  • リンクありがとうございます。自分も1つ目のリンクの方法で投稿フォームは作りました。jsかかなくていいので便利ですよね。reject_ifというのがあったんですね。procも出来て便利そうなのですが、これは既存のコードを返すという事は出来るのでしょうか? -

Active Recordにfind_or_initialize_byというのがありますよ。
http://guides.rubyonrails.org/active_record_querying.html#find-or-initialize-by

DBに保存まで一気に行う場合はfind_or_create_by!が使えます。
http://guides.rubyonrails.org/active_record_querying.html#find-or-create-by-bang

編集 履歴 (0)
  • お返事ありがとうございます。実はfind_or_initialize_byも検討したのですが、accepts_nested_attributes_forの場合はどうやって使えばいいのか分からず断念していました。今回の場合はどのように書けばよいのでしょうか? -
  • すみません。accepts_nested_attributes_forというのをよく知らず、ピントがずれた回答をしてしまいました。

    ぐぐってみたところ、以下のブログ記事が参考になりそうです。
    Railsでネストしたモデルをまとめて受け取って保存する [俺の備忘録]
    http://o.inchiki.jp/obbr/81#toc_7
    -
  • Rails4でstrong parametersを使う場合の例はこちらで解説されていました。
    【Rails】fields_for と accepts_nested_attributes_for - kzy52's blog
    http://kzy52.com/entry/2013/07/10/200144
    -
  • 色々調べていただいてありがとうございます、助かります。リンク等も見てみた結果、特定の物は更新させたいという様な処理を書きたい場合、newに入れるhashを事前に変更してidを割り振るか、new自体を書き換えるという二択でしょうか?どちらを使うかは色々考えた方が良さそうですね。ありがとうございます。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。