You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为自定义SQL查询创建兼容ActiveRecord的类以启用预加载?

问题

我正在尝试寻找一种简洁的方式将自定义SQL查询与ActiveRecord结合使用,核心需求是利用ActiveRecord的预加载(preloader/include)系统。

理想实现方式的超简化示例如下:

class ArticleViewsByUser

  query <<-SQL
    SELECT user_id, SUM(articles.view_count) as view_count
    FROM articles
    GROUP BY articles.user_id 
  SQL

  class Row
    belongs_to :user
    attr_reader :view_count
  end

end

class ArticleViewsByCategory

  query <<-SQL
    SELECT category_id, SUM(articles.view_count) as view_count
    FROM articles
    GROUP BY articles.category_id 
  SQL

  class Row
    belongs_to :category
    attr_reader :view_count
  end

end

# 使用方式
ArticleViewsByUser.new.includes(:user)
# ...
ArticleViewsByUser.new.includes(:category)
# ...

我知道仅通过User和Article模型也能实现,但需要在这些模型中添加无关代码(通过attr_reader设置伪字段),当需计算10+个字段时,这种方式会迅速失控。

创建数据库视图是一种解决方案,但会带来大量数据库管理开销。目前尚未找到相关方案,是否存在此类工具?比如相关gem?

解决方案

1. 使用 poro_preloader gem

专门为普通Ruby对象(PORO)提供ActiveRecord风格的预加载能力,可在自定义查询返回的Row类中集成,手动关联对应ActiveRecord模型后调用预加载方法。示例调整:

require 'poro_preloader'

class ArticleViewsByUser
  def self.query
    <<-SQL
      SELECT user_id, SUM(articles.view_count) as view_count
      FROM articles
      GROUP BY articles.user_id 
    SQL
  end

  def self.all
    results = ActiveRecord::Base.connection.execute(query)
    results.map { |row| Row.new(row) }
  end

  class Row
    include PoroPreloader::Preloadable
    attr_reader :user_id, :view_count

    def initialize(attributes)
      @user_id = attributes['user_id']
      @view_count = attributes['view_count']
    end

    def user
      @user ||= User.find(user_id)
    end

    preload_association :user, ->(rows) { User.where(id: rows.map(&:user_id)) }
  end
end

# 使用预加载
rows = ArticleViewsByUser.all
PoroPreloader.preload(rows, :user)

2. 使用 active_record_extended gem

该gem扩展了ActiveRecord的查询能力,支持自定义SQL片段同时保留ActiveRecord的关联和预加载特性。可直接构建带聚合的查询,无需单独定义PORO类,还能直接调用includes预加载关联模型。

3. 手动实现轻量级预加载

若不想引入额外gem,可手动模拟ActiveRecord的预加载逻辑:

  1. 执行自定义SQL获取结果集
  2. 提取所有关联ID(如user_id列表)
  3. 批量查询对应的ActiveRecord模型(如User.where(id: user_ids))
  4. 把模型映射回结果对象,避免N+1查询

示例:

class ArticleViewsByUser
  def self.with_preloaded_user
    results = ActiveRecord::Base.connection.execute(<<-SQL)
      SELECT user_id, SUM(articles.view_count) as view_count
      FROM articles
      GROUP BY articles.user_id 
    SQL

    user_ids = results.map { |r| r['user_id'] }
    users = User.where(id: user_ids).index_by(&:id)

    results.map do |row|
      Row.new(row.merge(user: users[row['user_id']]))
    end
  end

  class Row
    attr_reader :user, :view_count

    def initialize(attributes)
      @user = attributes[:user]
      @view_count = attributes['view_count']
    end
  end
end

# 使用
rows = ArticleViewsByUser.with_preloaded_user

内容的提问来源于stack exchange,提问作者maxigs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 20:13:23