Initialize instance variable through named scope
-
05-07-2021 - |
문제
Is it possbile to initialize an instance variable through a named scope? I want to remember my search criteria after the initial search. See the following:
class Foo < ActiveRecord::Base
attr_accessor :search
named_scope :bar, lambda {|search|
# Do something here to set the @search variable to search
{
:conditions => [
"name LIKE :search OR description LIKE :search",
{:search => "%#{search}%"} ]
}
}
def match
if name.match(@search)
:name
elsif description.match(@search)
:description
end
end
end
Alternatively, I could create a class method that calls the named scope and then iterates through the results to set the instance variable on each, but I lose the named scope chainability in that case.
해결책
named scopes are not instantiating any single record, until they are iterated. And they may be, as you correctly say, be chained with more scopes.
I see two possible solutions. One relies on the database to populate a fake column with the matching value. It should be simple using a special :select
option to the query (or a special argument to .select()
in rails 3)
The other is to rely on the controller. This is much easier to implement, but you have to ensure that the "match" in ruby is the same than the "LIKE" in the database. I'm speaking about collation, unicode normalized forms etc. Most probably there is at least one case in which they behave differently in both engines.
다른 팁
thanks alot for the latest post with inspiration of how to do it. This monkey patches still work with Rails 5.
I just encountered the situation, that the context wasnt forwarded, when chaining the scope with "where()" conditions.
To accomplish this, I added another patch to Association's CollectionProxy, which seem to work for my current situation.
Perhaps it is useful for you or somebody else:
module ActiveRecord
module Associations
class CollectionProxy
alias_method :original_scope, :scope
def scope
return @scope if @scope
original_scope.tap { |new_scope|
new_scope.scope_context = scope_context
}
end
end
end
end
I've been frantically trying to accomplish this same problem for the past week and have finally managed it. Since I found this question amidst my troubles, I figured that I'd share the answer here just in case anyone else finds themselves faced with a similar problem.
As rewritten stated in his answer, you can't set an instance variable on your model from the scope as there is no model instance yet. The scope is just for building up the sql expression that will be evaluated when you try to iterate over the result of the scope. What does exist, however, is the ActiveRecord::Relation object. This is the thing that holds the details of your scope. By putting the variables you want to keep on that object, they'll be still accessible later.
So, how to get the variable on to the Relation object? Monkey patching comes to your rescue here:
/lib/core_ext/add_scope_context_to_activerecord_relation.rb:
module ActiveRecord
class Relation
attr_writer :scope_context
def scope_context
@scope_context ||= {}
end
end
module SpawnMethods
alias_method :old_merge, :merge
def merge(r)
merged = old_merge(r)
merged.scope_context.deep_merge! r.scope_context
merged
end
end
end
Now, in all of your scopes, you have a scope_context variable. The SpawnMethods#merge malarky is to make sure that the scope_context is kept along a chain of scopes (eg Foo.search(xxx).sort(xxx).paginate(xxx)
)
/app/concerns/searchable.rb:
require 'active_support/concern'
module Searchable
extend ActiveSupport::Concern
included do
self.scope :search, Proc.new { |params|
s = scoped
s.scope_context[:searchable] = {}
s.scope_context[:searchable][:params] = parse_params params
s.scope_context[:searchable][:params].each do |param|
s = s.where generate_where_expression param
end
s
} do
def search_val col_name
param = scope_context[:searchable][:params].find do |param|
param[:col_name] == field.to_s
end
param.nil? ? '' : param[:val]
end
end
end
module ClassMethods
def parse_params params
# parse and return the params
end
def generate_where_expression param
"#{param[:col_name]} like '%#{param[:val]}%'"
end
end
end
Now, our controller, model and view will look like the following:
/app/controllers/foo_controller.rb:
class FooController < ApplicationController
def index
@foos = Foo.search(params[:search])
end
end
/app/models/foo.rb:
class Foo < ActiveRecord::Base
include Searchable
attr_accessor :name, :description
end
/app/views/foos/index.html.erb:
<p>You searched for:</p>
<table>
<tr>
<th>Name</th>
<td><%= @foos.search_val :name %>
</tr>
<tr>
<th>Description</th>
<td><%= @foos.search_val :description %>
</tr>
</table>
<p>And you got:</p>
<table>
<% @foos.each do |foo| %>
<tr> xxx </tr>
<% end %>
</table>
Now, you may have noticed the core_ext and concerns directories alluded to above. Your application will need to be made aware of these:
/config/application.rb:
# Should be fairly obvious where to put this line
config.autoload_paths += Dir[Rails.root.join('app', 'concerns', '{**}')]
/config/initializers/load_extensions.rb:
Dir[File.join(Rails.root, "lib", "core_ext", "*.rb")].each {|l| require l }
Don't forget to restart your server and away you go (assuming I haven't forgotten to include anything).
Oh, and I've come up with this solution with Rails 3.2.13 and Ruby 1.9.3; no idea how it behaves with other versions.