I am trying to rewrite this old Rails 2 stuff such that it will work with Rails 3.2.1:
Rails 2.2.2 query to rewrite
B2bPrivilege.find_by_sql("SELECT b2b_privileges.*, vendors.name AS vendor_name FROM b2b_privileges LEFT JOIN vendors ON vendors.id = b2b_privileges.vendor_id WHERE b2b_user_id = ?", current_user_id)
This is what I came up with and it creates the same SQL as seen in the Rails 2.2.2 example above but it doesn't bring in the vendors.name column into the stored result and I don't know why. The generated SQL when ran in the database console, does work so the issue is definitely related to Rails.
B2bPrivilege.select("b2b_privileges.*, vendors.name AS vendor_name").joins('LEFT JOIN vendors ON vendors.id = b2b_privileges.vendor_id').where('b2b_user_id' => current_user_id)
Debug info from .inspect
B2bPrivilege Load (1.4ms) SELECT b2b_privileges.*, vendors.name AS vendor_name FROM "b2b_privileges" LEFT JOIN vendors ON vendors.id = b2b_privileges.vendor_id WHERE "b2b_privileges"."b2b_user_id" = 398
[
#<B2bPrivilege id: 1363, b2b_user_id: 398, vendor_id: 53, can_setup_instant_electronic_delivery: true, can_setup_purchase_orders: true, can_setup_advance_ship_notification: true, can_setup_xml_pushes: true>,
#<B2bPrivilege id: 1923, b2b_user_id: 398, vendor_id: 103, can_setup_instant_electronic_delivery: false, can_setup_purchase_orders: false, can_setup_advance_ship_notification: true, can_setup_xml_pushes: true>
]
It turns out the joined content is there it just doesn't output to console, just the primary relation does. If you are viewing the output in your console window I think that the console output only reflects/inspects the root level object that is returned.
This was a gotcha for me when I was debugging my queries that use joins
Do
B2bPrivilege.joins('LEFT JOIN vendors ON vendors.id = b2b_privileges.vendor_id').
where(:b2bprivileges => {'b2b_user_id' => current_user_id} ).
select("b2b_privileges.*, vendors.name AS 'vendor_name'").first.vendor_name
Related
I am making scopes for a model that looks something like this:
class PressRelease < ActiveRecord::Base
has_many :publications
end
What I want to get is all press_releases that does not have publications, but from a scope method, so it can be chained with other scopes. Any ideas?
Thanks!
NOTE: I know that there are methods like present? or any? and so on, but these methods does not return an ActiveRecord::Relation as scope does.
NOTE: I am using RoR 3
Avoid eager_loading if you do not need it (it adds overhead). Also, there is no need for subselect statements.
scope :without_publications, -> { joins("LEFT OUTER JOIN publications ON publications.press_release_id = press_releases.id").where(publications: { id: nil }) }
Explanation and response to comments
My initial thoughts about eager loading overhead is that ActiveRecord would instantiate all the child records (publications) for each press release. Then I realized that the query will never return press release records with publications. So that is a moot point.
There are some points and observations to be made about the way ActiveRecord works. Some things I had previously learned from experience, and some things I learned exploring your question.
The query from includes(:publications).where(publications: {id: nil}) is actually different from my example. It will return all columns from the publications table in addition to the columns from press_releases. The publication columns are completely unnecessary because they will always be null. However, both queries ultimately result in the same set of PressRelease objects.
With the includes method, if you add any sort of limit, for example chaining .first, .last or .limit(), then ActiveRecord (4.2.4) will resort to executing two queries. The first query returns IDs, and the second query uses those IDs to get results. Using the SQL snippet method, ActiveRecord is able to use just one query. Here is an example of this from one of my applications:
Profile.includes(:positions).where(positions: { id: nil }).limit(5)
# SQL (0.8ms) SELECT DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "positions" ON "positions"."profile_id" = "profiles"."id" WHERE "positions"."id" IS NULL LIMIT 5
# SQL (0.8ms) SELECT "profiles"."id" AS t0_r0, ..., "positions"."end_year" AS t1_r11 FROM "profiles" LEFT OUTER JOIN "positions" ON "positions"."profile_id" = "profiles"."id" # WHERE "positions"."id" IS NULL AND "profiles"."id" IN (107, 24, 7, 78, 89)
Profile.joins("LEFT OUTER JOIN positions ON positions.profile_id = profiles.id").where(positions: { id: nil }).limit(5)
# Profile Load (1.0ms) SELECT "profiles".* FROM "profiles" LEFT OUTER JOIN positions ON positions.profile_id = profiles.id WHERE "positions"."id" IS NULL LIMIT 5
Most importantly
eager_loading and includes were not intended to solve the problem at hand. And for this particular case I think you are much more aware of what is needed than ActiveRecord is. You can therefore make better decisions about how to structure the query.
you can de the following in your PressRelease:
scope :your_scope, -> { where('id NOT IN(select press_release_id from publications)') }
this will return all PressRelease record without publications.
Couple ways to do this, first one requires two db queries:
PressRelease.where.not(id: Publications.uniq.pluck(:press_release_id))
or if you don't want to hardcode association foreign key:
PressRelease.where.not(id: PressRelease.uniq.joins(:publications).pluck(:id))
Another one is to do a left join and pick those without associated elements - you get a relation object, but it will be tricky to work with it as it already has a join on it:
PressRelease.eager_load(:publications).where(publications: {id: nil})
Another one is to use counter_cache feature. You will need to add publication_count column to your press_releases table.
class Publications < ActiveRecord::Base
belongs_to :presss_release, counter_cache: true
end
Rails will keep this column in sync with a number of records associated to given mode, so then you can simply do:
PressRelease.where(publications_count: [nil, 0])
I am currently implementing Ransack for searching functionality.
I have a model Campaigns which collaborates campaigns that the user directly created as well as others so long as the user belongs to the same vendor.
I can combine the results as such:
#search = current_user.campaigns + current_user.vendor.campaigns.where.not(:user_id => current_user.id)
Problem with this is that Ransack will not accept this combination and spits out
no implicit conversion of Ransack::Search into Array
Can someone point me in the direction on how to refactor this code?
TIA
Adding Addition Data
When looking at my console I can see *current_user.campaigns*:
Campaign Load (0.3ms)
SELECT DISTINCT "campaigns".* FROM "campaigns"
WHERE "campaigns"."user_id" = ? [["user_id", 2]]
Running *current_user.vendor.campaigns* give me:
Campaign Load (0.4ms)
SELECT DISTINCT "campaigns".* FROM "campaigns"
INNER JOIN "weeks" ON "campaigns"."id" = "weeks"."campaign_id"
INNER JOIN "products" ON "weeks"."product_id" = "products"."id"
INNER JOIN "locations" ON "products"."location_id" = "locations"."id"
WHERE "locations"."vendor_id" = ? [["vendor_id", 2]]
I can get the first filter of current_user achieved with:
#search = Campaign.where("campaigns.user_id" => current_user.id).search(params[:q])
But I am lost of how I go about building the rest of the join tables to include both elements of data
Solved
#search = Campaign.includes(:weeks).where('(campaigns.user_id LIKE ?) OR (weeks.vendor_id LIKE ?)', current_user.id, current_user.vendor.id).search(params[:q])
I'm trying to do something that I thought it would be simple but it seems not to be.
I have a project model that has many vacancies.
class Project < ActiveRecord::Base
has_many :vacancies, :dependent => :destroy
end
I want to get all the projects that have at least 1 vacancy.
I tried something like this:
Project.joins(:vacancies).where('count(vacancies) > 0')
but it says
SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).
1) To get Projects with at least 1 vacancy:
Project.joins(:vacancies).group('projects.id')
2) To get Projects with more than 1 vacancy:
Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')
3) Or, if Vacancy model sets counter cache:
belongs_to :project, counter_cache: true
then this will work, too:
Project.where('vacancies_count > ?', 1)
Inflection rule for vacancy may need to be specified manually?
joins uses an inner join by default so using Project.joins(:vacancies) will in effect only return projects that have an associated vacancy.
UPDATE:
As pointed out by #mackskatz in the comment, without a group clause, the code above will return duplicate projects for projects with more than one vacancies. To remove the duplicates, use
Project.joins(:vacancies).group('projects.id')
UPDATE:
As pointed out by #Tolsee, you can also use distinct.
Project.joins(:vacancies).distinct
As an example
[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""
Yeah, vacancies is not a field in the join. I believe you want:
Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
# None:
Project.left_joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')
Performing an inner join to the has_many table combined with a group or uniq is potentially very inefficient, and in SQL this would be better implemented as a semi-join that uses EXISTS with a correlated subquery.
This allows the query optimiser to probe the vacancies table to check for the existence of a row with the correct project_id. It doesn't matter whether there is one row or a million that have that project_id.
That's not as straightforward in Rails, but can be achieved with:
Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)
Similarly, find all projects that have no vacancies:
Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)
Edit: in recent Rails versions you get a deprecation warning telling you to not to rely on exists being delegated to arel. Fix this with:
Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)
Edit: if you're uncomfortable with raw SQL, try:
Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)
You can make this less messy by adding class methods to hide the use of arel_table, for example:
class Project
def self.id_column
arel_table[:id]
end
end
... so ...
Project.where.not(
Vacancies.where(
Vacancy.project_id_column.eq(Project.id_column)
).arel.exists
)
In Rails 4+, you can also use includes or eager_load to get the same answer:
Project.includes(:vacancies).references(:vacancies).
where.not(vacancies: {id: nil})
Project.eager_load(:vacancies).where.not(vacancies: {id: nil})
I think there's a simpler solution:
Project.joins(:vacancies).distinct
Without much Rails magic, you can do:
Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')
This type of conditions will work in all Rails versions as much of the work is done directly on the DB side. Plus, chaining .count method will work nicely too. I've been burned by queries like Project.joins(:vacancies) before. Of course, there are pros and cons as it's not DB agnostic.
You can also use EXISTS with SELECT 1 rather than selecting all the columns from the vacancies table:
Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
If I want to know how many records have at least one of an associated record, I would do:
Project.joins(:vacancies).uniq.count
The error is telling you that vacancies is not a column in projects, basically.
This should work
Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')
I have following SQL Query:
SELECT campaigns.* , campaign_countries.points, offers.image
FROM campaigns
JOIN campaign_countries ON campaigns.id = campaign_countries.campaign_id
JOIN countries ON campaign_countries.country_id = countries.id
JOIN offers ON campaigns.offer_id = offers.id
WHERE countries.code = 'US'
This works perfectly well. I want its rails active record version some thing like:
Campaign.includes(campaign_countries: :country).where(countries: {code: "US"})
Above code runs more or less correct query (did not try to include offers table), issue is returned result is collection of Campaign objects so obviously it does not include Points
My tables are:
campaigns --HAS_MANY--< campaign_countries --BELONGS_TO--< countries
campaigns --BELONGS_TO--> offers
Any suggestions to write AR version of this SQL? I don't want to use SQL statement in my code.
I some how got this working without SQL but surely its poor man's solution:
in my controller I have:
campaigns = Campaign.includes(campaign_countries: :country).where(countries: {code: country.to_s})
render :json => campaigns.to_json(:country => country)
in campaign model:
def points_for_country country
CampaignCountry.joins(:campaign, :country).where(countries: {code: country}, campaigns: {id: self.id}).first
end
def as_json options={}
json = {
id: id,
cid: cid,
name: name,
offer: offer,
points_details: options[:country] ? points_for_country(options[:country]) : ""
}
end
and in campaign_countries model:
def as_json options={}
json = {
face_value: face_value,
actual_value: actual_value,
points: points
}
end
Why this is not good solution? because it invokes too many queries:
1. It invokes query when first join is performed to get list of campaigns specific to country
2. For each campaign found in first query it will invoke one more query on campaign_countries table to get Points for that campaign and country.
This is bad, Bad and BAD solution. Any suggestions to improve this?
If You have campaign, You can use campaign.campaign_countries to get associated campaign_countries and just get points from them.
> campaign.campaign_countries.map(&:points)
=> [1,2,3,4,5]
Similarly You will be able to get image from offers relation.
EDIT:
Ok, I guess now I know what's going on. You can use joins with select to get object with attached fields from join tables.
cs = Campaign.joins(campaign_countries: :country).joins(:offers).select('campaigns.*, campaign_countries.points, offers.image').where(countries: {code: "US"})
You can than reference additional fields by their name on Campaign object
cs.first.points
cs.first.image
But be sure, that additional column names do not overlap with some primary table fields or object methods.
EDIT 2:
After some more research I came to conclusion that my first version was actually correct for this case. I will use my own console as example.
> u = User.includes(:orders => :cart).where(:carts => { :id => [5168, 5167] }).first
> u.orders.length # no query is performed
=> 2
> u.orders.count # count query is performed
=> 5
So when You use includes with condition on country, in campaign_countries are stored only campaign_countries that fulfill Your condition.
Try this:
Campaign.joins( [{ :campaign_countries => :countries}, :offers]).where('`countries`.`code` = ?', "US")
There are 3 models log (which belongs to customer), customer and project in rails 3.2 app. Both customer and project have sales_id field. Here is the query we want to do:
return the following logs for customers 1) logs for customers whose sales_id is equal to session[:user_id] and 2) logs for customers whose projects' sales_id is equal to session[:user_id]
The rails query for 1) could be:
Log.joins(:customer).where(:customers => {:sales_id => session[:user_id]})
Rails query for 2) could be:
Log.joins(:customer => :projects).where(:projects => {:sales_id => session[:user_id})
To combine the queries above, is it the right way to do the following?
Log.joins([:customer, {:customer => :projects}]).where('customers.sales_id = id OR projects.sales_id = id', id: session[:user_id])
Chapter 11.2.4 in http://guides.rubyonrails.org/v3.2.13/active_record_querying.html talks about an interesting query case. We haven't tested the query above yet. We would like to know if the union query above is indeed correct.
Rails doesn't support union natively. In your case, I think it doesn't need union, just use left outer join.
Log.joins('left outer JOIN `customers` ON `customers`.`id` = `logs`.`customer_id`
left outer JOIN `projects` ON `projects`.`customer_id` = `customers`.`id`').where('customers.sales_id = :id OR projects.sales_id = :id', id: session[:user_id]).distinct