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中不起作用,因为函数不是一流的对象......
您可以非常轻松地在实例化对象中重新定义方法,但语法是定义而不是赋值.例如,当对需要调整其他只读对象的模式进行更改时,我已知使用此表单:
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)
我很喜欢这个; 它将所有领域知识放在模型中,它使用支持和内置机制,不需要任何猴子修补或元编程,并为您提供可以一直传播回视图的好错误消息.
我在单个readonly字段中遇到了类似的问题,并使用update_all解决了该问题。
它必须是一个,ActiveRecord::Relation所以会是这样的...
Invoice.where(id: id).update_all("field1 = 'value1', field2 = 'value2'")
这是一个答案,但我不喜欢它。我建议对设计三思而后行:如果你使这些数据不可变,并且你确实需要改变它,那么可能存在设计问题。如果 ORM 和数据存储“不同”,那么就不用担心了。
一种方法是使用元编程工具。假设您想将item_numof更改invoice_line1为 123,您可以继续:
invoice_line1.instance_variable_set(:@item_num, 123)
Run Code Online (Sandbox Code Playgroud)
请注意,上述内容不能直接与 ActiveRecord 模型的属性一起使用,因此需要进行调整。但是,我真的建议重新考虑设计,而不是陷入黑魔法。