Forum: Ruby on Rails Big hole in my understanding of RoR

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Mark C. (Guest)
on 2007-02-02 16:32
I'm a Java refugee, who is a bit of a Ruby and Rails noob, but I'm
certainly enjoying myself and have had a bit of success in creating a
few RoR bits and pieces.. however, I think I must be missing something.

I just get the feeling that, although I've read through a couple of good
books, there is something quite basic in RoR that I'm just not
'getting'. So, here is a (mildly contrived) example of the sort of thing
I've been trying to do - maybe some experts out there could
metaphorically slap me on the back of the head and say "Duh!"

Firstly, I have a simple model:
    create_table :uris do |t|
      t.column :loc, :string, :null => false
    end
    create_table :links do |t|
      t.column :uri_id, :integer, :null => false
      t.column :text, :string, :null => false
      t.column :color, :string
    end

I've then used generate scaffold_resource to create the usual default
files, to which, I've made the following changes

uri.rb:
class Uri < ActiveRecord::Base
  has_many :links

  def to_s
    loc
  end
end

link.rb:
class Link < ActiveRecord::Base
  belongs_to :uri
end

link_controller.rb:
  def update
    @link = Link.find(params[:id])
    @uri = Uri.new(:loc=>params[:uri][:loc])
    @link.uri = @uri
    ...

  def create
    @link = Link.new(params[:link])
    @uri = Uri.new(:loc=>params[:uri][:loc])
    @link.uri = @uri
    ... # (the rest is the same as the auto-generated code)

views/links/new.rhtml:
...
<% form_for(:link, :url => links_path) do |f| %>
  <p>Text: <%= f.text_field :text %></p>
      <% fields_for :uri do |u| %>
        <p>URL: <%= u.text_field :loc %></p>
      <% end %>
  <p>Color: <%= f.text_field :color %></p>
  <p><%= submit_tag "Create" %></p>
<% end %>
...

views/uris/new.rhtml:
...
<% form_for(:uri, :url => uris_path) do |f| %>
  <p>URI Loc: <%= f.text_field :loc %></p>
  <p><%= submit_tag "Create" %></p>
<% end %>
...


This seems to be working OK, although I'm fairly sure I've not gone
about it quite the right way (in terms of the best way of writing the
views and controller to manipulate the Uri model and it's relationship
with Link).
If anyone has any comments on that, I'm all ears! But anyway, at least
it works...!
Incidentally, my Uri view *didn't* work when I originally named the Uri
model Url... it failed at the line <% form_for(:url, :url => links_path)
do |f| %>. I'm guess some sort of naming conflict.. any ideas what I
should have done on that, apart from changing the name of my model?

What I'm really struggling with, though, is this: I want to have default
values for my attributes. That is, values that a new Link object would
default to when it is instantiated. These would show up on the form when
a user asks to create a new object.
For a while, I tried using the before_create filter, but had no joy
until I realised that this inserts the default values if the attributes
are empty before saving the record to the database, which is too late
for me...

So, the most obvious place, I thought, was the initialize methods:
link.rb:
  def initialize
    super
    @color="#999999"
    @text="default text"
  end

This doesn't work. I've no idea why, it looks like it should work. I'm
setting the instance variables to a default value, yet when the "new"
form is displayed, the fields are blank. :(
I played around with a couple more permutations of this, but to no
avail...
The best that happens is that a) no default values are displayed, and b)
I get "wrong number of arguments (1 for 0)" when the create method is
called. Bah!

My next effort was to add my own reader methods:
link.rb:
  def color
    @color || "#999999"
  end

  def text
    @text || "default text"
  end

No joy here either. Again, it looks to me like these should work... I
don't understand why they don't. They system appears to be acting as
though these variables aren't used, and/or these methods aren't called,
but the way I understand everything I've read says that they are! I'm
*sure* they are - I've seen "password" and "password=" methods
overwritten in several different examples so that a hashed password can
be created and saved to the DB.
So, my next thought is that maybe whatever ActiveRecord sets @color to
does not evaluate to false, so my code always returns @color, and never
"#9999999".
But:
  def color
    "#999999"
  end

does not work either! Argh! In fact, now the following happens: I try to
create a new link. The form is displayed with no defaults. I enter some
values, and the object seems to be created properly. However, the value
"#999999" is displayed when I use the "show" view, and the value I
actually entered is displayed when I display the "edit" view.

OK, so IIRC, my next attempt was using something along the lines of
<p>Text: <%= f.text_field :text, :value=>"default text" %></p>
Which looked promising until I wanted to use the same _partial files for
both the edit and create methods... in which case, "default text" shows
up even when editing an object that already has a value for that
attribute.

