combine 2 objects and sort rails 5 - ruby-on-rails

I want to show a time link mixing comments and post so I have this objects
#posts = Post::all()
#comments = Comment::all()
If I do this
#post.each ...
... end
#comments.each ...
... end
I will get first posts and after this, the comments. But I want a timeline, how i can create this?
I need to combine both object to create just one ordered list, example:
In post:
id | name | date
1 | post1 | 2015-01-01
2 | post2 | 2013-01-01
In comments:
id | name | date
1 | comment1 | 2014-01-01
2 | comment2 | 2016-01-01
if I do this
post.each ...
comments.each ...
the result will that:
-post1
-post2
-comment1
-comment2
But i need order by date to get
-post2
-comment1
-post1
-comment2
Thanks, and sorry for my ugly english.

Posts and comments are different models (and different tables), so we can't write SQL to get sorted collection, with pagination etc.
Usually I use next approach when I need mixed timeline.
I have TimelineItem model with source_id, source_type and timeline_at fields.
class TimelineItem < ApplicationRecord
belongs_to :source, polymorphic: true
end
Then I add in models logic to create timeline_item instance when needed:
has_many :timeline_items, as: :source
after_create :add_to_timeline
def add_to_timeline
timeline_items.create timeline_at: created_at
end
Then search and output as simple as
TimelineItem.includes(:source).order(:timeline_at).each { |t| pp t.source }

Solution#1
You can achieve this using UNION query.
sql= 'SELECT id, name, date FROM posts UNION ALL SELECT id, name, date FROM comments ORDER BY date'
ActiveRecord::Base.connection.execute(sql)
but union query will only work when you have same column names in both tables.
Solution#2 Add ordering logic in your view.
If you are simply displaying these records on html page then let them load on page without any specific order i.e.(first posts and then comments). Write javascript code to sort DOM elements which will run after page load.
for ref: Jquery - sort DIV's by innerHTML of children
Solution#3 Refactor your DB schema and put posts and comments in same database table. Then you will be able to query on single table.
Something like this,
class Text < ActiveRecord::Base
end
class Post < Text
end
class Comment < Text
end
Query will be Text.order(:date)
Refactoring your DB schema is too much to solve this problem. Do it if it makes sense for your application.

Related

Group models with exact same has_many through relationships

Forgive me if this has been asked before, I had a hard time thinking of good search queries.
Lets say I have 2 models, Posts and Tags. Posts have many tags through a pivot model, PostTags.
What I'd like to do is group posts that have the exact same combination of tags. I know how to group posts that have any of the same tags, but I've been having a harder time with this.
For example, if I have a post with and ID of 1, and the post has two Tags- one with an ID of 5, another with an ID of 7. I would have 2 PostTags, one with a post_id of 1, and a tag_id of 5, and then another with a post_id of 1 and a tag_id of 7. I have another Post with an id of 3, and it also has 2 PostTags - one with a post_id of 3 and a tag_id of 5, and another with a post_id of 3 and a tag_id of 7. I'd like to group these together so that I can get a count of how many posts have both of these tags, and no others.
Thanks, and I hope I was able to explain this properly.
I think you could probably do something like this in a nested query:
SELECT tag_ids,
string_agg(post_id, ',')
FROM (
SELECT post_id,
string_agg(tag_id, ',') as tag_ids
FROM post_tags
GROUP BY post_id)
GROUP BY tag_ids;
Explanation:
First in the inside query, you concatenate tag_ids grouped by post_id, so you can get the combination of tags for each post.
Then in the outside query, you concatenate post_ids by the combination of tag_ids, so you get all the post_ids for each tag combination.
This might not be the end yet, you could further process the post ids, or modify the query to fetch whatever data you need.
Hope this help!
Hope the Model relationships are sets properly.
# Post Model
class Post
has_many :post_tags
has_many :tags, through: :post_tags
end
# Fetch Tags to match with posts collection
tag_ids = []
# Query to fetch posts
Post.joins(:tags).where(tags: { id: tag_ids }).
group("posts.id").having("count(posts.id) >= ?", tag_ids.size)
First line ensure that only posts having tags included in the specified
tag list are fetched
Second line ensure that posts are having both of the tags.
If you want to match for exact tags (no other tags should present), then
change the condition to = instead of >=
Happy Hacking!

How can I sort two different models with different attributes by created_at with postgres? [duplicate]

