What is good way to query chaining conditionally in rails? - ruby-on-rails

If I need to query conditionally, I try a way like this:
query = Model.find_something
query = query.where(condition1: true) if condition1 == true
query = query.where(condition2: true) if condition2 == true
query = query.where(condition3: true) if condition3 == true
It's work well.
But I think that it is a way to repeat same code and a look is not good.
Is it possible query does not reassign to variable per every conditional expression? like this:
query.where!(condition1: true) # But it can not be in rails :)
I recently started as Rails5.
What else is the best way to do Rails5?

You can use model scopes:
class Article < ApplicationRecord
scope :by_title, ->(title) { where(title: title) if title }
scope :by_author, ->(name) { where(author_name: name) if name }
end
Just chain scopes anywhere you need:
Article.by_title(params[:title]).by_author(params[:author_name])
If parameter present you get scoped articles, if there is no such parameter - you get all Articles

Related

How can I access a Rails scope lambda without calling it?

I would like to access the lamda defined in a rails scope as the lambda itself and assign it to a variable. Is this possible?
So if I have the following scope
scope :positive_amount, -> { where("amount > 0") }
I would like to be able to put this lambda into a variable, like "normal" lambda assignment:
positive_amount = -> { where("amount > 0") }
So something like this:
positive_amount = MyClass.get_scope_lambda(:positive_amount)
For clarification, I'm wanting the body of the method that I generally access with method_source gem via MyClass.instance_method(method).source.display. I'm wanting this for on-the-fly documentation of calculations that are taking place in our system.
Our invoicing calculations are combinations of smaller method and scopes. I'm trying to make a report that says how the calculations were reached, that uses the actual code. I've had luck with instance methods, but I'd like to show the scopes too:
Edit 1:
Following #mu's suggestion below, I tried:
Transaction.method(:positive_amount).source.display
But this returns:
singleton_class.send(:define_method, name) do |*args|
scope = all
scope = scope._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
scope
end
And not the body of the method as I'd expect.
If you say:
class MyClass < ApplicationRecord
scope :positive_amount, -> { where("amount > 0") }
end
then you're really adding a class method called positive_amount to MyClass. So if you want to access the scope, you can use the method method:
positive_amount = MyClass.method(:positive_amount)
#<Method: MyClass(...)
That will give you a Method instance but you can get a proc if you really need one:
positive_amount = MyClass.method(:positive_amount).to_proc
#<Proc:0x... (lambda)>
If I get your idea right. Here is one approach to do this
class SampleModel < ApplicationRecord
class << self
##active = ->(klass) { klass.where(active: true) }
##by_names = ->(klass, name) { klass.where("name LIKE ?", "%#{name}%") }
def get_scope_lambda(method_name, *args)
method = class_variable_get("###{method_name}")
return method.call(self, *args) if args
method.call(self)
end
end
end
So after that you can access the scopes like this:
SampleModel.get_scope_lambda(:by_names, "harefx")
SampleModel.get_scope_lambda(:active)
Or you can define some more class methods above, the one extra klass argument might be not ideal. But I don't find a way to access the self from inside the lambda block yet, so this is my best shot now.
By the way, I don't think this is a good way to use scope. But I just express your idea and to point it out that it's possible :D
UPDATED:
Here I come with another approach, I think it could solve your problem :D
class SampleModel < ApplicationRecord
scope :active, -> { where(active: true) }
scope :more_complex, -> {
where(active: true)
.where("name LIKE ?", "%#{name}%")
}
class << self
def get_scope_lambda(method_name)
location, _ = self.method(:get_scope_lambda).source_location
content = File.read(location)
regex = /scope\s:#{method_name}, -> {[\\n\s\w\(\):\.\\",?%\#{}]+}/
content.match(regex).to_s.display
end
end
end
So now you can try this to get the source
SampleModel.get_scope_lambda(:active)
SampleModel.get_scope_lambda(:more_complex)

Combine many scopes

I would like to combine two different scopes in my model. I have this:
Post_model
scope :with_tasks, -> { where(cat: 3).includes(:user).includes(task: :users) }
scope :with_events, -> { where(cat: 4).includes(:user).includes(event: :users) }
scope :with_comments, -> {where(comented: true).includes(comments: :user)}
Post_controller
def index
#posts = current_user.posts.with_tasks + current_user.posts.with_events
end
But I think it is not a really elegant way to achieve it, and I cannot include the comments scope.
Do you know a method to join this scopes into a new one (like the example below)?
scope :with_elements, -> { self.with_tasks.merge(self.with_events) }
What would allow me to call this method into my post#index:
#posts = current_user.posts.with_elements
TASKS = 3
EVENTS = 4
scope :with_tasks_and_or_events, ->(cat) {
cond = {}.tap do |c|
c.merge!(task: :users) if cat.include? TASKS
c.merge!(event: :users) if cat.include? EVENTS
end
where(cat: cat).includes(:user).includes(**cond)
}
And use it like:
with_tasks_and_or_events([TASKS])
with_tasks_and_or_events([TASKS, EVENTS])
Or, better, use Relational Algebra.
Or, even better, revise your database structure.

