Re-write ActiveRecord query in Arel - ruby-on-rails

I just got started with ARel. I'm finding it difficult converting this bit of complex AR query into Arel:
Offer.where(
"offers.ended_at IS NULL OR offers.started_at < ? AND offers.ended_at >= ?",
Time.zone.now, Time.zone.now
)
I think having this in Arel will aid readability

I think using chained scopes would make it more readable too:
# in app/models/offer.rb
scope :without_end, -> { where(ended: nil) }
scope :still_valid, -> { where('started_at < :now AND offers.ended_at >= :now', now: Time.current) }
And to be used like this:
Offer.still_valid.or(Offer.without_end)

This should work:
offers = Offer.arel_table
offers_with_nil_ended_at = offers[:ended_at].eq(nil)
offers_within_range = offers[:started_at].lt(Time.zone.now).and(
offers[:ended_at].gteq(Time.zone.now)
)
Offer.where(offers_with_nil_ended_at.or(offers_within_range))

Related

Rails - Order index by complex sort

How do I order my index results by featured_end_date >= Time.now() in :asc order and then have the rest of the results sort by by publish_at: :desc.
Currently I have BlogPost.order(featured_end_date: :asc, publish_at: :desc). I am missing that >= Time.now() comparison.
I assume scopes might need to be used, but I am not sure how to achieve this.
BlogPost model
scope :featuredfuture, -> { where("featured_end_date >= ?", Time.now()).order(featured_end_date: :asc) }
scope :other, -> { where("featured_end_date < ? or featured_end_date is null", Time.now()).order(publish_at: :desc) }
Controller
#blogposts = BlogPost.featuredfuture + BlogPost.other
You need to specify your collection before ordering. You have to use where.
BlogPost.where('featured_end_date >= ?', Time.now).order(featured_end_date: :asc, publish_at: :desc)

Rails 4 ActiveRecord append conditions (multiple .where)

I would like to append AND conditions depend on condition like this:
#flag = true || false;
#results = Model.where(conditions).where(conditions_depend_on_flag);
// The simple way:
if (#flag) {
#results = Model.where(conditions);
} else {
#results = Model.where(conditions).where(conditions_depend_on_flag);
}
Example for my expected:
#results = Model.where(conditions).where(conditions_depend_on_flag, #flag == true);
I don't know is it possible or not.
Could you give me some suggestion?
#results = Model.where(conditions)
#results = #results.where(conditions_depend_on_flag) if #flag
We can combine 2 conditions into 1 where statement:
To make it easy I assume:
conditions = created_at < 1.day.ago
conditions_depend_on_flag = updated_at > 1.day.ago
So the query will be:
Model.where(
'created_at < ? AND (? OR updated_at > ?)', 1.day.ago, !#flag, 1.day.ago
)
Beautiful SQL :)
use scopes for sql conditions
in model
scope :conditions_depend_on_flag, ->(flag) { where(....) if flag }
anywhere
#results = Model.where(conditions).conditions_depend_on_flag(#flag)

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 } }

Arel syntax for ( this AND this) OR ( this AND this)

how would I do this in Arel ( this AND this) OR ( this AND this)
Context is rails 3.0.7
Mike's answer was basically what you're looking for
class Model < ActiveRecord::Base
def self.this_or_that(a1, a2, a3, a4)
t = Model.arel_table
q1 = t[:a].eq(a1).and(t[:b].eq(a2))
q2 = t[:c].eq(a3).and(t[:d].eq(a4))
where(q1.or(q2))
end
end
some slides I put together on Arel
I'd put this all in a single where block. For example, here's a 'scope' based on a similar complex 'where' clause:
class Foo < ActiveRecord::Base
def self.my_complex_where(args)
where("(col1 = ? AND col2 = ?) OR (col3 = ? AND col4 = ?)",
args[:val1], args[:val2], args[:val3], args[:val4])
end
end
Alternatively, you could do it using this notation:
class Foo < ActiveRecord::Base
def self.my_complex_where(args)
where("(col1 = :val1 AND col2 = :val2) OR (col3 = :val3 AND col4 = :val4)",
:val1 => args[:val1],
:val2 => args[:val2],
:val3 => args[:val3],
:val4 => args[:val4])
end
end
You need to drop down to more raw AREL
t = Model.arel_table
a = (t[:a].eq(nil).and(t[:b].eq(nil)))
b = (t[:a].not_eq(nil).and(t[:b].not_eq(nil)))
Model.where(a.and(b))
Besides the answers already given, you might also want to take a look at the gem 'squeel'
Example:
Person.where{(name =~ 'Ernie%') & (salary < 50000) | (name =~ 'Joe%') & (salary > 100000)}
I don't know about helper methods, but you can always drop down to (almost) pure SQL:
Client.where("orders_count = ? AND locked = ?", params[:orders], false)

