Search code examples
ruby-on-railsactiverecordruby-on-rails-5rails-activestorage

How to update attachment in ActiveStorage (Rails 5.2)


I recently upgraded my project to the latest Rails version (5.2) to get ActiveStorage - a library that handles attachment uploads to cloud services like AWS S3, Google Cloud etc..

Almost everything works fine. I can upload and attach images with

user.avatar.attach(params[:file])

and receive it with

user.avatar.service_url

But now I want to replace/update an user's avatar. I thought I can run

user.avatar.attach(params[:file])

again. But this throws an error:

ActiveRecord::RecordNotSaved: Failed to remove the existing associated avatar_attachment. The record failed to save after its foreign key was set to nil.

What is that suppoed to mean? How can I change an users avatar?


Solution

  • The cause of the error

    This error is being raised by the has_one association between your model and the attachment record. It occurs because trying to replace the original attachment with a new one will orphan the original and cause it to fail the foreign key constraint for belongs_to associations. This is the behavior for all ActiveRecord has_one relationships (i.e. it’s not specific to ActiveStorage).

    An analogous example

    class User < ActiveRecord::Base
       has_one :profile
    end
    class Profile < ActiveRecord::Base
       belongs_to :user
    end
    
    # create a new user record
    user = User.create!
    
    # create a new associated profile record (has_one)
    original_profile = user.create_profile!
    
    # attempt to replace the original profile with a new one
    user.create_profile! 
     => ActiveRecord::RecordNotSaved: Failed to remove the existing associated profile. The record failed to save after its foreign key was set to nil.
    

    In attempting to create a new profile, ActiveRecord tries to set the user_id of the original profile to nil, which fails the foreign key constraint for belongs_to records. I believe this is essentially what is happening when you try and attach a new file to your model using ActiveStorage... doing so tries to nullify the foreign key of the original attachment record, which will fail.

    The solution

    The solution for has_one relationships is to destroy the associated record before trying to create a new one (i.e. purging the attachment before trying to attach another one).

    user.avatar.purge # or user.avatar.purge_later
    user.avatar.attach(params[:file])
    

    Is this desired behavior?

    Whether or not ActiveStorage should automatically purge the original record when trying to attach a new one for has_one relationships is a different question best posed to the core team...

    IMO having it work consistently with all other has_one relationships makes sense, and it may be preferable to leave it up to the developer to be explicit about purging an original record before attaching a new one rather than doing it automatically (which may be a bit presumptuous).

    Resources: