Reputation: 1636
Stuck in a - at first sight - simple problem in RoR. I am sure that is easy, but none answer here in SO helped me too much.
I have two ActiveRecord models: Foo
has many Bars
:
class Foo < ApplicationRecord
has_many :bars
end
class Bar < ApplicationRecord
belongs_to :foo
end
That works like a charm. But I would like to use another field of Foo
as foreign_key. The default is foo_id
I would like to use custom_id
as my foreign key. So I tried this (as many solutions over the web suggested):
class Foo < ApplicationRecord
has_many :bars, :foreign_key => 'custom_id', :class_name => 'Bars'
end
class Bars < ApplicationRecord
belongs_to :foo, :class_name => 'Foo'
end
But that doesn't work. i.e. ActiveRecord keeps binding Foo
to Bars
using foo_id
.
Note: Including a self.primary_key='custom_id' in Foo would partially work. but I don't think that is a good idea. I want to keep foo_id as the primary key
UPDATE:
Given the feedback -Thank you guys-, I uploaded that example here https://github.com/montenegrodr/temporary_repository_ror :
UPDATE #2:
The answers does not satisfy the question above. Why the test is failing, my assumption is that it shouldn't fail.
UPDATE #3:
There're a couple of new answers I still need to assess. Will do that within 24 hours. Thanks.
UPDATE #4:
Thank you guys for all the answers. But none of them satisfied the criteria. I need to have that test passing. If it is not possible, can someone explain why? Is it a rails constraint?
Upvotes: 13
Views: 6701
Reputation: 1427
I will leave you a complete file you can save and run with ruby filename.rb
that will show the test pass. (The template for this test was taken from the Rails bug_report_templates)
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
gem "activerecord", "5.2.0"
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 :foos, force: true do |t|
t.integer :custom_id, index: { unique: true }
t.timestamps
end
create_table :bars, force: true do |t|
t.integer :custom_id, index: true
t.timestamps
end
end
class Foo < ActiveRecord::Base
has_many :bars, foreign_key: :custom_id, primary_key: :custom_id
end
class Bar < ActiveRecord::Base
belongs_to :foo, foreign_key: :custom_id, primary_key: :custom_id
end
class BugTest < Minitest::Test
def test_the_truth
foo = Foo.new id: 1, custom_id: 100
bar = Bar.new foo: foo
assert foo.custom_id == bar.custom_id
end
end
The class of both Foo
and Bar
can both be infered in the associations, so you dont have to specify class_name
in either of them.
If you don't include the primary_key
the relation will use the one it has by default: id
. This is why your test bar.foo_id == 1
, because 1
is the id
of Foo
, being that the default primary_key
Remember the column
id
is created in each table by Rails unless you tell it explicitly not to.
Knowing hich column belongs to which table in the relation may be quite confusing, I will leave another example with different column names on each table for clarification. I also changed the model names to better understand which is playing each role.
ActiveRecord::Schema.define do
create_table :classrooms, force: true do |t|
t.integer :my_classroom_id, index: { unique: true }
t.timestamps
end
create_table :students, force: true do |t|
t.integer :student_c_id, index: true
t.timestamps
end
end
class Classroom < ActiveRecord::Base
has_many :students, foreign_key: :student_c_id, primary_key: :my_classroom_id
end
class Student < ActiveRecord::Base
belongs_to :classroom, foreign_key: :student_c_id, primary_key: :my_classroom_id
end
class BugTest < Minitest::Test
def test_the_truth
classroom = Classroom.new id: 1, my_classroom_id: 100
student = Student.new classroom: classroom
assert student.student_c_id == classroom.my_classroom_id
end
end
Adding only the correct primary_key
in both your models will make your test pass.
Upvotes: 3
Reputation: 71
Set the foreign_key and the primary_key of Bar model to the desired column name (custom_id in this case).
class Bar < ApplicationRecord
belongs_to :foo, foreign_key: "custom_id", primary_key: "custom_id"
end
Upvotes: 2
Reputation: 598
You need to specify a different primary key for the relationship if you wish to achieve what you are looking to do.
To clarify, this is not the same as changing the primary_key
of the model. This way is only changing the primary key used by the relationship. Please see the bottom of this post for examples.
I changed the keys from both using custom_id
and changed one to foo_id
. This way you have a better idea of what is going on between the models. You can use both custom_id
if you wish, but I would suggest keeping the rails norm of foo_id
for the belongs_to association.
If you want to use both of custom_id you'll have to add some specific foreign_keys
Foo
class Foo < ApplicationRecord
has_many :bars,
primary_key: :custom_id,
foreign_key: :foo_id
end
Bar
class Bar < ApplicationRecord
belongs_to :foo,
primary_key: :custom_id
end
CreateFoos
class CreateFoos < ActiveRecord::Migration[5.2]
def change
create_table :foos do |t|
t.integer :custom_id, index: {unique: true}
t.timestamps
end
end
end
CreateBars
class CreateBars < ActiveRecord::Migration[5.2]
def change
create_table :bars do |t|
t.integer :foo_id, index: true
t.timestamps
end
end
end
Here is the updated Test that should now be passing:
Test
require 'test_helper'
class BarTest < ActiveSupport::TestCase
test "the truth" do
foo = Foo.new(id: 1, custom_id: 100)
bar = Bar.new(foo: foo)
assert bar.foo_id == foo.custom_id
# bar.foo_id = 100
# foo.custom_id = 100
end
end
Examples
Foo.find(1) #<Foo id: 1, custom_id: 100>
Bar.first #<Bar id: 1, foo_id: 100>
Bar.first.foo = #<Foo id: 1, custom_id: 100>
Bar.first.foo == Foo.find(1) # true
As you can see, this method does not change the primary key of Foo
itself. It changes the primary key the relationship between Foo
and Bar
uses. Bar is realated to foo via custom_id: 100
, but foo is still found with it's id: 1
key, not its custom_id
key.
Upvotes: 13
Reputation: 2397
I cloned your repository and run the test code, it was fail
the other answers on your question are correct, but you wrote wrong test code and add unnecessary column on Foo model
actually you only need to add custom_id attribute to Bar model
class CreateBars < ActiveRecord::Migration[5.2]
def change
create_table :bars do |t|
t.integer :custom_id, index: true
t.timestamps
end
end
end
class Bar < ApplicationRecord
belongs_to :foo, :class_name => "Foo", :foreign_key => "custom_id"
end
and for Foo model
class CreateFoos < ActiveRecord::Migration[5.2]
def change
create_table :foos do |t|
t.timestamps
end
end
end
class Foo < ApplicationRecord
has_many :bars, :class_name => "Bar", :foreign_key => "custom_id"
end
and then to test the relation
require 'test_helper'
class BarTest < ActiveSupport::TestCase
test 'the truth' do
foo = Foo.new
foo.save!
bar = Bar.new(foo: foo)
bar.save!
assert foo.bar_ids.include?(bar.id)
assert bar.foo_id == foo.id
end
end
actually this is not how I wrote rails code only for answering your question
Upvotes: 1
Reputation: 23661
While the above answer is correct I am adding some explanation to the answer.
For the has_many
association to work properly you need to add foreign_key: :custom_id
to Foo model. This will search bars table for records with custom_id = id
of Foo
class Foo < ApplicationRecord
has_many :bars, foreign_key: :custom_id
end
Old Query
SELECT "bars".* FROM "bars" WHERE "bars"."foo_id" = $1 [["foo_id", 1]]
New Query
SELECT "bars".* FROM "bars" WHERE "bars"."custom_id" = $1 [["custom_id", 1]]
For the belongs_to
association to work properly you need to add foreign_key: :custom_id
to Bar model as well. This will search the foos
table and return the record with id = custom_id
of Foo instead of foo_id
class Bars < ApplicationRecord
belongs_to :foo, foreign_key: :custom_id
end
Old Query
# <Bar id: 1, foo_id: 1, custom_id: 2, ...>
SELECT "foos".* FROM "foos" WHERE "foos"."id" = $1 [["id", 1]]
New Query
SELECT "foos".* FROM "foos" WHERE "foos"."id" = $1 [["id", 2]]
Upvotes: 4
Reputation: 6531
1 => Given implementation needs custom_id: integer
in Bar
model
class Foo < ApplicationRecord
has_many :bars, :class_name => "Bar", :foreign_key => "custom_id"
end
class Bar < ApplicationRecord
belongs_to :foo, :class_name => "Foo", :foreign_key => "custom_id"
end
Upvotes: 1