How to do this PG query in Rails? - ruby-on-rails

I have the following simple relations:
class Company
has_many :users
end
class User
belongs_to :company
has_and_belongs_to_many :roles
end
class Role
has_and_belongs_to_many :users
end
The only column that matters is :name on Role.
I'm trying to make an efficient PostgreSQL query which will show a comma separated list of all role_names for each user.
So far I have got it this far, which works great if there's only single role assigned. If I add another role, I get duplicate users. Rather than trying to parse this after, I'm trying to just get it to return a comma separated list in a role_names field by using the string_agg() function.
This is my query so far and I'm kind of failing at taking it this final step.
User.where(company_id: id)
.joins(:roles)
.select('distinct users.*, roles.name as role_name')
EDIT
I can get it working via raw SQL (gross) but rails doesn't know how to understand it when I put it in ActiveRecord format
ActiveRecord::Base.connection.execute('SELECT users.*, string_agg("roles"."name", \',\') as roles FROM "users" INNER JOIN "roles_users" ON "roles_users"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "roles_users"."role_id" WHERE "users"."company_id" = 1 GROUP BY users.id')
User.where(company_id: id)
.joins(:roles)
.select('users.*, string_agg("roles"."name" \',\')')
.group('users.id')

Looks to me that you want to do:
User.roles.map(&:name).join(',')
(In my opionion SQL is a better choice when working with databases but when you are on rails you should probably do as much as possible with Active Record. Be aware of performance issues!)

Related

Using joins to query by attribute on associated recored

I currently have this horribly written query:
membership_ids = User.where(skip_membership_renewal: true).includes(:memberships).map(&:membership_ids).flatten
Memberships.where(id: membership_ids)
I have been trying to use joins so that I can just make one query.
Membership.includes(:user).where("user.skip_membership_renewal", true)
However, this doesn't work since I keep getting the error: ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR.
My relationship is:
User has_many :memberships
Membership belongs_to :user
What am I doing incorrectly?
You just have a pluralization error. In Rails, you define models as singular (User) and the database table is pluralized (users).
Membership.includes(:user).where("users.skip_membership_renewal" => true)
That said, you don't need to resort to using SQL literals for such a simple case. There are a bunch of other ways of assembling this query, like the scope option David Aldridge suggested, or either of these:
non_renewing_users = User.where(skip_membership_renewal: true)
Membership.joins(:user).merge(non_renewing_users)
Membership.where(user: non_renewing_users)
What's more is that these both only execute a single SQL query for most adapters because they use subqueries:
SELECT "memberships".*
FROM "memberships"
WHERE "memberships"."user_id" IN (
SELECT "users"."id" FROM "users"
WHERE "users"."skip_membership_renewal" = true
)
You can probably aim to use:
Membership.where(:user => User.skip_membership_renewal)
Add a scope onto User ...
def self.skip_membership_renewal
where(skip_membership_renewal: true)
end
You should find that it runs as a single query.

How would I go about searching for Users querying multiple associations?

Okay. Let's say there is a User model with multiple join tables.
User
has_many :languages
has_many :skills
has_many :languages, through: :user_langauges
has_many :skills, through: :user_skills
If I hit an endpoint with params like
"user"=>
{ "email"=>"test#123.com",
"languages"=>"["French", Spanish]",
"skills"=>"["accounting", "leadership"]"
}
How would I search for users that match the above? My main concern is how do I search for users that match BOTH associations of "languages" and "skills". Is the best to take all the users and compare which users are the same? Something like:
french = French.users
leadership = Leadership.users
french & leadership # => "users that exist in both?"
Obviously, there is more logic, but I feel like that general idea is taxing and inefficient. But I'm sure rails has thought of a more elegant way, which is why I need direction from you rails masters :)
edit: I have a Psql DB if that matters.
No, you don't want to compare the users in Ruby. It will load lots of data and won't be performant if you have lots of users/skills/languages.
This is what a DB is for. Check the AR guides to learn how to query the DB: http://guides.rubyonrails.org/active_record_querying.html
I also suggest you read up on SQL because AR only helps you to build SQL queries. But the power comes from your DB.
Here is a rough sample how it might work:
User.joins(:languages, :skills).where(skills: {name: ['Ruby', 'Marketing']}, languages: {name: 'French'}).uniq
Assuming: user, skill and language have an attribute name adjust for your schema.
This will search for Users that have: (skills Ruby OR Marketing) AND speak French.
If you wan't to have: ruby AND marketing AND french, then the query get's more complex.
UPDATE:
As mentioned, the AND combination is more complex. You will need to use GROUP BY and HAVING or can give subqueries to count the skills a try. Here is raw SQL query that will work:
SELECT users.* FROM (
SELECT users.* FROM "users"
INNER JOIN "user_skills" ON "user_skills"."user_id" = "users"."id"
INNER JOIN "skills" ON "skills"."id" = "user_skills"."skill_id"
WHERE skills.name IN ('Ruby', 'Marketing')
GROUP BY users.id
HAVING COUNT(*) = 2
) users
INNER JOIN "user_languages" ON "user_languages"."user_id" = "users"."id"
INNER JOIN "languages" ON "languages"."id" = "user_languages"."language_id"
WHERE languages.name IN ('German', 'French', 'English')
GROUP BY users.id, users.name, users.created_at, users.updated_at
HAVING COUNT(users.*) = 3
The numbers (2, 3) must be the number of matches you need (2 skills, 3 languages).

