如何在ActiveStorage中更新附件(Rails 5.2)

18

最近我将我的项目升级到了最新的Rails版本(5.2),以使用ActiveStorage - 这是一个处理上传附件到云服务,如AWS S3、Google Cloud等的库。

几乎所有的功能都正常工作。我可以上传并附加图片。

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

并使用以下方式接收它

user.avatar.service_url

但是现在我想更改/更新用户的头像。我认为我可以运行以下命令:
user.avatar.attach(params[:file])

再试一次。但是这会抛出一个错误:

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

这句话是什么意思?我该如何更换用户头像?

4个回答

26

错误的原因

此错误是由您的模型和附件记录之间的has_one关联引起的。它发生的原因是尝试用新附件替换原始附件将使原始附件变成孤儿,并导致其无法满足belongs_to关联的外键约束。这是所有ActiveRecord has_one关系的行为(即不仅限于ActiveStorage)。

类比例子

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.

在尝试创建新配置文件时,ActiveRecord 尝试将原始配置文件的 user_id 设置为 nil,这会导致 belongs_to 记录的外键约束失败。我认为当您尝试使用 ActiveStorage 将新文件附加到模型时,本质上就是这样的情况... 这样做会尝试使原始附件记录的外键为空,这将失败。

解决方案

has_one 关系的解决方案是,在尝试创建新记录之前销毁关联记录(即在尝试附加另一个记录之前清除附件)。

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

这是期望的行为吗?

关于ActiveStorage是否应该在尝试附加新记录以替换has_one关系中的原始记录时自动清除原始记录,这是一个不同的问题,最好向核心团队提出......

我认为,使其与所有其他has_one关系一致工作是有意义的,并且可能更倾向于让开发人员在附加新记录之前显式清除原始记录,而不是自动执行此操作(这可能有点傲慢)。

资源:


1
感谢你提供如此详细的答案。 - zarathustra
3
这个回答的同一天提交的更改解决了这个问题:https://github.com/rails/rails/commit/656ee8b2dd1b06541f03ea19302d3529085f5139#diff-220c5602a1721b74f7ee61a8e09699da - ybart
Carlos,我遇到了相同的错误。我有一个用户,该用户具有个人资料,而个人资料具有一个 :avatarhas_one_attach。但是,我仍然遇到了相同的错误。我是这样编写 create 方法的吗?def create @profile = current_user.create_profile(profile_params) end - Steven Aguilar
1
https://stackoverflow.com/questions/52469191/activemodelunknownattributeerror-unknown-attribute-avatar-activestorage - Steven Aguilar
1
讲解得非常清晰易懂,谢谢。 - ARK

8

1

我有同样的问题,即图像保存。希望这能帮到你。

class User < ApplicationRecord
  has_one_attached :avatar
end

让我们看一下表单和控制器。
= 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

但是如果我想用这种方式来做怎么办呢?def create @profile = current_user.create_profile(profile_params) end - Steven Aguilar
所以我假设你在 user.rb 模型中有一个名为 create_profile 的方法,并且你已经粘贴了参数,在该方法中,例如:self.profile.attach(params[:post][:profile])。并查看 current_user 是否是 devise gem 的帮助程序方法或实际的 user 对象。 - Kiry Meas
以下是我遇到的问题的帖子。 我认为这是因为我传递附加文件的方式不正确导致的。https://stackoverflow.com/questions/52469191/activemodelunknownattributeerror-unknown-attribute-avatar-activestorage - Steven Aguilar

0

如果您正在使用嵌套属性,并且子模型中没有其他属性更改,则Rails不会自动检测附件的更改。为了这样做,您必须重写changed_for_autosave?方法:

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

这也会在父模型保存时触发子模型的回调函数(before_save,...)。 我不知道这种方法是否可以在没有嵌套属性的情况下工作,但我认为它可以。 一般来说,这种逻辑不应该像许多人建议的那样在控制器内处理(在我看来)。

我花了一些时间才弄清楚,希望这可以帮到你。干杯!


1
不需要为Rails >= 6.0.3添加changed_for_autosave? 修复#37701与ActiveStorage :: Attachments #37786的自动保存关联错误 - askrynnikov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接