PaperClipとnested_formでmongoDB embeds_manyな複数画像ファイルアップロードフォームを作る


# mongoDBで embeds_many な画像たち

とにかく複数の画像ファイルを一括でアップロードしたいよーという時に。

今回の例で行くと、Articleモデルがあり、その中に複数のAdモデルがあり、その中に複数のAdImageモデルがあるパターン。リレーションではなく、埋込み型（embedded）。まぁ、リレーションでもやり方は変わらない。画像ファイルはAWSのS3に配置するけど、特にS3でなくても良い。

![Database.png](https://qiita-image-store.s3.amazonaws.com/0/2152/62ff0325-c953-0aa0-b001-09421c7607ba.png "Database.png")

なので、mongoDBのスキーマでいうとこんな感じ。

```
{
  _id: ObjectId("54ed54326c6f632ec2010000"),
  title: "Let's Job Change",
  content: "piyo piyo pi-yo",
  ads: [
    {
      _id: ObjectId("54ed54326c6f632ec2010000"),
      catch: "Imasugu TensyokuDA!",
      uri: "http://www.example.com/ad",
      ad_images: [
        {
          _id: ObjectId("54ed54326c6f632ec2010000"),
          ad_image_file_name : "photo.jpg",
          ad_image_content_type : "image/jpg",
          ad_image_file_size : 29979,
          ad_image_fingerprint : "559cf44f69af4a2e4790c86011f64ee3",
          ad_image_updated_at : ISODate("2015-03-17T01:09:57.909Z")
        }
      ]
    }
  ]
}
```

## 使用するGemたち

```Gemfile
gem 'nested_form'
gem 'rmagick'
gem 'mongoid-paperclip', :require => 'mongoid_paperclip'
```

nested_formで、動的にフォームのパーツを追加削除できる。has_many / embeds_manyなモデルの一括更新フォームを作るにはとても便利。大量の画像ファイルをアップロードするのであれば`Delayed::Paperclip`のGemを使うのもあり。

- https://github.com/jrgifford/delayed_paperclip/

## モデル

ポイントは`:cascade_callbacks`で、これがないと画像のアップロードなどが行われない。

```ruby:app/models/article.rb
class Article
  include Mongoid::Document
  include Mongoid::Timestamps

  embeds_many :ads, :cascade_callbacks => true

  accepts_nested_attributes_for :ads, :allow_destroy => true

  field :title, type: String
  field :content, type: String
end
```

```ruby:app/models/ad.rb
class Ad
  include Mongoid::Document

  embedded_in :article
  embeds_many :ad_images, :cascade_callbacks => true

  accepts_nested_attributes_for : ad_images, :allow_destroy => true

  field :catch, type: String
  field :uri, type: String
end
```

```ruby:app/models/ad_image.rb
class AdImage
  include Mongoid::Document
  include Mongoid::Paperclip

  embedded_in :ad, :inverse_of => :ad_images

  has_mongoid_attached_file :ad_image,
                            :path           => ':attachment/:id/:style.:extension',
                            :storage        => :s3,
                            :s3_credentials => File.join(Rails.root, 'config', 's3.yml'),
                            :styles => {
                              :original => ['1920x1680>', :png],
                              :small    => ['100x100#',   :png],
                              :medium   => ['250x250',    :png],
                              :large    => ['500x500>',   :png]
                            },
                            :default_url => 'missing.png'

  validates_attachment_content_type :ad_image, :content_type => %w(image/jpeg image/jpg image/png image/gif)
end
```

## コントローラー

Rails4からのStrong Parametersで、Adモデルと、更にその中のAdImageモデルを許可する。PaperClipの場合、フィールドは`has_mongoid_attached_file`で指定したものだけでOK。

```ruby:app/controllers/articles.rb
def app_params
  params.require(:article).permit(
    :title, :content,
    ads_attributes: [
      :id, :catch,
      ad_images_attributes: [:id, :ad_image]
    ]
  )
end
```

## ビュー

通常だと、`form_for`でフォーム展開をしているが、`nested_form_for`を使う。これで枠の追加をJavaScriptで行ってくれる。問題は、画像ファイルの参照がフォームヘルパーオブジェクトからは参照できず、`@article.ads`などの実データから引っ張ってくる必要があること…正直ださい。どうにかしたい。

```slim:app/views/articles/_form.html.slim
= nested_form_for @article, :html => { multipart: true, class: '' } do |f|
  .el-group
    h2 記事情報
    .el-inner
      .form-group
        = f.label 'タイトル'
        = f.text_field :title, class: 'form-control'
      .form-group
        = f.label '本文'
        = f.text_field :content, class: 'form-control'

  = f.fields_for :ads do |ad|
    .el-group
      h2 広告枠
      .el-inner
        .form-groups
          = ad.label 'キャッチ'
          = ad.number_field :catch, class: 'form-control'
          
        = ad.link_to_remove "広告枠の削除"

        h4 広告画像
        - @article.ads.each do |t_ad|
          - if t_ad.id ==  ad.object.id
            - t_ad.ad_images.each do |pic|
              = image_tag pic.ad_image.url(:small)
              
        = ad.fields_for :ad_images do |ad_image|
          .form-groups
            = ad_image.file_field :ad_image, class: 'form-control'
            
        = ad.link_to_add "画像の追加", :ad_images

  = f.link_to_add "広告枠の追加", :ads

  .actions = f.submit
```