简体   繁体   English

Rails Polymorphic Association 在同一个 model 上具有多个关联

[英]Rails Polymorphic Association with multiple associations on the same model

My question is essentially the same as this one: Polymorphic Association with multiple associations on the same model我的问题与这个问题基本相同: Polymorphic Association with multiple associations on the same model

However, the proposed/accepted solution does not work, as illustrated by a commenter later.但是,提议/接受的解决方案不起作用,正如稍后评论者所说明的那样。

I have a Photo class that is used all over my app.我有一张照片 class 在我的整个应用程序中使用。 A post can have a single photo.一个帖子可以有一张照片。 However, I want to re-use the polymorphic relationship to add a secondary photo.但是,我想重新使用多态关系来添加辅助照片。

Before:前:

class Photo 
   belongs_to :attachable, :polymorphic => true
end

class Post
   has_one :photo, :as => :attachable, :dependent => :destroy
end

Desired:期望:

class Photo 
   belongs_to :attachable, :polymorphic => true
end

class Post
   has_one :photo,           :as => :attachable, :dependent => :destroy
   has_one :secondary_photo, :as => :attachable, :dependent => :destroy
end

However, this fails as it cannot find the class "SecondaryPhoto".但是,这失败了,因为它找不到 class“SecondaryPhoto”。 Based on what I could tell from that other thread, I'd want to do:根据我从其他线程中可以看出的内容,我想做的是:

   has_one :secondary_photo, :as => :attachable, :class_name => "Photo", :dependent => :destroy

Except calling Post#secondary_photo simply returns the same photo that is attached via the Photo association, eg Post#photo === Post#secondary_photo.除了调用 Post#secondary_photo 仅返回通过照片关联附加的相同照片,例如 Post#photo === Post#secondary_photo。 Looking at the SQL, it does WHERE type = "Photo" instead of, say, "SecondaryPhoto" as I'd like...查看 SQL,它会在 WHERE type = "Photo" 而不是我想要的 "SecondaryPhoto" ...

Thoughts?想法? Thanks!谢谢!

I have done that in my project.我在我的项目中做到了这一点。

The trick is that photos need a column that will be used in has_one condition to distinguish between primary and secondary photos.诀窍是照片需要一个列,将在 has_one 条件下使用以区分主要和次要照片。 Pay attention to what happens in :conditions here.注意这里:conditions发生的事情。

has_one :photo, :as => 'attachable', 
        :conditions => {:photo_type => 'primary_photo'}, :dependent => :destroy

has_one :secondary_photo, :class_name => 'Photo', :as => 'attachable',
        :conditions => {:photo_type => 'secondary_photo'}, :dependent => :destroy

The beauty of this approach is that when you create photos using @post.build_photo , the photo_type will automatically be pre-populated with corresponding type, like 'primary_photo'.这种方法的美妙之处在于,当您使用@post.build_photo创建照片时,photo_type 将自动预先填充相应的类型,例如“primary_photo”。 ActiveRecord is smart enough to do that. ActiveRecord 足够聪明,可以做到这一点。

In Rails 5 you have to define attr_accessor for :attachable_id and specify for relation :class_name and :foreign_key options only.Rails 5 中,您必须为 :attachable_id 定义 attr_accessor并仅指定关系:class_name 和 :foreign_key选项。 You will get ...AND attachable_type = 'SecondaryPhoto' if as: :attachable used你会得到 ...AND attachable_type = 'SecondaryPhoto' 如果 as: :attachable used

class Post
  attr_accessor :attachable_id
  has_one :photo, :as => :attachable, :dependent => :destroy
  has_one :secondary_photo, -> { where attachable_type: 'SecondaryPhoto' }, class_name: "Photo", dependent: :destroy, foreign_key: :attachable_id

Rails 4.2+导轨 4.2+

class Photo
   belongs_to :attachable, :polymorphic => true
end

class Post
   has_one :photo, :as => :attachable, :dependent => :destroy
   has_one :secondary_photo, -> { where attachable_type: "SecondaryPhoto"},
     class_name: Photo, foreign_key: :attachable_id,
     foreign_type: :attachable_type, dependent: :destroy
