Query for where method through three associations - sql

I have three models:
Invitation.rb
belongs_to :department
Department.rb
belongs_to :organisation
has_many :invitations
Organisation.rb
has_many :departments
Organization table has tag field.
I need to write a query to search for Invitations, for the organization of which there is a tag field with the content 'First tag'
Something like this:
Invitation.joins(:department).joins(:organisation).where(tag: 'First tag')
But I don't know how to build such a query.

Invitation.joins(department: :organisation).where(organisations: { tag: 'First tag' })
Why? Because to "reach" the organisations table from invitations you can use the relationship between the departments table.
Table.joins(...).joins(...)
Is "joining" twice the same table, what you need is to reference the relatinoship between departments and organisations. That you can do it as:
{ department: :organisation }
Passing that to join loads a JOIN clause in the receiver with departments and in departments with organisations.
Notice the where clause can be used as { organisations: { tag: ... } } or ["organisations.tag = ?", ...], they can be used interchangeably. What you use is just a matter of preference yours or from your colleagues.

To do what you are trying to do without changing your models you should pass a hash to the joins method like so:
Invitation.joins(department: :organisation).where(organisations: { tag: 'First tag' })
This tells ActiveRecord to use the department association as defined on the Invitation model and the organisation association from the Department model
To make your life a bit easier you can add a has_many through association like this:
class Invitation < ApplicationRecord
belongs_to :department
has_one :organisation, through: :department
end
class Department < ApplicationRecord
belongs_to :organisation
has_many :invitations, inverse_of: :department
end
class Organisation
has_many :departments, inverse_of: :organisation
has_many :invitations, through: :departments
end
Now you can use the invitations relation on Organisation as follows:
Invitation.joins(:organisation).where(tag: 'First tag')
To see the sql (e.g. in the console) you can use the #to_sql method:
Invitation.joins(:organisation).where(tag: 'First tag').to_sql
Finally, if you use a scope (and dont like the hash in the where method in which you have to specify the table) you can use a "scope" and merge as follows:
class Organisation
class << self
def just_tagged
where(tag: 'First tag')
end
end
end
Now you can refer to the scope within your query using merge:
Invitation.joins(:organistion).merge(Organisation.just_tagged)

Related

How to manually join two different table with different attribute name in Ruby on Rails controller

I am currently making a website that runs on Ruby on Rails. I am facing some issues while I was trying to join two tables, Rates and Locations, that I have with two different attributes name.
Rates: id rater_id rateable_id (and a few more attributes in this table)
Locations: id title body user_id (and a few more attributes in this table)
Here is the query that I am trying to do in SQL.
SELECT *
FROM rates, locations
WHERE rates.rater_id = locations.user_id AND rates.rateable_id = locations.id
I have read the official active record documents that provided by rubyonrails.org. I have tried doing these, but it does not work. Here is the code that I am trying to implant in app\controllers\users_controller.rb
#join_rating = Rate.joins(:locations).where("rates.rateable_id = locations.id AND rates.rater_id = locations.id")
#all_rating = #all_rating.where(rater_id: #user)
#count_all_rating = #all_rating.count
#join_rating, is trying to join the attributes with different names.
#all_rating, is trying to filter which location to show using the user ID
#join_rating, is trying to calculate the total numbers of locations that are rated by the user
Assume that everything is setup correctly and the only error is in the query that I am trying to do, how should I rewrite the statement so that I am able to show the locations that the user has rated using #all_rating.
Thank you!
A few points:
When in ActiveRecord you're starting a statement with the Rate class, it means the result is going to be a collection of Rate objects. So if you're trying to show locations, you should start with a Location class.
#locations_user_rated = Location.joins('INNER JOIN rates ON
rates.rateable_id = locations.id').where('rates.rater_id' => #user)
And if your ActiveRecord associations are well defined, you could simply do:
#locations_user_rated = Location.joins(:rates).where('rates.rater_id' => #user)
"Well defined" simply means you'll need to do something like the following. Note that I am not sure I understand your model relationships correctly. I assume below that every location has multiple rates, and that the reason your Rate model has the field called rateable_id instead of a location_id is because you want :rateable to be polymorphic. This means you probably also have a rateable_type field in rates table.
class Location < ActiveRecord::Base
has_many :rates, as: :rateable
end
class Rate < ActiveRecord::Base
belongs_to :rateable, polymorphic: true
end
If this polymorphism is not the case, things should actually be simpler, and I highly recommend that you follow Rails's conventions and simply name the relationship field location_id on your Rate model instead of rateable_id. Then you can do:
class Location < ActiveRecord::Base
has_many :rates
end
class Rate < ActiveRecord::Base
belongs_to :location
end
If still you are not convinced about the field name, you can customize things and do:
class Location < ActiveRecord::Base
has_many :rates, foreign_key: :rateable_id
end
class Rate < ActiveRecord::Base
belongs_to :location, foreign_key: :rateable_id
end
You can find more about how to customize associations here, and here.
I highly recommend taking advantage of ActiveRecord's has_many, belongs_to, and has_many through: functionality.
If you set up a model for each of these tables, with the correct relationships:
class User < ActiveRecord::Base
has_many :ratings, foreign_key: :rater_id
has_many :rated_locations, through: ratings, class_name: Location.name, source: :rater
end
class Rating < ActiveRecord::Base
belongs_to :rater, class_name: User.name
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :ratings
end
Then to access the locaitons that a user has rated, you just call
user.rated_locations

