QA@IT

Railsでモデルと紐づいた画像と、モデルとの連携がうまくいかない。

2973 PV

現在Rails4.0.0で開発しているのですが、画像の紐付けがうまくいかずにこまっています。
もう少し具体的に言うと、フォーム上で画像だけ変更された場合に、updateが走らなくて困っています。

具体的に現在の状況を説明させていただきます。

まず「Game」というオブジェクトと、「Result」というオブジェクトがあります。
GameとResultは1:多の関係にあります。様は1つのゲームに対して、複数のリザルトがあるという感じです。
そしてこのリザルトは、画像を持っています。結果画面には、テキストだけではなく画像のイメージを表示させたいからです。
この画像は、DBに直接保存するのではなく、S3という外部のサーバに保存します。

このモデルをコードで書くと、以下の様な感じになります。

class Game  < ActiveRecord::Base
      has_many :results, :dependent => :destroy
      accepts_nested_attributes_for :results, :allow_destroy => true
end
class Result < ActiveRecord::Base
     belongs_to :game
     after_save :commit_after
     before_destroy :destory_before

    def img=(file)
        @img = file.read
    end

     def img
       return @img  
     end

     def commit_after
           upload_to_s3(@img)
     end

      def destory_before
          delete_from_s3
     end
end

「Result」の説明を少しさせていただきます。
def img=(file)
def img
このように書く事で、さもResultがimgというカラムを持っている様にしています。
そしてコールバックの「commit_after」で、メンバ変数内の画像をS3にアップするようにしています。

ただここで困った事がおこりました。このゲームとリザルトの内容は、Webの管理画面から変更することができるのですが、フォームから画像だけ変更して、PUTが送られてきた場合において、Resultのアップデートが走らないのです。
どうもRailsは賢いらしく、モデルのカラムの内容が変更されていないと、アップデートが走らない様なのです。
ただそうするともちろん、S3に対して変更した画像がアップロードされずに、画像の変更が反映されません。

このような場合に、commit_afterのようなコールバックを明示的に呼び出すためにはどうしたらよいでしょうか?
またそれとは別に設計的な問題として、モデルに紐づいた画像を扱うさいにはこのような設計でよいのでしょうか?

抽象的な質問で恐縮なのですが、ご回答いただけると助かります。

回答

私も普段は carrierwave を使ってるのですが、別のケースで子レコードのコールバックを実行させたかったので、調べてみました。

解決方法

画像を変更したかどうかのフラグを用意して、ActiveRecord::AutosaveAssociation#changed_for_autosave? をオーバーライドすればよさそうです。

class Result
  after_save do
    @img_changed = false # save 後はフラグを false にしておく
  end

  def img=(file)
    @img_changed = true # 画像の変更があればフラグを true に
    @img = file.read
  end

  def changed_for_autosave?
    @img_changed || super # フラグが true なら true、それ以外はデフォルトの挙動
  end
end

解説

has_many の子を save する処理は ActiveRecord::AutosaveAssociation で定義されているようです。

この中で、個々の子を save するかどうかは、子の changed_for_autosave? で決定しているようです。

# activerecord-4.1.0/lib/active_record/autosave_association.rb
module ActiveRecord
  module AutosaveAssociation
    def changed_for_autosave?
      new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
    end
  end
end

save させたいときは、このメソッドが true を返すようにすればよいので、"解決方法" で書いたようにオーバーライドしました。

編集 履歴 (0)
  • お返事ありがとうございます。なるほど、内部ではここをみて判別していいたのですね。この方法でやってみたところ、無事にsaveできました。ありがとうございました。 -

自分ならimage_pathというカラムを実際に追加してしまいますが、
この方法だとまずいのでしょうか? 正直あまり自信はありません…。

また独自実装にこだわらないのであれば
carrierwavepaperclipを使うのが良いのではないかと思います。

https://github.com/carrierwaveuploader/carrierwave
https://github.com/thoughtbot/paperclip

どちらもオプションの設定をするだけでS3をアップロード先に変更できます。

編集 履歴 (0)
  • お返事ありがとうございます。

    確かにGemを使うべきでした。ただ既に動いているサービスなので、後からgemを加えにくいのが現状です。

    今度アプリを作成する際には、こちらにあげていただいたGemを使ってみようと思います。ありがとうございました。
    -
ウォッチ

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