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)