Deep model associations with Rails

Let's imagine that I have a CPA tracking system.
I would have following models: an Offer, it has some Landings, each of them has multiple Links, each of the links has a bunch of Visits.
So, I what I want is DRY code, therefore offer_id column within visits table is unacceptable. The workaround here is delegated methods like this:
class Offer < ActiveRecord::Base
has_many :landings
has_many :links, through: :landings
has_many :visits, through: :landings
end
class Landing < ActiveRecord::Base
belongs_to :offer
has_many :links
has_many :visits, through: :links
end
class Link < ActiveRecord::Base
belongs_to :landing
has_many :visits
delegate :offer, to: :landing
end
class Visit < ActiveRecord::Base
belongs_to :link
delegate :landing, to: :link
delegate :offer, to: :link
end
It works nice with a single visit, e.g. visit.offer.id. But what if I need different visits associated with one offer?
The issue is that I'm unable to construct a valid query using ActiveRecord API. It might look like Visits.where(offer: Offer.first), but it doesn't work this way, saying ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: visits.offer: SELECT "visits".* FROM "visits" WHERE "visits"."offer" = 1, which is predictable.
Question: How should I organize my code to make statements like Visits.where(offer: Offer.first) work efficiently without duplicating offer_id column within visits table?
You code was organized nicely, don't need to refactor I think. You can achieve that by defining a scope in Visit like this:
class Visit < ActiveRecord::Base
scope :from_offer, -> (offer) {
joins(link: :landing).where(ladings: {offer_id: offer.id})
}
scope :from_landing, -> (landing) {
joins(:link).where(links: {landing_id: landing.id})
}
end
So the query will be:
Visit.from_offer(Offer.first)

Where conditions on ActiveRecord associations conflicting with each other in has_many through?

In my app (Rails 4.2.0.rc2), users can be either students or admins of a given institution. There's an association :admin_institutions on User that returns all the institutions the user is an admin of by checking their role in the join table. There's also an association :students on Institution that returns all the users who are students at that institution, again according to institution_users.role.
These associations work as expected, so I added an association :admin_students to User, meant to return all the students at all the institutions for which a given user is an admin.
class InstitutionUser < ActiveRecord::Base
belongs_to :institution
belongs_to :user
end
class Institution < ActiveRecord::Base
has_many :institution_users
has_many :users, :through => :institution_users
has_many :students, -> { where "institution_users.role = 'Student'" }, :through => :institution_users, source: :user
...
end
class User < ActiveRecord::Base
has_many :institution_users
has_many :admin_institutions, -> { where "institution_users.role = 'Admin'" }, through: :institution_users, source: :institution
has_many :admin_students, through: :admin_institutions, source: :students
...
end
However, :admin_students does not work as expected. It generates the following SQL:
SELECT "users".* FROM "users" INNER JOIN "institution_users" ON "users"."id" = "institution_users"."user_id" INNER JOIN "institutions" ON "institution_users"."institution_id" = "institutions"."id" INNER JOIN "institution_users" "institution_users_admin_students_join" ON "institutions"."id" = "institution_users_admin_students_join"."institution_id" WHERE "institution_users_admin_students_join"."user_id" = $1 AND (institution_users.role='Student') AND (institution_users.role = 'Admin') [["user_id", 190]]
Instead of looking for all the institutions where the user is an admin and selecting all their students, it seems to be looking for institutions where the user is BOTH a student and an admin, so it returns an empty collection.
Is there a way to write an association (as opposed to just a method) that will give me the results I want, without my conditions conflicting like this?
(Side note: Is this the expected behavior for this kind of association? If so, I'd really appreciate further insight into why ActiveRecord interprets it the way it does.)
This may not be the answer, but maybe it will lead to one.
I'm not a fan of the associations with hard-coded SQL:
-> { where "institution_users.role = 'Student'" }
They are definitely at least part of the problem because they cannot be interpreted by ActiveRecord to determine which table alias for institution_users to apply it to.
You can allow ActiveRecord that flexibility by referencing a class method of the InsitutionUser model:
def self.students
where(role: "Student")
end
This also keeps the InstitutionUser logic all in one place.
Then the association becomes:
has_many :students, -> {merge(InstitutionUser.students)}, :through => :institution_users, source: :user
Perhaps try it with this and see if that sorts it out for you, and if not it might get things going in the right direction.

How to get this SQL query into rails (3) syntax

I have a sql-Statement and I'd like to "convert" it into rails (activerecord) method calls.
This is my query
'SELECT * FROM clients WHERE company_id IN (SELECT company_id FROM companies_projects WHERE project_id= ? )
companies_projects is a join table for an n:n relation of companies and projects
clients belong to companies (1:n)
project is an external resource and has no has_many companies, so I can't go from that direction
I want to get all clients that belong to companies that belong to one project, so I can list them in the index-page
My models
class Client < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :companies_projects
has_many :clients
has_many :projects, :through => :companies_projects
end
I checked the statement in rails console and it works.
I have two problems impelementing this query.
1. find_by_sql
I tried this method
Client.find_by_sql('SELECT * FROM clients WHERE company_id IN (SELECT company_id FROM companies_projects WHERE project_id= ? )',project.id)
But it throws an InvalidStatement Exception, MySQL Syntax Error near "?"
I also tried to put the sql and bindings into an array [sql,bind1], that works but I get an array and need an ActiveRecordRelation
2. where
I'm new to rails and can't figure out a valid method chain for such a query.
Could someone point me in the right direction?
I would prefer using ActiveRecord methods for the query, but I just don't know which methods to use for the nested selects.
You should have following associations between your models:
class Client < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_and_belongs_to_many :projects
has_many :clients
end
class Project < ActiveRecord::Base
has_and_belongs_to_many :companies
has_many :clients, through: :companies
end
Then it is simply:
project.clients
Client.where(company_id: CompanyProject.where(project_id: project.id).pluck(:id))
Or you can use JOIN
Client.joins(:company_project).where('companies_projects.project_id = ?', project.id)
But the best solution was proposed by #arup-rakshit
Considering that you have an intermediate model CompanyProject, this can be achieved with following query:
Client.where(:company_id => CompanyProject.where(:project_id => project_id).map(&:company_id) )
[Edit: made company_id a symbol)

Efficient way to return select columns from Rails 4 ActiveRecord nested query

In my Rails 4 app, I have the following models:
class Person < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
belongs_to :city
end
class City < ActiveRecord::Base
has_many :addresses
end
I'm using the :includes function to return query result into one variable:
Address.includes(:person, :city).where("person_id = 1")
It works as expected, except that I do not want the query to return every single column.
Here's what I've tried:
use select and specify table name and column names explicitly, e.g. "city.name", but Rails generates a big query with outer joins, that can be very costly, especially when there are lots of concurrent requests, so prefer a better solution.
don't want to hard code complete and raw SQL statements, because of maintenance issue later on
create a new "dummy" belongs_to relationship like in Address: belongs_to :city_select_columns, -> { select('name') }, :class => 'City', but that doesn't work (actually I'm not sure if that select is even supported, only came across documentation about where so far).
maybe define scope in City and Person? but I'm not sure how it should be defined or if it'd make sense to do it this way
Suggestions? Thanks
Have you tried this?
class Person < ActiveRecord::Base
has_many :addresses
has_many :cities, :through => :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
belongs_to :city
end
class City < ActiveRecord::Base
has_many :addresses
end
Then:
Person.find(1).cities.pluck(:name)
Looks like this generates an INNER JOIN but with indexes it shouldn't be too costly?
Did you try select?
Address.select(<output_columns>).includes(:person, :city).where("person_id = 1")
Could not find a good query method using Rails' API, I ended up writing a raw inner join SQL, then call ActiveRecord::Base.connection.execute to run it.