Are these Rails query syntaxes interchangable? - ruby-on-rails

>scope :a, -> { joins(:b).where('bs.c': false) }
>scope :a, -> { joins(:b).where('bs.c = ?', false) }
Just wanted to ask whether those 2 lines do the same thing? The first one seemed to work fine in development but gave me a syntax error when I tried to push to Heroku. Is the first one deprecated?

I believe these are more interchangeable, without syntax errors:
scope :a, -> { joins(:b).where(bs: { c: false }) }
scope :a, -> { joins(:b).where('bs.c' => false }) }
scope :a, -> { joins(:b).where('bs.c = ?', false) }
scope :a, -> { joins(:b).where('bs.c = :q', { q: false }) }
Personally, the first line is my preferred because you can list several columns within that nested hash without needing to keep repeating the table name/alias.

Only Ruby 2.2 and above allow use the JSON like hash syntax with quoted keys, ie
{'foo' : bar}
Instead of
{foo: bar}
Of course in your case not quoting the key probably won't work either because of the . in the key.
This suggests you are running different Ruby versions locally and on heroku.
Other than that, they should be equivalent.

Related

Mongoid Aggregate result into an instance of a rails model

Introduction
Correcting a legacy code, there is an index of object LandingPage where most columns are supposed to be sortable, but aren't. This was mostly corrected, but few columns keep posing me trouble.
Theses columns are the one needing an aggregation, because based on a count of other documents. To simplify the explanation of the problem, I will speak only about one of them which is called Visit, as the rest of the code will just be duplication.
The code fetch sorted and paginate data, then modify each object using LandingPage methods before sending the json back. It was already like this and I can't modify it.
Because of that, I need to do an aggregation (to sort LandingPage by Visit counts), then get the object as LandingPage instance to let the legacy code work on them.
The problem is the incapacity to transform Mongoid::Document to a LandingPage instance
Here is the error I got:
Mongoid::Errors::UnknownAttribute:
Message:
unknown_attribute : message
Summary:
unknown_attribute : summary
Resolution:
unknown_attribute : resolution
Here is my code:
def controller_function
landing_pages = fetch_landing_page
landing_page_hash[:data] = landing_pages.map do |landing_page|
landing_page.do_something
# Do other things
end
render json: landing_page_hash
end
def fetch_landing_page
criteria = LandingPage.where(archived: false)
columns_name = params[:columns_name]
column_direction = params[:column_direction]
case order_column_name
when 'visit'
order_by_visits(criteria, column_direction)
else
criteria.order_by(columns_name => column_direction).paginate(
per_page: params[:length],
page: (params[:start].to_i / params[:length].to_i) + 1
)
end
def order_by_visit(criteria, order_direction)
def order_by_visits(landing_pages, column_direction)
LandingPage.collection.aggregate([
{ '$match': landing_pages.selector },
{ '$lookup': {
from: 'visits',
localField: '_id',
foreignField: 'landing_page_id',
as: 'visits'
}},
{ '$addFields': { 'visits_count': { '$size': '$visits' }}},
{ '$sort': { 'visits_count': column_direction == 'asc' ? 1 : -1 }},
{ '$unset': ['visits', 'visits_count'] },
{ '$skip': params[:start].to_i },
{ '$limit': params[:length].to_i }
]).map { |attrs| LandingPage.new(attrs) { |o| o.new_record = false } }
end
end
What I have tried
Copy and past the hash in console to LandingPage.new(attributes), and the instance was created and valid.
Change the attributes key from string to symbole, and it still didn't work.
Using is_a?(hash) on any element of the returned array returns true.
Put it to json and then back to a hash. Still got a Mongoid::Document.
How can I make the return of the Aggregate be a valid instance of LandingPage ?
Aggregation pipeline is implemented by the Ruby MongoDB driver, not by Mongoid, and as such does not return Mongoid model instances.
An example of how one might obtain Mongoid model instances is given in documentation.

Sorting select results

