Rails N + 1查询:monkeypatching ActiveRecord :: Relation#as_json

时间:2017-04-06 08:45:12

标签: ruby-on-rails ruby activerecord

情况

我有一个模型User

def User
  has_many :cars

  def cars_count
    cars.count
  end

  def as_json options = {}
    super options.merge(methods: [:cars_count])
  end
end

问题

当我需要向json呈现一组用户时,我最终会遇到N + 1查询问题。我的理解是,包括汽车并不能解决我的问题。

尝试修复

我想要做的是向User添加方法:

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_json方法,因此不会选择定义的类。如何在定义时使ActiveRecord::Relation使用类中的as_json方法?有更好的方法吗?

编辑

1。缓存

我可以缓存我的cars_count方法:

def cars_count
  Rails.cache.fetch("#{cache_key}/cars_count") do
    cars.count
  end
end  

一旦缓存温暖,这很好,但是如果很多用户同时更新,则可能导致请求超时,因为必须在单个请求中更新许多查询。

2。专用方法

我可以将其称为as_json而不是调用我的方法my_dedicated_as_json_method,而每次我需要渲染一组用户,而不是

render json: users

render json: users.my_dedicated_as_json_method

但是,我不喜欢这种做法。我可能忘记在某个地方调用此方法,其他人可能会忘记调用它,并且我正在失去代码的清晰度。由于这些原因,猴子补丁似乎是更好的途径。

3 个答案:

答案 0 :(得分:0)

您是否考虑过将counter_cache用于cars_count?它非常适合您想要做的事情。

blog article还提供了其他一些替代方案,例如如果你想手动建立一个哈希。

如果您真的想继续沿着猴子修补路线前进,请确保您正在修补ActiveRecord::Relation而不是User,并覆盖实例方法,而不是创建类方法。请注意,这会影响每个 ActiveRecord::Relation,但您可以使用@klass添加仅运行User的逻辑的条件

# 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 :(得分:0)

选项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

在您的控制器中,您可以拨打as_json

User.users_with_cars.as_json

答案 2 :(得分:0)

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

# 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