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')
Related
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 have three tables: Fee, Project and Company:
------------------ ------------------ ------------------
| Fee | | Project | | Company |
------------------ ------------------ ------------------
| fee:float | | id:int | | id:int |
| project_id:int | | currency:string| | currency:string|
| ... | | company_id:int | | ... |
------------------ | ... | ------------------
------------------
Each Company has many Projects and a Project typically has many Fees. I am trying to group (and sum) Fees by their currency type which, for legacy reasons is now set either on Projects or on Companies (a mess I know, but one I've inherited).
The following ActiveRecord queries nearly do what I require, but not quite:
Fee.all.joins(project: :company)
.group('projects.currency, companies.currency').sum(:fee)
...
=> {"USD"=>100893.0, "AUD"=>320.0, "GBP"=>10279.99, nil=>4242.42}
.
.
.
Fee.all.joins(project: :company)
.group('companies.currency, projects.currency').sum(:fee)
...
=> {nil=>10279.99, "KRW"=>4242.42}
I can't figure out how to have both currency columns from (Project and Company) summed correctly in a single query which would return a result like so:
=> {"USD"=>100893.0, "AUD"=>320.0, "GBP"=>10279.99, "KRW"=>4242.42}
I could solve this by joining and grouping by one table and column at a time then totalling the results programatically, but that feels clumsy.
EDIT: The full snippet for the first query above (note Fee is implemented as FeeMilestone):
[2] pry(main)> FeeMilestone.joins(project: :company).group('projects.currency, companies.currency').sum(:fee)
(11.0ms) SELECT SUM("fee_milestones"."fee") AS sum_fee, projects.currency, companies.currency AS projects_currency_companies_currency FROM "fee_milestones" INNER JOIN "projects" ON "projects"."id" = "fee_milestones"."project_id" INNER JOIN "companies" ON "companies"."id" = "projects"."company_id" GROUP BY projects.currency, companies.currency
=> {"USD"=>100893.0, "AUD"=>320.0, "GBP"=>10279.99, nil=>4242.42}
I'm hoping to get the Projects.currency to return instead of nil above (it would be "KRW" in my dev environment).
Use the COALESCE function which will go through the parameters pass to it and get the first one that isn't NULL. Since projects belong to companies, I'm assuming that the currency in the project take precedence over the one in the company so pass projects.currency first.
COALESCE(projects.currency, companies.currency)
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] })
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