I'm trying to sort c.description alphabetically but I'm not familiar with Ruby and can't seem to get .sort to work.
cakes.select do |cake|
cake.categories.pluck(:id).any? do |ca|
category.self_and_descendant_ids.include? ca
end.map { |c| { id: c.form_descriptor, name: "#{c.description}" } }
end
I don't believe the code you wrote would function if you ran it in IRB. Could we have a better example of your data structure? You probably want to stay in the database for this. It's not clear where your "category" variable is coming from. The internal any? followed by a map won't work (map operates on an enumerable, any? returns a boolean).
Your pluck(:id) is an N+1 and will round-trip the to the database for every cake, not including whatever self_and_descendant_ids is doing - seems like a modeling problem since you could just associate categories with cakes and be done with any notion of descendants but this all conjecture without knowing more about your data or what you're modeling.
Categories.joins(:cakes)
.where(cakes: { id: cake_ids })
.order(description: :asc)
.pluck(:form_descriptor, :description)
.map { |id, name| { id: id, name: name } }
If I were to sort the descriptions alphabetically, I could use
sorted_cakes = cakes.sort { |cake_a, cake_b| cake_a.description <=> cake_b.description }
If you wanted them in reverse alphabetical order, you can change it to cake_b.description <=> cake_a.description
Why not sort it before traversing? You can do like
cakes.select do |cake|
cake.categories.order('categories.description ASC').pluck(:id).any? do |ca|
category.self_and_descendant_ids.include? ca
end.map { |c| { id: c.form_descriptor, name: "#{c.description}" } }
end

Drying up multiples scopes with simialar queries

I am noticing a trend with my scopes and trying to figure out how to make it dry
scope :newest, -> { order('created_at DESC') }
scope :top_sold, -> { order('qty_sold DESC') }
scope :most_viewed, -> { order('qty_viewed DESC') }
scope :most_downloaded, -> { order('qty_download DESC') }
scope :most_favorited, -> { order('qty_favorited DESC') }
I would like to pass in the column I want sorted so that I can call it on Photo. I tried this, but running into problems
scope :sort_photos, -> type { order('type DESC') }
Photo.sort_photos('qty_download')
Am I on the right path or is there a smarter way to accomplish this?
Pass type as a scope parameter and use that in order clause with string interpolation:
scope :sort_photos,->(type) { order("#{type} DESC") }
Then do:
Photo.sort_photos('qty_download')
The order method takes a String or a Hash. So instead of order('created_at DESC') you can do order(created_at: :desc), for example. So, to accomplish what you want, it's as simple as changing the key to your type variable:
scope :sort_photos, -> type { order(type => :desc) }
I would also recommend using a sentinel for your order scopes such as by_. So that the scope by_sort_photos doesn't get overridden by definition of a sort_photos method or assoication later.
Finally, it's good to have a public interface full of methods, as opposed to requiring knowledge of the class attributes and passing those attribute names into a public interface method. So I'd keep the many different scopes that you have, but perhaps have them all refer to the one, general scope as we've defined here. So:
scope :newest, -> { by_most_recent_type('created_at') }
scope :top_sold, -> { by_most_recent_type('qty_sold') }
scope :by_most_recent_type, -> type { order(type => :desc) }

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

call that calls result of call

Using a framework I need 2 ActiveRecord scopes:
scope :tagged_with, lambda { |tag| {:conditions => [" tags like ? ", "% #{tag} %"] } }
scope :tagged_with_any, lambda { |tag_array | [HERE NEW IMPLEMENTATION] }
I want the second scope to be based on the first scope. If you would do it hard coded, you would do for a 2 element array:
lambda { | tag_array | tagged_with(tag_array[0]).tagged_with(tag_array[1]) }
which works, but how do I do it generic
lambda { | tag_array | tags.each { |t| tagged_with(t) } }
clearly doesn't do the job.
Is this acceptable?
named_scope :tagged_with_all, lambda { |tag_array| tag_array.inject(self, :tagged_with) }
[edit] renamed to tagged_with_all since it's what it really does. For a tagged_with_any, Vanilla named scopes do not implement OR-concatenations; concatenating ORs conditions "manually" from scopes is doable but a bit messy. Note that you have libraries like Arel or Metawhere.

Resources