ActiveRecord has_one,关联模型有两个belongs_to关联



我有两个以这种方式相互关联的ActiveRecord模型:

class Address < ApplicationRecord
has_one :user, class_name: User.name
end
class User < ApplicationRecord
belongs_to :home_address, class_name: Address.name
belongs_to :work_address, class_name: Address.name
end

User ->地址关联工作正常:

home_address = Address.new
#=> <Address id:1>
work_address = Address.new
#=> <Address id:2>
user = User.create!(home_address: home_address, work_address: work_address)
#=> <User id:1, home_address_id: 1, work_address_id: 2>
user.home_address
#=> <Address id:1>
user.work_address
#=> <Address id:2>

我遇到的麻烦是让Addresshas_one正常工作。一开始我得到了一个错误,User#address_id does not exist,这是有意义的,因为这不是外键字段的名称。它将是home_address_idwork_address_id(并且我通过迁移添加了这些fk)。但是我不确定如何让它知道使用哪个地址,直到我了解到您可以将作用域传递到has_one声明:

class Address < ApplicationRecord
has_one :user,
->(address) { where(home_address_id: address.id).or(where(work_address_id: address.id)) },
class_name: User.name
end

返回和之前一样的错误:Caused by PG::UndefinedColumn: ERROR: column users.address_id does not exist。这是令人困惑的,因为在这个范围内我没有声明我正在寻找address_id。我猜has_one隐式地有一个foreign_key:address_id,但我不知道我如何设置这个,因为在技术上有两个,:home_address_id和:work_address_id。

我觉得我很接近这里-我如何修复这个has_one关联?

<标题>

更新我的直觉说,这里的解决方案是创建一个user方法来执行我要运行的查询,而不是声明一个has_one。如果has_one支持此功能就太好了,但如果不支持,我将退回到该功能。

class Address < ApplicationRecord
def user
User.find_by("home_address_id = ? OR work_address_id = ?", id, id)
end
end
<标题>

解决方案感谢下面的@max !最后,我根据他的回答提出了一个解决方案。我还使用了Enumerizegem,它将在Address模型中发挥作用。

class AddAddressTypeToAddresses < ActiveRecord::Migration[5.2]
add_column :addresses, :address_type, :string
end
class User < ApplicationRecord
has_many :addresses, class_name: Address.name, dependent: :destroy
has_one :home_address, -> { Address.home.order(created_at: :desc) }, class_name: Address.name
has_one :work_address, -> { Address.work.order(created_at: :desc) }, class_name: Address.name
end
class Address < ApplicationRecord
extend Enumerize
TYPE_HOME = 'home'
TYPE_WORK = 'work'
TYPES = [TYPE_HOME, TYPE_WORK]
enumerize :address_type, in: TYPES, scope: :shallow
# Shallow scope allows us to call Address.home or Address.work
validates_uniqueness_of :address_type, scope: :user_id, if: -> { address_type == TYPE_WORK }
# I only want work address to be unique per user - it's ok if they enter multiple home addresses, we'll just retrieve the latest one. Unique to my use case.
end

Rails中的每个关联只能有一个外键,因为在SQL中需要的是:

JOINS users 
ON users.home_address_id = addresses.id OR users.work_address_id = addresses.id

使用lambda为关联添加默认范围在这里不起作用,因为ActiveRecord实际上不允许您在关联级别上如何连接。如果你考虑到它生成了多少不同的查询,以及该特性将导致的边缘情况的数量,这是非常可以理解的。

如果你真的想在你的users表上有两个不同的外键,你可以用单表继承来解决这个问题:

class AddTypeToAddresses < ActiveRecord::Migration[6.1]
def change
add_column :addresses, :type, :string
end
end
class User < ApplicationRecord
belongs_to :home_address, class_name: 'HomeAddress'
belongs_to :work_address, class_name: 'WorkAddress'
end
class HomeAddress < Address
has_one :user, foreign_key: :home_address_id
end
class WorkAddress < Address
has_one :user, foreign_key: :work_address_id
end

但是我会将外键放在另一个表上,并使用一对多关联:

class Address < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :addresses
end

这允许您添加任意多的地址类型,而无需占用用户表。

如果你想限制用户只能使用一个家庭地址和一个工作地址,你可以这样做:

class AddTypeToAddresses < ActiveRecord::Migration[6.1]
def change
add_column :addresses, :address_type, :integer, index: true, default: 0
add_index :addresses, [:user_id, :address_type], unique: true
end
end
class Address < ApplicationRecord
belongs_to :user
enum address_type: {
home: 0,
work: 1
}
validates_uniqueness_of :type, scope: :user_id
end    
class User < ApplicationRecord
has_many :addresses
has_one :home_address,
-> { home },
class_name: 'Address'
has_one :work_address,
-> { work },
class_name: 'Address'
end

最新更新