After much googling, I came across another idea. For some reason not
clear to me, this should work:
link.rb:
def text
    self[:text] or "default text"
  end

It doesn't seem to work for me either. I'm not sure why it would, but
the blog I read it on seemed to think it did work, and so did the
comments on the blog, so I've no idea what I'm doing wrong.

Ho hum... My next idea is to set these default values in the controller.
This seems wrong to me - default values for a model ought to be defined
in the model, shouldn't they? But by now, I just want to get the thing
working even if its not where I think it ought to be!
so, my next attempt is:
link_controller.rb
  def new
    @link = Link.new
    @link.text='link_controller default text'
    @link.color='#ffffff'
  end
Yay! That worked. It's not, IMO, the right place to put the code, but it
works, so I'm putting up with it for now...

But how do I get a default value for the Uri?
I try adding this line to my "new" method:
@link.uri = Uri.new(:loc=>'www.example.com/example.html')

It doesn't work. At this point, I'm all out of ideas, and this is where
I decided that my understanding just has a big whole in it that needs
filling, so I've written this long post!

Whatever it is that I'm not 'getting', I think its causing me other
problems too. For example, if have a list of languages, and want to have
a select box to assign a language to a link, I'd assume that at some
point the "language_id=" method would get called, so I write:
  def language_id=(lid)
    self.language=Language.find(lid)
  end
or
  def language_id=(lid)
    @language=Language.find(lid)
  end
or
  def language_id=(lid)
    language=lid
  end
or any number of permutations of this sort of thing, none of which are
wiping the scowl off my face!

---

Thanks for taking the time to read down this far in the post, and any
advice is much appreciated!
Andrew F. (Guest)
on 2007-02-02 18:00
(Received via mailing list)
Hi,

After skim reading your post, is your question "How do I set default
values for ActiveRecord?". Which I find a bit more concise.

If so, a quick Google search indicates that you have merely used the
wrong way to access the attributes, you should use their accessor as I
don't believe they are stored in "@" variables.

See here:
http://made-of-stone.blogspot.com/2007/01/default-...

Ignore me if I've missed the point of your post.

Regards,
Andrew
Mark C. (Guest)
on 2007-02-02 18:57
Andrew F. wrote:
> Hi,
>
> After skim reading your post, is your question "How do I set default
> values for ActiveRecord?". Which I find a bit more concise.
>
> If so, a quick Google search indicates that you have merely used the
> wrong way to access the attributes, you should use their accessor as I
> don't believe they are stored in "@" variables.
>
> See here:
> http://made-of-stone.blogspot.com/2007/01/default-...
>
> Ignore me if I've missed the point of your post.
>
> Regards,
> Andrew

Thanks, that's helped me a bit.. There were a whole bunch of questions
up there, but that was certainly one of them :)
I still don't understand why the things I tried don't work. Why, for
example, does it appear that either implementation of the method:
  def text
    @text || "default text"
  end
or
  def text
    self[:text] or "default text"
  end
don't appear to be called by the form builders? If @text isn't used by
ActiveRecord, I'd have thought the first one would at least return the
default text, and if ActiveRecord does use a hash, then I'd have thought
the second one does work...

From your post, I think I've made a bit of progress, so thank you for
that :)

I can understand that I should use accessor methods instead of @
variables. That makes sense and seems like good practice whatever models
you are working with, since you can't relying on the underlying
implementation. However...
I did this:
link.rb:
  def initialize (params = nil)
    super
    self.text="default text from init" unless self.text
    self.color="#999999" unless self.color
  end

and the "new" form now provides a default for color, but not for text.
This just makes me more confused...!  And, I still haven't got this
working with the Uri relationship!
Robert W. (Guest)
on 2007-02-02 19:57
(Received via mailing list)
>   def initialize (params = nil)
>     super
>     self.text="default text from init" unless self.text
>     self.color="#999999" unless self.color
>   end
>
> and the "new" form now provides a default for color, but not for text.
> This just makes me more confused...!  And, I still haven't got this
> working with the Uri relationship!

I'm not sure if this has any bearing on your particular problem, but
"text" may no be a very "safe" column name to use.  I have had issues
in the past with MySQL when using keywords as column names.  It's best
to use column names that are not database specific keywords.
Robert W. (Guest)
on 2007-02-02 21:03
(Received via mailing list)
I've put some thought into this and have done some experimenting and
thought I would bring up a few points of discussion:

It seems to me there are multiple definitions of a "default value."

1. Database/Model level defaults:
In relation to a model object it would be values that would define the
"natural state" of an attribute.  Take for example an attribute
access_level on a User object.  A natural default state may be a value
representing a "regular" user (say 1 = "Regular user").

