Rails relational issue - Find user's next outing - ruby-on-rails

I have an outings table, which basically holds some information about an 'outing'. These outings then have crews, which have users (through crew_users)
Just as a note, outings have many crews, so there's an outing_id in the crews table
Now, I need to find the user's next outing, as in the first outing where outing.start_time > Time.now
I've got this in my user.rb model:
has_many :crew_users
has_many :crews, :through => :crew_users
has_many :outings, :through => :crews
And when I try to do this:
>> Users.find(1).outings
I get this error:
ActiveRecord::StatementInvalid: Mysql::Error: Unknown column 'crews.user_id'
in 'where clause': SELECT `outings`.* FROM `outings` INNER JOIN `crews` ON
`outings`.id = `crews`.outing_id WHERE ((`crews`.user_id = 1))
Anyone any ideas? Like I said, my goal is to get the user's next outing from the current time so there may very well be a much better to go about this!

You can try and reverse this is if doing a double :through is out of the question by filtering the Outing records instead. The SQL for this would look something like:
SELECT outings.id FROM outings
LEFT JOIN crews ON crews.id=outings.crew_id
LEFT JOIN crews_users ON crews_users.crew_id=crews.id
WHERE crews_users.user_id=? AND start_time>=NOW()
ORDER BY created_at
LIMIT 1
The quick and dirty approach is to run this and extract the outing_id value you need, then call Outing.find with that.

Related

How to pluck id of has_one associations?

class Post
has_one :latest_comment, -> { order(created_at: :desc) }, class_name: 'Comment'
end
I want to do something like:
Post.joins(:latest_comment).pluck('latest_comment.id')
but it's not valid syntax and it doesn't work.
Post.joins(:latest_comment).pluck('comments.id')
Above works but it returns ids of all comments for a post, not only of the latest.
ActiveRecord::Assocations are a very leaky abstraction around SQL joins so your has_one :latest_comment assocation won't actually return a single row in the join table per record unless you're calling it on an instance of Post.
Instead when you run Post.joins(:latest_comment).pluck('comments.id')you get:
SELECT "comments"."id"
FROM "posts"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
ActiveRecord isn't actually smart enough to know that you want to get unique values from the comments table - and it actually just behaves like a has_many association. In its defence this isn't actually something thats even realistic to do in polyglot fashion.
What you want to do can instead is to select the rows from the comments table and get distinct values:
Comment.order(:post_id, created_at: :desc)
.pluck(Arel.sql('DISTINCT ON (post_id) id'))
DISTINCT ON is Postgres specific. The exact approach here will vary between RDBMS:es and there are many other alternatives such as lateral joins, window functions etc depending on your performance requirements.

Efficent way to select belongs_to model from another Model, in Rails 5?

I have 2 models:
Contact -> has_many :messages
and
Message -> belongs_to :contact
I want to select the last 10 contacts (as array of object, not only ids) from my messages table
this is my attempt (and it works fine):
Contact.where(id: Message.pluck(:contact_id).uniq.last(10))
BUt is there another better or rails way to do this?
EDIT
I want to select the last 10 contacts from my messages table
I would do this in one database query.
Contact
.distinct
.joins(:messages)
.order('messages.created_at DESC')
.limit(10)
You could try the following query:
Contact.where(id: Message.distinct.order(created_at: :desc).limit(10).select(:id))
This would produce the following SQL:
"SELECT DISTINCT \"messages\".\"id\" FROM \"messages\" ORDER BY \"messages\".\"created_at\" DESC LIMIT 10"

ActiveRecord - find records that its association count is 0

In my Ruby on Rails app I have the following model:
class SlideGroup < ApplicationRecord
has_many :survey_group_lists, foreign_key: 'group_id'
has_many :surveys, through: :survey_group_lists
end
I want to find all orphaned slide groups. Orphaned slide group is slide group which is not connected to any survey. I've been trying following query but it does not return anything and I'm sure that I have orphaned records in my test database:
SlideGroup.joins(:surveys).group("slide_groups.id, surveys.id").having("count(surveys.id) = ?",0)
this generates following sql query:
SlideGroup Load (9.3ms) SELECT "slide_groups".* FROM "slide_groups" INNER JOIN "survey_group_lists" ON "survey_group_lists"."group_id" = "slide_groups"."id" INNER JOIN "surveys" ON "surveys"."id" = "survey_group_lists"."survey_id" GROUP BY slide_groups.id, surveys.id HAVING (count(surveys.id) = 0)
You're using joins, which is INNER JOIN, whereas what you need is an OUTER JOIN -
includes:
SlideGroup.includes(:surveys).group("slide_groups.id, surveys.id").having("count(surveys.id) = ?",0)
A bit cleaner query:
SlideGroup.includes(:surveys).where(surveys: { id: nil })
Finding orphan records has been explained by others.
I see problems with this approach:
There should not be any orphan in the first place
The presence of a survey.id does not guarantee the presence of a Survey
What about SurveyGroupList that are orphan?
So the proper solution would be to ensure that no orphans are left in the DB. By implementing the proper logic AND adding foreign keys with on delete cascade to the DB. You can also add dependent: :destroy option to your associations but this only works if you use #destroy on your models (not delete) and of course does not work if you delete directly via SQL.

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

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