Ruby on Rails 设计模式(第二部分):查询对象

2025-06-07

Ruby on Rails 设计模式(第二部分):查询对象

这篇文章是 Ruby on Rails 设计模式系列文章的第二部分。
其他部分请见:
第一部分

查询对象

查询对象是一种模式,它通过将复杂的 SQL 查询或范围提取到易于重用和测试的分离类中,帮助分解您的肥胖 ActiveRecord 模型并保持您的代码精简和可读。

命名约定

通常位于app/queries目录中,当然也可以组织到多个命名空间中。
典型的文件名有_query后缀,类名也有Query后缀。
例如:
Posts::ActivePostsQuery

module Posts
  class ActivePostsQuery
    def self.call
      Post.where(status: 'active', deleted_at: nil)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

类方法更加实用,也更容易存根。

有没有办法把这个查询拆分成多个部分,从而提高可重用性?有的,就是使用链接方法。

class PostsQuery
  def initialize(posts = Post.all)
    @posts = posts
  end

  def active
    @posts.where(active: true, pending: false)
  end

  def pending
    @posts.where(pending: true, active: false)
  end

  def deleted
    @posts.with_deleted
  end
end
Enter fullscreen mode Exit fullscreen mode

这边走:

query = PostsQuery.new
query.deleted.pending
Enter fullscreen mode Exit fullscreen mode

查询对象和模型范围

我在 Post 类中定义了以下范围:

class Post < ActiveRecord::Base
  scope :active, -> {
    where(section: ['web', 'mobile'], status: 'active', deleted_at: nil)
  }
end
Enter fullscreen mode Exit fullscreen mode

并且您想要提取此查询并保留此行为Post.active,您该怎么做呢?

解决方案显而易见:

class Post < ActiveRecord::Base
  def self.active
    ActivePostsQuery.call
  end
end
Enter fullscreen mode Exit fullscreen mode

但它添加了更多代码,并且在范围定义中不再可见。

使用带有查询对象的作用域

不要忘记你的查询对象必须返回一个关系。

class Post < ActiveRecord::Base
  scope :active, ActivePostsQuery
end
Enter fullscreen mode Exit fullscreen mode

让我们设计我们的查询对象类:

class ActivePostsQuery
  class << self
    delegate :call, to: :new
  end

  def initialize(relation = Post)
    @relation = relation
  end

  def call
    @relation.where(status: 'active', deleted_at: nil)
  end
end
Enter fullscreen mode Exit fullscreen mode

现在查询仍然可以通过Post.active范围获得。

重构

让我们看看在重构更大的查询时如何在实践中使用查询对象。

class PostsController < ApplicationController
  def index
    @posts = Post.where(active: true, deleted_at: nil)
      .joins(:authors).where(emails: { active: true })
  end
end
Enter fullscreen mode Exit fullscreen mode

如果不查询数据库,编写此操作的测试就无法进行。
将此查询提取到单独的类后,会变得更容易:

class ActivePostsWithAuthorQuery
  attr_reader :relation

  def self.call(relation = Post)
    new(relation).call
  end

  def new(relation = Post)
    @relation = relation
  end

  def call
    active.with_author
  end

  def active
    relation.where(active: true, deleted_at: nil)
  end

  def with_author
    relation.joins(:authors).where(emails: { active: true })
  end
end
Enter fullscreen mode Exit fullscreen mode

现在可以更新控制器了:

class PostsController < ApplicationController
  def index
    @posts = ActivePostsWithAuthorQuery.call
  end
end
Enter fullscreen mode Exit fullscreen mode

测试

查询不再是控制器关注的问题,它简化了您的测试,现在您只需调用allow(ActivePostsWithAuthorQuery).to receive(:call).and_return(...)您的规范即可。

结论

总是存在权衡,并且转移到单独的类并不总是一个好主意,它会给你的代码增加一层复杂性并增加可维护性成本,请谨慎使用这种模式,并享受你的可扩展代码。

文章来源:https://dev.to/renatamarques97/design-patterns-with-ruby-on-rails-part-2-query-object-1h65
PREV
使其简短 - 使其更好
NEXT
4分钟了解微服务 - 微服务简介