如何为自定义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的预加载逻辑:
- 执行自定义SQL获取结果集
- 提取所有关联ID(如user_id列表)
- 批量查询对应的ActiveRecord模型(如
User.where(id: user_ids)) - 把模型映射回结果对象,避免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
相关产品推荐
相关产品推荐

