Currently, as a portfolio for practical use, I am creating an SNS application called Revorite that allows you to share your favorite things with reviews. The basic posting function and the like function will be a lot of hits when you google, but I felt that there were few articles about the retweet function, so I thought that it would be helpful for someone as a memorandum.
A `user``` model that manages users, a
`postmodel that manages posts (tweets), and a
relationship``` model that manages follower / follower relationships have been created.
Create a `` `repost``` model that manages reposts (hereinafter referred to as reposts instead of retweets because the word post is used instead of tweets in this app).
$ rails g model repost
YYYYMMDDHHMMSS_create_reposts.rb
class CreateReposts < ActiveRecord::Migration[5.2]
def change
create_table :reposts do |t|
t.references :user, foreign_key: true
t.references :post, foreign_key: true
t.timestamps
end
end
end
$ rails db:migrate
Create a `` `reposts``` table with. Manage who reposted which post.
Next is the association. Since users can repost multiple posts and one post can be reposted to multiple users, the `` `reposts``` table is positioned as a ** many-to-many intermediate table ** between users and posts.
user.rb
has_many :reposts, dependent: :destroy
post.rb
has_many :reposts, dependent: :destroy
repost.rb
belongs_to :user
belongs_to :post
Then create a controller so that users can repost posts. You can also cancel the reposted post by pressing the repost button again.
$ rails g controller reposts
reposts_controller.rb
class RepostsController < ApplicationController
before_action :set_post
def create #When you press the repost button, it will be registered in the reposts table from the user who pressed it and the ID of the post you pressed.
if Repost.find_by(user_id: current_user.id, post_id: @post.id)
redirect_to root_path, alert: 'Already reposted'
else
@repost = Repost.create(user_id: current_user.id, post_id: @post.id)
end
end
def destroy #If you press the repost button of a post that has already been reposted again, the repost will be canceled (=Delete data from the table)
@repost = current_user.reposts.find_by(post_id: @post.id)
if @repost.present?
@repost.destroy
else
redirect_to root_path, alert: 'I have already canceled the repost'
end
end
private
def set_post #Identify the post that pressed the repost button
@post = Post.find(params[:post_id])
if @post.nil?
redirect_to root_path, alert: 'Not found'
end
end
end
Edit the route at the same time.
routes.rb
resources :posts, only: [:index, :new, :create, :destroy] do
resources :reposts, only: [:create, :destroy]
end
Include a repost button in the view. Since we need to display a button on each post, we are implementing it as a partial template.
_repost.html.haml
- if user_signed_in?
- unless post.user.id == current_user.id #I can't repost my post
- if current_user.reposted?(post.id)
= link_to "/posts/#{post.id}/reposts/#{post.reposts.ids}", method: :delete, title: "Cancel repost", remote: true do
%i.fas.fa-retweet.post-action__repost--reposted
= post.reposts.length
- else
= link_to "/posts/#{post.id}/reposts", method: :post, title: "Repost", data: {confirm: "Would you like to repost this post?"}, remote: true do
%i.fas.fa-retweet
= post.reposts.length
- else
%i.fas.fa-retweet.nonactive
= post.reposts.length
- else
%i.fas.fa-retweet.nonactive
= post.reposts.length
reposted? (Post.id)
`method is defined below. It is for determining whether the user has reposted the post.user.rb
def reposted?(post_id)
self.reposts.where(post_id: post_id).exists?
end
Now when the user presses the repost button on any post, that information will be registered in the `` `reposts``` table.
Next, on the page that displays the user's post list, let's display not only the user's posts but also the reposts. Normally, you can get only user posts with `` `user.posts```, but when it comes to reposting, the difficulty increases immediately. (I had a lot of trouble here ...)
We will define a method called `` `posts_with_reposts``` so that we can get reposts at the same time.
user.rb
def posts_with_reposts
relation = Post.joins("LEFT OUTER JOIN reposts ON posts.id = reposts.post_id AND reposts.user_id = #{self.id}")
.select("posts.*, reposts.user_id AS repost_user_id, (SELECT name FROM users WHERE id = repost_user_id) AS repost_user_name")
relation.where(user_id: self.id)
.or(relation.where("reposts.user_id = ?", self.id))
.with_attached_images
.preload(:user, :review, :comments, :likes, :reposts)
.order(Arel.sql("CASE WHEN reposts.created_at IS NULL THEN posts.created_at ELSE reposts.created_at END"))
end
Let's look at it from the top in order.
Suddenly, `relation = Post ....`
appears as a variable because the `or``` method that appears later requires repeated description. As for the contents,
reposts``` is acquired by left outer join for ``` posts```. Since multiple users may repost one post, we are narrowing down to only our own repost in the ON clause of the join condition. Since it seems that the ON clause cannot be set freely with the ``
left_joins method, it is written solidly with the `` `joins
method.
I also considered a method of simply JOINing with `eager_load``` and narrowing down only my own reposts with
where```, but as the number of posts and reposts increases, the impact on performance will jump up. The description is a little complicated, but it is in the form of getting the ``
repost``` in the narrowed down form by JOIN.
The next `select``` method is specified because I want to get not only
posts``` but also ``
reposts``` information (which will be used later in the view).
The table up to this point shows the table to be acquired and the items to be acquired. The following are acquisition conditions.
relation.where(user_id: self.id).or(relation.... but this means getting the user's own post, or the reposted user's own post.
#### **`.with_attached_with images.preload(... and N+1 Get the associations that accompany the post while avoiding the problem. Here, other than posts and reposts are not used for data narrowing etc. (used only for view display), so eager in consideration of performance_It uses preload instead of load.`**
The last `.order (・ ・ ・ ```, in the order of acquisition, but basically in the order of posting date and time. If it is your own post, each item of
`reposts``` that you JOINed is NULL, so it is judged that the sort item is used properly.
`order``` method, you have to write it solidly, but if you do so, an error like ** Dangerous query method ... ** will occur. This is a feature from Rails 5.2, there is a risk of SQL injection if you write SQL solidly in the order clause! It seems that it will warn you. If you enclose it in
Arel.sql (...)
, the warning will not be issued, so it means that you should use it after confirming that there is no danger. (Reference article: [Don't just add Arel.sql !? What to do when a "Dangerous query method ..." warning appears in Rails](https://qiita.com/jnchito/items/5f2f00c93c0ba68e4d31)) This time, just use the time stamp item
created_at``` as an order judgment item, it is clear that there is no problem, so enclose it in ``` Arel.sql (...) ``
to avoid the warning. doing.users_controller.rb
class UsersController < ApplicationController
before_action :set_user
def show
@posts = @user.posts_with_reposts
end
private
def set_user
@user = User.find(params[:id])
end
end
_posts.html.haml
- posts.each_with_index.reverse_each do |post, i|
%li.post
- if post.has_attribute?(:repost_user_id) #This judgment is included because the same partial template is used for post list pages (favorite list, etc.) that do not get reposts.
- if post.repost_user_id.present?
.post-repost
%i.fas.fa-retweet
= link_to post.repost_user_name, "/users/#{post.repost_user_id}", "data-turbolinks": false
Repost
.post-container
.post-left
= link_to user_path(post.user.id), "data-turbolinks": false do
- if post.user.image.attached?
= image_tag(post.user.image, alt: 'Default icon')
- else
= image_tag('sample/default_icon.png', alt: 'Default icon')
.post-right
= render partial: "posts/post", locals: {post: post}
= render partial: "posts/comment", locals: {post: post}
- if i != 0
%hr
Like this, I was able to display a list of user posts and reposted posts.
In the timeline (post list of people (= followy) that the user is following), it is necessary to display not only the posts of followy but also their reposts as well as the post list page of the user. However, unlike the user's post list page, it is necessary to consider ** if multiple different followers are reposting the same post, prevent duplicate display **. The method `` `followings_posts_with_reposts``` implemented with this in mind is as follows.
user.rb
def followings_posts_with_reposts
relation = Post.joins("LEFT OUTER JOIN reposts ON posts.id = reposts.post_id AND (reposts.user_id = #{self.id} OR reposts.user_id IN (SELECT follow_id FROM relationships WHERE user_id = #{self.id}))")
.select("posts.*, reposts.user_id AS repost_user_id, (SELECT name FROM users WHERE id = repost_user_id) AS repost_user_name")
relation.where(user_id: self.followings_with_userself.pluck(:id))
.or(relation.where(id: Repost.where(user_id: self.followings_with_userself.pluck(:id)).distinct.pluck(:post_id)))
.where("NOT EXISTS(SELECT 1 FROM reposts sub WHERE reposts.post_id = sub.post_id AND reposts.created_at < sub.created_at)")
.with_attached_images
.preload(:user, :review, :comments, :likes, :reposts)
.order(Arel.sql("CASE WHEN reposts.created_at IS NULL THEN posts.created_at ELSE reposts.created_at END"))
end
Basically, the configuration is similar to the posts_with_reposts
method mentioned above. I will list the different points in order.
** · relation = Post.joins (
**
posts_with_reposts
The method focused only on its own repostreposts
I was joining, but here I am narrowing down to only myself or followy reposts.
Actually, I wanted to implement it like `` user_id IN (# {self.followings_with_userself.pluck (: id)})
, but in the case of this description, when compiled, the inside of the IN clause is ``` ( [1,2,3]) It's a bit redundant because it looks like ``` and I can't get it well, but I get the follower ID from
`relationshipsand specify it as a condition. It works well for using pluck with the normal
where``` method etc. which is not solid writing ... but it seems that it can be refactored a little if there is a good way.
** · relation.where (user_id: self.followings_with_userself.pluck (: id))` `` ** I'm getting a list of user or follower posts. For the time being,
followings_with_userself``` is defined in advance as follows.
user.rb
def followings_with_userself
User.where(id: self.followings.pluck(:id)).or(User.where(id: self.id))
end
** ・ .or (relation.where (・ ・ ・` `` ** ** ・
.where ("NOT EXISTS (・ ・ ・ `** Before looking at the contents, the structure of the sentence is complicated, so if you preface it, if the previous where statement is A, the or statement of this explanation is B, and the where statement is C **
A OR (B AND C)
It has a structure of `` **.
The conditions for the data you want to acquire are as follows.
** (Posts by users and followers themselves) ** or ** ((Posts reposted by users and followers) ** and ** (Posts with the latest repost date and time among duplicates)) **
reposts
Is acquired by left join, so if multiple followers repost for one post, the same post will be acquired multiple records. for that reasonMultiple for one postreposts
If the records are joined,reposts
ofcreated_at
が一番新しいレコードofみを取得する, Is included. this isnot exists
This can be achieved by using the subquery and the condition that "there is no newer record than itself".
posts_controller.rb
def index
if user_signed_in?
@user = User.find(current_user.id)
@posts = @user.followings_posts_with_reposts
else
#If you are not logged in, there is no concept such as follow or timeline, so only posts are displayed without displaying reposts.
@posts = Post.with_attached_images.preload(:user, :review, :comments, :likes)
end
end
Since the view is the same as above (common to partial templates), I will omit it.
I was able to display the timeline like this.
When retrieving the table with which the association is formed, it is includes for the time being to avoid the N + 1 problem! I only had a lot of knowledge, but with the implementation of this retweet function, it was a good opportunity to understand the difference between eager_loading and lazy loading, and the difference between join / preload / eager_load / includes. Although it is a popular SNS app for portfolio creation, I think the retweet function is relatively more difficult than other functions. Does the fact that there are few articles mean that few people have implemented it? Personally, I learned a lot, so if you are developing an SNS application, why not try implementing it?
The following is an article that I referred to as a person who thought about data acquisition. Reason for using preload and eager_load properly without using ActiveRecord includes Differences between ActiveRecord joins, preload, includes and eager_load
Recommended Posts