Active Record
The activerecord plugin adds Active Record integration to
the attachment interface. It is built on top of the model plugin.
Shrine.plugin :activerecordAttachment
Including a Shrine::Attachment module into an ActiveRecord::Base subclass
will:
- add model attachment methods
- add validations and callbacks to tie attachment process to the record lifecycle
class Photo < ActiveRecord::Base # has `image_data` column
include ImageUploader::Attachment(:image) # adds methods, callbacks & validations
endphoto = Photo.new
photo.image = file # cache attachment
photo.image #=> #<Shrine::UploadedFile id="bc2e13.jpg" storage=:cache ...>
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
photo.save # persist, promote attachment, then persist again
photo.image #=> #<Shrine::UploadedFile id="397eca.jpg" storage=:store ...>
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
photo.destroy # delete attachment
photo.image.exists? #=> falseCallbacks
After Save
After a record is saved and the transaction is committed, Attacher#finalize
is called, which promotes cached file to permanent storage and deletes previous
file if any.
photo = Photo.new
photo.image = file
photo.image.storage_key #=> :cache
photo.save
photo.image.storage_key #=> :storeAfter Destroy
After a record is destroyed and the transaction is committed,
Attacher#destroy_attached method is called, which deletes stored attached
file if any.
photo = Photo.find(photo_id)
photo.image #=> #<Shrine::UploadedFile>
photo.image.exists? #=> true
photo.destroy
photo.image.exists? #=> falseCaveats
Active Record currently has a bug with transaction callbacks, so if you have any "after commit" callbacks, make sure to include Shrine's attachment module after they have all been defined.
Duplicating records
Since a record being created can't yet have a confirmed attachment of its own
to safely replace, Shrine never deletes the previous file when the attachment
changes as part of creating a record, only when updating one. This
matters most when duplicating a record: #dup performs a shallow copy, so a
duplicated record initially points to the same underlying file as the
original (they'll have the same attached file data), and replacing the
attachment on the duplicate before it's ever saved won't affect the original:
photo = Photo.create(image: file)
photo2 = photo.dup
photo2.update!(image: new_file) # replaces the attachment on `photo2`
photo.image.exists? #=> true (not affected)Once photo2 has been saved, replacing its attachment again behaves
normally (the previously attached file is deleted):
previous_image = photo2.image
photo2.update!(image: another_file)
previous_image.exists? #=> falseKeep in mind this only protects against replacing the attachment on create.
As long as photo and photo2 continue to reference the same underlying
file (i.e. photo2 is saved without ever changing its attachment), destroying
either record will still delete the file the other one references, since
Shrine has no way of knowing the file is shared:
photo2.save! # still references the same file as `photo`
photo2.destroy
photo.image.exists? #=> falseIf you want the duplicated record to have its own independent copy of the file from the start, so that destroying either record is also safe, upload a new copy explicitly after duplicating:
photo = Photo.create(image: file)
photo2 = photo.dup
photo2.image_attacher.set(nil)
photo2.image_attacher.attach(photo.image, storage: photo.image.storage_key)
photo2.save!
photo2.destroy # no longer affects `photo`
photo.image.exists? #=> trueOverriding callbacks
You can override any of the following attacher methods to modify callback behaviour:
Attacher#activerecord_before_saveAttacher#activerecord_after_saveAttacher#activerecord_after_destroy
class Shrine::Attacher
def activerecord_after_save
super
# ...
end
endSkipping Callbacks
If you don't want the attachment module to add any callbacks to your model, you
can set :callbacks to false:
plugin :activerecord, callbacks: falseValidations
If you're using the validation plugin, the attachment module
will automatically merge attacher errors with model errors.
class ImageUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_max_size 10 * 1024 * 1024
end
endphoto = Photo.new
photo.image = file
photo.valid?
photo.errors #=> { image: ["size must not be greater than 10.0 MB"] }Attachment Presence
If you want to validate presence of the attachment, you can use Active Record's presence validator:
class Photo < ActiveRecord::Base
include ImageUploader::Attachment(:image)
validates_presence_of :image
endI18n
If you want Active Record to translate attacher error messages, you can use symbols or arrays of symbols and options for validation errors:
class ImageUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_max_size 10 * 1024 * 1024, message: -> (max) { [:too_large, max: max] }
validate_mime_type %w[image/jpeg image/png], message: :not_image
end
enden:
activerecord:
errors:
models:
photo:
attributes:
image:
max_size: "must not be larger than %{max_size} bytes"
not_image: "must be a common image format"Skipping Validations
If don't want the attachment module to merge file validations errors into
model errors, you can set :validations to false:
plugin :activerecord, validations: falseJSON columns
When the data attribute is backed by a json or jsonb database column,
Active Record already serializes hashes into JSON, so Shrine skips its own
serialization to avoid double encoding.
class Photo < ActiveRecord::Base # `image_data` is a jsonb column
include ImageUploader::Attachment(:image)
endphoto.image = file
photo.image_data #=> { "id" => "bc2e13.jpg", "storage" => "cache", "metadata" => { ... } }By default this detection only looks at real database columns. If you declare
the data attribute type via the Attributes API (e.g. on top of a text
column), enable the :attribute_types option so it's recognized as well:
plugin :activerecord, attribute_types: trueclass Photo < ActiveRecord::Base # `image_data` is a text column
include ImageUploader::Attachment(:image)
attribute :image_data, :json
endThis option defaults to false for backwards compatibility.
Attacher
You can also use Shrine::Attacher directly (with or without the
Shrine::Attachment module):
class Photo < ActiveRecord::Base # has `image_data` column
endphoto = Photo.new
attacher = ImageUploader::Attacher.from_model(photo, :image)
attacher.assign(file) # cache
attacher.file #=> #<Shrine::UploadedFile id="bc2e13.jpg" storage=:cache ...>
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
photo.save # persist
attacher.finalize # promote
photo.save # persist
attacher.file #=> #<Shrine::UploadedFile id="397eca.jpg" storage=:store ...>
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'Persistence
The following persistence methods are added to Shrine::Attacher:
| Method | Description |
|---|---|
Attacher#atomic_promote | calls Attacher#promote and persists if the attachment hasn't changed |
Attacher#atomic_persist | saves changes if the attachment hasn't changed |
Attacher#persist | saves any changes to the underlying record |
See persistence docs for more details.