Rails4

すでにimage_urlというカラムを持ったModelにCarrierWaveマウントしてハマったのでまとめ

More than 3 years have passed since last update.

はじめに

image_urlというカラムが存在してるModelにCarrierWaveをマウントして画像のアップロード等通常の処理は行えていたので何も気にしてなかったのですが、機能追加に伴って、マウントしたModelをto_jsonすると、なぜか意図しない値が返ってくる不思議な状態で半日ほどハマったのでそのあたりまとめておこうと思います。

先に要点だけ書くと

  • マウントするModelを作成するときにはカラム名にはimage_urlというものを含めないほうが良いと思います。
    • CarrierWaveのインスタンスメソッドにimage_urlというものが存在するため
  • すでにimage_urlというカラム名が存在するModelに対してCarrierWaveをマウント、かつ、そのModelをto_json する時にはRailsでモデルをto_jsonするときにデータを追加するオプションで紹介されてるmethodsという引数で任意のメソッドを指定してうまく対処

という感じです

自分の開発環境

  • Mac OS X 10.8.5
  • Ruby 2.2.2
    • rbenv利用してインストールしてます
  • Rails 4.2
  • carrierwave-0.10.0

事の経緯

他の人が開発していたものを引き継ぐ形で実装をしてました。

こんな感じのItemというモデルがすでに存在しててて、機能拡張のために画像情報を持たせるようにするためにCarrierWaveを利用することにしました。

# == Schema Information
#
# Table name: items
#
#  id              :integer          not null, primary key
#  name            :string(255)
#  image_url       :string(255)
#  created_at      :datetime         not null
#  updated_at      :datetime         not null
#

class Item < ActiveRecord::Base

end

CarrierWave利用できるように既存のModelを修正

CarrierWaveのインストール手順などは割愛しますが最終的にこんな感じになったとします。

class Item < ActiveRecord::Base
  mount_uploader :image, ImageUploader
end
class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::RMagick

  storage :file
  # 以下省略
end

上記のようにしたことで

item = Item.last
item.image.url
=> "/uploads/item/image/1576/117532-m-01-dl.jpg"

ということが実現できたので特に気にせずにそのまま実装を進めてました。

Itemモデルでto_jsonすると意図した値が取得できない

元々こんな感じで値が格納されてました

Item.last
  Item Load (0.4ms)  SELECT  `items`.* FROM `items`  ORDER BY `items`.`id` DESC LIMIT 1
=> #<Item:0x007fc7ea2deec0
 id: 1576,
 name: "アイテムの名前",
 image_url: "http://www.example.co.jp/some_piture.jpg",

ある機能を実現するために、Itemモデルでto_jsonする必要が出てきたのですが、JSON化された値を見るとimage_urlの値がhttp://www.example.co.jp/some_piture.jpg ではなく、CarrierWaveの通じてアップロードされた画像のパス、つまり/uploads/item/image/1576/117532-m-01-dl.jpg が返ってきました。

※実際には状況がもうちょっと複雑でItemの先頭はまだCarrierWave通じて画像アップロードされてなかったのでnilが返ってきたので何が生じてるのかわからずに大分ハマりました

原因

find ./vendor/bundle/ruby/2.2.0/gems/carrierwave-0.10.0  -type f -name "*.rb" | xargs grep "image_url"

という感じで検索して気づいたのですがcarrierwave-0.10.0/lib/carrierwave/mount.rbを見ると

    # === Added instance methods
    #
    # Supposing a class has used +mount_uploader+ to mount an uploader on a column
    # named +image+, in that case the following methods will be added to the class:
    #
    # [image]                   Returns an instance of the uploader only if anything has been uploaded
    # [image=]                  Caches the given file
    #
    # [image_url]               Returns the url to the uploaded file
    #
    # [image_cache]             Returns a string that identifies the cache location of the file
    # [image_cache=]            Retrieves the file from the cache based on the given cache name
    #
    # [remote_image_url]        Returns previously cached remote url
    # [remote_image_url=]       Retrieve the file from the remote url
    #
    # [remove_image]            An attribute reader that can be used with a checkbox to mark a file for removal
    # [remove_image=]           An attribute writer that can be used with a checkbox to mark a file for removal
    # [remove_image?]           Whether the file should be removed when store_image! is called.
    #
    # [store_image!]            Stores a file that has been assigned with +image=+
    # [remove_image!]           Removes the uploaded file from the filesystem.
    #
    # [image_integrity_error]   Returns an error object if the last file to be assigned caused an integrity error
    # [image_processing_error]  Returns an error object if the last file to be assigned caused a processing error
    # [image_download_error]    Returns an error object if the last file to be remotely assigned caused a download error
    #
    # [write_image_identifier]  Uses the write_uploader method to set the identifier.
    # [image_identifier]        Reads out the identifier of the file

という感じで、CarrierWaveにimage_urlというインスタンスメソッドがありました。

to_jsonした時には、Itemモデルのimage_urlではなく、CarrierWaveのimage_urlの値が参照されてJSON化されていたようでした。

対策

カラム名を変更すれば解決出来ると思うのですが、色々事情があって、すぐに変更するわけにはいかなさそうだったので、別の対策を考えてました。

Railsでモデルをto_jsonするときにデータを追加するオプションで紹介されてるmethodsという引数で任意のメソッドを指定するという方法を取ることにしました。

Itemモデルを修正

上記の記事だけだと理解がちょっと進まなかったのですがas_json include carrierwave modelを見てたらどういうことかわかりItemモデルをこのように変更しました

class Item < ActiveRecord::Base
  mount_uploader :image, ImageUploader

  def origin_imaeg_url
    self[:image_url]
  end
end

このようにすることで、origin_imaeg_urlを通じて、Itemモデルが参照するitemsテーブルのimage_urlを取得することが出来るようになります

Item.to_jsonする時に一工夫する

to_jsonする時にCarrierWaveにimage_urlが参照されてしまうため

item.to_json(methods: :origin_imaeg_url)

という感じで書いてあげることでItemモデルのorigin_imaeg_urlメソッド通じて取得される値もJSONに含まれるようになりました。