This type of default value can be provided right in your database
migration like:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :created_at, :datetime  #<< Notice that Rails will
automatically take care of the default value
      t.column :first_name, :string
      t.column :last_name, :string
      t.column :access_level, :integer, :default => 1  #<< 1 = Regular
user
    end
  end

  def self.down
    drop_table :users
  end
end

It is also possible (although it seems unnecessary given the above) to
set these types of defaults in the model class like:

class User < ActiveRecord::Base
  def initialize(params = nil)
    super
    self.access_level ||= 1
  end
end

In other cases the "default value" may actually be related more to a
particular view.

I recently ran into an example like this:  I had a event scheduler
that I wanted a default value for the current time to be set on the
datatime selection control.  In my opinion this is not a model level
default value.  To create an event, expecting people to subscribe to
the event later, it is not the "natural state" of the Event object to
have a start_time = Time.now.  However, it is convenient to initialize
the form's control to default to the current date and time to make the
user's selection process easier.

It just happens that the built-in time widgets do this (I think so
anyway).  But in any case the initial state of the model object's
start_time attribute to be nil and not Time.now.

So assuming that this behavior is not built into the form's control
object Rails provides a convenient way to construct a new object with
some attributes initialized that can be set from with the controller
like:

  # GET /people/new
  def new
    @person = User.new(access_level => 1)
  end

Maybe some purist MVC guys would say that is breaking the MVC design
pattern, and maybe it is, but sometimes the convenience is so great
that it's worth bending the rules a bit.  However, this is similar to
many "factory constructor" design patterns that I often see in use.

It would be more "pure" if the form's control were extended to provide
a default option.

Something like:
  <p>
    <b>Access Level</b><br />
    <%= f.text_field :access_level, :options => {:default => 1 }%>   #
DON'T do this it's just made up to illustrate a point
  </p>
Michael K. (Guest)
on 2007-02-02 21:20
(Received via mailing list)
I recently ran across the problem of needing to initialize a model's
relation on creation and used the 'faux accessor' solution because
you really shouldn't be overriding an AR initialize method.

Josh outlines the technique here:

http://blog.hasmanythrough.com/2007/1/22/using-fau...
initialize-values

-Michael
http://javathehutt.blogspot.com
Andrew F. (Guest)
on 2007-02-02 21:40
(Received via mailing list)
>   def text
>     self[:text] or "default text"
>   end
> don't appear to be called by the form builders? If @text isn't used by
> ActiveRecord, I'd have thought the first one would at least return the
> default text, and if ActiveRecord does use a hash, then I'd have thought
> the second one does work...

I think the form builders use the "attribute_before_type_cast"
accessor methods. Actually I tried submitting a patch for it: http://
dev.rubyonrails.org/ticket/5427


>     self.text="default text from init" unless self.text
>     self.color="#999999" unless self.color
>   end
Just a guess, maybe self.text.empty?

Best regards,
Andrew
Mark C. (Guest)
on 2007-02-03 01:35
Andrew F. wrote:
>>   def text
>>     self[:text] or "default text"
>>   end
>> don't appear to be called by the form builders? If @text isn't used by
>> ActiveRecord, I'd have thought the first one would at least return the
>> default text, and if ActiveRecord does use a hash, then I'd have thought
>> the second one does work...
>
> I think the form builders use the "attribute_before_type_cast"
> accessor methods. Actually I tried submitting a patch for it: http://
> dev.rubyonrails.org/ticket/5427

Ah, that's interesting. I wonder why they would do that?

>>     self.text="default text from init" unless self.text
>>     self.color="#999999" unless self.color
>>   end
> Just a guess, maybe self.text.empty?

That did the trick. Thanks! I did try Robert's idea of changing the
attribute name , but it didn't work in this case.

Thanks for all the contributions above from everyone, I've got lots to
think about now :)
Wes G. (Guest)
on 2007-02-03 10:52
Mark C. wrote:

Dude, don't feel bad.  As a noob last March coming from J2EE-land, I,
like you, foolishly thought that ActiveRecord::Base descendants behaved
like _normal_ objects.  But, alas, they don't.  And as far as I can
tell, we all go through the same fire to discover these facts...

To summarize for you, and hopefully, the next guy who runs into this:

1)
> So, the most obvious place, I thought, was the initialize methods:
> link.rb:
>   def initialize
>     super
>     @color="#999999"
>     @text="default text"
>   end

You know the answer to this is because you need the (params = nil).  So:

def initialize(params = nil)
  super
  self.color = "#999999"
  self.text = "default text"
end