How to UNION tables and make results accessible in a Ruby view

I'm quite new to RoR and creating a student project for a course I'm taking. I'm wanting to construct a type of query we didn't cover in the course and which I know I could do in a snap in .NET and SQL. I'm having a heck of a time though getting it implemented the Ruby way.
What I'd like to do: Display a list on a user's page of all "posts" by that user's friends.
"Posts" are found in both a questions table and in a blurbs table that users contribute to. I'd like to UNION these two into a single recordset to sort by updated_at DESC.
The table column names are not the same however, and this is my sticking point since other successful answers I've seen have hinged on column names being the same between the two.
In SQL I'd write something like (emphasis on like):
SELECT b.Blurb AS 'UserPost', b.updated_at, u.username as 'Author'
FROM Blurbs b
INNER JOIN Users u ON b.User_ID = u.ID
WHERE u.ID IN
(SELECT f.friend_id FROM Friendships f WHERE f.User_ID = [current user])
ORDER BY b.updated_at DESC
UNION
SELECT q.Question, q.updated_at, u.username
FROM Questions q
INNER JOIN Users u ON q.User_ID = u.ID
WHERE u.ID IN
(SELECT f.friend_id FROM Friendships f WHERE f.User_ID = [current user])
ORDER BY b.updated_at DESC
The User model's (applicable) relationships are:
has_many :friendships
has_many :friends, through: :friendships
has_many :questions
has_many :blurbs
And the Question and Blurb models both have belongs_to :user
In the view I'd like to display the contents of the 'UserPost' column and the 'Author'. I'm sure this is possible, I'm just too new still to ActiveRecord and how statements are formed. Happy to have some input or review any relevant links that speak to this specifically!
Final Solution
Hopefully this will assist others in the future with Ruby UNION questions. Thanks to #Plamena's input the final implementation ended up as:
def friend_posts
sql = "...the UNION statement seen above..."
ActiveRecord::Base.connection.select_all(ActiveRecord::Base.send("sanitize_sql_array",[sql, self.id, self.id] ) )
end
Currently Active Record lacks union support. You can use SQL:
sql = <<-SQL
# your sql query goes here
SELECT b.created_at ...
UNION(
SELECT q.created_at
....
)
SQL
posts = ActiveRecord::Base.connection.select_all(sql)
Then you can iterate the result:
posts.each do |post|
# post is a hash
p post['created_at']
end
Your best way to do this is to just use the power of Rails
If you want all of something belonging to a user's friend:
current_user.friends.find(id_of_friend).first.questions
This would get all of the questions from a certain friend.
Now, it seems that you have writings in multiple places (this is hard to visualise without your providing a model of how writings is connected to everywhere else). Can you provide this?
#blurbs = Blurb.includes(:user)
#blurbs.each do |blurb|
p blurb.blurb, blurb.user.username
end

How to write query in active record to select from two or more tables in rails 3

I don't want to use join
I want to manually compare any field with other table field
for example
SELECT u.user_id, t.task_id
FROM tasks t, users u
WHERE u.user_id = t.user_id
how can i write this query in Rails ??
Assuming you have associations in your models, you can simply do as follow
User.joins(:tasks).select('users.user_id, tasks.task_id')
you can also do as follow
User.includes(:tasks).where("user.id =tasks.user_id")
includes will do eager loading check the example below or read eager loading at here
users = User.limit(10)
users.each do |user|
puts user.address.postcode
end
This will run 11 queries, it is called N+1 query problem(first you query to get all the rows then you query on each row again to do something). with includes Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.
Now when you do;
users = User.includes(:address).limit(10)
user.each do |user|
puts user.address.postcode
end
It will generate just 2 queries as follow
SELECT * FROM users LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.user_id IN (1,2,3,4,5,6,7,8,9,10))
Plus if you don't have associations then read below;
you should be have to look at http://guides.rubyonrails.org/association_basics.html
Assuming your are trying to do inner join, by default in rails when we associate two models and then query on them then we are doing inner join on those tables.
You have to create associations between the models example is given below
class User
has_many :reservations
...# your code
end
And in reservations
class Reservations
belongs_to :user
... #your code
end
Now when you do
User.joins(:reservations)
the generated query would look like as follow
"SELECT `users`.* FROM `users` INNER JOIN `reservations` ON `reservations`.`user_id` = `users`.`id`"
you can check the query by doing User.joins(:reservations).to_sql in terminal
Hopefully it would answer your question
User.find_by_sql("YOUR SQL QUERY HERE")
You can use as follows..
User.includes(:tasks).where("user.id =tasks.user_id").order(:user.id)

