How to update attachment in ActiveStorage (Rails 5.2)
Asked Answered
D

4

18

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?

Demanding answered 24/8, 2017 at 20:3 Comment(0)
V
26

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:

Vestment answered 29/8, 2017 at 14:7 Comment(5)
Thank you for the detailed answer.Demanding
This commit from the same day of this answer fixes this issue: github.com/rails/rails/commit/…Sean
Carlos, I'm getting the same error I have a User that has a profile and the profile has_one_attach :avatar However, I get the same error. I'm doing the create method this way? def create @profile = current_user.create_profile(profile_params) endRamsay
https://stackoverflow.com/questions/52469191/activemodelunknownattributeerror-unknown-attribute-avatar-activestorageRamsay
Beautifully explained sir. tySavannasavannah
S
8

You can call purge_later before attach when using has_one_attached:

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

Update

Rails now purges previous attachment automatically (since Aug 29th).

Sean answered 29/8, 2017 at 7:59 Comment(0)
D
1

I have the same problem the with image saving. I hope this will help

class User < ApplicationRecord
  has_one_attached :avatar
end

let look at the form and controller

= simple_form_for(@user) do |f|
  = f.error_notification
  .form-inputs
    = f.input :name
    = f.input :email
    = f.input :avatar, as: :file

  .form-actions
    = f.button :submit

controllers/posts_controller.rb

def create
    @user = User.new(post_params)
    @user.avatar.attach(params[:post][:avatar])
    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'Post was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end
Discant answered 2/2, 2018 at 10:12 Comment(3)
but what if I'm trying to do it this way def create @profile = current_user.create_profile(profile_params) endRamsay
So I assume you have a method in user.rb model called create_profile and you already paste the params, in the method you do for example: self.profile.attach(params[:post][:profile]). And look at the current_user if it is the helper method of the devise gem or the actual user object.Discant
#52469691 Here is the post for the issue I'm having. Which I belive is because of the way I'm passing the attached fileRamsay
T
0

If you are using nested attributes and no other attribute has changed in child model, Rails won't automatically detect changes to your attachment. To do so, you must override the changed_for_autosave? method :

def Child
  belongs_to :parent
  has_one_attached :attachment

  # Magic happens here
  def changed_for_autosave?
    super || attachment.changed_for_autosave?
  end
end

def Parent
  has_many :children

  accepts_nested_attributes_for :children
end

This also triggers the child models callbacks (before_save, ...) on parent save. I don't know if this approach works without nested attributes, but I suppose it does. In general, this kind of logic shouldn't be handled inside controllers as many suggested (in my opinion).

It took me a while to figure, I hope this helps. Cheers !

Tumble answered 4/5, 2020 at 15:31 Comment(1)
no need to add changed_for_autosave? for rails >= 6.0.3 Fix #37701 Autosave association bug with ActiveStorage::Attachments #37786Chryselephantine

© 2022 - 2024 — McMap. All rights reserved.