I'm building a web application with Rails 5 and have run into an issue updating associated records through a parent record when I have defined non-persistent attributes (with attr_accessor
) on the associated records. Specifically, I have the user supply the non-persistent attributes on the child records in some way, and, based on the values of those attributes, assign values to persistent attributes in a before_save
callback. The problem is that the child records are not saved to the database (and hence the save callback is not called) unless persistent attributes are changed on the child records through the parent.
I've run into this issue in several different situations, but the (simplified) example here deals with using the Paperclip Gem to process images uploaded to AWS S3 by a client browser.
app/models/dog.rb
class Dog < ApplicationRecord
has_many :certificates, :dependent => :destroy
accepts_nested_attributes_for :certificates, :allow_destroy => true
end
app/models/certificate.rb
class Certificate < ApplicationRecord
# load with path to client-uploaded file on S3 and save to
# update digitized_proof attachment
attr_accessor :s3_key
belongs_to :dog
has_attached_file :digitized_proof,
:content_type => { :content_type => ['image/jpg', 'image/png'] }
before_save :fetch_digitized_proof_from_s3
def fetch_digitized_proof_from_s3
return unless self.s3_key.present?
# note `S3_BUCKET` is set in the aws initializer
s3_obj = S3_BUCKET.object(self.s3_key)
# load paperclip attachment via S3 presigned URL
s3_presigned_url = s3_obj.presigned_url(:get,
:expires_in => 10.minutes.to_i)
self.digitized_proof = URI.parse(s3_presigned_url)
end
end
apps/controllers/dogs_controller.rb excerpt
def update
@dog = Dog.find(params[:id])
if @dog.update(dog_params)
redirect_to ...
...
end
private
def dog_params
params.require(:dog).permit(
...,
:certificates_attributes => [:id, :_destroy, :s3_key]
)
end
I've written javascript that uploads images to a temporary folder in an S3 bucket directly from the client's browser and adds the s3_key
to the update form so the image can be identified and processed server-side (see the fetch_digitized_proof_from_s3
method in certificate.rb). The issue is that the certificates are never updated unless an actual database attribute has changed in the update parameters.
Why is this occurring and how can I work around it?
Sample parameters
{
...,
certificates_attributes: [
{id: '1', _destroy: '0', s3_key: 'tmp/uploads/certificates/.../photo.jpg'},
{id: '2', _destroy: '0', s3_key: 'tmp/uploads/certificates/.../photo2.jpg'}
]
}
Gem Versions
rails-5.0.0
activerecord-5.0.0
paperclip-5.1.0
aws-sdk-2.10.0
EDIT
I'm able to accomplish the update on the certificates by calling fetch_digitized_proof_from_s3
from within the setter method for s3_key
(and removing the before_save
callback):
# app/models/certificate.rb
def s3_key=(key)
@s3_key = key
self.fetch_digitized_proof_from_s3
end
This triggers the associated certificates to save properly (I'm thinking this occurs since digitized_proof
, which is a persistent attribute, is updated by the call to fetch_digitized_proof_from_s3
). This works, but I'd still rather fetch the image from S3 when the record is saved.
before_validation
instead? The save call won't get run if the record is invalid. – max pleanerbefore_validation
is producing the same behavior. The callbacks don't seem to trigger whens3_key
is the only modified attribute. – w_hile