[RUBY] [Rails] Implementation of retweet function in SNS application

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.

Premise

A `user``` model that manages users, a `postmodel that manages posts (tweets), and a relationship``` model that manages follower / follower relationships have been created.

Creating a model

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

Creating a controller

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

Built-in repost button

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

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.

Display a list of user posts including reposts

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 ...)

Get user's own posts and reposted posts

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.

Described in the controller and displayed in the view

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. 参考画像

View timeline including reposts

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_repostsThe method focused only on its own repostrepostsI 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)) **

repostsIs 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 postrepostsIf the records are joined,repostsofcreated_atが一番新しいレコードofみを取得する, Is included. this isnot existsThis can be achieved by using the subquery and the condition that "there is no newer record than itself".

Described in the controller and displayed in the view

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. 参考画像

Finally

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

[Rails] Implementation of retweet function in SNS application
[Rails 6] Implementation of SNS (Twitter) sharing function
Implement application function in Rails
[Rails 6] Implementation of search function
[Rails] Implementation of category function
[Rails] Implementation of tutorial function
[Rails] Implementation of like function
[Rails] Implementation of SNS authentication (Twitter, Facebook, Google) function
[Rails] Implementation of CSV import function
[Rails] Asynchronous implementation of like function
[Rails] Implementation of image preview function
[Rails] About implementation of like function
[Rails] Implementation of user withdrawal function
[Rails] Implementation of CSV export function
Implementation of like function in Java
Rails sorting function implementation (displayed in order of number of like)
Rails [For beginners] Implementation of comment function
[Vue.js] Implementation of menu function Implementation version rails6
[Ruby on rails] Implementation of like function
[Vue.js] Implementation of menu function Vue.js introduction rails6
[Rails] Implementation of new registration function in wizard format using devise
Let's create a TODO application in Java 6 Implementation of search function
[Rails 6] Implementation of new registration function by SNS authentication (Facebook, Google)
[Rails] Implementation of search function using gem's ransack
Implementation of Ruby on Rails login function (Session)
[Rails 6] Implementation of inquiry function using Action Mailer
Create authentication function in Rails application using devise
[Rails] Implementation of image enlargement function using lightbox2
Implementation of search function
[Rails] Implementation of "notify notification in some way"
Rails search function implementation
Implementation of pagination function
Ruby on Rails <2021> Implementation of simple login function (form_with)
Implement post search function in Rails application (where method)
[Rails] Implementation of drag and drop function (with effect)
Implementation of Ruby on Rails login function (devise edition)
[Rails] Implementation of multi-layer category function using ancestry "Preparation"
[Rails] Implementation of multi-layer category function using ancestry "seed"
Rails implementation of ajax removal
Rails fuzzy search function implementation
Implement follow function in Rails
Implementation of image preview function
Implementation of category pull-down function
Login function implementation with rails
Implementation of gzip in java
Implementation of tri-tree in Java
Implementation of HashMap in kotlin
[Rails 6] Pagination function implementation (kaminari)
[Rails] Implementation of multi-layer category function using ancestry "Editing form"
[Rails] Implementation of multi-layer category function using ancestry "Creation form"
[Rails] Implementation of tagging function using intermediate table (without Gem)
Add a search function in Rails.
[rails] Login screen implementation in devise
Kaminari --Added pagination function of Rails
Implement simple login function in Rails
Implementation of asynchronous processing in Tomcat
[Rails] Implementation of many-to-many category functions
[Rails] gem ancestry category function implementation
[Ruby on Rails] Comment function implementation
[Rails 6] Like function (synchronous → asynchronous) implementation
Implement CSV download function in Rails