基于列值的自定义has_one

Custom has_one based on column value

提问人:João Menighin 提问时间:10/26/2023 更新时间:10/27/2023 访问量:110

问:

刚接触 Rails 的人想知道是否可以用 ActiveRecords 来模拟这种关系:

我有以下模型(简化):

User:
 - name
 - email 
Client:
 - name
 - address

然后我有一个模型,它与这两个表相关,但有一个类型列:attendee

| id | join_type | join_id | 
|----|-----------|---------|
|  1 |    user   |    1    |
|  2 |   client  |    1    |

有没有办法使用关系对活动记录进行建模?has_one

像这样:

class Attendee < ApplicationRecord

    # OBVIOUSLY DOESNT WORK
    has_one :user, ->() { where(join_type: 'user') }, :class_name => 'User'
    has_one :client, ->() { where(join_type: 'client') }, :class_name => 'Client'

end

所以它会生成正确的左连接,所以?

谢谢

Ruby-on-Rails Ruby ActiveRecord

评论

4赞 tadman 10/26/2023
是的,但不是这样。您需要多态关联
0赞 ian root 10/27/2023
提供更多信息会更容易。用户/客户端是否参加 Event 对象?这是多对多的关系吗?Attendee 表是否作为连接表存在,将用户和客户端连接到事件?如果是这样,@tadman是对的,你需要一个多态关联。具体来说,是多对多的多态性关联。您在后来的评论中提到,某些表已经存在,并且您有遗留数据需要保留。这意味着您需要仔细编写向上和向下迁移。如果您提供更详细的问题,我们可以更好地帮助您编写这些内容

答:

0赞 Les Nightingill 10/26/2023 #1

我的方法是定义一个模型和一个模型,两者都继承自与会者(= 单表继承,STI)。因此,与会者将需要一个列,该列将在创建子类时自动填充。UserAttendeeClientAttendeetype

# app/models/client_attendee.rb
class ClientAttendee < Attendee
  has_one :client
end

# app/models/user_attendee.rb
class UserAttendee < Attendee
  has_one :user
end

您仍然可以根据需要使用该类。 您在与会者表中显示一个字段,这对于has_one关系不是必需的,只有belongs_to关系是必需的。 并将有列 user_attendee_id 和 client_attendee_id。Attendeejoin_idUserClient

class User < ActiveRecord::Base
  belongs_to :user_attendee
end

class Client < ActiveRecord::Base
  belongs_to :client_attendee
end
3赞 daxanhkun 10/26/2023 #2

使用此结构创建与会者表

class CreateAttendees < ActiveRecord::Migration[7.0]
  def change
    create_table :attendees do |t|
      t.timestamps
      t.string :linked_object_type
      t.bigint :linked_object_id
    end

    # optinal
    add_index :attendees, %i[linked_object_type linked_object_id]
  end
end

# app/models/client_attendee.rb
class Attendee < ApplicationRecord
  belongs_to :linked_object, polymorphic: true, foreign_type: :linked_object_type
end

然后,您可以像这样从Attendee实例访问用户/客户端

   attendee = Attendee.first
   linked_object = attendee.linked_object # Depends on linked object type it will return the relative model instance

如果要从“用户/客户”访问与会者,可以按如下方式定义他们

# frozen_string_literal: true

# This model is used to store user data
class User < ApplicationRecord
  has_many :attendees, as: :linked_object, linked_object_type: 'User' # You can scope it yourself, and define has_one depends on your design. 
end

评论

0赞 João Menighin 10/26/2023
嘿,大班坤!感谢您的回答,我认为这更像是我想要的。但是有一个问题:该表当前存在。这意味着该列已经具有不需要的值,这些值是我的其他模型的类名。我有没有办法映射它?比如,“如果列是,我想使用模型。如果列是我想使用模型“...?linked_object_typeclientClientUseruserSystemUser
0赞 max 10/26/2023
你可以只使用 .您无需配置 .t.references :linked_object, polymorphic: truelinked_object_type: 'User'
0赞 max 10/26/2023
@JoãoMenighin我认为没有办法做到这一点。ActiveRecord 进行类型推断的方式是查看列并直接向上常量化值,然后从那里获得表名。如果你正在使用一个遗留数据库,如果你想要它的所有功能,你很可能必须调整它才能与ActiveRecord一起使用。
0赞 daxanhkun 10/27/2023
@JoãoMenighin确定应使用哪个 Model 类的工作方式正是上述内容。因此,我认为您应该运行一个脚本来执行数据迁移,以使其适用于 Rails。如果您需要其他系统的原始值。如果可能的话,我建议您在该层执行映射。ActiveRecordmax
0赞 zaphodbln_ 10/26/2023 #3

欢迎来到 Rails。我想以这种方式提出一个更简单的解决方案:

class CreateAttendees < ActiveRecord::Migration[7.0]
  def change
    create_table :attendees do |t|
      t.timestamps
      t.integer :user_id
      t.integer :client_id
    end

    #add Indices and Foreign Keys as you wish

    #add_index...
    #add_foreign_key...
  end
end

这样,你就可以使用你最喜欢的数据库系统的所有数据完整性功能,并简化你的rails代码,如下所示:

class Attendee < ApplicationRecord

   belongs_to :user, optional: true
   belongs_to :client, optional: true

   #if you like you can add a getter method:
   def linked_object
     return user || client  
   end
end

class User < ActiveRecord::Base
  has_one :attendee        # or has_many - as your data model requires
  
  # You can even add some data integrity features in rails using this:
  # has_one :attendee, dependent: destroy
end

class Client < ActiveRecord::Base
  has_one :attendee
  #has_one :attendee, dependent: destroy
end

这样,代码就更易于阅读和维护。你会很高兴没有无处可去的与会者记录。

2赞 max 10/26/2023 #4

首先,你正在成为 和 的可怕命名造成的经典混乱的受害者。has_onebelongs_to

belongs_to表示模型表具有引用另一个表(或同一表)的列。这才是你真正想要的。

has_one表示另一端有一列引用此表。有点像,但它生成不同的方法并附加到查询上。如果它被命名,它将为我们节省很多 greif .has_manyLIMIT 1referenced_by

我认为您可能正在寻找的是通过具有多态关联的连接表的间接关联。

class Client
  has_many :attendences
  has_many :meetings, 
    through: :attendences, 
    as: :attendee
end

class User
  has_many :attendences
  has_many :meetings, 
    through: :attendences,
    as: :attendee
end

# I took the liberty of making the naming less confusing
class Attendence
  belongs_to :meeting
  belongs_to :attendee, 
    polymorpic: true
end

# Just an example - this class could be whatever you want
class Meeting
  has_many :attendences
  has_many :attendees, through: :attendences
  has_many :users,     through: :attendences
  has_many :clients,   through: :attendences
end

其工作方式是,出勤表同时具有attendee_id列和attendee_type列。后者存储关联项的类名。

您可以在迁移中同时创建这两个列,方法是将选项传递给:polymorphicadd_reference

class CreateAttendences < ActiveRecord::Migration[7.0]
  def change
    create_table :attendences do |t|
      t.references :attendee, null: false, polymorphic: true
      t.references :meeting, null: false, foreign_key: true
      t.timestamps
    end
  end
end

如果这实际上是一个好的设计,那么这是值得商榷的,因为您不能使用外键约束来维护数据库级别的引用完整性。

通过简单地创建两个联接表可以实现相同的目标,即使通过 ActiveRecord 也无法真正将其视为从最后开始的单个关联。meetings