end

You need to provide foreign_key according ....able'ness or Rails will ask for post_id column in photo table.您需要根据 ....able'ness 提供外键,否则 Rails 会要求在 photo 表中提供 post_id 列。 Attachable_type column will fills with Rails magic as SecondaryPhoto Attachable_type 列将填充 Rails 魔法作为SecondaryPhoto

None of the previous answers helped me solve this problem, so I'll put this here incase anyone else runs into this.以前的答案都没有帮助我解决这个问题,所以我会把它放在这里以防其他人遇到这个问题。 Using Rails 4.2 +.使用 Rails 4.2 +。

Create the migration (assuming you have an Addresses table already):创建迁移(假设您已经有一个 Addresses 表):

class AddPolymorphicColumnsToAddress < ActiveRecord::Migration
  def change
    add_column :addresses, :addressable_type, :string, index: true
    add_column :addresses, :addressable_id, :integer, index: true
    add_column :addresses, :addressable_scope, :string, index: true
  end
end

Setup your polymorphic association:设置你的多态关联:

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true
end

Setup the class where the association will be called from:设置将从中调用关联的类:

class Order < ActiveRecord::Base
  has_one :bill_address, -> { where(addressable_scope: :bill_address) }, as: :addressable,  class_name: "Address", dependent: :destroy
  accepts_nested_attributes_for :bill_address, allow_destroy: true

  has_one :ship_address, -> { where(addressable_scope: :ship_address) }, as: :addressable, class_name: "Address", dependent: :destroy
  accepts_nested_attributes_for :ship_address, allow_destroy: true
end

The trick is that you have to call the build method on the Order instance or the scope column won't be populated.诀窍是您必须在Order实例上调用 build 方法,否则将不会填充scope列。

So this does NOT work:所以这不起作用:

address = {attr1: "value"... etc...}
order = Order.new(bill_address: address)
order.save!

However, this DOES WORK.但是,这确实有效。

address = {attr1: "value"... etc...}
order = Order.new
order.build_bill_address(address)
order.save!

Hope that helps someone else.希望能帮助别人。

Something like following worked for querying, but assigning from User to address didn't work像下面这样的东西用于查询,但从用户分配到地址不起作用

User Class用户类

has_many :addresses, as: :address_holder
has_many :delivery_addresses, -> { where :address_holder_type => "UserDelivery" },
       class_name: "Address", foreign_key: "address_holder_id"

Address Class地址类

belongs_to :address_holder, polymorphic: true

Future reference for people checking this post供查看此帖子的人的未来参考

This can be achieved using the following code...这可以使用以下代码实现...

Rails 3:轨道 3:

has_one :banner_image, conditions: { attachable_type: 'ThemeBannerAttachment' }, class_name: 'Attachment', foreign_key: 'attachable_id', dependent: :destroy

Rails 4:导轨 4:

has_one :banner_image, -> { where attachable_type: 'ThemeBannerAttachment'}, class_name: 'Attachment', dependent: :destroy

Not sure why, but in Rails 3, you need to supply a foreign_key value alongside the conditions and class_name.不知道为什么,但在 Rails 3 中,您需要在条件和 class_name 旁边提供一个外键值。 Do not use 'as: :attachable' as this will automatically use the calling class name when setting the polymorphic type.不要使用 'as::attachable' 因为这将在设置多态类型时自动使用调用类名。

The above applies to has_many too.以上也适用于 has_many。

I didn't use it, but I googled around and looked into Rails sources and I think that what you're looking for is :foreign_type .我没有使用它,但我用谷歌搜索并查看了 Rails 资源,我认为你要找的是:foreign_type Try it and tell if it works :)试试看,看看它是否有效:)

has_one :secondary_photo, :as => :attachable, :class_name => "Photo", :dependent => :destroy, :foreign_type => 'SecondaryPost'