Specifying conditions on eager loaded associations returns ActiveRecord::RecordNotFound

The problem is that when a Restaurant does not have any MenuItems that match the condition, ActiveRecord says it can't find the Restaurant. Here's the relevant code:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
def self.with_meals_of_the_week
includes({menu_items: :meal}).where(:'menu_items.date' => Time.now.beginning_of_week..Time.now.end_of_week)
end
end
And the sql code generated:
Restaurant Load (0.0ms)←[0m ←[1mSELECT DISTINCT "restaurants".id FROM "restaurants"
LEFT OUTER JOIN "menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN "meals" ON "meals"."id" = "menu_items"."meal_id" WHERE
"restaurants"."id" = ? AND ("menu_items"."date" BETWEEN '2012-10-14 23:00:00.000000'
AND '2012-10-21 22:59:59.999999') LIMIT 1←[0m [["id", "1"]]
However, according to this part of the Rails Guides, this shouldn't be happening:
Post.includes(:comments).where("comments.visible", true)
If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded.
The SQL generated is a correct translation of your query. But look at it,
just at the SQL level (i shortened it a bit):
SELECT *
FROM
"restaurants"
LEFT OUTER JOIN
"menu_items" ON "menu_items"."restaurant_id" = "restaurants"."id"
LEFT OUTER JOIN
"meals" ON "meals"."id" = "menu_items"."meal_id"
WHERE
"restaurants"."id" = ?
AND
("menu_items"."date" BETWEEN '2012-10-14' AND '2012-10-21')
the left outer joins do the work you expect them to do: restaurants
are combined with menu_items and meals; if there is no menu_item to
go with a restaurant, the restaurant is still kept in the result, with
all the missing pieces (menu_items.id, menu_items.date, ...) filled in with NULL
now look aht the second part of the where: the BETWEEN operator demands,
that menu_items.date is not null! and this
is where you filter out all the restaurants without meals.
so we need to change the query in a way that makes having null-dates ok.
going back to ruby, you can write:
def self.with_meals_of_the_week
includes({menu_items: :meal})
.where('menu_items.date is NULL or menu_items.date between ? and ?',
Time.now.beginning_of_week,
Time.now.end_of_week
)
end
The resulting SQL is now
.... WHERE (menu_items.date is NULL or menu_items.date between '2012-10-21' and '2012-10-28')
and the restaurants without meals stay in.
As it is said in Rails Guide, all Posts in your query will be returned only if you will not use "where" clause with "includes", cause using "where" clause generates OUTER JOIN request to DB with WHERE by right outer table so DB will return nothing.
Such implementation is very helpful when you need some objects (all, or some of them - using where by base model) and if there are related models just get all of them, but if not - ok just get list of base models.
On other hand if you trying to use conditions on including tables then in most cases you want to select objects only with this conditions it means you want to select Restaurants only which has meals_items.
So in your case, if you still want to use only 2 queries (and not N+1) I would probably do something like this:
class Restaurant < ActiveRecord::Base
has_many :menu_items, dependent: :destroy
has_many :meals, through: :menu_items
cattr_accessor :meals_of_the_week
def self.with_meals_of_the_week
restaurants = Restaurant.all
meals_of_the_week = {}
MenuItems.includes(:meal).where(date: Time.now.beginning_of_week..Time.now.end_of_week, restaurant_id => restaurants).each do |menu_item|
meals_of_the_week[menu_item.restaurant_id] = menu_item
end
restaurants.each { |r| r.meals_of_the_week = meals_of_the_week[r.id] }
restaurants
end
end
Update: Rails 4 will raise Deprecation warning when you simply try to do conditions on models
Sorry for possible typo.
I think there is some misunderstanding of this
If there was no where condition, this would generate the normal set of two queries.
If, in the case of this includes query, there were no comments for any
posts, all the posts would still be loaded. By using joins (an INNER
JOIN), the join conditions must match, otherwise no records will be
returned.
[from guides]
I think this statements doesn't refer to the example Post.includes(:comments).where("comments.visible", true)
but refer to one without where statement Post.includes(:comments)
So all work right! This is the way LEFT OUTER JOIN work.
So... you wrote: "If, in the case of this includes query, there were no comments for any posts, all the posts would still be loaded." Ok! But this is true ONLY when there is NO where clause! You missed the context of the phrase.

Resources