Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
208 views
in Technique[技术] by (71.8m points)

ruby on rails - Looking for advise: Cleanest way to write scopes for filtering purposes


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

None of the above.

I would say that the cleanest way is to neither burdon your controller or User model further. Instead create a separate object which can be tested in isolation.

# Virtual model which represents a search query.
class UserQuery
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :first_name
  attribute :last_name
  attribute :email
  attribute :project_name

  # Loops through the attributes of the object and contructs a query 
  # will call 'filter_by_attribute_name' if present.
  # @param [ActiveRecord::Relation] base_scope - is not mutated
  # @return [ActiveRecord::Relation]
  def resolve(base_scope = User.all)
    valid_attributes.inject(base_scope) do |scope, key|
      if self.respond_to?("filter_by_#{key}")
        scope.merge(self.send("filter_by_#{key}"))
      else
        scope.where(key => self.send(key))
      end
    end
  end

  private 

  def filter_by_project_name
    User.joins(:projects)
        .where(projects: { name: project_name })
  end

  # Using compact_blank is admittedly a pretty naive solution for testing 
  # if attributes should be used in the query - but you get the idea.
  def valid_attributes
    attributes.compact_blank.keys
  end
end

This is especially relevant when you're talking about a User class which usually is the grand-daddy of all god classes in a Rails application.

The key to the elegance here is using Enumerable#inject which lets you iterate accross the attributes and add more and more filters successively and ActiveRecord::SpawnMethods#merge which lets you mosh scopes together. You can think of this kind of like calling .where(first_name: first_name).where(last_name: last_name)... except in a loop.

Usage:

@users = UserQuery.new(
  params.permit(:first_name, :last_name, :email, :project_name)
).resolve

Having a model means that you can use it for form bindings:

<%= form_with(model: @user_query, url: '/users/search') do |f| %>
  # ...
<% end %>

And add validations and other features without making a mess.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...