I've got a model in which attributes are allowed to be null, but when a null attribute is read I'd like to take a special action. In particular, I'd like to throw a certain exception. That is, something like
class MyModel < ActiveRecord::Base
def anAttr
read_attribute(:anAttr) or raise MyException(:anAttr)
end
end
that's all fine, but it means I have to hand-code the identical custom accessor for each attribute.
I thought I could override read_attribute, but my overridden read_attribute is never called.
Not sure why you'd need to do this, but alas:
def get(attr)
val = self.send(attr)
raise MyException unless val
val
end
#object.get(:name)
That's funny, we were looking into this same thing today. Check into attribute_method.rb which is where all the Rails logic for the attributes exists. You'll see a define_attribute_methods method which you should be able to override.
In the end, I think we're going to do this in a different way, but it was a helpful exercise.
Related
Developing in Rails 5.2.2.1. I want to define a "global" rescue handler for my model, so that I can catch NoMethodError and take appropriate action. I find that controllers can do this with rescue_from, but models cannot. Knowing that the Rails Developers are smart people ;) I figure there must be some Good Reason for this, but I'm still frustrated. Googling around, and I can't even find any examples of people asking how to do this, and other people either telling them how, or why they can't, or why they shouldn't want to. Maybe it's because rescue handlers can't return a value to the original caller?
Here's what I'm trying to do: I need to refactor my app so that what used to be a single model is now split into two (let's call them Orig and New). Briefly, I want to make it so that when an attribute getter method (say) is called against an Orig object, if that attribute has moved to New, then I can catch this error and call new.getter instead (understanding that Orig now belongs_to a New). This solution is inspired by my experience doing just this sort of thing with Perl5's AUTOLOAD feature.
Any ideas of how to get this done are much appreciated. Maybe I just have to define getters/setters for all the moved attributes individually.
Overide method_missing :) !?
You could try overriding the method_missing method. This could potentially cause confusing bugs, but overriding that method is definitely used to great effect in at least one gem that i know of.
I didn't want to call the class new because it is a reserved keyword and can be confusing. So I changed the class name to Upgraded.
This should get you started.
class Upgraded
def getter
puts "Congrats, it gets!"
end
end
class Original
def initialize
#new_instance = Upgraded.new
end
def method_missing(message, *args, &block)
if message == :attribute_getter
#new_instance.send(:getter, *args, &block)
else
super
end
end
def respond_to_missing?(method_name, *args)
method_name == :attribute_getter or super
end
end
c = Original.new
c.attribute_getter
You will have to change names of the getter and setter methods. Because you have a belongs_to association you can just use that.
Or you could try just using delegate_to
like #mu_is_too_short suggests, you could try something like this?
class Original < ApplicationRecord
belongs_to :upgraded
delegate :getter_method, :to => :upgraded
end
class Upgraded < ApplicationRecord
def getter_method
end
end
Apparently what I needed to know is the word "delegation". It seems there are a variety of ways to do this kind of thing in Ruby, and Rails, and I should have expected that Ruby's way of doing it would be cleaner, more elegant, and more evolved than Perl5. In particular, recent versions of Rails provide "delegate_missing_to", which appears to be precisely what I need for this use case.
So the goal is to turn for instance "ProductCustomer", which comes from the class, into "product customer".
I used to have this:
notification.notifiable.model_name.human.downcase
It didn't work out of course, since if the notifiable is nil it breaks. I don't
want to use try or something similar since it can be solved with using notifiable_type.
So now I changed to this:
notification.notifiable_type.split(/(?=[A-Z])/).join(' ').downcase
But this is way too complex to use every time in the view. So either I would like to define this as a view helper or using some ruby formatting method if there is a simple one.
Can somebody tell me what the Rails convention is in this case? If it's a helper, how does the method looks like and where should I put it?
Options:
Initializer
/your_app/config/initializers/my_initializer.rb
module MyModule
def human_model_name
self.class.to_s.tableize.singularize.humanize.downcase
end
end
ActiveRecord::Base.send(:include, MyModule)
Including MyModule in ActiveRecord::Base will add human_model_name in all ActiveRecord instances. So, you will be able to do...
user.human_model_name #=> user
notification.human_model_name #=> notification
notification.notifiable.human_model_name #=> product customer
any_active_record_instance.human_model_name #=> etc.
To avoid exceptions when notifiable is nil, you can use try method.
notification.try(:notifiable).try(:human_model_name)
A cleaner way can be use delegate
class Notification < ActiveRecord::Base
delegate :human_model_name, to: :notifiable, prefix: true, allow_nil: true
end
Then, you can do:
notification.notifiable_human_model_name # if notifiable is nil will return nil as result instead of an exception
A simple method in your Notification model
class Notification < ActiveRecord::Base
def human_notifable_name
return unless self.notifiable # to avoid exception with nil notifiable
self.notifiable.class.to_s.tableize.singularize.humanize.downcase
end
end
Then...
notification.human_notifable_name
View Helper (If you think this is a view related method only)
module ApplicationHelper # or NotificationHelper
def human_model_name(instance)
return unless instance # to avoid exception with nil instance
instance.class.to_s.tableize.singularize.humanize.downcase
end
end
Then, in your view...
<%= human_model_name(notification.notifiable) %>
Either option is fine. I would use one or the other depending on the case. In this case, I would use the first option. I think you are adding behaviour that can be useful in any model. I mean your method is not directly related with something about notifications. In a more generic way you want a method to return the class name of an ActiveRecord's instance. Today you want the model name of the notifiable ActiveRecord's instance. But, tomorrow you may want the model name of any ActiveRecord model.
To answer the question "Where should I put a method?" I suggest to break (without fear) a little bit the MVC pattern and read about:
Decorators, presenters and delegators
Services
(a little bit old, but you can get the idea)
"ProductCustomer".tableize.singularize.humanize.downcase
I use the readonly? function to mark my Invoice as immutable after they've been sent; for by InvoiceLines, I simply proxy the readonly? function to the Invoice.
A simplified example:
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
This works great, except that in one specific scenario I want to update an InvoiceLine regardless of the readonly? attribute.
Is there are way to do this?
I tried using save(validate: false), but this has no effect. I looked at persistence.rb in the AR source, and that seems to just do:
def create_or_update
raise ReadOnlyRecord if readonly?
...
end
Is there an obvious way to avoid this?
A (somewhat dirty) workaround that I might do in Python:
original = line.readonly?
line.readonly? = lambda: false
line.save()
line.readonly? = original
But this doesn't work in Ruby, since functions aren't first-class objects ...
You can very easily redefine a method in an instantiated object, but the syntax is definition rather than assignment. E.g. when making changes to a schema that required a tweak to an otherwise read-only object, I have been known to use this form:
line = InvoiceLine.last
def line.readonly?; false; end
Et voila, status overridden! What's actually happening is a definition of the readonly? method in the object's eigenclass, not its class. This is really grubbing around inside the guts of the object, though; outside of a schema change it's a serious code smell.
One crude alternative is forcing Rails to write an updated column directly to the database:
line.update_columns(description: "Compliments cost nothing", amount: 0)
and it's mass-destruction equivalent:
InvoiceLine.where(description: "Free Stuff Tuesday").update_all(amount: 0)
but again, neither should appear in production code outside of migrations and, very occasionally, some carefully written framework code. These two bypass all validation and other logic and risk leaving objects in inconsistent/invalid states. It's better to convey the need and behaviour explicitly in your model code & interactions somehow. You could write this:
class InvoiceLine < ActiveRecord::Base
attr_accessor :force_writeable
def readonly?
invoice.readonly? unless force_writeable
end
end
because then client code can say
line.force_writable = true
line.update(description: "new narrative line")
I still don't really like it because it still allows external code to dictate an internal behaviour, and it leaves the object with a state change that other code might trip over. Here's a slightly safer and more rubyish variant:
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
Client code can then write:
line.force_update do
line.update(description: "new description")
end
Finally, and this is probably the most precision mechanism, you can allow just certain attributes to change. You could do that in a before_save callback and throw an exception, but I quite like using this validation that relies on the ActiveRecord dirty attributes module:
class InvoiceLine < ActiveRecord::Base
validate :readonly_policy
def readonly_policy
if invoice.readonly?
(changed - ["description", "amount"]).each do |attr|
errors.add(attr, "is a read-only attribute")
end
end
end
end
I like this a lot; it puts all the domain knowledge in the model, it uses supported and built-in mechanisms, doesn't require any monkey-patching or metaprogramming, doesn't avoid other validations, and gives you nice error messages that can propagate all the way back to the view.
I ran into a similar problem with a single readonly field and worked around it using update_all.
It needs to be an ActiveRecord::Relation so it would be something like this...
Invoice.where(id: id).update_all("field1 = 'value1', field2 = 'value2'")
Here is an answer, but I don't like it. I would recommend to think twice about the design: If you make this data immutable, and you do need to mutate it, then there may be a design issue. Let aside any headache if the ORM and the datastore "differ".
One way is to use the meta programming facilities. Say you want to change the item_num of invoice_line1 to 123, you can proceed with:
invoice_line1.instance_variable_set(:#item_num, 123)
Note that the above will not work directly with ActiveRecord models' attributes, so it would need be adapted. But well, I would really advice to reconsider the design rather than falling for black magic.
Here's an elegant solution how to disallow modification generally but allow it if it is specifically requested:
In your model, add the following two methods:
def readonly?
return false if #bypass_readonly
return true # Replace true by your criteria if necessary
end
def bypass_readonly
#bypass_readonly=true
yield
#bypass_readonly=false
end
Under normal circumstances, your object is still readonly, so no risk of accidentally writing to a readonly object:
mymodel.save! # This raises a readonly error
However in privileged places where you are sure that you want to ignore the readonlyness, you can use:
mymodel.bypass_readonly do
# Set fields as you like
mymodel.save!
end
Everything inside the bypass_readonly block is now allowed despite readonly. Have fun!
This overrides the #readonly? method for one particular only, not affecting anything else:
line.define_singleton_method(:readonly?) { false }
readonly_attrs = described_class.readonly_attributes.dup
described_class.readonly_attributes.clear
# restore readonly rails constraint
described_class.readonly_attributes.merge(readonly_attrs)
This worked for us with Rails 7.
I have an instance variable in an active record class called hash_value. It's a serialized hash.
I want to display the hash as XML. Is it right to call hash_value.to_xml? Many nodes are numbers, and XML tags are not allowed to be only number (or start with a number).
I want to override the to_xml method of hash_value. I don't want to override on all hashes, just the hash that's in this record.
class ProductVersion < ActiveRecord::base
serialize :hash_value, Hash
def hash_value.to_xml
end
end
I tried the answer here redefining a single ruby method on a single instance with a lambda
but it doesn't seem to be applicable. I suspect because when I load the record, it creates a new hash_value object and thus the singleton adjustment on the original is moot.
Any thoughts would be great.
I know I could write a function hash_value_to_xml, but I'd rather avoid doing something like that.
Thanks to the first comment, I came up with a solution. Not a good one, but one that works. I'd love to see if there's a better way, because this one smells a bit.
class MyHash < Hash
def to_xml
1/0 #to see if it's run.
end
end
def hash_value
MyHash.new().merge( attributes['hash_value'] );
end
I would personally go for hash_value_to_xml route. But since you insist, here's an idea that might work (haven't tested that)
class ProductVersion < ActiveRecord::base
serialize :hash_value, Hash
alias_method :old_hash_value, :hash_value
def hash_value
h = old_hash_value
h.define_singleton_method(:to_xml) do
# your custom xml logic here
end
h
end
end
The idea is that you intercept value returned from hash_value and patch it on the fly.
By default, ActiveRecord takes all fields from the corresponding database table and creates public attributes for all of them.
I think that it's reasonable not to make all attributes in a model public. Even more, exposing attributes that are meant for internal use clutters the model's interface and violates the encapsulation principle.
So, is there a way to make some of the attributes literally private?
Or, maybe I should move on to some other ORM?
Jordini was most of the way there
Most of active_record happens in method_missing. If you define the method up front, it won't hit method_missing for that method, and use yours instead (effectively overwriting, but not really)
class YourModel < ActiveRecord::Base
private
def my_private_attribute
self[:my_private_attribute]
end
def my_private_attribute=(val)
write_attribute :my_private_attribute, val
end
end
Stumbled upon this recently. If you want private writing and reading and public reading something like this
class YourModel < ActiveRecord::Base
attr_reader :attribute
private
attr_accessor :attribute
end
seems to work fine for me. You can fiddle with attr_reader, attr_writer and attr_accessor to decide what should be exposed and what should be private.
well, you could always override the methods...
class YourModel < ActiveRecord::Base
private
def my_private_attribute
self[:my_private_attribute]
end
def my_private_attribute=(val)
self[:my_private_attribute] = val
end
end
For me methods from both Otto and jordinl are working fine and make rspec for object of Class to pass:
object.should_not respond_to :attribute
But when I use jordinl method, I have a deprecation message to not write to database directly, but use attr_writer instead.
UPDATE:
But the truly "right" metod to do it turns out to be simple. Thanks to Mladen Jablanović and Christopher Creutzig from here. To make predefined method private or protected... simply redefine it:
Class Class_name
private :method_name
protected :method_name_1
end
What's important, you needn't rewrite previously defined method's logic.
You can make an existing method private:
YourClass.send(:private, :your_method)
Making the setting private does generate ActiveRecord error.
I put access control code in the overwritten method of the public setter by checking the caller:
def my_private_attribute=(val)
if (caller.first.include?('active_record/base.rb') || caller.first.include?('app/models/myclass.rb'))
self[:my_private_attribute] = val
else
raise Exception.new("Cannot set read-only attribute.")
end
end
I don't think there is 100% reliable way to do this. It's also worth checking the most popular ways to access attributes:
http://www.davidverhasselt.com/set-attributes-in-activerecord/