Ruby on rails 轨道N+;1查询:monkeypatching ActiveRecord::关系#as_json 处境

Ruby on rails 轨道N+;1查询:monkeypatching ActiveRecord::关系#as_json 处境,ruby-on-rails,ruby,activerecord,Ruby On Rails,Ruby,Activerecord,我有一个型号用户: def User has_many :cars def cars_count cars.count end def as_json options = {} super options.merge(methods: [:cars_count]) end end def User ... def self.as_json options = {} cars_counts = Car.group(:user_id).cou

我有一个型号
用户

def User
  has_many :cars

  def cars_count
    cars.count
  end

  def as_json options = {}
    super options.merge(methods: [:cars_count])
  end
end
def User
  ...
  def self.as_json options = {}
    cars_counts = Car.group(:user_id).count
    self.map do |user|
      user.define_singleton_method(:cars_count) do
        cars_counts[user.id]
      end
      user.as_json options
    end
  end
end
问题 当我需要将一组用户呈现给json时,我最终会遇到N+1查询问题。我的理解是,包括汽车并不能解决我的问题

试图修复 我想做的是向
User
添加一个方法:

def User
  has_many :cars

  def cars_count
    cars.count
  end

  def as_json options = {}
    super options.merge(methods: [:cars_count])
  end
end
def User
  ...
  def self.as_json options = {}
    cars_counts = Car.group(:user_id).count
    self.map do |user|
      user.define_singleton_method(:cars_count) do
        cars_counts[user.id]
      end
      user.as_json options
    end
  end
end
这样,所有的汽车计数都将在一个查询中查询

剩余问题
ActiveRecord::Relation
已经有一个
as\u json
方法,因此不选择类定义的方法。定义类时,如何使
ActiveRecord::Relation
使用
as_json
方法?有更好的方法吗

编辑 1.缓存 我可以缓存我的
cars\u count
方法:

def cars_count
  Rails.cache.fetch("#{cache_key}/cars_count") do
    cars.count
  end
end  
一旦缓存变热,这很好,但是如果同时更新了很多用户,则可能会导致请求超时,因为在单个请求中必须更新很多查询

2.专用方法 我不需要将我的方法
称为_json
,我可以将它称为
我的_专用的_as_json_方法
,每次我需要呈现一组用户时,我都可以将它称为

render json: users


然而,我不喜欢这样做。我可能会忘记在某个地方调用这个方法,其他人可能会忘记调用它,而我正在失去代码的清晰性。出于这些原因,猴子补丁似乎是一种更好的方法。

您是否考虑过对
车辆计数使用
计数器缓存
?它非常适合你想做的事情

这还提供了一些其他的替代方案,例如,如果您想手动构建散列

如果您确实想继续monkey修补程序,那么请确保您正在修补
ActiveRecord::Relation
而不是
User
,并重写实例方法而不是创建类方法。请注意,这将影响每个
ActiveRecord::relationship
,但您可以使用
@klass
添加一个只为
用户运行逻辑的条件

# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
  def as_json(options = nil)
    puts @klass
  end
end
选择1 在您的用户模型中:

def get_cars_count
  self.cars.count
end
在控制器中:

User.all.as_json(method: :get_cars_count)
选择2 您可以创建一个方法来获取所有用户及其汽车数量。然后你就可以在上面调用as_json方法了

它大致看起来像:

#In Users Model:

def self.users_with_cars
  User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')

  # OR may be something like this
  User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")

end
在控制器中,您可以调用
作为\u json

User.users_with_cars.as_json

这是我的解决方案,以防其他人感兴趣

# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'

ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization

# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'

module CoreExtensions
  module ActiveRecord
    module Relation
      module Serialization
        extend ActiveSupport::Concern

        included do
          old_as_json = instance_method(:as_json)

          define_method(:as_json) do |options = {}|
            if @klass.respond_to? :collection_as_json
              scoping do
                @klass.collection_as_json options
              end
            else
              old_as_json.bind(self).(options)
            end
          end
        end
      end
    end
  end
end

# app/models/user.rb
def User
  ...
  def self.collection_as_json options = {}
    cars_counts = Car.group(:user_id).count
    self.map do |user|
      user.define_singleton_method(:cars_count) do
        cars_counts[user.id]
      end
      user.as_json options
    end
  end
end

感谢@gwcodes为我指出
ActiveSupport::Concern

在我的控制器中,我希望能够
呈现json:car\u品牌。用户
呈现json:User。在最后一个小时内更新
以及更多。选项1有N+1查询问题,选项2意味着我每次都要用车给用户打电话,这不是干巴巴的。