I am new to rails so forgive the question if its silly :S
I am using Spree as my e-commerce platform of choice.
In the Rails Console I run:
Spree::Product.joins(:product_properties, :taxons, :variants).limit(10)
this returns the sql:
SELECT `spree_products`.* FROM `spree_products` INNER JOIN `spree_product_properties` ON `spree_product_properties`.`product_id` = `spree_products`.`id` INNER JOIN `spree_products_taxons` ON `spree_products_taxons`.`product_id` = `spree_products`.`id` INNER JOIN `spree_taxons` ON `spree_taxons`.`id` = `spree_products_taxons`.`taxon_id` INNER JOIN `spree_variants` ON `spree_variants`.`product_id` = `spree_products`.`id` AND `spree_variants`.is_master = 0 AND `spree_variants`.deleted_at IS NULL LIMIT 10
However, I need the sql to set the values
`spree_variants`.is_master = 1 AND `spree_variants`.deleted_at IS NOT NULL
Does an one know how to do this? I have tried
Spree::Product.joins(:product_properties, :taxons, :variants).limit(10).where("`spree_variants`.is_master = 1 AND `spree_variants`.deleted_at IS NOT NULL")
but this doesnt work and generates the following sql
SELECT `spree_products`.* FROM `spree_products` INNER JOIN `spree_product_properties` ON `spree_product_properties`.`product_id` = `spree_products`.`id` INNER JOIN `spree_products_taxons` ON `spree_products_taxons`.`product_id` = `spree_products`.`id` INNER JOIN `spree_taxons` ON `spree_taxons`.`id` = `spree_products_taxons`.`taxon_id` INNER JOIN `spree_variants` ON `spree_variants`.`product_id` = `spree_products`.`id` AND `spree_variants`.is_master = 0 AND `spree_variants`.deleted_at IS NULL WHERE (`spree_variants`.is_master = 1 AND `spree_variants`.deleted_at IS NOT NULL) LIMIT 10
Thanks!
You'll save a lot of time by using squeel gem. It uses blocks extensively and allows you to write (among other features) queries like this on pure Ruby:
.where{(spree_variants.is_master == 1) & (spree_variants.deleted_at != nil)}
Note the using of square brackets and absence of symbols.
To use these features just add this line to your Gemfile:
gem 'squeel'
and run bundle install.
Related
I have a couple really hard DB queries I need help to re-write in the correct way for Rails 6 Active Record. These are currently working in an app I an re-writing to the new version of Ruby on Rails (6.1.4.2).
It was originally written on Rails v3.2 with a Hell gem called squeel which uses its own DSL Language.
https://github.com/activerecord-hackery/squeel
I have been trying for days now and haven't been able to get it figured out. The first time I asked it I probably wasn't as clear as I needed to be. So this time I am going to put the query as it was written in squeel, and the SQL that the console from Heroku is spitting out and that's all. If anyone wants any additional information ask and I will HAPPILY post it. I want to keep it simple to start with as they are confusing enough.
WARNING: These seem to be EXTREMLY COMPLICATED.
ANY HELP would be VERY Appreciated! :)
Here is squeel DB Query 1:
Project.joins{vendor}.joins{certifications.outer}.where{
(projects.vendor_id.eq my{ vendor_id }) |
(vendors.parent_vendor_id.eq my{ vendor_id }) |
((certifications.cdti == true) & (certifications.published == true))
}.uniq
Here is the strait SQL from query 1 out of Rails v3.2:
SELECT DISTINCT "vendors".* FROM "vendors" INNER JOIN "projects" ON "projects"."vendor_id" = "vendors"."id"
INNER JOIN "certifications" ON "certifications"."project_id" = "projects"."id"
WHERE (("certifications"."cdti" = 't' AND "certifications"."published" = 't'))
ORDER BY "vendors"."parent_vendor_id", "vendors"."name"
Here is the squeel DB query 2:
Fleet.joins{vendor.projects.certifications}.
where{(certifications.cdti.eq true) & (certifications.published.eq true)}.
uniq.includes(:vendor).
order(:vendor_id, :name)
Here is the strait SQL from query 2 out of Rails v3.2:
(I hit enter in a few places so you could see the entire statement without having to scroll to the right
SELECT DISTINCT "fleets".* FROM "fleets" INNER JOIN "vendors" ON "vendors"."id" = "fleets"."vendor_id"
INNER JOIN "projects" ON "projects"."vendor_id" = "vendors"."id"
INNER JOIN "certifications" ON "certifications"."project_id" = "projects"."id"
WHERE (("certifications"."cdti" = 't' AND "certifications"."published" = 't'))
ORDER BY "fleets"."vendor_id", "fleets"."name"
Again if anyone wants to see or know anything else just let me know as I am trying my best to figure this out, but these seem so advanced I just don't think I know the correct syntax.
Thank You for your time,
Scott
Query 1 equivalent is:
Vendor.joins(projects: :certifications).where(certifications: { cdti: 't', published: 't' }).order(:parent_vendor_id, :name).distinct
Query 2:
Fleet.joins(vendor: { projects: :certifications }).where(certifications: { cdti: 't', published: 't' }).order(:vendor_id, :name).distinct
I have the following relationship in Rails: Campaign -has-many- Promise(s)
And I have the following Ruby code to return verbose list of campaigns with promises count:
def campaigns
results = Campaign
.where(user_id: current_user.id)
.left_outer_joins(:promises)
.select('campaigns.*', 'COUNT(DISTINCT promises.id) AS promises_count')
.group('campaigns.id')
.ransack(params[:q])
.result(distinct: true)
render json: {
results: results.page(params[:page]).per(params[:per_page]),
total_results: results.count(:id)
}
end
Everything works fine, unless I try to sort by promises_count. Ransack (or something else?) generates the following SQL for Postgres:
SELECT DISTINCT
campaigns.*,
COUNT(DISTINCT promises.id) AS promises_count
FROM "campaigns"
LEFT OUTER JOIN "promises"
ON "promises"."campaign_id" = "campaigns"."id"
LEFT OUTER JOIN "promises" "promises_campaigns"
ON "promises_campaigns"."campaign_id" = "campaigns"."id"
WHERE "campaigns"."user_id" = 1 GROUP BY campaigns.id;
It works, but there is no ORDER BY for some reason. When I sort by other properties, it works fine. I think Ransack is missing something, and handles promises_count different way because it's generated property and not real one.
It's possible to sort in Postgres, for example, manual query with added ORDER BY works:
SELECT DISTINCT
campaigns.*,
COUNT(DISTINCT promises.id) AS promises_count
FROM "campaigns"
LEFT OUTER JOIN "promises"
ON "promises"."campaign_id" = "campaigns"."id"
LEFT OUTER JOIN "promises" "promises_campaigns"
ON "promises_campaigns"."campaign_id" = "campaigns"."id"
WHERE "campaigns"."user_id" = 1
GROUP BY campaigns.id
ORDER BY promises_count desc;
How do I make Ransack work? I tried different combinations of queries without too much luck.
I am trying to achieve the following with Laravel Query builder.
I have a table called deals . Below is the basic schema
id
deal_id
merchant_id
status
deal_text
timestamps
I also have another table called merchants whose schema is
id
merchant_id
merchant_name
about
timestamps
Currently I am getting deals using the following query
$deals = DB::table('deals')
-> join ('merchants', 'deals.merchant_id', '=', 'merchants.merchant_id')
-> where ('merchant_url_text', $merchant_url_text)
-> get();
Since only 1 merchant is associated with a deal, I am getting deals and related merchant info with the query.
Now I have a 3rd table called tbl_deal_votes. Its schema looks like
id
deal_id
vote (1 if voted up, 0 if voted down)
timestamps
What I want to do is join this 3rd table (on deal_id) to my existing query and be able to also get the upvotes and down votes each deal has received.
To do this in a single query you'll probably need to use SQL subqueries, which doesn't seem to have good fluent query support in Laravel 4/5. Since you're not using Eloquent objects, the raw SQL is probably easiest to read. (Note the below example ignores your deals.deal_id and merchants.merchant_id columns, which can likely be dropped. Instead it just uses your deals.id and merchants.id fields by convention.)
$deals = DB::select(
DB::raw('
SELECT
deals.id AS deal_id,
deals.status,
deals.deal_text,
merchants.id AS merchant_id,
merchants.merchant_name,
merchants.about,
COALESCE(tbl_upvotes.upvotes_count, 0) AS upvotes_count,
COALESCE(tbl_downvotes.downvotes_count, 0) AS downvotes_count
FROM
deals
JOIN merchants ON (merchants.id = deals.merchant_id)
LEFT JOIN (
SELECT deal_id, count(*) AS upvotes_count
FROM tbl_deal_votes
WHERE vote = 1 && deal_id
GROUP BY deal_id
) tbl_upvotes ON (tbl_upvotes.deal_id = deals.id)
LEFT JOIN (
SELECT deal_id, count(*) AS downvotes_count
FROM tbl_deal_votes
WHERE vote = 0
GROUP BY deal_id
) tbl_downvotes ON (tbl_downvotes.deal_id = deals.id)
')
);
If you'd prefer to use fluent, this should work:
$upvotes_subquery = '
SELECT deal_id, count(*) AS upvotes_count
FROM tbl_deal_votes
WHERE vote = 1
GROUP BY deal_id';
$downvotes_subquery = '
SELECT deal_id, count(*) AS downvotes_count
FROM tbl_deal_votes
WHERE vote = 0
GROUP BY deal_id';
$deals = DB::table('deals')
->select([
DB::raw('deals.id AS deal_id'),
'deals.status',
'deals.deal_text',
DB::raw('merchants.id AS merchant_id'),
'merchants.merchant_name',
'merchants.about',
DB::raw('COALESCE(tbl_upvotes.upvotes_count, 0) AS upvotes_count'),
DB::raw('COALESCE(tbl_downvotes.downvotes_count, 0) AS downvotes_count')
])
->join('merchants', 'merchants.id', '=', 'deals.merchant_id')
->leftJoin(DB::raw('(' . $upvotes_subquery . ') tbl_upvotes'), function($join) {
$join->on('tbl_upvotes.deal_id', '=', 'deals.id');
})
->leftJoin(DB::raw('(' . $downvotes_subquery . ') tbl_downvotes'), function($join) {
$join->on('tbl_downvotes.deal_id', '=', 'deals.id');
})
->get();
A few notes about the fluent query:
Used the DB::raw() method to rename a few selected columns.
Otherwise, there would have been a conflict between deals.id
and merchants.id in the results.
Used COALESCE to default null votes to 0.
Split the subqueries into separate PHP strings to improve readability.
Used left joins for the subqueries so deals with no upvotes/downvotes still show up.
The following code gets all the residences which have all the amenities which are listed in id_list. It works with out a problem with SQLite but raises an error with PostgreSQL:
id_list = [48, 49]
Residence.joins(:listed_amenities).
where(listed_amenities: {amenity_id: id_list}).
references(:listed_amenities).
group(:residence_id).
having("count(*) = ?", id_list.size)
The error on the PostgreSQL version:
What do I have to change to make it work with PostgreSQL?
A few things:
references should only be used with includes; it tells ActiveRecord to perform a join, so it's redundant when using an explicit joins.
You need to fully qualify the argument to group, i.e. group('residences.id').
For example,
id_list = [48, 49]
Residence.joins(:listed_amenities).
where(listed_amenities: { amenity_id: id_list }).
group('residences.id').
having('COUNT(*) = ?", id_list.size)
The query the Ruby (?) code is expanded to is selecting all fields from the residences table:
SELECT "residences".*
FROM "residences"
INNER JOIN "listed_amenities"
ON "listed_amentities"."residence_id" = "residences"."id"
WHERE "listed_amenities"."amenity_id" IN (48,49)
GROUP BY "residence_id"
HAVING count(*) = 2
ORDER BY "residences"."id" ASC
LIMIT 1;
From the Postgres manual, When GROUP BY is present, it is not valid for the SELECT list expressions to refer to ungrouped columns except within aggregate functions or if the ungrouped column is functionally dependent on the grouped columns, since there would otherwise be more than one possible value to return for an ungrouped column.
You'll need to either group by all fields that aggregate functions aren't applied to, or do this differently. From the query, it looks like you only need to scan the amentities table to get the residence ID you're looking for:
SELECT "residence_id"
FROM "listed_amenities"
WHERE "listed_amenities"."amenity_id" IN (48,49)
GROUP BY "residence_id"
HAVING count(*) = 2
ORDER BY "residences"."id" ASC
LIMIT 1
And then fetch your residence data with that ID. Or, in one query:
SELECT "residences".*
FROM "residences"
WHERE "id" IN (SELECT "residence_id"
FROM "listed_amenities"
WHERE "listed_amenities"."amenity_id" IN (48,49)
GROUP BY "residence_id"
HAVING count(*) = 2
ORDER BY "residences"."id" ASC
LIMIT 1
);
I have a PostgreSQL query that I would like to write in ActiveRecord (Rails 4), but I'm having trouble getting it to work correctly.
UPDATE chats AS c
SET email = m.source_name
FROM messages AS m
WHERE c.id = m.chat_id
AND m.created_at >= '2014-10-10'
This is what I've tried:
Chat.joins(:messages)
.where("message.created_at >= '2014-10-10'")
.update_all('chat.email = message.source_name')
But it creates a query like this:
UPDATE "chats"
SET chat.email = message.source_name
WHERE "chats"."id" IN (
SELECT "chats"."id"
FROM "chats"
INNER JOIN "messages"
ON "messages"."chat_id" = "chats"."id"
WHERE (message.created_at >= '2014-10-10')
)
Any help on this?
Since Chat.update_all will add UPDATE chats SET... the only way that I can think of get rails to do an update with an alias (UPDATE chats AS c) is by using connection.update and a sql string:
Chat.connection.update(Q%{
UPDATE chats AS c
SET email = m.source_name
FROM messages AS m
WHERE c.id = m.chat_id
AND m.created_at >= '2014-10-10'
});
Not great if you want to avoid SQL fragments, but using an a join as in your question may be the only way if you want to use AREL.