Застрял - на первый взгляд - простая проблема в RoR. Я уверен, что это легко, но ни один ответ здесь, в SO, мне слишком не помог.
У меня две модели ActiveRecord: Foo
имеет много Bars
:
class Foo < ApplicationRecord
has_many :bars
end
class Bar < ApplicationRecord
belongs_to :foo
end
Это работает как шарм. Но я бы хотел использовать другое поле Foo
как foreign_key. По умолчанию это foo_id
. Я хотел бы использовать custom_id
в качестве внешнего ключа. Итак, я попробовал это (как предлагали многие решения в Интернете):
class Foo < ApplicationRecord
has_many :bars, :foreign_key => 'custom_id', :class_name => 'Bars'
end
class Bars < ApplicationRecord
belongs_to :foo, :class_name => 'Foo'
end
Но это не работает. т.е. ActiveRecord сохраняет привязку Foo
к Bars
с помощью foo_id
.
Примечание. Включение self.primary_key = 'custom_id' в Foo частично сработает. но я не думаю, что это хорошая идея. Я хочу сохранить foo_id в качестве первичного ключа
Обновлено:
Учитывая обратную связь - спасибо, ребята-, я загрузил этот пример здесь https://github.com/montenegrodr/ Contemporary_repository_ror:
ОБНОВЛЕНИЕ №2:
Ответы не удовлетворяют поставленный выше вопрос. Я полагаю, что почему тест не проходит, он не должен терпеть неудачу.
ОБНОВЛЕНИЕ # 3:
Есть несколько новых ответов, которые мне еще нужно оценить. Сделаем это в течение 24 часов. Спасибо.
ОБНОВЛЕНИЕ # 4:
Спасибо, ребята, за все ответы. Но ни один из них не соответствовал критериям. Мне нужно пройти этот тест. Если это невозможно, может кто-нибудь объяснить, почему? Это ограничение рельсов?
1 => Данная реализация требует custom_id: integer
в модели Bar
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
Я почти уверен, что пробовал это. Но я сделаю второй шанс. Спасибо.
@Montenegrodr Конечно, дайте мне знать, если у вас возникнут проблемы с этой реализацией.
Спасибо. Я пропатчил POST, не могли бы вы взглянуть еще раз?
Хотя приведенный выше ответ верен, я добавляю к нему некоторые пояснения.
Для правильной работы ассоциации has_many
необходимо добавить foreign_key: :custom_id
в модель Фу. Это будет искать в таблице баров записи с custom_id = id
из Foo.
class Foo < ApplicationRecord
has_many :bars, foreign_key: :custom_id
end
Старый запрос
SELECT "bars".* FROM "bars" WHERE "bars"."foo_id" = $1 [["foo_id", 1]]
Новый запрос
SELECT "bars".* FROM "bars" WHERE "bars"."custom_id" = $1 [["custom_id", 1]]
Чтобы ассоциация belongs_to
работала правильно, вам также необходимо добавить foreign_key: :custom_id
в модель Бар. Это выполнит поиск в таблице foos
и вернет запись с id = custom_id
из Foo вместо foo_id
.
class Bars < ApplicationRecord
belongs_to :foo, foreign_key: :custom_id
end
Старый запрос
# <Bar id: 1, foo_id: 1, custom_id: 2, ...>
SELECT "foos".* FROM "foos" WHERE "foos"."id" = $1 [["id", 1]]
Новый запрос
SELECT "foos".* FROM "foos" WHERE "foos"."id" = $1 [["id", 2]]
Отличное объяснение !, могу я спросить, почему нужно также выделять class_name с помощью ассоциации.?
Спасибо. Я пропатчил POST, не могли бы вы взглянуть еще раз?
Вам нужно указать другой первичный ключ для отношений, если вы хотите достичь того, что вы хотите сделать.
Чтобы уточнить, это не то же самое, что замена primary_key
модели. Таким образом изменяется только первичный ключ, используемый отношениями. Примеры см. В нижней части этого сообщения.
Я поменял ключи с обоих, используя custom_id
, и поменял один на foo_id
. Таким образом, вы лучше понимаете, что происходит между моделями. Вы можете использовать оба custom_id
, если хотите, но я бы посоветовал сохранить норму рельсов foo_id
для ассоциации own_to.
Если вы хотите использовать оба custom_id, вам нужно будет добавить какой-то конкретный foreign_keys
Фу
class Foo < ApplicationRecord
has_many :bars,
primary_key: :custom_id,
foreign_key: :foo_id
end
Бар
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
Вот обновленный тест, который теперь должен пройти:
Тестовое задание
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
Примеры
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
Как видите, этот метод не меняет первичный ключ самого Foo
. Он изменяет первичный ключ, используемый отношениями между Foo
и Bar
. Bar связан с foo через custom_id: 100
, но foo все еще находится с его ключом id: 1
, а не с ключом custom_id
.
Спасибо, что нашли время ответить. Искал решение, которое мешает мне изменить первичный ключ ..
Это не меняет первичный ключ модели, это меняет только первичный ключ отношения. Таким образом, первичным ключом Foo
по-прежнему является id
, однако отношение Bar
к Foo
использует Foo.custom_id
, а не Foo.id
. Этот ответ отличается от того, что вы сделали с self.primary_key='custom_id'
. Пожалуйста, смотрите обновленный раздел Примеры внизу моего ответа для получения дополнительной информации.
Я клонировал ваш репозиторий и запустил тестовый код, он не прошел
другие ответы на ваш вопрос верны, но вы написали неправильный тестовый код и добавили ненужный столбец в модели Foo
на самом деле вам нужно только добавить атрибут custom_id в модель Bar
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
и для модели Foo
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
а затем проверить соотношение
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
на самом деле это не то, как я писал код рельсов только для ответа на ваш вопрос
Спасибо, что нашли время ответить. Вопрос в том, как спроектировать модели, чтобы этот тест assert bar.foo_id == foo.custom_id
прошел.
foo.custom_id означает, что основным идентификатором вашей таблицы Foo является custom_id, а не id?
Установите внешний ключ и основной ключ модели Bar на желаемое имя столбца (custom_id в данном случае).
class Bar < ApplicationRecord
belongs_to :foo, foreign_key: "custom_id", primary_key: "custom_id"
end
Я оставлю вам полный файл, который вы можете сохранить и запустить с ruby filename.rb
, который покажет тестовый проход. (Шаблон для этого теста был взят из 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
Оба класса Foo
и Bar
могут быть включены в ассоциации, поэтому вам не нужно указывать class_name
ни в одной из них.
Если вы не включите primary_key
, отношение будет использовать то, которое у него есть по умолчанию: id
. Вот почему ваш тестовый bar.foo_id == 1
, потому что 1
- это id
из Foo
, который является primary_key
по умолчанию.
Remember the column
id
is created in each table by Rails unless you tell it explicitly not to.
Знание, какой столбец принадлежит какой таблице в отношении, может быть довольно запутанным, я оставлю другой пример с разными именами столбцов в каждой таблице для пояснения. Я также изменил названия моделей, чтобы лучше понять, кто играет каждую роль.
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
Добавление только правильного primary_key
в обе ваши модели позволит пройти ваш тест.
Я добавил ответ, который сделает ваш тест успешным, можете взглянуть на него?