ActiveStorage: How to update an attachment?

We currently switched to Rails 5.2 to get the awesome functionalities of ActiveStorage.
Up until now I was able to upload, delete (purge), and show files as expected - but I am not quite sure on how to update an attachment the right way.

Here are the steps to reproduce:

user.avatar.attached?

=> false

user.avatar.attach(io: File.open("~/avatar.png"), content_type: “image/png”, filename: “avatar”)

=> #<ActiveStorage::Attachment id: 2, name: “avatar”, record_type: “User”, …

user.avatar.attached?

=> true

user.avatar.attach(io: File.open("~/avatar2.png"), content_type: “image/png”, filename: “avatar2”)

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

user.avatar.purge

=> nil

user.avatar.attached?

=> false

user.avatar.attach(io: File.open("~/avatar2.png"), content_type: “image/png”, filename: “avatar2”)

=> #<ActiveStorage::Attachment id: 2, name: “avatar2”, record_type: “User”, …

I don’t think it should be necessary to purge the first attachment to attach a new one. So how can this be achieved properly?

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

user = User.create!
original_profile = user.create_profile!
user.create_profile! # attempt to replace the original profile with a new one
=> 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 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).

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: