Is possible to rewrite this query using only named scopes? - ruby-on-rails

I would love to write this query using named scopes only. The reason is simple, I don't want to change code everywhere when I change the way a Client is considered active (same for User considered connectable)
Here is my code
client.rb
class Client < ActiveRecord::Base
has_one :user
scope :actives, -> { where(starting_date: a_date, finishing_date: another_date, enabled: true) }
end
user.rb
class User < ActiveRecord::Base
belongs_to :client
scope :connectable, -> { where.not(access_token: nil, instance_url: nil) }
end
And here you can find my attempts to write this query:
# What I would like to write
User.eager_load(:client).connectable.where("client is in :actives scope")
# Current options
# 1 - (but this violates dry)
User.eager_load(:client).connectable.where(['clients.starting_date = ? AND clients.finishing_date = ? AND clients.enabled = ?', a_date, another_date, true).references(:client)
# 2 - My best attempt so far
User.connectable.where(client_id: Client.actives.pluck(:id))
And a link to the GIST for reference

What you're looking for is ARel's merge method. (An article on this here.)
Try this:
User.connectable.includes(:client).merge(Client.actives).references(:clients)
The includes(:client) will introduce the :client relation from User which will make the .merge(Client...) scope work. And the .references is required by Rails 4+ to explicitly state which table the includes statement will be referencing.

I'm not sure how good of an idea it is, but as long as you ensure your column names are name spaced properly (ie. you can't have 'foo' in both Client and User without prefixing the table name) then this should work:
User.eager_load(:client).connectable.where(Client.active.where_values.map(&:to_sql).join(" AND "))
There must be a nicer way, but the above works for me:
Course model:
scope :valid, -> { enabled.has_country }
scope :has_country, -> { where.not(country_id:nil) }
Rails console:
> Course.where(Course.valid.where_values.map(&:to_sql).join(" AND ")).to_sql
=> "SELECT \"courses\".* FROM \"courses\"
WHERE (\"courses\".\"is_enabled\" = 't'
AND \"courses\".\"country_id\" IS NOT NULL)"

Related

Rails - ActiveRecord Reputation System scoped query issue

I get the following error whenever I try to execute find_with_reputation or count_with_reputation methods.
ArgumentError: Evaluations of votes must have scope specified
My model is defined as follows:
class Post < ActiveRecord::Base
has_reputation :votes,
:source => :user,
:scopes => [:up, :down]
The error raises when I try to execute for example:
Post.find_with_reputation(:votes, :up)
or
Post.find_with_reputation(:votes, :up, { order: "likes" } )
Unfortunately, the documentation isn't very clear on how to get around this error. It only states that the method should be executed as follows:
ActiveRecord::Base.find_with_reputation(:reputation_name, :scope, :find_options)
On models without scopes ActiveRecord Reputation System works well with methods such as:
User.find_with_reputation(:karma, :all)
Any help will be most appreciated.
I've found the solution. It seems that ActiveRecord Reputation System joins the reputation and scope names on the rs_reputations table. So, in my case, the reputation names for :votes whose scopes could be either :up or :down are named :votes_up and :votes_down, respectively.
Therefore, find_with_reputation or count_with_reputation methods for scoped models need to be built like this:
Post.find_with_reputation(:votes_up, :all, { conditions: ["votes_up > ?", 0] })
instead of:
Post.find_with_reputation(:votes, :up, { conditions: ["votes_up > ?", 0] })
Note that you'll need to add the conditionsoption to get the desired results, otherwise it will bring all the records of the model instead of those whose votes are positive, for example.

Query based on a child collection count with has_many association is showing always no results

I'm trying to base a query on the number of documents in the child collection.
This is my context:
class Pro
include Mongoid::Document
has_many :recommendations
scope :lost, -> { where(:recommendation_ids => []) }
scope :normal, -> { where(:recommendation_ids.ne => []) }
end
And the child collection:
class Recommendation
include Mongoid::Document
belongs_to :pro
end
So, now I execute:
>> Pro.lost.count
0
>> Pro.all.select{|p| p.recommendations.count == 0}.count
1
What am I doing wrong? I've tried also with Pro.with_size(recommendation_ids: 0) and some other variations but nothing new.
Any ideas would be highly appreciated, thanks in advance.
Moped 2.0.1, Mongoid 4.0.0, Rails 4.0.6
You've coded the scope to search for IDs in an empty array, so it's always searching for an array of 0 ids.
scope :lost, -> { where(:recommendation_ids => []) }
Maybe something like this, if you want to pass an argument to it.
scope :lost, ->(rec_ids) { where(:recommendation_ids => [rec_ids]) }
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, instead of declaring a scope you can define a class function like so:
def self.lost
ids = Pro.all.select{|p| p.recommendations.count == 0}.map(&:id)
Pro.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (Recommendations) model and return those Pro instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Pro.lost.where(:some_field => some_value)

Rails-y way to query a model with a belongs_to association

I have two models:
class Wine
belongs_to :region
end
class Region
has_many :wines
end
I am attempting to use the #where method with a hash built from transforming certain elements from the params hash into a query hash, for example { :region => '2452' }
def index
...
#wines = Wine.where(hash)
...
end
But all I get is a column doesn't exist error when the query is executed:
ActiveRecord::StatementInvalid: PGError: ERROR: column wines.region does not exist
LINE 1: SELECT "wines".* FROM "wines" WHERE "wines"."region" =...
Of course, the table wines has region_id so if I queried for region_id instead I would not get an error.
The question is the following:
Is there a rails-y way to query the Wine object for specific regions using the id in the #where method? I've listed some options below based on what I know I can do.
Option 1:
I could change the way that I build the query hash so that each field has _id (like { :region_id => '1234', :varietal_id => '1515' } but not all of the associations from Wine are belongs_to and thus don't have an entry in wines for _id, making the logic more complicated with joins and what not.
Option 2:
Build a SQL where clause, again using some logic to determine whether to use the id or join against another table... again the logic would be somewhat more complicated, and delving in to SQL makes it feel less rails-y. Or I could be wrong on that front.
Option(s) 3..n:
Things I haven't thought about... your input goes here :)
You could set up a scope in the Wine model to make it more rails-y ...
class Wine < ActiveRecord::Base
belongs_to :region
attr_accessible :name, :region_id
scope :from_region, lambda { |region|
joins(:region).where(:region_id => region.id)
}
end
So then you can do something like:
region = Region.find_by_name('France')
wine = Wine.from_region(region)
Edit 1:
or if you want to be really fancy you could do a scope for multiple regions:
scope :from_regions, lambda { |regions|
joins(:region).where("region_id in (?)", regions.select(:id))
}
regions = Region.where("name in (?)", ['France','Spain']) # or however you want to select them
wines = Wine.from_regions(regions)
Edit 2:
You can also chain scopes and where clauses, if required:
regions = Region.where("name in (?)", ['France','Spain'])
wines = Wine.from_regions(regions).where(:varietal_id => '1515')
Thanks to all who replied. The answers I got would be great for single condition queries but I needed something that could deal with a varying number of conditions.
I ended up implementing my option #1, which was to build a condition hash by iterating through and concatenating _id to the values:
def query_conditions_hash(conditions)
conditions.inject({}) do |hash, (k,v)|
k = (k.to_s + "_id").to_sym
hash[k] = v.to_i
hash
end
end
So that the method would take a hash that was built from params like this:
{ region => '1235', varietal => '1551', product_attribute => '9' }
and drop an _id onto the end of each key and change the value to an integer:
{ region_id => 1235, varietal_id => 1551, product_attribute_id => 9 }
We'll see how sustainable this is, but this is what I went with for now.

How to use a scope in combination with a Model method?

I'm currently working on some scope filters, and I'd like to have it filter on a method (don't know how to name it otherwise). My code:
class Product < ActiveRecord::Base
def price_with_discount
price-discount.to_f
end
scope :by_price, lambda {|min,max|{ :conditions => { :price_with_discount => min.to_f..max.to_f }}}
end
This doesn't work: "No attribute named price_with_discount exists for table products". How can I trick my scope into using the method I defined instead of searching for a column named price_with_discount?
Bjorn
You can't use ruby methods inside :conditions. This is not an exclusive scope thing, it applies in all ActiveRecord queries:
# This won't work either
Product.where(:price_with_discount => min.to_f)
The reason for this is that what ActiveRecord needs is something that can be "translated" into database fields. The symbol :price_with_discount can not be translated to the database.
Besides, if you are on rails 3, you don't really need scopes any more. A regular class method will do the same, and it's easier to write (no lambdas).
class Product < ActiveRecord::Base
def self.by_price(min, max)
# Assuming that products.discount is a database field:
where([ "(products.price - products.discount) >= ? AND " +
"(products.price - products.discount) <= ?",
min, max
])
end
end
You can't try to do a SQL restriction on our Ruby method. Avoid using it and I suppose it's works if your price-discount is not a String
scope :by_price, lambda {|min,max|{ :conditions => { :price-discount => min.to_f..max.to_f }}}

Ruby on Rails 3: Combine results from multiple has_many or has_many_through associations

I have the following models. Users have UserActions, and one possible UserAction can be a ContactAction (UserAction is a polymorphism). There are other actions like LoginAction etc. So
class User < AR::Base
has_many :contact_requests, :class_name => "ContactAction"
has_many :user_actions
has_many_polymorphs :user_actionables, :from => [:contact_actions, ...], :through => :user_actions
end
class UserAction < AR::Base
belongs_to :user
belongs_to :user_actionable, :polymorphic => true
end
class ContactAction < AR::Base
belongs_to :user
named_scope :pending, ...
named_scope :active, ...
end
The idea is that a ContactAction joins two users (with other consequences within the app) and always has a receiving and a sending end. At the same time, a ContactAction can have different states, e.g. expired, pending, etc.
I can say #user.contact_actions.pending or #user.contact_requests.expired to list all pending / expired requests a user has sent or received. This works fine.
What I would now like is a way to join both types of ContactAction. I.e. #user.contact_actions_or_requests. I tried the following:
class User
def contact_actions_or_requests
self.contact_actions + self.contact_requests
end
# or
has_many :contact_actions_or_requests, :finder_sql => ..., :counter_sql => ...
end
but all of these have the problem that it is not possible to use additional finders or named_scopes on top of the association, e.g. #user.contact_actions_or_requests.find(...) or #user.contact_actions_or_requests.expired.
Basically, I need a way to express a 1:n association which has two different paths. One is User -> ContactAction.user_id, the other is User -> UserAction.user_id -> UserAction.user_actionable_id -> ContactAction.id. And then join the results (ContactActions) in one single list for further processing with named_scopes and/or finders.
Since I need this association in literally dozens of places, it would be a major hassle to write (and maintain!) custom SQL for every case.
I would prefer to solve this in Rails, but I am also open to other suggestions (e.g. a PostgreSQL 8.3 procedure or something simliar). The important thing is that in the end, I can use Rails's convenience functions like with any other association, and more importantly, also nest them.
Any ideas would be very much appreciated.
Thank you!
To provide a sort-of answer to my own question:
I will probably solve this using a database view and add appropriate associations as needed. For the above, I can
use the SQL in finder_sql to create the view,
name it "contact_actions_or_requests",
modify the SELECT clause to add a user_id column,
add a app/models/ContactActionsOrRequests.rb,
and then add "has_many :contact_actions_or_requests" to user.rb.
I don't know how I'll handle updating records yet - this seems not to be possible with a view - but maybe this is a first start.
The method you are looking for is merge. If you have two ActiveRecord::Relations, r1 and r2, you can call r1.merge(r2) to get a new ActiveRecord::Relation object that combines the two.
If this will work for you depends largely on how your scopes are set up and if you can change them to produce a meaningful result. Let's look at a few examples:
Suppose you have a Page model. It has the normal created_at and updated_at attributes, so we could have scopes like:
:updated -> { where('created_at != updated_at') }
:not_updated -> { where('created_at = updated_at') }
If you pull this out of the database you'll get:
r1 = Page.updated # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at)
r2 = Page.not_updated # SELECT `pages`.* FROM `pages` WHERE (created_at = updated_at)
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at) AND (created_at = updated_at)
=> []
So it did combine the two relations, but not in a meaningful way. Another one:
r1 = Page.where( :name => "Test1" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test1'
r2 = Page.where( :name => "Test2" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
So, it might work for you, but maybe not, depending on your situation.
Another, and recommended, way of doing this is to create a new scope on you model:
class ContactAction < AR::Base
belongs_to :user
scope :pending, ...
scope :active, ...
scope :actions_and_requests, pending.active # Combine existing logic
scope :actions_and_requests, -> { ... } # Or, write a new scope with custom logic
end
That combines the different traits you want to collect in one query ...

Resources