default_scope and associations

Suppose I have a Post model, and a Comment model. Using a common pattern, Post has_many Comments.
If Comment has a default_scope set:
default_scope where("deleted_at IS NULL")
How do I easily retrieve ALL comments on a post, regardless of scope?
This produces invalid results:
Post.first.comments.unscoped
Which generates the following queries:
SELECT * FROM posts LIMIT 1;
SELECT * FROM comments;
Instead of:
SELECT * FROM posts LIMIT 1;
SELECT * FROM comments WHERE post_id = 1;
Running:
Post.first.comments
Produces:
SELECT * FROM posts LIMIT 1;
SELECT * FROM comments WHERE deleted_at IS NULL AND post_id = 1;
I understand the basic principle of unscoped removing all existing scopes, but shouldn't it be aware and to keep the association scope?
What is the best way to pull ALL comments?
For some strange reasons,
Comment.unscoped { Post.last.comments }
includes the default_scope of Comment,
however,
Comment.unscoped { Post.last.comments.to_a }
Comment.unscoped { Post.last.comments.order }
do not include the default_scope of Comment.
I experienced this in a rails console session with Rails 3.2.3.
with_exlusive_scope is deprecated as of Rails 3. See this commit.
Before (Rails 2):
Comment.with_exclusive_scope { Post.find(post_id).comments }
After (Rails 3):
Comment.unscoped { Post.find(post_id).comments }
Rails 4.1.1
Comment.unscope(where: :deleted_at) { Post.first.comments }
Or
Comment.unscoped { Post.first.comments.scope }
Note that I added .scope, it seems like this block should return kind of ActiveRecord_AssociationRelation (what .scope does) not ActiveRecord_Associations_CollectionProxy (without a .scope)
This is indeed a very frustrating problem which violates the principle of least surprise.
For now, you can just write:
Comment.unscoped.where(post_id: Post.first)
This is the most elegant/simple solution IMO.
Or:
Post.first.comments.scoped.tap { |rel| rel.default_scoped = false }
The advantage of the latter:
class Comment < ActiveRecord::Base
# ...
def self.with_deleted
scoped.tap { |rel| rel.default_scoped = false }
end
end
Then you can make fun things:
Post.first.comments.with_deleted.order('created_at DESC')
Since Rails 4, Model.all returns an ActiveRecord::Relation , rather than an array of records.
So you can (and should) use all instead of scoped:
Post.first.comments.all.tap { |rel| rel.default_scoped = false }
How about this?
# Use this scope by default
scope :active, -> { where(deleted_at: nil) }
# Use this whenever you want to include all comments regardless of their `deleted_at` value
scope :with_soft_deleted, -> { unscope(where: :deleted_at)
default_scope, -> { active }
post.comments would fire this query:
SELECT "comments".* FROM "comments" WHERE "comments"."deleted_at" IS NULL AND "comments"."post_id" = $1;
post.comments.with_soft_deleted would send this:
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1;
class Comment
def post_comments(post_id)
with_exclusive_scope { find(all, :conditions => {:post_id => post_id}) }
end
end
Comment.post_comments(Post.first.id)

Resources