I guess I wasn't very clear in the title, but I find it hard to describe exactly what I'm after. So it's better to start with an example:
I have the following models:
class Song < ActiveRecord::Base
has_many :learnt_items, as: :learnable
has_many :trained_items, as: :learnable
end
class LearntItem < ActiveRecord::Base
belongs_to :learnable, polymorphic: true
end
class TrainedItem < LearntItem
end
If, for example, I select all songs that have trained items:
Song.joins(:trained_items)
I would see (roughly) records returned like:
learnable_type | learnable_id | type | label
Song | 1 | TrainedItem | happy
Song | 1 | TrainedItem | sad
Song | 1 | TrainedItem | lively
Song | 2 | TrainedItem | lively
If I would like to select all songs that have trained items with specific labels, I'd do:
Song.joins(:trained_items).where(learnt_items: { label: [:happy, :sad] })
Now, I need to get all songs that don't have trained items for given labels. One would think the following would suffice:
Song.joins(:trained_items).where.not(learnt_items: { label: [:happy, :sad] })
But this will still produce the following records:
learnable_type | learnable_id | type | label
Song | 1 | TrainedItem | lively
Song | 2 | TrainedItem | lively
which is indeed not what I intended. You can see that the query filtered out records with the given labels, but the one with label = 'lively' is still there, hence returning the song with id = 1. I would need only the song with id = 2 to be returned from this query.
How can I build a query with ActiveRecord so that my scenario is fulfilled?
Use a subquery to find the ids you don't want and use those in the where.not condition:
Song.joins(:trained_items)
.where.not(learnable_id: Song.select(:learnable_id).where(learnt_items: { label: [:happy, :sad] })
Related
Hello guys please bear with me here. I'm using PostgreSQL, Sequelize, Express, and NodeJS to create a backend. I'm wondering if these lines of raw query code can be implemented using Sequelize Model findAll function.
First of all, what I am trying to do here is to calculate the total score of these students. Here are some tables and their relations.
Student Level
| student_id | name | level_id | | level_id | level_name |
|:----------:|:----------:|:--------:| |:--------:|:----------:|
| 1 | John | 1 | > | 1 | Rookie |
| 2 | Jane | 2 | | 2 | Expert |
v
StudentQuiz
| quiz_id | student_id | score |
|:----------:|:----------:|:--------:|
| 1 | 1 | 40 |
| 1 | 1 | 100 |
| 2 | 1 | 80 |
| 1 | 2 | 100 |
| 2 | 2 | 100 |
If I run line of codes below.
SELECT table2.student_id,
s.canvasser_name,
l.level_name,
table2.total_score
FROM (SELECT table1.student_id,
sum(table1.max_score) total_score
FROM (SELECT sq.student_id,
max(sq.score) max_score
FROM public.StudentQuiz sq
GROUP BY sq.quiz_id, sq.student_id) table1
GROUP BY table1.student_id) table2
INNER JOIN public.Student s
ON s.student_id = table2.student_id
INNER JOIN public.Level l
ON l.level_id = s.level_id
ORDER BY table2.total_score DESC
LIMIT 10;
I will get something like this.
| student_id | name | level | total_score |
|:----------:|:----------:|:--------:|:--------------:|
| 1 | John | Rookie | 180 |
| 2 | Jane | Expert | 200 |
Please note that I'm selecting the highest score if more than one quiz with the same id found.
Anyway, I want to implement it using sequelize built in function. What I've been trying to do is something like this.
const result = await StudentQuiz.findAll({
attributes: ['studentId', [sequelize.fn('sum', sequelize.fn('max', sequelize.col('score'))), 'totalPrice'], 'quizId'],
group: 'studentId',
include: [
{
model: Student,
include: [{
model: Level
}],
},
],
offset: 0,
limit: 10
});
The code above throws an error message which is "aggregate function calls cannot be nested".
Any kind of help will be appreciated. Thank you.
P.S. I know i can use sequelize.query() function to use the first code block shown, but that's not the point.
Sequelize is not intended to work with complex aggregations using models. Its primary goal is to provide CRUD operations with models.
To use models in such scenario you can use a model definition to get a schema, a table name and fields to build a query dynamically not knowing exact field names.
I find the solution without using any raw query, though I need to get two tables, that is StudentQuiz and Student that coupled by Level. Here is my answer.
// Finding maximum score and group it based on studentId and quizId
const maxScoreList = await StudentQuiz.findAll({
attributes: ['studentId', 'quizId', [sequelize.fn('max', sequelize.col('score')), 'maxScore']],
group: ['studentId', 'quizId'],
order: ['studentId', 'quizId'],
raw: true
});
// Calculating total score for the same student for each quiz recorded
const scoreArray = [maxScoreList.shift()];
let index = 0;
const unfilteredStudentId = maxScoreList.map((item) => {
if (scoreArray[index].studentId !== item.studentId) {
scoreArray.push(item);
index += 1;
}
scoreArray[index].maxScore += item.maxScore;
return item.studentId;
});
// Filtering studentId that show up more than one time
const extractedStudentId = [...new Set(unfilteredStudentId)];
// Finding student based on studentId inside extractedStudentId array
const student = await Student.findAll({
where: { id: extractedStudentId },
attributes: ['id', 'canvasserId', 'canvasserName', 'canvasserImageUrl'],
include: {
model: Level,
attributes: [['level_name', 'level'], ['icon_url', 'level_image_url']]
},
order: ['id'],
raw: true,
nest: true
});
// Combining total score list to student list
const rankList = student.map((item, idx) => ({ ...item, totalScore: scoreArray[idx] }));
With this much complexity, I agree that using raw query by far the best approach for this case.
I have three tables offers, sports and the join table offers_sports.
class Offer < ActiveRecord::Base
has_and_belongs_to_many :sports
end
class Sport < ActiveRecord::Base
has_and_belongs_to_many :offers
end
I want to select offers that include a given array of sport names. They must contain all of the sports but may have more.
Lets say I have these three offers:
light:
- "Yoga"
- "Bodyboarding"
medium:
- "Yoga"
- "Bodyboarding"
- "Surfing"
all:
- "Yoga"
- "Bodyboarding"
- "Surfing"
- "Parasailing"
- "Skydiving"
Given the array ["Bodyboarding", "Surfing"] I would want to get medium and all but not light.
I have tried something along the lines of this answer but I get zero rows in the result:
Offer.joins(:sports)
.where(sports: { name: ["Bodyboarding", "Surfing"] })
.group("sports.name")
.having("COUNT(distinct sports.name) = 2")
Translated to SQL:
SELECT "offers".*
FROM "offers"
INNER JOIN "offers_sports" ON "offers_sports"."offer_id" = "offers"."id"
INNER JOIN "sports" ON "sports"."id" = "offers_sports"."sport_id"
WHERE "sports"."name" IN ('Bodyboarding', 'Surfing')
GROUP BY sports.name
HAVING COUNT(distinct sports.name) = 2;
An ActiveRecord answer would be nice but I'll settle for just SQL, preferably Postgres compatible.
Data:
offers
======================
id | name
----------------------
1 | light
2 | medium
3 | all
4 | extreme
sports
======================
id | name
----------------------
1 | "Yoga"
2 | "Bodyboarding"
3 | "Surfing"
4 | "Parasailing"
5 | "Skydiving"
offers_sports
======================
offer_id | sport_id
----------------------
1 | 1
1 | 2
2 | 1
2 | 2
2 | 3
3 | 1
3 | 2
3 | 3
3 | 4
3 | 5
4 | 3
4 | 4
4 | 5
Group by offer.id, not by sports.name (or sports.id):
SELECT o.*
FROM sports s
JOIN offers_sports os ON os.sport_id = s.id
JOIN offers o ON os.offer_id = o.id
WHERE s.name IN ('Bodyboarding', 'Surfing')
GROUP BY o.id -- !!
HAVING count(*) = 2;
Assuming the typical implementation:
offer.id and sports.id are defined as primary key.
sports.name is defined unique.
(sport_id, offer_id) in offers_sports is defined unique (or PK).
You don't need DISTINCT in the count. And count(*) is even a bit cheaper, yet.
Related answer with an arsenal of possible techniques:
How to filter SQL results in a has-many-through relation
Added by #max (the OP) - this is the above query rolled into ActiveRecord:
class Offer < ActiveRecord::Base
has_and_belongs_to_many :sports
def self.includes_sports(*sport_names)
joins(:sports)
.where(sports: { name: sport_names })
.group('offers.id')
.having("count(*) = ?", sport_names.size)
end
end
One way to do it is using arrays and the array_agg aggregate function.
SELECT "offers".*, array_agg("sports"."name") as spnames
FROM "offers"
INNER JOIN "offers_sports" ON "offers_sports"."offer_id" = "offers"."id"
INNER JOIN "sports" ON "sports"."id" = "offers_sports"."sport_id"
GROUP BY "offers"."id" HAVING array_agg("sports"."name")::text[] #> ARRAY['Bodyboarding','Surfing']::text[];
returns:
id | name | spnames
----+--------+---------------------------------------------------
2 | medium | {Yoga,Bodyboarding,Surfing}
3 | all | {Yoga,Bodyboarding,Surfing,Parasailing,Skydiving}
(2 rows)
The #> operator means that the array on the left must contain all the elements from the one on the right, but may contain more. The spnames column is just for show, but you can remove it safely.
There are two things you must be very mindful of with this.
Even with Postgres 9.4 (I haven't tried 9.5 yet) type conversion for comparing arrays is sloppy and often errors out, telling you it can't find a way to convert them to comparable values, so as you can see in the example I've manually cast both sides using ::text[].
I have no idea what the level of support for array parameters is Ruby, nor the RoR framework, so you may end-up having to manually escape the strings (if input by user) and form the array using the ARRAY[] syntax.
I'm attempting to use single-table inheritance in Rails as a means of allowing a user have multiple user sub-types (e.g., faculty, vendor, etc.). I've ended up with a user table with records containing only a single user type. While still using single-table inheritance, how do I get my users to have multiple types? (I know this is essentially a many-to-many relationship; I'm just not sure of how to accomplish this using STI.)
id | first_name | last_name | birth_date | city | zip_code | email | type | created_at | updated_at
----+------------+-----------+------------+------+----------+-------+---------+----------------------------+----------------------------
1 | Akira | Yamaoka | | | | | Vendor | 2014-08-30 14:58:26.917333 | 2014-08-30 14:58:26.917333
2 | Pyramid | Head | | | | | Faculty | 2014-08-30 15:02:04.70209 | 2014-08-30 15:02:04.70209
Here are my models' classes:
user.rb
1 class User < ActiveRecord::Base
2 end
vendor.rb
1 class Vendor < User
2 belongs_to :user
3 belongs_to :event
4 end
faculty.rb
1 class Faculty < User
2 belongs_to :user
3 belongs_to :event
4
5 end
you could write something like:
class User < ActiveRecord::Base
belongs_to :event
end
class Vendor < User
end
class Faculty < User
end
then get records with different types through User model, like User.all
A short answer: no, you can't do that, type column can only hold one value.
A longer answer: a many-to-many relationship requires that an extra model exists (it has to, HABTM just won't do here) that contains references to a User and an Event. I called it a Pass in this answer to your another question.
You should subclass a Pass, not User. And since User has_many :passes, which can possibly be Pass subclasses, a User can participate in an event in multiple ways: as a vendor or as a faculty member.
I've supplied some example code here.
Let's say we have these two models:
class Attendee < ActiveRecord::Base
has_many :dances
has_many :partners, through: :events
end
class Dance < ActiveRecord::Base
belongs_to :attendee
belongs_to :partner, class_name: Attendee
end
I'd like to implement a method, for example Dance.matches_of(attendee) that returns the dances of an attendee only if his partners have a dance where he is the partner. How would you implement an efficient solution?
EDIT:
Sorry because I couldn't explain my question well, I'll try to clarify it (I changed the Event class name to Dance). If an user wants to dance with another one, he/she will create an object Dance, specifying the partner. So, let's say that Bob would like to dance with Sarah, the database table dances would look like this (I'll use usernames instead of ids to make it clearer, hopefully):
| id | attendee_id | partner_id |
---------------------------------------------------
| 1 | bob | sarah |
So, Dance.matches_of(bob) would return nothing, as there is no one that he wants to dance with who also wants him as a dancing partner (poor Bob). After a while Sarah thinks that maybe Bob is not such a bad guy, and he deserves a chance to hit the floor. Now, the dances table looks like this:
| id | attendee_id | partner_id |
---------------------------------------------------
| 1 | bob | sarah |
---------------------------------------------------
| 2 | sarah | bob |
Dance.matches_of(bob) now returns the dance with id 1, because it's the record that shows Bob's interest on dancing with Sarah AND Sarah wants to dance with him as well (Dance.matches_of(sarah) would retrieve the second record). If Bob wanted to dance with Anna too, and so did she, the method would retrieve two records and Bob would be the happiest guy at the party, as he'd have two dancing partners.
I hope this explanation is clearer, but I'm aware that if it's so hard to be explained and understood, maybe the approach I'm following is not correct, so I'm open to suggestions.
EDIT 2:
The solution I came up with:
def self.matches_of(attendee)
attendee.dances.select { |dance| dance.partner.partners.include? attendee }
end
But I don't expect it to be much efficient anyway.
Given your models, you can add a matches_of scope to the Dance model:
class Attendee < ActiveRecord::Base
has_many :dances
has_many :partners, class_name: 'Attendee', through: :dances
end
class Dance < ActiveRecord::Base
belongs_to :attendee
belongs_to :partner, class_name: 'Attendee'
scope :matches_of, -> (attendee) {
where(partner_id: attendee.id, attendee_id: attendee.partner_ids)
}
end
Given dances like
| id | attendee_id | partner_id |
---------------------------------------------------
| 1 | bob | sarah |
matches_of will return []
bob = Attendee.find 1
Dance.matches_of(bob)
# returns []
Given dances like
| id | attendee_id | partner_id |
---------------------------------------------------
| 1 | bob | sarah |
---------------------------------------------------
| 2 | sarah | bob |
matches_of will return Dance #2
Dance.matches_of(bob)
# returns [#<Dance id: 2, attendee_id: 2, partner_id: 1>]
I can think of a method like this, if there is a partner_id relating to an id in attendees table:
def partner_events
Event.where('partner_id = ? and attendee_id in (?)', id, events.map(&:partner_id))
end
def matches_of(attendee)
partner_ids = Event.pluck(:attendee_id).where(:partern_id = > attendee.id).uniq
Event.where('attendee_id = ? and partner_id in (?)', attendee.id, partner_ids)
end
Let's say I have the following tables:
product transaction
------------------ ------------------
| id | name | | id | product |
------------------ ------------------
| 1 | Product A | | 1 | 2 |
| 2 | Product B | | 2 | 3 |
| 3 | Product C | | 3 | 2 |
------------------ ------------------
Now, let's say I want to make a listing of the transaction table, but I want to display product names instead of product IDs.
In raw SQL, I would do something like this:
SELECT product.name FROM transaction, product WHERE transaction.product = product.id
I'm having a hard time figuring out how this would be done using Rails' Active Record Query Interface. I'm sure this is trivial to you Rails experts out there.
If you want to solve it the "Rails way":
first: change column product in transactions to product_id, and table names should be in plural, otherwise you must add e.g. set_table_name 'transaction' in the models, you will miss a lot of Rails' futures if you don't change it.
add/change these models:
class Product < ActiveRecord::Base
has_many :transactions
end
class Transaction < ActiveRecord::Base
belongs_to :product
end
If you persist on using column name "product" you must change the belongs_to to:
belongs_to :product, :foreign_key => 'product'
but that is kind of ugly
Finally, using this:
Transaction.includes(:product)
Or if you want only those who has a product
Transaction.joins(:product)
Note:
I don't remember exactly, but I think I had problems with using "transactions" as table name in some application. Somebody else perhaps knows about that.
transaction.joins('LEFT OUTER JOIN product ON transaction.id = product.id')