何でも屋エンジニアのブログ

当分はRails中級者向け勉強会Step-to-Rails-Expert.rb関連になると思います

モデルと画像のリレーションが多対多のケースで画像をcarrierwave + fogでAWS S3に登録する

carrierwave + fogでAWS S3に画像を保存する方法に関して、画像と当該モデルのリレーションを多対多で設定したいことがあると思います。例えばある写真を他のブログ記事に使いたいという場合がそのケースにあたります。 画像と当該モデルのリレーションを多対多に設定して画像アップロード機能を実装する際に少しハマったので記事に残しておきます。

ハマった内容

恥ずかしいのですが、画像登録機能実装時にaccepts_nested_attributes_forやbuildを使用せずに登録しようとしていたところ、 以下の内容でハマりました。

hmu29.hatenablog.com

実際、本来なら画像データに関して

"name"=>#<ActionDispatch::Http::UploadedFile:0x007fa32c0726d8 @tempfile=#<Tempfile:hogehoge.jpg>, @original_filename="hogehoge.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"hogehoge\"; filename=\"hogehoge.jpg\"\r\nContent-Type: image/jpeg\r\n”>

のようなリクエストを送るのが正しい動作なのですが、

Parameters: {~略~, "image"=>{"name"=>"hogehoge.jpg"}, "commit"=>"登録"}

となってしまい画像のデータがリクエスト時に送れないという事態に陥りました。

今回考えるケース

  • ブログ記事に画像をアップロードする。
  • 但し、ブログ記事(entryテーブル)と画像(imageテーブル)は多対多の関係とする。
  • 登録機能のみ例示する

前提

  • 基本的なcarrierwave, fogの設定は終了しているものとする。
  • 必要最低限しか記載していないので、必要に応じて処理を追加してください。

実装

migrationファイル

  • 説明・注意点
    • add_foreign_keyにnameオプションを渡すことで、いちいち制約名が変わらなくなるので便利
    • 中間テーブルにて記事IDと画像IDにユニーク制約を付与すること

entryテーブル

class CreateEntries < ActiveRecord::Migration
  def change
    create_table :entries do |t|
      t.string :title, null: false
      t.text :content, null: false
      t.timestamps, null: false
    end
end

imageテーブル

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.string :name, null: false
      t.timestamps null: false
    end
  end
end

中間テーブル

class CreateEntryImages < ActiveRecord::Migration
  def change
    create_table :entry_images do |t|
      t.references :entry, null: false
      t.references :image, null: false
      t.timestamps null: false
    end
    add_index :entry_images, [:entry_id, :image_id], unique: true
    add_foreign_key :entry_images, :entries, name: 'fk_rails_from_entry_images_to_entries', option: 'ON UPDATE CASCADE ON DELETE CASCADE'
    add_foreign_key :entry_images, :images, name: 'fk_rails_from_entry_images_to_images', option: 'ON UPDATE CASCADE ON DELETE CASCADE'
  end
end

modelの作成(バリデーションなどは省略)

  • 説明・注意点
    • entry.rbにて「accepts_nested_attributes_for」を付与しないと入れ子のフォームができないので注意!!
    • entry_image.rbにて、image_idとentry_idへ複合ユニーク制約を付与するのを忘れないでください。

entry.rb

class Entry < ActiveRecord::Base
  # アソシエーションの設定
  has_many :images, through: :entry_images
  has_many :entry_images
  
  # viewにて入れ子のフォームを作成するために必要
  accepts_nested_attributes_for :images
end

image.rb

class Image < ActiveRecord::Base
    has_many :entries, through: :entry_images
    has_many :entry_images
    # ImageUploaderとカラムを紐づける
    mount_uploader :name, ImageUploader
end

entry_image.rb

class EntryImage < ActiveRecord::Base
  belongs_to :entry
  belongs_to :image
  
  validates :image_id, presence: true, uniqueness: {scope: [:entry_id]}
end

controllerの作成

  • それぞれのコントローラーで以下を記述して下さい
  • 説明・注意点
    • @entry.images.buildでentryテーブルとimageテーブルと紐付けます。
    • accepts_nested_attributes_for :imagesとmodelに記載した場合、画像の情報はparams[:entry][:images_attributes]に入ります。
    • images_attributesを許可して下さい。
def new
  @entry = Entry.new
  @entry.images.build
end

def create
  @entry = Entry.new(permitted_attr)
  @entry.save!
end

private
def permitted_attr
 params.require(:entry).permit(
  :title, :content, images_attributes: [:name]
 )
end

viewの作成

new.html.erb

<%= form_for @entry do |f| %>
  <%= f.fields_for :images do |i| %>
    <%= i.label :name, '画像' %>
    <%= i.file_field :name, disabled: false %>
  <% end %>
  <%= f.submit '登録'%>
<% end %>

以上で、entriesテーブル、imagesテーブル、entry_imagesテーブルに正しく画像が登録できるようになります。

さいごに

  • 多対多でも、リレーションをきちんと定義してRailsのお作法に則れば、画像アップロードが正しくできるという記事でした。本記事で触れた個々の内容に関して、詳しく説明された記事があるので以下に貼ります。
  • carrierwaveの基本的な設定は以下を参考にして下さい。
    morizyun.github.io

  • fogの基本的な設定は以下を参考にして下さい。 morizyun.github.io

  • accepts_nested_attributes_forに関する詳しい説明は以下を参考にして下さい。 ruby-rails.hatenadiary.com

参考

rails4 fields_forの要素に対してはfile_fieldが働かない? - 日々の記録。