DO not that the reason that the above method works is that initialize
only get called on an ActiveRecord object when "new" is invoked, but
never after it is saved (on a "find").  The point being that AR::Base
descendants are not instantiated like _normal_ Ruby objects (which
always call new and hence initialize).

But wait, there's more - the _recommended_ approach to handling
attribute initialization is to use "after_initialize".  But wait,
"after_initialize" gets called after a Obj.new _and_ after a Obj.find,
so you have to check and see if it's a new object so you know when to
initialize it (note the if new_record? statement to guard against
re-initializing an already saved object):

def after_initialize
  if self.new_record?
    self.color = "#999999"
    self.text = "default text"
  end
end

But wait, there's another wrinkle - if you ever _clone_ this object,
then after_initialize gets called on the clone target and thus your
initialization would overwrite any attribute values in the original
object (see Rails trac ticket: http://dev.rubyonrails.org/ticket/7191),
so the best way to handle this is:

def after_initialize
  if self.new_record?
    self.color ||= "#999999"
    self.text ||= "default text"
  end
end

Crazy, huh?

2)
> My next effort was to add my own reader methods:
> link.rb:
>   def color
>     @color || "#999999"
>   end
>
>   def text
>     @text || "default text"
>   end
>

Form helpers consult the attribute hash in the AR::Base object
(@attributes) _directly_, bypassing any overridden getters.  The
reasoning is that if your overridden getter coerces your attribute value
(from the hash) to another type, then it would appear that a text field
that you entered as a string would "disappear", and be replaced with
whatever the overridden getter returned after doing something to the
attribute from the attribute hash.  I think an example would be a text
field that represents an integer, and the getter method does a to_i on
the value from the attribute hash.  If you type text into that field, it
would turn from "blah" to "0" on a re-render of the page.  I don't 100%
get it (maybe 88%), but all I know is that bypassing the getter method
is confusing for those of us who expect the ability to overload methods
with ease ;).  Just one of those things.

3)
> OK, so IIRC, my next attempt was using something along the lines of
> <p>Text: <%= f.text_field :text, :value=>"default text" %></p>
> Which looked promising until I wanted to use the same _partial files for
> both the edit and create methods... in which case, "default text" shows
> up even when editing an object that already has a value for that
> attribute.

This behavior makes sense, since the approach above is just hard-coding
"default text" as a value.  As you now probably know, you would have to
lose the :value and just make sure that the field "text" was initialized
properly.

4)
> After much googling, I came across another idea. For some reason not
> clear to me, this should work:
> link.rb:
> def text
>     self[:text] or "default text"
>   end

Same as #2, this method is not consulted when the page renders.

> But how do I get a default value for the Uri?
> I try adding this line to my "new" method:
> @link.uri = Uri.new(:loc=>'www.example.com/example.html')
>
> It doesn't work. At this point, I'm all out of ideas, and this is where
> I decided that my understanding just has a big whole in it that needs
> filling, so I've written this long post!

Read through all of the API docs. for
ActiveRecord::Associations::ClassMethods
and this will clear up a lot of confusion about how to properly
create/relate association objects.

I believe that either
@link.build_uri = Uri.new(:loc => ...)

OR

@link.uris.build(:loc => ...)

will work in this case.

Wes
Wes G. (Guest)
on 2007-02-03 10:54
Mild corrections - it's late:

Wes G. wrote:
> DO note that the reason that the above method works is that initialize
> only gets called on an ActiveRecord object once when you invoke new, but
> never after it is saved (on a "find").  A find causes an "allocate" followed by some 
internal AR::Base initialization.  A find does not create an object via "new".
Mark C. (Guest)
on 2007-02-03 18:59
Wes G. wrote:

> To summarize for you, and hopefully, the next guy who runs into this:
> ... <plus lots more> ...

Brilliant! Thanks for posting this, it's helped me lots!
Ben M. (Guest)
on 2007-02-03 23:52
(Received via mailing list)
Great post... dragging this one to my "saved" folder. :-)

b
Wes G. (Guest)
on 2007-02-04 08:36
Wes G. wrote:
> Form helpers consult the attribute hash in the AR::Base object
> (@attributes) _directly_, bypassing any overridden getters.  The
> reasoning is that if your overridden getter coerces your attribute value
> (from the hash) to another type, then it would appear that a text field
> that you entered as a string would "disappear", and be replaced with
> whatever the overridden getter returned after doing something to the
> attribute from the attribute hash.  I think an example would be a text
> field that represents an integer, and the getter method does a to_i on
> the value from the attribute hash.  If you type text into that field, it
> would turn from "blah" to "0" on a re-render of the page.  I don't 100%
> get it (maybe 88%), but all I know is that bypassing the getter method
> is confusing for those of us who expect the ability to overload methods
> with ease ;).  Just one of those things.