Is there a way to default ActiveRecord query by attribute to "any"?

I have an available attribute on a Product model which is a boolean.
I'm defining a helper method that takes an argument, for example:
def family_products(available: true)
Product.where(available: available)
end
This is fine for true or false -- but what I would like is a default of all.
Is it possible without creating a conditional wrapper?
I would define a scope in your Product model like this.
class Product < ActiveRecord::Base
scope :all_family_products, -> { where('available = ? or available = ?', true, false) }
end
Now calling Product.all_family_products returns both available products(available = true) and unavailable products(available = false)
Try this :
def family_products(available: [true, false])
Product.where(available: [available])
end

Is it possible to have a scope with optional arguments?

Is it possible to write a scope with optional arguments so that i can call the scope with and without arguments?
Something like:
scope :with_optional_args, lambda { |arg|
where("table.name = ?", arg)
}
Model.with_optional_args('foo')
Model.with_optional_args
I can check in the lambda block if an arg is given (like described by Unixmonkey) but on calling the scope without an argument i got an ArgumentError: wrong number of arguments (0 for 1)
Ruby 1.9 extended blocks to have the same features as methods do (default values are among them):
scope :cheap, lambda{|max_price=20.0| where("price < ?", max_price)}
Call:
Model.cheap
Model.cheap(15)
Yes. Just use a * like you would in a method.
scope :print_args, lambda {|*args|
puts args
}
I used scope :name, ->(arg1, arg2 = value) { ... } a few weeks ago, it worked well, if my memory's correct. To use with ruby 1.9+
You can conditionally modify your scope based on a given argument.
scope :random, ->(num = nil){ num ? order('RANDOM()').limit(num) : order('RANDOM()') }
Usage:
Advertisement.random # => returns all records randomized
Advertisement.random(1) # => returns 1 random record
Or, you can provide a default value.
scope :random, ->(num = 1000){ order('RANDOM()').limit(num) }
Usage:
Product.random # => returns 1,000 random products
Product.random(5) # => returns 5 random products
NOTE: The syntax shown for RANDOM() is specific to Postgres. The syntax shown is Rails 4.
Just wanted to let you know that according to the guide, the recommended way for passing arguments to scopes is to use a class method, like this:
class Post < ActiveRecord::Base
def self.1_week_before(time)
where("created_at < ?", time)
end
end
This can give a cleaner approach.
Certainly.
scope :with_optional_args, Proc.new { |arg|
if arg.present?
where("table.name = ?", arg)
end
}
Use the *
scope :with_optional_args, -> { |*arg| where("table.name = ?", arg) }
You can use Object#then (or Object#yield_self, they are synonyms) for this. For instance:
scope :cancelled, -> (cancelled_at_range = nil) { joins(:subscriptions).merge(Subscription.cancelled).then {|relation| cancelled_at_range.present? ? relation.where(subscriptions: { ends_at: cancelled_at_range }) : relation } }

search models tagged_with OR title like (with acts_as_taggable_on)

I'm doing a search on a model using a scope. This is being accessed by a search form with the search parameter q. Currently I have the code below which works fine for searches on tags associated with the model. But I would also like to search the title field. If I add to this scope then I will get all results where there is a tag and title matching the search term.
However, I need to return results that match the company_id and category_id, and either/or matching title or tag. I'm stuck with how to add an OR clause to this scope.
def self.get_all_products(company, category = nil, subcategory = nil, q = nil)
scope = scoped{}
scope = scope.where "company_id = ?", company
scope = scope.where "category_id = ?", category unless category.blank?
scope = scope.tagged_with(q) unless q.blank?
scope
end
I'm using Rails 3.
Ever consider Arel? You can do something like this
t = TableName.arel_table
scope = scope.where(t[:title].eq("BLAH BLAH").or(t[:tag].eq("blah blah")))
or else you can do
scope = scope.where("title = ? OR tag = ", title_value, tag_value)
I could be wrong, but I don't think scopes can help you to construct an or condition. You'll have to hand-write the code to buid your where clause instead. Maybe something like this...
clause = "company_id=?"
qparams = [company]
unless category.blank?
clause += " or category_id=?"
qparams <= category
end
scope.where clause, *qparams

Resources