I want to show a time link mixing comments and post so I have this objects
#posts = Post::all()
#comments = Comment::all()
If I do this
#post.each ...
... end
#comments.each ...
... end
I will get first posts and after this, the comments. But I want a timeline, how i can create this?
I need to combine both object to create just one ordered list, example:
In post:
id | name | date
1 | post1 | 2015-01-01
2 | post2 | 2013-01-01
In comments:
id | name | date
1 | comment1 | 2014-01-01
2 | comment2 | 2016-01-01
if I do this
post.each ...
comments.each ...
the result will that:
-post1
-post2
-comment1
-comment2
But i need order by date to get
-post2
-comment1
-post1
-comment2
Thanks, and sorry for my ugly english.
Posts and comments are different models (and different tables), so we can't write SQL to get sorted collection, with pagination etc.
Usually I use next approach when I need mixed timeline.
I have TimelineItem model with source_id, source_type and timeline_at fields.
class TimelineItem < ApplicationRecord
belongs_to :source, polymorphic: true
end
Then I add in models logic to create timeline_item instance when needed:
has_many :timeline_items, as: :source
after_create :add_to_timeline
def add_to_timeline
timeline_items.create timeline_at: created_at
end
Then search and output as simple as
TimelineItem.includes(:source).order(:timeline_at).each { |t| pp t.source }
Solution#1
You can achieve this using UNION query.
sql= 'SELECT id, name, date FROM posts UNION ALL SELECT id, name, date FROM comments ORDER BY date'
ActiveRecord::Base.connection.execute(sql)
but union query will only work when you have same column names in both tables.
Solution#2 Add ordering logic in your view.
If you are simply displaying these records on html page then let them load on page without any specific order i.e.(first posts and then comments). Write javascript code to sort DOM elements which will run after page load.
for ref: Jquery - sort DIV's by innerHTML of children
Solution#3 Refactor your DB schema and put posts and comments in same database table. Then you will be able to query on single table.
Something like this,
class Text < ActiveRecord::Base
end
class Post < Text
end
class Comment < Text
end
Query will be Text.order(:date)
Refactoring your DB schema is too much to solve this problem. Do it if it makes sense for your application.

Rails query for record and latest association record efficiently with includes?

I'm having trouble with a rails activerecord query.
I have 2 tables.
One for Projects:
id | name | created_at | updated_at
One for Project Reports:
id | project_id | number | created_at | updated_at
The corresponding class for Project:
class Project < ActiveRecord::Base
has_many :project_reports
def latest_report_number
project_reports.last.number
end
end
And ProjectReports:
class ProjectReport < ActiveRecord::Base
belongs_to :project
end
I'm attempting to write a query to get a list of the projects and the most recent report for the project.
I can do this in the following way, but this leads to a N+1 situation:
Project
.select("projects.*, max(project_reports.created_at) as latest_report")
.joins(:project_reports)
.order("latest_report desc")
.group("projects.id")
.map { |p| "#{p.name}: #{p.latest_report_number}" }
I therefor wanted to use rails 'includes' feature so that I don't run into the N+1 situation:
Project
.select("projects.*, max(project_reports.created_at) as latest_report")
.joins(:project_reports)
.order("latest_report desc")
.group("projects.id")
.includes(:project_reports) # <----
.map { |p| "#{p.name}: #{p.latest_report_number}" }
This causes my query to no longer work:
PG::GroupingError: ERROR: column "project_reports.id" must appear in
the GROUP BY clause or be used in an aggregate function
How can I write my query so that I both accomplish the goal and limit the number of times I hit the database (ideally with includes)?
In the case of a join query, includes asks for extra columns from the associated table (i.e. tasks) in order to build the associated objects without an additional query. Normally, that's a welcome optimization, but in the case of a GROUP BY clause, you wind up with an invalid SELECT.
In your case, you want a completely separate query to load the associations, so preload is a better choice.
Project
.select("projects.*, max(project_reports.created_at) as latest_report")
.joins(:project_reports)
.order("latest_report desc")
.group("projects.id")
.preload(:project_reports)
.map { |p| "#{p.name}: #{p.latest_report_number}" }

How to filter association_ids for an ActiveRecord model?