I think that type in your question should be Post instead of Photo and, respectively, it would be better to use SecondaryPost as it assigned to Post model.我认为您的问题中的类型应该是Post而不是Photo并且分别使用SecondaryPost会更好,因为它分配给Post模型。

EDIT:编辑:

Above answer is completly wrong.以上答案完全错误。 :foreign_type is availble in polymorphic model in belongs_to association to specify name of the column that contains type of associated model. :foreign_typebelongs_to关联中的多态模型中belongs_to用于指定包含关联模型类型的列的名称。

As I look in Rails sources, this line sets this type for association:当我查看 Rails 源代码时,这一行设置了这种关联类型:

dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]

As you can see it uses base_class.name to get type name.如您所见,它使用base_class.name来获取类型名称。 As far as I know you can do nothing with it.据我所知,您对此无能为力。

So my sugestion is to add one column to Photo model, on example: photo_type .所以我的建议是向 Photo 模型添加一列,例如: photo_type And set it to 0 if it is first photo, or set it to 1 if it is second photo.如果是第一张照片,则将其设置为 0,如果是第二张照片,则将其设置为 1。 In your associations add :conditions => {:photo_type => 0} and :conditions => {:photo_type => 1} , respectively.在您的关联中分别添加:conditions => {:photo_type => 0}:conditions => {:photo_type => 1} I know it is not a solution you are looking for, but I can't find anything better.我知道这不是您正在寻找的解决方案,但我找不到更好的解决方案。 By the way, maybe it would be better to just use has_many association?顺便说一句,也许只使用has_many关联会更好?

Your going to have to monkey patch the notion of foreign_type into has_one relationship.您将不得不将foreign_type 的概念修补到has_one 关系中。 This is what i did for has_many.这就是我为 has_many 所做的。 In a new .rb file in your initializers folder i called mine add_foreign_type_support.rb It lets you specify what your attachable_type is to be.在您的初始化文件夹中的一个新 .rb 文件中,我将其命名为 add_foreign_type_support.rb 它可以让您指定您的 attachable_type 是什么。 Example: has_many photo, :class_name => "Picture", :as => attachable, :foreign_type => 'Pic'示例:has_many photo, :class_name => "Picture", :as => attachable, :foreign_type => 'Pic'

module ActiveRecord
  module Associations
    class HasManyAssociation < AssociationCollection #:nodoc:
      protected
        def construct_sql
          case
            when @reflection.options[:finder_sql]
              @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
           when @reflection.options[:as]
              resource_type = @reflection.options[:foreign_type].to_s.camelize || @owner.class.base_class.name.to_s
              @finder_sql =  "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND "
              @finder_sql += "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(resource_type)}"
              else
                @finder_sql += ")"
              end
              @finder_sql << " AND (#{conditions})" if conditions

            else
              @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
              @finder_sql << " AND (#{conditions})" if conditions
          end

          if @reflection.options[:counter_sql]
            @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
          elsif @reflection.options[:finder_sql]
            # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
            @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
            @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
          else
            @counter_sql = @finder_sql
          end
        end
    end
  end
end
# Add foreign_type to options list
module ActiveRecord
  module Associations # :nodoc:
     module ClassMethods
      private
        mattr_accessor :valid_keys_for_has_many_association
        @@valid_keys_for_has_many_association = [
          :class_name, :table_name, :foreign_key, :primary_key, 
          :dependent,
          :select, :conditions, :include, :order, :group, :having, :limit, :offset,
          :as, :foreign_type, :through, :source, :source_type,
          :uniq,
          :finder_sql, :counter_sql,
          :before_add, :after_add, :before_remove, :after_remove,
          :extend, :readonly,
          :validate, :inverse_of
        ]

    end
  end

None of these solutions seem to work on Rails 5. For some reason, it looks like the behaviour around the association conditions has changed.这些解决方案似乎都不适用于 Rails 5。出于某种原因,关联条件的行为似乎发生了变化。 When assigning the related object, the conditions don't seem to be used in the insert;分配相关对象的时候,插入中好像没有用到条件; only when reading the association.只有在阅读协会时。

