Eager Load Depending on Type of Association in Ruby on Rails

后端 未结 7 2338
温柔的废话
温柔的废话 2021-02-07 22:13

I have a polymorphic association (belongs_to :resource, polymorphic: true) where resource can be a variety of different models. To simplify the questio

相关标签:
7条回答
  • 2021-02-07 22:20

    You can do that with the help of ActiveRecord::Associations::Preloader class. Here is the code:

    @issues = Issue.all # Or whatever query
    ActiveRecord::Associations::Preloader.new.preload(@issues.select { |i| i.resource_type == "Order" }, { resource: :address })
    ActiveRecord::Associations::Preloader.new.preload(@issues.select { |i| i.resource_type == "Customer" }, { resource: :location })
    

    You can use different approach when filtering the collection. For example, in my project I am using group_by

    groups = sale_items.group_by(&:item_type)
    groups.each do |type, items|
      conditions = case type
      when "Product" then :item
      when "Service" then { item: { service: [:group] } }
    end
    
    ActiveRecord::Associations::Preloader.new.preload(items, conditions)
    

    You can easily wrap this code in some helper class and use it in different parts of your app.

    0 讨论(0)
  • 2021-02-07 22:21

    I would like to share one of my query that i have used for conditional eager loading but not sure if this might help you, which i am not sure but its worth a try.

    i have an address model, which is polymorphic to user and property. So i just check the addressable_type manually and then call the appropriate query as shown below:-

    after getting either user or property,i get the address to with eager loading required models

    ##@record can be user or property instance
    if @record.class.to_s == "Property"
         Address.includes(:addressable=>[:dealers,:property_groups,:details]).where(:addressable_type=>"Property").joins(:property).where(properties:{:status=>"active"})
    else if @record.class.to_s == "User"
         Address.includes(:addressable=>[:pictures,:friends,:ratings,:interests]).where(:addressable_type=>"User").joins(:user).where(users:{is_guest:=>true})
    end
    

    The above query is a small snippet of actual query, but you can get an idea about how to use it for eager loading using joins because its a polymorphic table.

    Hope it helps.

    0 讨论(0)
  • 2021-02-07 22:23

    You need to define associations in models like this:

    class Issue < ActiveRecord::Base
      belongs_to :resource, polymorphic: true
    
      belongs_to :order, -> { includes(:issues).where(issues: { resource_type: 'Order' }) }, foreign_key: :resource_id
      belongs_to :customer, -> { includes(:issues).where(issues: { resource_type: 'Customer' }) }, foreign_key: :resource_id
    end
    
    class Order < ActiveRecord::Base
      belongs_to :address
      has_many :issues, as: :resource
    end
    
    class Customer < ActiveRecord::Base
      belongs_to :location
      has_many :issues, as: :resource
    end
    

    Now you may do required preload:

    Issue.includes(order: :address, customer: :location).all
    

    In views you should use explicit relation name:

    <%- @issues.each do |issue| -%>
    <%- case issue.resource -%>
    <%- when Customer -%>
    <%= issue.customer.name %> <%= issue.customer.location.name %>
    <%- when Order -%>
    <%= issue.order.number %> <%= issue.order.address.details %>
    <%- end -%>
    

    That's all, no more n-plus-one queries.

    0 讨论(0)
  • 2021-02-07 22:29

    This is now working in Rails v6.0.0.rc1: https://github.com/rails/rails/pull/32655

    You can do .includes(resource: [:address, :location])

    0 讨论(0)
  • 2021-02-07 22:33

    I've come up with a viable solution for myself when I was stuck in this problem. What I followed was to iterate through each type of implementations and concatenate it into an array.

    To start with it, we will first note down what attributes will be loaded for a particular type.

    ATTRIBS = {
      'Order' => [:address],
      'Customer' => [:location]
    }.freeze
    
    AVAILABLE_TYPES = %w(Order Customer).freeze
    

    The above lists out the associations to load eagerly for the available implementation types.

    Now in our code, we will simply iterate through AVAILABLE_TYPES and then load the required associations.

    issues = []
    
    AVAILABLE_TYPES.each do |type|
      issues += @issues.where(resource_type: type).includes(resource: ATTRIBS[type])
    end
    

    Through this, we have a managed way to preload the associations based on the type. If you've another type, just add it to the AVAILABLE_TYPES, and the attributes to ATTRIBS, and you'll be done.

    0 讨论(0)
  • 2021-02-07 22:41

    You can break out your polymorphic association into individual associations. I have followed this and been extremely pleased at how it has simplified my applications.

    class Issue
      belongs_to :order
      belongs_to :customer
    
      # You should validate that one and only one of order and customer is present.
    
      def resource
        order || customer
      end
    end
    
    Issue.preload(order: :address, customer: :location)
    

    I have actually written a gem which wraps up this pattern so that the syntax becomes

    class Issue
      has_owner :order, :customer, as: :resource
    end
    

    and sets up the associations and validations appropriately. Unfortunately that implementation is not open or public. However, it is not difficult to do yourself.

    0 讨论(0)
提交回复
热议问题