In a domain like this:
class User
has_many :posts
has_many :topics, :through => :posts
end
class Post
belongs_to :user
belongs_to :topic
end
class Topic
has_many :posts
end
I can read all the Topic ids through user.topic_ids but I can't see a way to apply filtering conditions to this method, since it returns an Array instead of a ActiveRecord::Relation.
The problem is, given a User and an existing set of Topics, marking the ones for which there is a post by the user. I am currently doing something like this:
def mark_topics_with_post(user, topics)
# only returns the ids of the topics for which this user has a post
topic_ids = user.topic_ids
topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
end
But this loads all the topic ids regardless of the input set. Ideally, I'd like to do something like
def mark_topics_with_post(user, topics)
# only returns the topics where user has a post within the subset of interest
topic_ids = user.topic_ids.where(:id=>topics.map(&:id))
topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
end
But the only thing I can do concretely is
def mark_topics_with_post(user, topics)
# needlessly create Post objects only to unwrap them later
topic_ids = user.posts.where(:topic_id=>topics.map(&:id)).select(:topic_id).map(&:topic_id)
topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
end
Is there a better way?
Is it possible to have something like select_values on a association or scope?
FWIW, I'm on rails 3.0.x, but I'd be curious about 3.1 too.
Why am I doing this?
Basically, I have a result page for a semi-complex search (which happens based on the Topic data only), and I want to mark the results (Topics) as stuff on which the user has interacted (wrote a Post).
So yeah, there is another option which would be doing a join [Topic,Post] so that the results come out as marked or not from the search, but this would destroy my ability to cache the Topic query (the query, even without the join, is more expensive than fetching only the ids for the user)
Notice the approaches outlined above do work, they just feel suboptimal.
I think that your second solution is almost the optimal one (from the point of view of the queries involved), at least with respect to the one you'd like to use.
user.topic_ids generates the query:
SELECT `topics`.id FROM `topics`
INNER JOIN `posts` ON `topics`.`id` = `posts`.`topic_id`
WHERE `posts`.`user_id` = 1
if user.topic_ids.where(:id=>topics.map(&:id)) was possible it would have generated this:
SELECT topics.id FROM `topics`
INNER JOIN `posts` ON `topics`.`id` = `posts`.`topic_id`
WHERE `posts`.`user_id` = 1 AND `topics`.`id` IN (...)
this is exactly the same query that is generated doing: user.topics.select("topics.id").where(:id=>topics.map(&:id))
while user.posts.select(:topic_id).where(:topic_id=>topics.map(&:id)) generates the following query:
SELECT topic_id FROM `posts`
WHERE `posts`.`user_id` = 1 AND `posts`.`topic_id` IN (...)
which one of the two is more efficient depends on the data in the actual tables and indices defined (and which db is used).
If the topic ids list for the user is long and has topics repeated many times, it may make sense to group by topic id at the query level:
user.posts.select(:topic_id).group(:topic_id).where(:topic_id=>topics.map(&:id))
Suppose your Topic model has a column named id you can do something like this
Topic.select(:id).join(:posts).where("posts.user_id = ?", user_id)
This will run only one query against your database and will give you all the topics ids that have posts for a given user_id

Rails has_many association count child rows

What is the "rails way" to efficiently grab all rows of a parent table along with a count of the number of children each row has?
I don't want to use counter_cache as I want to run these counts based on some time conditions.
The cliche blog example:
Table of articles. Each article has 0 or more comments.
I want to be able to pull how many comments each article has in the past hour, day, week.
However, ideally I don't want to iterate over the list and make separate sql calls for each article nor do I want to use :include to prefetch all of the data and process it on the app server.
I want to run one SQL statement and get one result set with all the info.
I know I can hard code out the full SQL, and maybe could use a .find and just set the :joins, :group, and :conditions parameters... BUT I am wondering if there is a "better" way... aka "The Rails Way"
This activerecord call should do what you want:
Article.find(:all, :select => 'articles.*, count(posts.id) as post_count',
:joins => 'left outer join posts on posts.article_id = articles.id',
:group => 'articles.id'
)
This will return a list of article objects, each of which has the method post_count on it that contains the number of posts on the article as a string.
The method executes sql similar to the following:
SELECT articles.*, count(posts.id) AS post_count
FROM `articles`
LEFT OUTER JOIN posts ON posts.article_id = articles.id
GROUP BY articles.id
If you're curious, this is a sample of the MySQL results you might see from running such a query:
+----+----------------+------------+
| id | text | post_count |
+----+----------------+------------+
| 1 | TEXT TEXT TEXT | 1 |
| 2 | TEXT TEXT TEXT | 3 |
| 3 | TEXT TEXT TEXT | 0 |
+----+----------------+------------+
Rails 3 Version
For Rails 3, you'd be looking at something like this:
Article.select("articles.*, count(comments.id) AS comments_count")
.joins("LEFT OUTER JOIN comments ON comments.article_id = articles.id")
.group("articles.id")
Thanks to Gdeglin for the Rails 2 version.
Rails 5 Version
Since Rails 5 there is left_outer_joins so you can simplify to:
Article.select("articles.*, count(comments.id) AS comments_count")
.left_outer_joins(:comments)
.group("articles.id")
And because you were asking about the Rails Way: There isn't a way to simplify/railsify this more with ActiveRecord.
From a SQL perspective, this looks trivial - Just write up a new query.
From a Rails perspective, The values you mention are computed values. So if you use find_by_sql, the Model class would not know about the computed fields and hence would return the computed values as strings even if you manage to translate the query into Rails speak. See linked question below.
The general drift (from the responses I got to that question) was to have a separate class be responsible for the rollup / computing the desired values.
How to get rails to return SUM(columnName) attributes with right datatype instead of a string?
A simple way that I used to solve this problem was
In my model I did:
class Article < ActiveRecord::Base
has_many :posts
def count_posts
Post.where(:article_id => self.id).count
end
end
Now, you can use for example:
Articles.first.count_posts
Im not sure if it can be more efficient way, But its a solution and in my opinion more elegant than the others.
I made this work this way:
def show
section = Section.find(params[:id])
students = Student.where( :section_id => section.id ).count
render json: {status: 'SUCCESS', section: students},status: :ok
end
In this I had 2 models Section and Student. So I have to count the number of students who matches a particular id of section.

Resources