Update a legacy “ID” column that isn't the primary key with ActiveRecord

早过忘川 提交于 2019-12-24 10:31:39

问题


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:

  1. Is this the right approach (given I can't change the legacy DB schema)?
  2. Am I doing it right? I've never reopened a class [edit: or module] before (is that even the right terminology?)
  3. 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:

  1. 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 a write_id_attribute method to the existing module.
  2. I was reopening the module correctly (but see previous and next answers)
  3. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!