I misspoke here.  In fact, it's not concern about _overridden_ getters
but just about the attribute getters (readers) in general.  The data
type for a given column in the table may not be String, and a cast is
performed from String (in the attributes hash) into the appropriate
column type when an attribute is set.  If the cast would coerce an
inappropriate different value (think String -> Integer becomes 0 if the
string value is not composed of digits) for the attribute than what was
attempted to be set (what was entered into a form), and if you pull the
attribute value through the reader, then you might get unexpected
attribute values back when submitting an incorrect form entry.

So that's why the reader methods are not consulted to fill in form
values.  It's because the attributes hash is basically used as a "form
model" object to mediate between a form and the database.

You could argue that such a "form model" object should be exposed as
it's own object and data should be marshalled/unmarshalled between it
and the AR object, but that kind of interaction is hidden _inside_ the
AR object.

Wes
Andy K. (Guest)
on 2007-02-05 18:41
(Received via mailing list)
Thanks for digging into these details,

I have a similar situation, but I just hacked an rhtml to set defaults
when creating the new objects.  I quickly refactored after reading
through this, but I'm left with a couple of questions.

First,

when I write...

def after_initialize
   if self.new_record?
     self.password ||= "password"
   end
end

I get blanks when render rhtml value @user.password.

However, if I write...

def after_initialize
   if self.new_record?
     self.password = "password" if self.password.blank?
   end
end

I get expected, pre-filled values in the view results.  Curious why that
would be the case.  Is Rails evaluating the ||= differently?  Is it
defaulting the ruby behavior of evaluations?

Also, why is after_initialize recommended to overriding the constructor?

Both methods worked for me, although I did refactor from the overriding
constructor to the after_initialize - I do like best practices.

Is it preferred due to the cloning issue?

Regards,

/ak

Wes G. schrieb:
Wes G. (Guest)
on 2007-02-05 19:00
Andy K. wrote:
> Thanks for digging into these details,
>
> I have a similar situation, but I just hacked an rhtml to set defaults
> when creating the new objects.  I quickly refactored after reading
> through this, but I'm left with a couple of questions.
>
> First,
>
> when I write...
>
> def after_initialize
>    if self.new_record?
>      self.password ||= "password"
>    end
> end
>
> I get blanks when render rhtml value @user.password.
>
> However, if I write...
>
> def after_initialize
>    if self.new_record?
>      self.password = "password" if self.password.blank?
>    end
> end
>
> I get expected, pre-filled values in the view results.  Curious why that
> would be the case.  Is Rails evaluating the ||= differently?  Is it
> defaulting the ruby behavior of evaluations?

My guess is that password is defaulted to be an empty string in your DB
or somehow so that when you do

self.password ||= "password"

self.password has a value that happens to be an empty string.  If you
use script/console to do an interactive command line session in the
context of your app., create a new one of these objects and see what the
password attribute is.  I bet it's an empty string.

> Also, why is after_initialize recommended to overriding the constructor?

(someone please correct me on this if I make a mistake, thanks)

ActiveRecord doesn't instantiate objects in the normal "Ruby" way.  In a
regular Ruby object, when "new" is called, this causes a call to
"allocate" followed by the call to "initialize," which is what you would
normally override.  In AR::Base, "new" is never called directly.
Instead, "allocate" is called, and then custom initialization is
performed inside of ActiveRecord.  The only time that an "initialize"
method will be called on an AR::Base descendant is when you do
AR_obj.new, never after that, once it's saved, for example.

So, to make a long story short, after_initialize is a hook that AR::Base
gives you to allow for behavior that you would normally probably put in
an initialize method, but can't since AR::Base doesn't invoke it most of
the time.

> Is it preferred due to the cloning issue?

The ||= inside of the new_record? block is necessary to handle cloning,
since currently, the cloning occurs as part of the instantiation of the
clone, and then after_initialize is called (as usual).  If you do
straight assignment in after_initialize, you would re-initialize those
attributes in your new, cloned, object.  And chances are, since you
cloned it in the first place, you probably cloned it from an object that
has had state change from initialization significantly.

----------------------

I started using after_initialize after slogging through the AR::Base
code and seeing that "normal" initialization doesn't usually occur, so
that I wouldn't be lulled into the idea that AR::Base descendants
actually behave like regular Ruby objects.  And who knows how the custom
initialization sequence will change in future releases of ActiveRecord,
so I figured I'd better get on board :).

Wes
This topic is locked and can not be replied to.