【问题标题】:ActiveRecord has_one where associated model has two belongs_to associationsActiveRecord has_one 其中关联模型有两个 belongs_to 关联
【发布时间】:2021-08-26 04:58:01
【问题描述】:

我有两个以这种方式相互关联的 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

用户 -> 地址关联工作正常:

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,这是有道理的,因为这不是外键字段的名称。这将是 either 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!我最终根据他的回答提出了解决方案。我还使用了Enumerize gem,它将在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

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

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

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

    使用 lambda 为关联添加默认范围在这里不起作用,因为 ActiveRecord 实际上并没有让您在关联级别上进行连接。如果您考虑它生成多少不同的查询以及该功能会导致的边缘情况的数量,这是完全可以理解的。

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

    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
    

    【讨论】:

    • 关于完全避免这种情况的可靠提示/使用 has_many :addresses。另一种选择(我也会避免/不推荐)是 has_one :work_user, class_name: :user, foreign_key: :work_address_idhas_one :home_user, class_name: :user, foreign_key: :home_address_id 然后 def user; work_user || home_user; end
    • 字符串与整数的枚举是可以永远争论的事情之一。整数有轻微的性能优势,而字符串据说更便于维护。我使用整数作为文档中的内容。
    • enum 自动为枚举中的每个键生成作用域。
    • (小)字符串的优势是,如果您有数据仓库/BI 工具,则值的含义更明显(而不是使该工具中的查询与您的数据模型保持同步)
    • @melcher 是的,这就是我所说的维护性。您可以在没有文档的情况下理解数据。另一种选择是原生枚举列(在 Postgres 上),但如果您不得不更改定义,那就很痛苦了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-13
    相关资源
    最近更新 更多