ActiveRecord: Private or Readonly attributes? like attr_reader?

in a standard class, it is possible to allow reading of attributes while disallowing writing.

I know ActiveRecord has an "attr_readonly" deal but it doesn't stop you or throw an exception at the time of writing to the in-memory object, as I would like. It simply ignores the in-memory data, which would leave many on my staff scratching their heads as to why rails is countermanding their wishes. An exception thrown immediately would make it quite clear what's gone wrong.

Is this not a popular request? I've read that attr_readonly was itself a submitted patch. Would anyone be behind me if I made a patch for this idea? Would it be necessary?

Thanks. :slight_smile:

- - Jesse Thompson Webformix, Bend OR

There are probably a lot of ways to do this. Many of these ways are probably superior to this suggestion but one thought is to simply use metaprogramming to redefine attribute setter methods to throw an exception. For a single method,

def some_attribute=(value)   raise "You can't set this attribute. It is read-only" end

On a more meta level,

self.column_names.each do |column_name|   define_method("#{column_name}=")     raise "You can't set the attribute read-only '#{column_name}'."   end end

This could also be easily done that patches activerecord::base to contain a macro such as

attr_no_write :attribute1, :attribute2

and then simply use the metaprogramming shown above to throw it for those attributes. This could certainly be a plugin. It is also possible I totally missed a much better and more obvious way of achieving the same thing.

Hello Nathan, thanks for taking a crack at this.

I have already tried your first suggestion here. The trouble is that this pattern (def val=; raise ""; end) prevents the attribute from ever being written to, even from within the model itself. attr_reader and "private 'val='" approaches in core ruby prevent setting the attribute from outside of the class, but allow it from within the class. I really do need to allow control over the attribute from within the class. In the "initialize" method for example, or when counters internally increment, or cached values internally need to be updated.

If I have to I will look up how "attr_reader" is implemented and override that for ActiveRecord::Base and offer that as a plugin or a patch. But I just wanted to make sure that:

this isn't already somehow available a different way, and It's a popular enough option.

If this is not a popular pattern there must be a good reason; my view of the world may likely be out of whack. If it is a popular pattern I'm amazed it isn't already available. Solomon's adage "There is nothing new under the sun" really, really ought to apply here. If I have to write a new patch or plugin 3 hours into my first serious Ruby project, I've got to be overlooking something or leaving The Path somehow. I mean, who am I to find one of the most fundamental concepts from OOP 101 (private attribute setters) missing in the world's most popular web framework, built upon the most zealously object oriented language since SmallTalk? I've got to be looking at something wrongly here.

I've asked about this in irc with no more luck there. All I know is if I spend a day or two perfecting a patch or plugin, and only then does someone say "look, all you really should have done is (2 arbitrary lines of code)", I'll go clinically mad. Also, should I ever be found meddling in such deep affairs if nobody else seems interested in this feature?

Does my problem, and/or my paranoia about taking apart the engine to fix it, make any sense to anyone else here? :wink:

TIA for your wisdom!

- - Jesse Thompson Webformix, Bend OR

The trouble is that this pattern (def val=; raise ""; end) prevents the attribute from ever being written to, even from within the model itself.

Hi Jesse,

maybe you could use

def some_attribute=(val)   write_attribute(:some_attribute, val) unless *your readonly flag here* end

Let me know if this solved your problem.

Best Regards,

Pieter Visser, Illusoft

If you don't want users to be mass assigning the field, you can use attr_accessible and define all the fields that you DO want to be updated by the user. Any field not specified in this list will be unable to be assigned any value during mass assignation. If then later you decide somewhere your code that you do want to set the value, you can do:

@user.update_attribute("some_field", 1)