Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

alias_attribute does not work when building has_many records via relation. #51576

Open
dewyze opened this issue Apr 15, 2024 · 2 comments
Open

Comments

@dewyze
Copy link

dewyze commented Apr 15, 2024

Steps to reproduce

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  gem "activerecord", "7.1.3.2"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :users, force: :cascade do |t|
  end

  create_table :posts, force: :cascade do |t|
    t.integer :person_id
  end
end

class User < ActiveRecord::Base
  has_many :posts,
    inverse_of: :user
end

class Post < ActiveRecord::Base
  alias_attribute :user_id, :person_id

  belongs_to :user
end

class BugTest < Minitest::Test
  def test_association
    user = User.create!

    assert Post.create(user: user)        # Success
    assert Post.create(user_id: user.id)  # Success
    assert user.posts.create!             # Failure
  end
end

Expected behavior

Post.create(user: user) works fine. But when creating the associated record via user.posts.create! it breaks because the foreign key is not getting set properly.

ActiveModel::MissingAttributeError: can't write unknown attribute `user_id`
          raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"

Additionally, we cannot specify belongs_to :user, foreign_key: :user_id or has_many :posts, inverse_of: :user, foreign_key: :user_id. One of those has to at least set foreign_key: :person_id for this to work.

Actual behavior

The has_many association calls set_owner_attributes:

Set owner attributes is called and gets the foreign key:

primary_key_attribute_names = Array(reflection.join_primary_key)

If it's not specified, the name is derived, in this case user_id when the has many checks the inverse belongs_to:

def derive_foreign_key(infer_from_inverse_of: true)
if belongs_to?
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
elsif options[:inverse_of] && infer_from_inverse_of
inverse_of.foreign_key(infer_from_inverse_of: false)
else
active_record.model_name.to_s.foreign_key
end
end

Then we call _write_attribute:

record._write_attribute(primary_key, value)

This raises the error since _write_attribute does not reference aliases.

write_attribute would work:

name = self.class.attribute_aliases[name] || name

But @byroot mentions here a possible high cost to doing that.

I'm happy to help implement a solution, but not sure exactly where it should live. I don't know if doing it in the foreign key lookup would be cheaper at all, but it still requires a call to <record_class>.attribute_aliases.

System configuration

Rails version: 7.1.3.2

Ruby version: 3.1.0

@byroot
Copy link
Member

byroot commented Apr 15, 2024

Not sure if we really want to change this.

In your case I believe you can easily fix this with:

has_many :posts, inverse_of: :user, foreign_key: :person_id

@dewyze
Copy link
Author

dewyze commented Apr 15, 2024

Yeah, we can make that work. But in the case of trying to rename a class, column, etc. The presence of those can still be confusing.

If we don't want to introduce this that's fine, but I wanted to check first if this was the intended behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants