I have a model Shop:
class Shop < ActiveRecord::Base
has_and_belongs_to_many :services
end
and a model Service:
class Service < ActiveRecord::Base
has_and_belongs_to_many :shops
end
I would like to query every shop that provides all of the following services:
Reparation
Advise
Shipping
So return all shops where services contain reparation AND advise AND shipping.
Is this possible with ActiveRecord querying only?
Thanks!
See MySQL: Select records where joined table matches ALL values for how to do it in sql.
For a simpler method, which makes a series of simple queries instead of a single complex query, you could do this:
#starting with the services in an array called #services
#(which could come from params[:service_ids] for example)
#find shops which have ALL these services.
shop_ids = #services.map(&:shop_ids).inject{|a,b| a & b}
#shops = Shop.find(shop_ids)
Key to this is .inject{|a,b| a & b} : inject is an array method which performs a function between the first and second elements (a and b) then uses the result of that with the block again, with the third element etc, working it's way through the array. The & operator is array intersect, so you will end up only with the shop_ids which are returned for ALL services.
Related
as title said i am trying to access an array of objects of an association
This is a has_many association
here is my class
class Keyword < ApplicationRecord
has_many :rankings
end
class Ranking < ApplicationRercord
belongs_to :keyword
end
There are a attribute in ranking called position:integer, i want to be able to access all latest created rankings from all keyword here is what i got so far
Keyword.all.joins(:rankings).select( 'MAX(rankings.id) ').pluck(:created_at, :keyword_id, :position)
i've read some other post suggesting me to use MAX on rankings.id, but i am still not able to return the array
At the moment Keyword.count return 4597
Ranking.count return 9245
Each keyword has generated about 2 rankings, but i just want the latest ranking from each keyword in array format, so to get latest of each i should expect around 4597
Not sure if i explained clear enough, hope u guys can help me :'( thanks really appreciate it
If you are using Postgres. You can use DISTINCT ON
Keyword.joins(:rankings)
.select("DISTINCT ON(ratings.keyword_id) keywords.*, ratings.position, ratings.created_at AS rating_created_at")
.order("ratings.keyword_id, ratings.id DESC")
Now you can access position, rating_created_at
#keywords.each do |k|
k.position
....
#keywords.map { |k| [k.id, k.rating_created_at, k.position] }
If you have enough rankings you might want to store the latest ranking on the on keywords table as a read optimization:
class Keyword < ApplicationRecord
belongs_to :latest_ranking, class_name: :ranking
has_many :rankings, after_add: :set_latest_ranking
def set_latest_ranking(ranking)
self.update!(latest_ranking: ranking)
end
end
Keyword.joins(:latest_ranking)
.pluck(:created_at, :id, "rankings.position")
This makes it both very easy to join and highly performant. I learned this after dealing with an application that had a huge row count and trying every possible solution like lateral joins to improve the pretty dismal performance of the query.
The cost is an extra write query when creating the record.
Keyword.joins(:rankings).group("keywords.id").pluck("keywords.id", "MAX(rankings.id)")
This will give you an array which elements will include an ID of a keyword and an ID of the latest ranking, associated with that keyword.
If you need to fetch more information about rankings rather than id, you can do it like this:
last_rankings_ids_scope = Ranking.joins(:keyword).group("keywords.id").select("MAX(rankings.id)")
Ranking.where(id: last_rankings_ids_scope).pluck(:created_at, :keyword_id, :position)
So I have a rails module called DbsfnpService.
Within this, I have a class
class DbnsfpAccess < ActiveRecord::Base
establish_connection :dbnsfp
end
I then have many methods within DbsfnpService similar to this.
def get_tables
sql = "select * from dbnsfp limit 1"
results = DbnsfpAccess.connection.execute(sql)
return results
end
When I call these methods from another class by including DbsnfpService, I would like to only establish one connection to :dbnsfp and handle all my queries and then close that connection. I believe how it is now, every method I call that contains DbsnfpAccess.connection.execute(sql) is making a separate connection(?). What is the best way to achieve this? Passing in a connection object into these functions? Thanks in advance!
If I'm understanding your question correctly, you want to execute calls on the database without knowing the table names and therefore unable to create rails ActiveRecord table models ahead of time.
Instead of executing sql you can use ActiveRecord::Base.connection.tables
(see this link How to list of all the tables defined for the database when using active record?)
To use these table names what you will need is to turn DbnsfpAccess into an abstract class.
class DbnsfpAccess < ActiveRecord::Base
self.abstract_class = true
establish_connection :dbnsfp
end
Then you can create the models you want to connect to DbnsfpAccess like this:
class DynamicallyGenratedDbnsfpModel << DbnsfpAccess
I have two models, Recipes and Skills. In this scenario, a skill is a cooking technique, like baking, frying, etc. So each recipe has a certain set of associated skills.
I want to search all the recipes like this:
Find all recipes that use any given set of skills (e.g. Baking OR Frying OR both)
EDIT: This should NOT return recipes that require a skill that wasn't in the search query - e.g. If I search for skills [1, 2] I don't want a recipe that uses skills [1, 2, 4] or any other superset.
If you add a new skill to the search, return just the additional recipes (e.g. if you add Boiling to the previous query of Baking or Frying, how many NEW recipes can you now cook?)
I currently have this working in Rails using plain old Ruby methods:
class Recipe < ActiveRecord::Base
has_many :practices
has_many :skills, through: :practices
def self.find_recipes_that_require_any_of_these_skills(*known_skill_ids)
self.select do |recipe|
recipe.skill_ids.all? do |skill_id|
known_skill_ids.include?(skill_id)
end
end
end
# calls the above method twice, once with the new skill and once without
# and subtracts one result from the other
def self.find_newly_unlocked_recipes(*prior_skill_ids, new_skill_id)
self.find_recipes_that_require_any_of_these_skills(*(prior_skill_ids + [new_skill_id])) - self.find_recipes_that_require_any_of_these_skills(*prior_skill_ids)
end
end
In Rails console: Recipe.find_recipes_that_require_any_of_these_skills(1,4)
returns all the Recipe objects for skill 1, skill 4, or skills 1 & 4.
But this is inefficient because it generates a SQL query for every single recipe in my database.
How can I write these queries the ActiveRecord/SQL way?
def self.find_recipes_that_require_any_of_these_skills(*known_skill_ids)
self.includes(:skills).where(skills: { id: known_skill_ids })
end
Two queries to DB:
def self.find_recipes_that_require_any_of_these_skills(*known_skill_ids)
Recipe.joins(:skills)
.merge(Skill.where(id: known_skill_ids))
.where("recipes.id NOT IN (?)", Recipe.joins(:skills).merge(Skill.where("skills.id NOT IN (?)", known_skill_ids)).uniq.pluck(:id)).uniq
end
Since you're using has_many :skills, through: :practices - your Practices table should have both recipe_id and skills_id columns.
previous_recipe_ids = Recipe.joins(:practices).where('practices.skill_id in (?)', prior_skills_ids).map(&:id)
Recipe.joins(:practices).where('practices.skill_id = (?) and recipes.recipe_id not in (?)', new_skill_id, previous_recipe_ids)
The following method uses just three sql queries in total to create a collection of recipies
array_of_skills=["cooking","frying",...]
skills=Skill.where('name in (?)',array_of_skills).map(&:id)
Up to here you already have, so you might need just this:
practices=Practice.where('skill_id in (?)',skills).map(&:recipe_id)
recipes=Recipe.where('id in (?)', practices)
Maybe there is a better way, but I don't think there would be something with much less sql
It seems to be that your find_recipes_that_require_any_of_these_skills method isn't correct. It returns recipes that have all of the known_skill, not any.
So, the ActiveRecord/SQL way:
Known skills:
class Skill < ActiveRecord::Base
#known skills
scope :known_skills, -> { where(id: known_skill_ids) }
#not known skills
scope :not_known_skills, -> { where("skills.id NOT IN (?)", known_skill_ids) }
Recipes that have any of known skills:
Recipe.joins(:skills).merge(Skill.known_skills)
Newly_unlocked_recipes:
Recipe.joins(:skills).merge(Skill.not_known_skills).where("skills.id = ?", new_skill_id)
How do I add a condition to the ON clause generated by includes in active record while retaining eager loading?
Let's say I have these classes:
class Car
has_many :inspections
end
class Inspection
belongs_to :car
end
Now I can do:
Car.includes(:inspections)
Select * from cars LEFT OUTER JOIN inspections ON cars.id = inspections.car_id
But I want to generate this sql:
Select * from cars LEFT OUTER JOIN inspections ON cars.id = inspections.car_id
AND inspections.month = '2013-04-01'
(this doesn't work):
Car.includes(:inspections).where("inspections.month = 2013-04-01")
Select * from cars LEFT OUTER JOIN inspections ON cars.id = inspections.car_id
WHERE inspections.month = '2013-04-01'
I don't know this exactly, but what you are trying to do is probably not recommended i.e. violates one of Rails' conventions. According to this answer in a related question, the default behavior for such queries is to use two queries, like:
SELECT "cars".* FROM "cars";
SELECT "inspections".* FROM "inspections" WHERE "inspections"."car_id" IN (1, 2, 3, 4, 5);
This decision was made for performance reasons. That makes me guess that the exact type of query (JOIN or multiple queries) is an implementation detail that you cannot count on. Going along this train of thought, ActiveRecord::Relation probably wasn't designed for your use case, there is probably no way to add an ON condition in the query.
Going along this sequence of guesses, if you truly believe that your use case is unique, the best thing to do is probably for you to craft your own SQL query as follows:
Car.joins(sanitize_sql_array(["LEFT OUTER JOIN inspections ON inspections.car_id = cars.id AND inspections.month = ?", "2013-04-01"])
(Update: this was asked last year and did not receive a good answer.)
Alternative 1
As Carlos Drew suggested,
#cars = Cars.all
car_ids = #cars.map(&:id)
#inspections = Inspection.where(inspections: {month: '2013-04-01', car_id: car_ids})
# with scopes: Inspection.for_month('2013-04-01').where(car_id: car_ids)
However, in order to prevent car.inspections from triggering unnecessary SQL calls, you also need to do
# app/models/car.rb
has_many :inspections, inverse_of: :car
# app/models/inspection.rb
belongs_to :car, inverse_of: :inspections
Alternative 2
Perhaps you can find a way to cache the inspections for the current month, and then don't worry about eager loading. This might be the best solution, since the cache can be reused in various places.
#cars = Cars.all
#cars.each do |car|
car.inspections.where(month: '2013-04-01')
end
I've rethought your question more broadly. I think you are facing a code design problem as well as (instead of?) an ActiveRecord query problem.
You are asking to return a relation of Cars on which .inspections has been redefined to mean those Inspections matching a specific date. ActiveRecord does not allow you to redefine a model association on the fly, based on a query.
If you were not asking for a dynamic condition on the inspection date, I would tell you to use a has_many :through with a :condition.
has_many :passed_inspections, :through => :inspections, :conditions => {:passed => true}
#cars = Cars.includes(:passed_inspections)
Obviously, that would not work if you need to supply an inspection date on the fly.
So, in the end, I would tell you to do something like this:
#cars = Cars.all
#inspections = Inspection.where(inspections: {month: '2013-04-01', car_id: #cars.pluck(:id)})
(Exact, best implementation of that car_id where condition is up to debate. And you'll then need to group the #inspections by car_id to get the right subset in a given moment.)
Alternately, in a production environment, you might be able to rely on some fairly good/clever ActiveRecord caching. I'm not certain of this.
def inspections_dated(month)
inspections.where(month: month)
end
Car.includes(:inspections).each{|car| car.inspections_dated(month).each.etc. }
Alternately, Alternately
You can, through manual SQL, trick ActiveRecord into giving you extended Car objects with an unclear interface:
#cars_with_insp = Car.join("LEFT OUTER JOIN inspections ON inspections.car_id = cars.id AND inspections.month = '2013-04-01'").select("cars.*, inspections.*")
#cars_with_insp.each{|c| puts c.name; puts c.inspection_month}
You'll see, in that .each, that you have the inspection's attributes available directly on car, because you've convinced ActiveRecord with a join to return two records of one class as a single row. Rails will tell you its class is Car, but it's more than a Car. You'll either get each Car once, for no matching Inspections, or multiple times for each matching Inspection.
This should work:
Car.includes(:inspections).where( inspections: { month: '2013-04-01' })
The authors of Rails did not build this functionality into ActiveRecord, presumably because using WHERE returns the same result set, and they felt no need to have an alternative.
In the docs and code, we find the two "official" methods of adding conditions to included models.
In the actual source code: https://github.com/rails/rails/blob/5245648812733d2c31f251de3e05e78e68bfa3a5/activerecord/lib/active_record/relation/query_methods.rb we find them using WHERE to accomplish this:
And I quote: "
=== conditions
#
# If you want to add conditions to your included models you'll have
# to explicitly reference them. For example:
#
# User.includes(:posts).where('posts.name = ?', 'example')
#
# Will throw an error, but this will work:
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
_END_QUOTE_
The docs mention another approach: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html under the header "Eager loading of associations"
QUOTE:
If you do want eager load only some members of an association it is usually more natural to include an association which has conditions defined on it:
class Post < ActiveRecord::Base
has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
end
Post.includes(:approved_comments)
This will load posts and eager load the approved_comments association, which contains only those comments that have been approved.
END QUOTE
You can technically use such an approach, but it in your case it may not be so useful if you are using dynamic month values.
These are the only options, which in any case return the same results as your AND based query.
Say if #news_writers is an array of records. I then want to use #news_writers to find all news items that are written by all the news writers contained in #news_writers.
So I want something like this (but this is syntactically incorrect):
#news = News.find_all_by_role_id(#news_writers.id)
Note that
class Role < ActiveRecord::Base
has_many :news
end
and
class News < ActiveRecord::Base
belongs_to :role
end
Like ennen, I'm unsure what relationships your models are supposed to have. But in general, you can find all models with a column value from a given set like this:
News.all(:conditions => {:role_id => #news_writers.map(&:id)})
This will create a SQL query with a where condition like:
WHERE role_id IN (1, 10, 13, ...)
where the integers are the ids of the #news_writers.
I'm not sure if I understand you - #news_writers is a collection of Role models? If that assumption is correct, your association appears to be backwards - if these represent authors of news items, shouldn't News belong_to Role (being the author)?
At any rate, I would assume the most direct approach would be to use an iterator over #news_writers, calling on the association for each news_writer (like news_writer.news) in turn and pushing it into a separate variable.
Edit: Daniel Lucraft's suggestion is a much more elegant solution than the above.