views:

282

answers:

3

Hi all,

I'm having trouble creating a new model row in the database using ActiveRecord in a Sinatra app I'm developing. The object in question is being created without any errors (using save!, no exceptions are raised), but most of the data I specify for the save is not present.

class ProjectMeta < ActiveRecord::Base
    attr_accessor :completion_ratio, :num_stories, :num_completed_stories, :original_target_date, :current_target_date

    ...

    def self.create_from_project(project)
        meta = ProjectMeta.new
        meta.project_id = project.id
        meta.num_stories = project.num_stories
        meta.num_completed_stories = project.num_completed_stories
        meta.completion_ratio = ProjectMeta.calculate_ratio(project.num_completed_stories, project.num_stories)
        meta.current_target_date = project.current_target_date
        meta.save!
        meta
    end

    ...

end

All inspections on the data from the project object I'm sending as well as the new meta object I'm creating show that the data is present. But when I do a meta.inspect before and after the save, it shows that all the data (aside from project_id) is in it's default state (zeroes). I've also checked meta.errors.nil? and sure enough, there aren't any errors after the save.

What is most puzzling is that if I turn around and get a new meta instance with that project_id and put the data in, it saves no problem to the db.

This is frustrating me because I've built several sites in Rails and Sinatra with ActiveRecord. This one issue is completely perplexing me. Can anyone tell me what I'm doing wrong?

A: 

The attr_accessors will never be saved to the DB. These are internal variables within the instance. If you want to save the values, you have to create real columns.

Make a migration to declare the columns then try again.

François Beausoleil
A: 

There's an important distinction between database fields and temporary attr_accessor declared properties. If you've declared your columns, then attr_accessor declarations are unnecessary.

Keep in mind that the data should be stored in the model's attributes property to be properly saved, not as individual instance variables.

For example, to see what's scheduled to be saved:

class MyModel < ActiveRecord::Base
  attr_accessor :not_saved
end

model = MyModel.new(:not_saved => 'foo')

puts model.attributes.inspect

There are methods to get information on what columns are available in a model, such as:

MyModel.columns_names
tadman
A: 

Here is how it works

  1. On first access to model, columns from corresponding database table are retrieved and stored inside model data. This information can be retrieved through ::columns class method.

  2. When you access some model's attribute, Ruby doesn't find corresponding method in class and launches #method_missing method. That method inspects model's ::columns to check if corresponding column exists. If so, it creates an accessors for that column so that next time you access that model's attribute, an accessor method will be called directly, without need to call #method_missing (the later is slower).

The accessors look like this:

def my_attribute
  read_attribute(:my_attribute)
end

def my_attribute=(value)
  write_attribute(:my_attribute, value)
end

For #read_attribute and #write_attribute methods there is a shortcut: #[] and #[]=. If for some reason you will need to access underlying data directly (e.g. do some data conversion), you can write them short:

def my_attribute
  self[:my_attribute]
end

def my_attribute=(value)
  self[:my_attribute] = value
end

Model has a special accessor -- #attributes -- which returns a "column_name => value" Hash.

NOTE: the data for each column is stored in a special Hash instance inside your model instance, not in "@column_name" instance variables. When you define accessors with #attr_accessor, you block the usual way of defining attribute accessors via #method_missing. Your data is stored in instance variables instead of "attributes" hash, so it is not saved into database.

If you want to add new attribute to your model, you actually need to add column to database table that correspond to that model and then reload the whole application.

Maxim Kulkin
This is EXACTLY what I was missing. I found the fix was to remove the attr_accessor line, but was confused as to _why_ that was the case. Thanks!
localshred