只读旁路?保存ActiveRecord时

Mar*_*oij 3 ruby activerecord ruby-on-rails

我使用该readonly?函数Invoice在发送之后将其标记为不可变; 对于by InvoiceLine,我只是将readonly?函数代理到Invoice.

一个简化的例子:

class Invoice < ActiveRecord::Base
  has_many :invoice_lines
  def readonly?; self.invoice_sent?  end
end

def InvoiceLine < ActiveRecord::Base
  def readonly?; self.invoice.readonly?  end
end
Run Code Online (Sandbox Code Playgroud)

这很好用,除了在一个特定场景中我想要更新一个InvoiceLine而不管readonly?属性.

有办法做到这一点吗?

我尝试过使用save(validate: false),但这没有效果.我persistence.rb在AR源代码中查看过,这似乎就是:

def create_or_update
  raise ReadOnlyRecord if readonly?
  ...
end
Run Code Online (Sandbox Code Playgroud)

有没有明显的方法可以避免这种情况?

我可能在Python中做的一个(有点脏)的解决方法:

original = line.readonly?
line.readonly? = lambda: false
line.save()
line.readonly? = original
Run Code Online (Sandbox Code Playgroud)

但这在Ruby中不起作用,因为函数不是一流的对象......

ino*_*tus 9

您可以非常轻松地在实例化对象中重新定义方法,但语法是定义而不是赋值.例如,当对需要调整其他只读对象的模式进行更改时,我已知使用此表单:

line = InvoiceLine.last
def line.readonly?; false; end
Run Code Online (Sandbox Code Playgroud)

Et voila,状态被覆盖!实际发生的是readonly?对象的本征类中的方法的定义,而不是它的类.然而,这实际上是在物体的内部吞噬; 在架构之外改变它是一个严重的代码味道.

因此,最好以某种方式在模型代码中明确地传达需求.你可以这样写:

line.update_columns(description: "Compliments cost nothing", amount: 0)
Run Code Online (Sandbox Code Playgroud)

因为那时客户端代码可以说

InvoiceLine.where(description: "Free Stuff Tuesday").update_all(amount: 0)
Run Code Online (Sandbox Code Playgroud)

我仍然不喜欢它,因为它仍然允许外部代码指示内部行为,并且它使对象具有其他代码可能会跳过的状态更改.这是一个更安全,更红宝石的变体:

class InvoiceLine < ActiveRecord::Base
  attr_accessor :force_writeable

  def readonly?
    invoice.readonly? unless force_writeable
  end
end
Run Code Online (Sandbox Code Playgroud)

然后客户端代码可以写:

line.force_writable = true
line.update(description: "new narrative line")
Run Code Online (Sandbox Code Playgroud)

最后,这可能是最精确的机制,您可以只允许某些属性进行更改.您可以在before_save回调中执行此操作并抛出异常,但我非常喜欢使用依赖于ActiveRecord脏属性模块的验证:

class InvoiceLine < ActiveRecord::Base
  def force_update(&block)
    saved_force_update = @_force_update
    @_force_update = true
    result = yield
    @_force_update = saved_force_update
    result
  end

  def readonly?
    invoice.readonly? unless @_force_update
  end
end
Run Code Online (Sandbox Code Playgroud)

我很喜欢这个; 它将所有领域知识放在模型中,它使用支持和内置机制,不需要任何猴子修补或元编程,并为您提供可以一直传播回视图的好错误消息.


Der*_*rek 5

我在单个readonly字段中遇到了类似的问题,并使用update_all解决了该问题。

它必须是一个,ActiveRecord::Relation所以会是这样的...

Invoice.where(id: id).update_all("field1 = 'value1', field2 = 'value2'")


Eri*_*ton 2

这是一个答案,但我不喜欢它。我建议对设计三思而后行:如果你使这些数据不可变,并且你确实需要改变它,那么可能存在设计问题。如果 ORM 和数据存储“不同”,那么就不用担心了。


一种方法是使用元编程工具。假设您想将item_numof更改invoice_line1为 123,您可以继续:

invoice_line1.instance_variable_set(:@item_num, 123)
Run Code Online (Sandbox Code Playgroud)

请注意,上述内容不能直接与 ActiveRecord 模型的属性一起使用,因此需要进行调整。但是,我真的建议重新考虑设计,而不是陷入黑魔法。