可変個数の画像を1カラムにJSONで保存するためのベストプラクティスが知りたい

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 178
退会済みユーザー

退会済みユーザー

前提・実現したいこと

Rails で言語ラベルつきの可変個数の画像アップローダーからきた画像データを
AWS S3 にアップロードしつつ MySQL の1カラムにURLを JSON 形式で保存したいです

Rails 的には1フォーム:1カラムに対応させてと分けたほうが都合がいいのですが
API側の都合で入力内容を1カラムにまとめたいです

発生している問題

ActionDispatch 型でとんできたパラメーターを
S3 にアップロードして URL を取得する部分はようやく動くようになったのですが
フォームからきたハッシュをコントローラー上でどう扱って最終的にどう JSON にしていいのかがわかりません

試したこと

view 

<%= form_for @campaign do |f| %>
  <%= f.fields_for :image do |f| %>
    <%= f.file_field :default %><%= image_tag f.object.image.default %>
    <%= f.file_field :ja %>     <%= image_tag f.object.image.ja %>
      :


のようにかいたところ
"image"=>{"default"=>#<ActionDispatch::Http::UploadedFile:...}
という形でとんできます

これを 

@campaign = Campaign.new(params.require(:campaign).permit(image: I18n.available_locales.map(&:to_s)))


と最初コントローラー内でハッシュでデータをもとうとしたんですが
途中でエラー等がでてそのままビューに戻ってきたときに
@campaign.image がハッシュだと .default がないといわれてしまうのでオブジェクトで持たないと再表示できません

そこで以下のようなハッシュをオブジェクト化するためだけのクラスを定義して

class MultiLocale
  include ActiveModel::Model
    @@langs = ['default'] + I18n.available_locales.map(&:to_s)
    attr_accessor *@@langs

    # ここがよく意味がわかってないのですが
    # こうかいて serialize を定義すると勝手に JSON で保存してくれるらしい?
    class << self
      def dump(obj)
        obj.to_json if obj
      end

      def load(source)
        self.new(JSON.parse(source)) if source
      end
    end

Campaign モデル内で

  serialize :image,        MultiLocale

  def image=(val)
    super
    p image
  end


とパラメーターをうけとったときに MultiLocale オブジェクトとして保存するセッターをかいたんですが
デバッグ表示されたデータは 
{"default" => {"tempfile"=>[], "original_filename"=>"150x150.png", "content_type"=>"image/png", "headers"=>"Content-Disposition: form-data; name=\"campaign[image][default]\"; filename=\"150x150.png\"\r\nContent-Type: image/png\r\n"}
とハッシュ型になってしまい ActionDispatch 型ではなくなってしまいます

ActionDispatch 型でないと CarrierWave に渡せないので
MultiLocale クラスに initializer を追加して

  def image=(val)
      @image = MultiLocale.new(val)
      p @image
  end


というセッターを定義したところようやく @image の中身が
<MultiLocale @default=#<ActionDispatch::Http::UploadedFile:...>} 
という形でパラメータを受け取ることができたので

before_validation 内で

tmp = {}
@image.each{|k,v|
  image = CampaignImage.new # CarrierWave マウントクラス
  image.image = v
  if image.save
    tmp[k] = image.image.url
  else
    errors.add(:image, I18n.t('.invalid_image'))
  end
}
@image = MultiLocale.new(tmp)


とアップロードとURLの取得を行って成功した場合 @image の中身は
<MultiLocale @default="https://S3のURL" >
となり、バリデーションエラーでビューに戻ったとき、アップロードされた画像が表示できるようになりました

ただ途中でバリデーションエラーが出た場合等に ActionDispatch と変換された URL 文字列が混在することになり、
(たとえば 2 つめの画像保存に失敗した場合)

<MultiLocale @default="https://S3のURL" @ja=#<ActionDispatch::Http::UploadedFile:...> >


という状態になって ActionDispatch のままのものは画像が表示されません

①この状態でビューに戻った場合にもう1度 ActionDispatch から form にデータをセットする方法はないでしょうか?

②またこの状態で保存しようとすると
    validates :image,             presence: true
のバリデーションにひっかかって画像がないというバリデーションエラーになってしまいます

  def image
    @image
  end


とゲッターを追加するとバリデーションは通るようになりますが今度は MySQL エラーで image の default value がないといわれます
@image に保存したい内容を MultiLocale 型でもっているのですが
どうも MySQL に保存するときにこの値をみてくれていないようなのです
MySQL に JSON で保存するにはどうすればいいんでしょうか?

画像以外のカラムはセッターをかいていなくて、MutiLocale 型から自動的に JSON で保存してくれますが
画像の場合だけセッターをかいてしまってるせいで保存されないみたいなのです

self.image = @image 


をよんでもセッターをオーバーライドしてしまっているので @image が更新されてしまうだけで
MySQL にセットされる値をセットするにはどうすればいいんでしょうか

Rails にまだそれほどなれていなくて完全に自己流なので、明らかに変なことをやってる気がするのですが
ベストプラクティスを教えていただきたいです

とくに カラムのデータ @変数 セッターゲッター []でのアクセス あたりがあいまいで区別ができていないために
どこでどのデータにどの型が入っているかが把握できていなくてはまっています

まとめると

① ActionDispatch 型でもっている画像データをアップロードフォームに再セットしたい
(ローカルのファイルを勝手にセットすることはできないけど
Blob オブジェクトを作ることができればファイルフィールドにセットすることは可能みたいなのですが
どういう JS を生成すればいいかがわかりません)

② カラムと同名の @変数でもっている値を save 時に MySQL カラムに保存するにはどうすればいいでしょうか

③ そもそも途中で MultiLocale 型という独自モデルを定義して保持するというのが適切なやり方なのでしょうか

以上 3 点よろしくおねがいします

補足情報(FW/ツールのバージョンなど)

Rails 4.2.3 です

追記

  def image=(val)
      @image = MultiLocale.new(val)
  end

をオーバーライドしなければデフォルトではどういう挙動をするのでしょうか
@変数 にかきこめばそれ(をシリアライズしたもの)がカラムになる?
と思っていたのですがどうやら違うようです
save のときに MySQL に書き込まれる値はどこに保持されているのでしょうか

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

+2

MySQL に JSON で保存するにはどうすればいいんでしょうか?

素直に、「画像専用のモデル(画像1枚で1レコード)を立てる」という形にして、Campaignからはhas_manyでそのモデルをもたせる、という形にするほうが、Carrierwave的にもDBの正規化の面からも都合がいいと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/22 09:03 編集

    回答ありがとうございます

    has_many は使っていないですが
    画像ごとに CampaignImage という CarrierWave の Uploader をマウントしたクラスは作っています
    作ってアップロードしてURLを取り出すだけですが…

    ちなみに ActiveRecord ではなく ActiveModel に対しても has_many ってできるんでしょうか
    その場合 foreign_key とかはどう扱えばいいのでしょうか

    まとめてJSONで保存して画像毎にテーブルを持ちたくない理由としては
    保存したデータを読み出すAPI側では画像1つ1つは参照せずに全部JSONで返すだけ
    (JSONの中身を分解して見るのはアプリ側と今作ってる画面のみ)
    でAPIの負荷をなるべく抑えたいという要件もあって
    画像ごとに別のテーブルに保存して毎回JSONを生成するより
    はじめからJSONでもっておいた方がパフォーマンス面でメリットがあるのです…

    キャンセル

checkベストアンサー

+1

とりあえずカラムの保存したければ 
before_validation の最後で
self[:image] = @image
とすれば MySQL には入ると思います

  • [] : カラムのデータ
  • @変数: モデル内のローカル変数(同名であってもカラムとは実態が別)
  • .image, .image= :単なるメソッドでどの実体を読み書きするかは実装によります

デフォルトはカラムの読み書き(Serialize がかかれてると型変換もやってくれる)だけど
今回それをオーバーライドしてローカル変数に読み書きするようにしたために
カラムはずっと変化しない(new の場合 nil のまま) なのでエラーになったと思われます

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/05/28 12:55

    ありがとうございます
    保存できました!
    しかもちょうどわかってなかった部分の解説までしていただいて助かります

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.22%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる