Matt
Matt

Reputation: 2843

Rails ActiveRecord handle an id column that is not the primary key

Struggling with ActiveRecord auto assigning the :id attribute as the primary key even though it is a separate column.

Table - legacy-table

    id - int
    pk_id - int (primary key)
    name - varchar2
    info - varchar2

Model

class LegacyModel < ActiveRecord::Base
  self.table_name = 'legacy-table'
  self.primary_key = 'pk_id'
  default_scope {order(:name => :asc)}
  alias_attribute :other_id, :id

end

I don't care that ActiveRecord automatically assigns the primary key (pk_id) to the :id attribute however I lose all access to the actual id column. Trying to use the alias simply points me back at the primary key.

However one caveat to this issues is that from the view i can access the id column by using @legacymodel[:id]. But again when calling @legacymodel.id I get the value of the pk_id column. What i want is to be able to call @legacymodel.other_id and have it point to the id column. Instead @legacymodel.service_id, @legacymodel.id, and @legacymodel.pk_id all point to the same column pk_id

Please note that this is a legacy db and modifying the columns are out of the question. I am using Rails 4 with MySql.

Is there anyway to code around this? Why does @legacymodel[:id] give me different results then @legacymodel.id?

Upvotes: 5

Views: 3046

Answers (2)

cweston
cweston

Reputation: 11647

The answer by @cschroed did not work for me in the latest Rails (v4.2). Digging into the Rails source code, it appears that read_attribute will also use the primary key value if the key passed equals 'id':

  ID = 'id'.freeze

  # Returns the value of the attribute identified by <tt>attr_name</tt> after
  # it has been typecast (for example, "2004-12-12" in a date column is cast
  # to a date object, like Date.new(2004, 12, 12)).
  def read_attribute(attr_name, &block)
    name = attr_name.to_s
    name = self.class.primary_key if name == ID
    _read_attribute(name, &block)
  end

https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/attribute_methods/read.rb

Since, the [] method uses read_attribute, this no longer works.

I found that directly reading from the attributes hash worked instead:

# LegacyModel class
def other_id
  @attributes.fetch_value('id')
end

This provided a means of bypassing read_attribute by mimicking _read_attribute.

Upvotes: 5

cschroed
cschroed

Reputation: 6834

The read_attribute method will read a value out of the @attributes hash. The [] method uses read_attribute. So @legacymodel[:id] gets the value of the id column.

The write_attribute method always tries to translate id into the name of the primary key...

# ActiveRecord::AttributeMethods::Write
def write_attribute(attr_name, value)
  attr_name = attr_name.to_s
  attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key

...and the []= method uses write_attribute. So @legacymodel[:id] = <value> will set a value into the primary key column, pk_id.

The id method is a special method that is aliased to the primary_key here:

# ActiveRecord::AttributeMethods::PrimaryKey
if attr_name == primary_key && attr_name != 'id'
  generated_attribute_methods.send(:alias_method, :id, primary_key)
end

So @legacymodel.id will get the value of the pk_id column.

If you just want to read the id column through @legacymodel.other_id, then you could define a method like:

# LegacyModel class
def other_id
  self[:id]
end

But if you also need to write to the id column through @legacymodel.other_id=, then you might need to try to find a safe way to override the write_attribute method so that you can work around the attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key statement.

Upvotes: 4

Related Questions