为什么在关联记录上绕过Active Record回调?

时间:2017-06-30 20:36:23

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

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

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

应用/模型/ dog.rb

class Dog < ApplicationRecord
  has_many :certificates, :dependent => :destroy
  accepts_nested_attributes_for :certificates, :allow_destroy => true
end

应用/模型/ 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添加到更新表单中,以便可以在服务器端识别和处理图像(请参阅certificate.rb中的fetch_digitized_proof_from_s3方法。问题是除非更新参数中的实际数据库属性已更改,否则永远不会更新证书。

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

示例参数

{
  ...,
  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

修改

我可以通过在fetch_digitized_proof_from_s3的setter方法中调用s3_key来完成证书更新(并删除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获取图像。

1 个答案:

答案 0 :(得分: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时相关记录会相应更新。