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
类方法更加实用,也更容易存根。
有没有办法把这个查询拆分成多个部分,从而提高可重用性?有的,就是使用链接方法。
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
这边走:
query = PostsQuery.new
query.deleted.pending
查询对象和模型范围
我在 Post 类中定义了以下范围:
class Post < ActiveRecord::Base
scope :active, -> {
where(section: ['web', 'mobile'], status: 'active', deleted_at: nil)
}
end
并且您想要提取此查询并保留此行为Post.active
,您该怎么做呢?
解决方案显而易见:
class Post < ActiveRecord::Base
def self.active
ActivePostsQuery.call
end
end
但它添加了更多代码,并且在范围定义中不再可见。
使用带有查询对象的作用域
不要忘记你的查询对象必须返回一个关系。
class Post < ActiveRecord::Base
scope :active, ActivePostsQuery
end
让我们设计我们的查询对象类:
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
现在查询仍然可以通过Post.active
范围获得。
重构
让我们看看在重构更大的查询时如何在实践中使用查询对象。
class PostsController < ApplicationController
def index
@posts = Post.where(active: true, deleted_at: nil)
.joins(:authors).where(emails: { active: true })
end
end
如果不查询数据库,编写此操作的测试就无法进行。
将此查询提取到单独的类后,会变得更容易:
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
现在可以更新控制器了:
class PostsController < ApplicationController
def index
@posts = ActivePostsWithAuthorQuery.call
end
end
测试
查询不再是控制器关注的问题,它简化了您的测试,现在您只需调用allow(ActivePostsWithAuthorQuery).to receive(:call).and_return(...)
您的规范即可。
结论
总是存在权衡,并且转移到单独的类并不总是一个好主意,它会给你的代码增加一层复杂性并增加可维护性成本,请谨慎使用这种模式,并享受你的可扩展代码。
文章来源:https://dev.to/renatamarques97/design-patterns-with-ruby-on-rails-part-2-query-object-1h65