How to do this sql with pure activerecord (rails) - sql

I can't figure out how to manage this purely with active record. I understand it doesn't handle outer joins...
SQL:
SELECT * FROM people
LEFT OUTER JOIN responses
ON people.id = responses.person_id
WHERE responses.person_id is NULL;
MODELS:
Person
id
has_many :responses
Response
id
belongs_to :person
Any suggestions?

named_scope :without_responses, :include => :responses,
:conditions => "responses.id IS NULL"

Related

Accessing Join Attributes in Rails 4

I am trying to get all of a user's friends' posts and user info in one query.
I have tried to use includes(:users) but that doesn't seem to work and throws an error. I figured a manual join would be the best solution:
#friends = #user.friends
.joins('inner join users on users.id = friends.user_id left outer join posts on users.id = posts.user_id')
.select('posts.*, users.*, friends.*')
How do you access attributes from users and posts? doing a #friends.inspect only shows attributes from the friends table, and I can't do #friends.posts or #friends.users.
My model for friends and users looks like this:
class Friend < ActiveRecord::Base
belongs_to :users
end
class User < ActiveRecord::Base
has_many :friends
has_many :users, through: :friends
end
According to the SQL in the join clause, your posts are related to users, not to friends. Something like this will work:
friends.map{|f| f.user.posts}.flatten
if Friend had belongs_to :user (singular "user") which would be conventional Rails, and is reflected in the SQL you used, so has the overriding factor of being correct.

Rails 3 has many through query all parent records and only include children that meet conditions

I have a relationship where restaurants can have many users through a middle table called actions, and users has many restaurants through actions as well.
I am trying to query all restaurants and only include users that meet certain conditions; the restaurant should be returned regardless if there are no users that meet searched criteria, or if there are no users associated with it at all. My models are as follows.
Restaurant model:
has_many :actions
has_many :users, through: :actions
actions model:
belongs_to :user
belongs_to :restaurant
user model:
has_many :actions
has_many :restaurants, through: :actions
What you need to do for this situation is use a left outer join. If you just create the join to find users with as specific attribute like:
Restaurant.joins(:users).where("users.name LIKE 'user_name'")
an inner join is implicitly created which will leave out all of the restaurants without users. To include them do something like this:
Restaurant.joins(["LEFT OUTER JOIN actions on restaurants.id = actions.user_id", "LEFT OUTER JOIN users on users.id = actions.user_id"]).where("users.name LIKE 'James' OR users.name IS NULL ")

Find list of groups where at least one member is part of list (rails, SQL)