My solution was to override the setter method for the association:我的解决方案是覆盖关联的 setter 方法:

has_one :photo, -> { photo_type: 'primary_photo'},
        as: 'attachable',
        dependent: :destroy

def photo=(photo)
  photo.photo_type = 'primary_photo'
  super
end

For mongoid use this solution对于mongoid使用此解决方案

Had tough times after discovering this issue but got cool solution that works发现这个问题后度过了艰难的时期,但得到了很酷的解决方案

Add to your Gemfile添加到您的 Gemfile

gem 'mongoid-multiple-polymorphic' gem 'mongoid-multiple-polymorphic'

And this works like a charm:这就像一个魅力:

  class Resource

  has_one :icon, as: :assetable, class_name: 'Asset', dependent: :destroy, autosave: true
  has_one :preview, as: :assetable, class_name: 'Asset', dependent: :destroy, autosave: true

  end

Can you add a SecondaryPhoto model like:您可以添加一个 SecondaryPhoto 模型,例如:

class SecondaryPhoto < Photo
end

and then skip the :class_name from the has_one :secondary_photo?然后从 has_one :secondary_photo 中跳过 :class_name?

Might be a bit late, but this might help someone so here is how I fix this ( rails 5.2 , ruby 2.6 ):可能有点晚了,但这可能会对某人有所帮助,所以这是我解决此问题的方法( rails 5.2ruby 2.6 ):

I added an enum , called kind to the model and then added the proper scope to the has_one association:我向模型添加了一个名为kindenum ,然后向has_one关联添加了适当的范围:

class Photo 
   belongs_to :attachable, :polymorphic => true
   enum kind: %i[first_photo secondary_photo]
end

class Post
   has_one :photo, -> { where(kind: :first_photo) }, :as => :attachable, :dependent => :destroy
   has_one :secondary_photo, -> { where(kind: :secondary_photo) }, :as => :attachable, :dependent => :destroy
end

The scope is needed because ActiveRecord can discriminate between the objects/association.需要范围是因为 ActiveRecord 可以区分对象/关联。

Hope the above helps!希望以上有帮助! 👌 👌

  has_one :photo, -> { where attachable_type: "Photo" }, foreign_key: :attachable_id, class_name: Attachment.to_s, dependent: :destroy
  has_one :logo, -> { where attachable_type: "Logo" }, foreign_key: :attachable_id, class_name: Attachment.to_s, dependent: :destroy

when attaching:附加时:

  ActiveRecord::Base.transaction do
     attachment = user.attachments.find( id )
     user.logo = attachment
     user.save

     attachment.update( attachable_type: "Logo" )
     attachment.save
  end

Using rails 7, I used STI to solve this problem.使用rails 7,我用STI解决了这个问题。 This approach is simple and cleaner as I didn't have to state the class_name: in the association and the document type gets populated automatically.这种方法简单明了,因为我不必 state class_name:在关联中,文档类型会自动填充。

# migration file
class CreateDocuments < ActiveRecord::Migration[7.0]
  def change
    create_table :documents, id: :uuid do |t|
      t.string :type, null: false
      t.references :entity, type: :uuid, polymorphic: true
      ...
    end
  end
end

# Document model i.e the base model
class Document < ApplicationRecord
  belongs_to :entity, polymorphic: true
  ...
end

# Sub classes of Document
class InsuranceSticker < Document
end

class RoadWorthyCertificate < Document
end
...


# Vehicles and Drivers are entities that can hold documents
class Vehicle < ApplicationRecord
  has_one :insurance_sticker, as: :entity, dependent: :destroy
  has_one :road_worthy_certificate, as: :entity, dependent: :destroy

  # nested
  accepts_nested_attributes_for(:insurance_sticker, :road_worthy_certificate)
  ...
end

class Driver < User
  has_one drivers_license, as: :entity, dependent: :destroy

  # nested
  accepts_nested_attributes_for(
    :drivers_license,
    :reverse_side_of_drivers_license
  )
end

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM