【问题标题】:Why are Active Record callbacks bypassed on associated records?为什么在关联记录上绕过 Active Record 回调?
【发布时间】:2017-12-04 21:17:15
【问题描述】:

我正在使用 Rails 5 构建一个 Web 应用程序,当我在关联记录上定义了非持久属性(使用 attr_accessor)时,我遇到了通过父记录更新关联记录的问题。具体来说,我让用户以某种方式在子记录上提供 non-persistent 属性,并根据这些属性的值,将值分配给 persistent 中的属性before_save 回调。问题是子记录不会保存到数据库中(因此不会调用保存回调),除非通过父项更改子记录的持久属性。

我在几种不同的情况下都遇到过这个问题,但这里的(简化的)示例涉及使用Paperclip Gem 处理客户端浏览器上传到AWS S3 的图像。

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 摘录

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

我编写了 javascript,可以直接从客户端的浏览器将图像上传到 S3 存储桶中的临时文件夹,并将 s3_key 添加到更新表单中,以便可以在服务器端识别和处理图像(请参阅 @987654329证书.rb 中的@ 方法)。问题是证书永远不会更新,除非更新参数中的实际数据库属性已更改。

为什么会发生这种情况,我该如何解决?

示例参数

{
  ...,
  certificates_attributes: [
    {id: '1', _destroy: '0', s3_key: 'tmp/uploads/certificates/.../photo.jpg'},
    {id: '2', _destroy: '0', s3_key: 'tmp/uploads/certificates/.../photo2.jpg'}
  ]
}

宝石版本

rails-5.0.0
activerecord-5.0.0
paperclip-5.1.0
aws-sdk-2.10.0

编辑

我可以通过在 s3_key 的 setter 方法中调用 fetch_digitized_proof_from_s3 来完成证书更新(并删除 before_save 回调):

# app/models/certificate.rb
def s3_key=(key)
  @s3_key = key
  self.fetch_digitized_proof_from_s3
end

这会触发相关证书正确保存(我认为这是因为digitized_proof,这是一个持久属性,通过调用fetch_digitized_proof_from_s3 更新)。这可行,但我仍然宁愿在保存记录时从 S3 获取图像。

【问题讨论】:

  • 也许改用before_validation?如果记录无效,保存调用将不会运行。
  • @max 看来before_validation 正在产生相同的行为。当 s3_key 是唯一修改的属性时,回调似乎不会触发。

标签: ruby-on-rails ruby activerecord amazon-s3 paperclip


【解决方案1】:

除非在 ActiveModel::Dirty 中注册更改,否则相关记录似乎不会更新。设置非持久属性时不会发生这种情况:

cert = Certificate.find(1)
cert.s3_key = 'tmp/uploads/certificates/...'
cert.changed? # => false

将以下方法添加到 Certificate.rb 会产生所需的行为:

def s3_key=(key)
  attribute_will_change!('s3_key') unless s3_key == key
  @s3_key = key
end

现在结果是

cert = Certificate.find(1)
cert.s3_key = 'tmp/uploads/certificates/...'
cert.changed? # => true

当设置s3_key 时,相关记录会相应更新。

【讨论】:

    猜你喜欢
    • 2012-01-01
    • 2012-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-16
    • 2015-05-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多