问题
I'm using Rails and the activerecord-sqlserver-adapter gem to try and add data to a legacy MS SQL database whose dbo.Condition
table has a primary key called ConditionSeq
and a foreign key column ID
that stores the user ID.
class Condition < ActiveRecord::Base
# using lowercase_schema_reflection = true
self.table_name = :condition
self.primary_key = 'conditionseq'
end
Every time I write Condition.new(conditionseq: nil, id: 12345)
(or even Condition.new(id: 12345)
), hoping to let MS SQL auto increment the conditionseq column, ActiveRecord unhelpfully assumes I actually want to set the primary key to 12345.
Based on a similar question and particularly @cschroed's answer and follow up comment, I have tried reopening ActiveRecord::AttributeMethods::Write (source) to add a write_id_attribute method that forgoes the attr_name == "id" check:
# config/initializers/write_id_attribute.rb
module ActiveRecord
module AttributeMethods
module Write
extend ActiveSupport::Concern
# Add a method to allow us to update a column called "ID" instead of
# Rails trying to map ID attribute to the legacy tables primary key column
def write_id_attribute(attr_name, value)
name = if self.class.attribute_alias?(attr_name)
self.class.attribute_alias(attr_name).to_s
else
attr_name.to_s
end
primary_key = self.class.primary_key
sync_with_transaction_state if name == primary_key
_write_attribute(name, value)
end
end
end
end
I can now call that method but it throws a NoMethodError Exception for _write_attribute
.
Three questions:
- Is this the right approach (given I can't change the legacy DB schema)?
- Am I doing it right? I've never reopened a class [edit: or module] before (is that even the right terminology?)
- Why can't I call the existing
_write_attribute
method?
回答1:
I worked through this with some more experienced Ruby devs when I was back at work and we came to the conclusion that this is an unhandled edge case in ActiveRecord (see below for more info).
My eventual workaround (with comments) is to override the existing method(s) in a concern and only include that concern when we need it:
module Concerns::ARBugPrimaryKeyNotIDColumnWorkaround
extend ActiveSupport::Concern
# Override this (buggy?) method to allow us to update a column called "ID", even if there's a different primary key
# ActiveRecord tries to map ID attribute to the legacy table's primary key column...
# Doesn't check to make sure there isn't already another column called ID that we're actually trying to update
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/write.rb
# TODO: Submit a patch to rails/activerecord
def write_attribute(attr_name, value)
name = if self.class.attribute_alias?(attr_name)
self.class.attribute_alias(attr_name).to_s
else
attr_name.to_s
end
primary_key = self.class.primary_key
# name = primary_key if name == "id".freeze && primary_key # BUG: (?) assumes primary key is always called ID
sync_with_transaction_state if name == primary_key
_write_attribute(name, value)
end
# Also need to clone this for some reason
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/write.rb
def _write_attribute(attr_name, value) # :nodoc:
@attributes.write_from_user(attr_name.to_s, value)
value
end
end
We still can't call Condition.new(id: 12345, other_column: other_value)
(or Condition.create(...
or condition.id
) but the workaround does let us explicitly set the id using condition[:id]
:
condition = ::Condition.new(mapped_attributes) # Don't include ID in mapped_attributes
condition[:id] = 12345 # Must be set explicitly using [:id] due to AR bug
condition.save
Once our current sprint is over my goal is to add a test to ActiveRecord to demonstrate this behaviour and raise it is an issue.
Additional Info
Rails has tests for custom primary keys. It also has tests for having an ID column that isn't the primary key. The unhandled edge case seems to be when you have both at the same time...
Every time you try and update the ID column (by passing in an id attribute) the custom primary key handling kicks in and updates the primary key instead.
The answers to my specific questions are:
- Given I can't change the legacy schema, I don't have a lot of choice. FWIW, I ended up overwriting the
write_attribute
method (only when we include the monkey patch) rather than adding awrite_id_attribute
method to the existing module. - I was reopening the module correctly (but see previous and next answers)
- I'm still not sure why I couldn't call the existing
_write_attribute
method. I suspect there's some sort of Ruby or Rails "magic" that treats _methods differently. In the end I had to reimplement it too (see solution above)
来源:https://stackoverflow.com/questions/52341754/update-a-legacy-id-column-that-isnt-the-primary-key-with-activerecord