I have a simple has_many :through arrangement, as shown below
# employee.rb
class Employee < ActiveRecord::Base
has_many :group_assignments
has_many :groups, through: :group_assignments
# ...
end
# group.rb
class Group < ActiveRecord::Base
has_many :group_assignments
has_many :employees, through: :group_assignments
# ...
end
# group_assignment.rb
class GroupAssignment < ActiveRecord::Base
belongs_to :employee
belongs_to :group
end
I have a list of employees. For that list, I want to grab every group that contains at least one of the employees on that list. How would I accomplish this in a manner that isn't horridly inefficient? I'm newish to Rails and very new at SQL, and I'm pretty at a loss. I'm using SQLite in development and PostgreSQL in production.
For a list of employees named employees_list, this will work:
Group.includes(:employees).where('employees.id' => employees_list.map(&:id))
This is roughly the kind of SQL you will get:
SELECT "groups"."id" AS t0_r0,
"groups"."created_at" AS t0_r1, "groups"."updated_at" AS t0_r2,
"employees"."id" AS t1_r0, "employees"."created_at" AS t1_r1, "employees"."updated_at" AS t1_r2
FROM "groups"
LEFT OUTER JOIN "group_assignments" ON "group_assignments"."group_id" = "groups"."id"
LEFT OUTER JOIN "employees" ON "employees"."id" = "group_assignments"."employee_id"
WHERE "employees"."id" IN (1, 3)
So what is happening is that groups and group_assignments tables are first being joined with a left outer join (matching the group_id column in the group_assignments table to the id column in the groups table), and then employees again with a left outer join (matching employee_id in the group_assignments table to the id column in the employees table).
Then after that we're selecting all rows where 'employees'.'id' (the id of the employee) is in the array of employees in the employee list, which we get by mapping employees_list to their ids using map: employees_list.map(&:id). The map(&:id) here is shorthand for: map { |e| e.id }.
Note that you could use joins instead of includes here, but then you would get duplicates if one employee is a member of multiple groups. Kind of subtle but useful thing to know.
Hope that makes sense!
This is the general idea, but depending on your data, you may need to select distinct.
Group.includes(:group_assignments => :employee).where(:employee => {:id => ?}, #employees.map(&:id))
try
Group.joins(:group_assignments).where("group_assignments.employee_id in (?)", #employees.map(&:id))

Excluding records from a search which are associated with a record

I'm attempting to write a SQL search which will allow return any records which are tagged with certain values, and exclude any of those results which have other tags.
Tags are applied using a join model, like this:
class Customer < ActiveRecord::Base
has_many :tag_assignments
has_many :tags, :through => :tag_assignments
end
class Tag < ActiveRecord::Base
has_many :tag_assignments
has_many :customers, :through => :tag_assignments
end
class TagAssignment < ActiveRecord::Base
belongs_to :customer
belongs_to :tag
end
The query I currently have is:
SELECT DISTINCT customers.* FROM customers LEFT OUTER JOIN tag_assignments ON tag_assignments.customer_id = customers.id WHERE (tag_assignments.tag_id NOT IN (?))
The ? is then replaced in a query by the list of tags I don't want included.
This works fine when a customer has only a single tag applied, but as soon as they get multiple tags they'll show up despite the exclusion, since one of their other tags does match.
Something to keep in mind is that this needs to continue working when additional clauses are added (such as requiring other tags to be present, or matching on other customer attributes), but any point in the right direction would be appreciated.
I'm rusty with this ... but you need to get all things with tag's first and then negate ..
SELECT DISTINCT customers.*
FROM customers
OUTER JOIN (
SELECT DISTINCT customers.id
FROM customers
INNER JOIN tag_assignments ON tag_assignments.customer_id = customers.id
WHERE tag_assignments.tag_id IN (?)
) AS neg_customers ON (neg_customers.id = customers.id)
WHERE neg_customers.id IS NULL;

Is it possible to define a single SQL query that draws a set of permissible foreign_keys from one table and then uses them to filter another?

Specifically, I'm trying to figure out if it's possible to generate SQL that does what I want to feed into Ruby-on-Rails' find_by_sql method.
Imagine there are Users, who are joined cyclically to other Users by a join table Friendships. Each User has the ability to create Comments.
I'd like a SQL query to return the latest 100 comments created by any friends of a given user so I can display them all in one convenient place for the user to see.
This is tricky, since essentially I'm looking to filter the comments by whether their foreign keys for their author are contained in a set of keys obtained derived from the user's friends' primary keys.
Edit: Clarifying the setup. I'm not exactly sure how to write a schema definition, so I'll describe it in terms of Rails.
class User
has_many :friends, :through => :friendships
has_many :comments
end
class Friendship
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
end
def Comment
has_one :User
end
It's not that tricky, you just use joins. To get just the comments you only need to join the Friendships table and the Comments table, but you probably also want some information from the Users table for the person who wrote the comment.
This would get the last 100 comments from people who are friends with the user with id 42:
select top 100 c.CommentId, c.CommentText, c.PostDate, u.Name
from Friendships f
inner join Users u on u.UserId = f.FriendUserId
inner join Comments c on c.UserId = u.UserId
where f.UserId = 42
order by c.PostDate desc