Home > Net >  Preventing duplicates in a has_many through association?
Preventing duplicates in a has_many through association?

Time:12-29

I've got two tables/models (Users and Concerts) that has a join-table model of Posts. This is for a 'ticketmaster' style marketplace so a User can make a Post on any given Concert, and a Concert can also have info on a given User through the Post that this User made.

The problem is that I have user duplicates on my /concerts and concert duplicates on my users; I'm not sure why either. Below are the JSON output of /concerts and /users.

/concerts is here:

{
  "id": 45,
  "date": "2023-01-19T00:00:00.000Z",
  "location": "Brooklyn Steel",
  "image": "https://i.imgur.com/SmFrzTC.jpg",
  "artist_id": 33,
  "artist": {
     "id": 33,
     "name": "Adele",
     "image": "https://i.imgur.com/zmGbfKS.jpg",
     "genre": "Pop"
  },
   "posts": [],
   "users": [
     {
      "id": 257,
      "username": "onlineguy1",
      "email": "[email protected]"
      },
     {
      "id": 257,
      "username": "onlineguy1",
      "email": "[email protected]"
      },
     {
      "id": 273,
      "username": "L0V3MUSIC",
      "email": "[email protected]"
      }
   ]
},

For /users, it looks like this and you can see the issue more:

{
   "id": 257,
   "username": "onlineguy1",
   "email": "[email protected]",
   "posts": [],
   "concerts": [
      {
         "id": 45,
         "date": "2023-01-19T00:00:00.000Z",
         "location": "Brooklyn Steel",
         "image": "https://i.imgur.com/SmFrzTC.jpg",
         "artist_id": 33
      },
      {
         "id": 45,
         "date": "2023-01-19T00:00:00.000Z",
         "location": "Brooklyn Steel",
         "image": "https://i.imgur.com/SmFrzTC.jpg",
         "artist_id": 33
      },
      {
         "id": 46,
         "date": "2024-05-23T00:00:00.000Z",
         "location": "Mao Livehouse",
         "image": "https://i.imgur.com/CghhYym.jpg",
         "artist_id": 33
      },
      {
         "id": 46,
         "date": "2024-05-23T00:00:00.000Z",
         "location": "Mao Livehouse",
         "image": "https://i.imgur.com/CghhYym.jpg",
         "artist_id": 33
      },
      {
         "id": 47,
         "date": "2023-04-29T00:00:00.000Z",
         "location": "Madison Square Garden",
         "image": "https://i.imgur.com/0gd1dD0.jpg",
         "artist_id": 33
      },
      {
         "id": 47,
         "date": "2023-04-29T00:00:00.000Z",
         "location": "Madison Square Garden",
         "image": "https://i.imgur.com/0gd1dD0.jpg",
         "artist_id": 33
      },
    ]
},

Below are my post model, my user model, my concert model FWIW.

class User < ApplicationRecord
  has_secure_password

  validates_uniqueness_of :username, presence: true

  # validates :username, presence: true, uniqueness: true
  validates :password, length: { minimum: 8, maximum: 254}
  validates_presence_of :email
    validates_format_of :email, with: URI::MailTo::EMAIL_REGEXP
  # validates :my_email_attribute, email: true, presence: true

  has_many :posts
  has_many :concerts, through: :posts

end


class Post < ApplicationRecord
  belongs_to :user
  belongs_to :concert

  validates :body, presence: true
  validates :tickets, presence: true, numericality: { greater_than: 0 }
end

class Concert < ApplicationRecord
  belongs_to :artist
  
  has_many :posts
  has_many :users, through: :posts

end

If anybody's got a step in the right direction, I'll gladly take it because I can't figure it out. Been poring through docs but I've psyched myself out somewhere

EDIT: to include my Controllers, Serializers, and route.

Also, controllers here below, starting with Post:

class PostsController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
  rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
  
  def index
    posts = Post.all
    render json: posts
  end

  def show
    post = Post.find_by!(id: params[:id])
    render json: post, status: 200
  end


  def create
    post = Post.create!(new_post_params)
    render json: post, status: 201
  end

  # ## made this one to not-render duplicates but still rendered duplicates
  # def create
  #   ## links the proper user to the post
  #   correct_user = User.find_by!(id: params[:user_id])

  #   ## links the proper concert to the post
  #   correct_concert = Concert.find_by!(id: params[:concert_id])

  #   newPost = Post.create!(
  #     id: params[:id],
  #     body: params[:body],
  #     tickets: params[:tickets],
  #     for_sale: params[:for_sale],
  #     concert_id: correct_concert.id,
  #     user_id: correct_user.id
  #   )
  #   render json: newPost, status: 201
  # end

  def update
    post = Post.find_by!(id: params[:id])
    if session[:user_id] === post[:user_id]
      post.update!(
        body: params[:body],
        tickets: params[:tickets]
      )
      render json: post, status: 200
    end 
  end
        

  def destroy
    post = Post.find_by!(id: params[:id])
    if session[:user_id] === post[:user_id]
      post.destroy
      head :no_content
    end
  end

  private

  def new_post_params
    params.require(:concert_id, :user_id, :for_sale, :tickets, :body)
  end

  def render_unprocessable_entity_response(invalid)
    render json: { errors: invalid.record.errors.full_messages }, status: :unprocessable_entity
  end

  def render_not_found_response(invalid)
    render json: { error: invalid.message }, status: :not_found
  end

end

And here's for Users:

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
  rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response

  def index
    users = User.all
    render json: users
  end

  ## get '/me'
  def show
    user = User.find_by!(id: session[:user_id]) ## changed it to User.find_by! for it to work
    render json: user, status: 200
  end

  def create
    user = User.create!(signup_user_params)
    session[:user_id] = user.id
    render json: user, status: :created
  end

# # the original show
#   def show
#     user = User.find_by(id: session[:user_id])
#     if user
#       render json: user, status: 200
#     else
#       render json: user.errors.full_messages, status: :unprocessable_entity
#     end
#   end

#   # the original create
#   def create
#     user = User.create(signup_user_params)

#     if user.valid?
#       session[:user_id] = user.id
#       render json: user, status: :created
#     else
#       render json: user.errors.full_messages, status: :unprocessable_entity
#     end
#   end

  # # update a specific user
  # def update
  #   if user.update(user_params)
  #     render json: user
  #   else
  #     render json: user.errors, status: :unprocessable_entity
  #   end
  # end

  # # delete a specific user
  # def destroy
  #   user.destroy
  # end

  private


  def signup_user_params
    params.permit(:username, :password, :password_confirmation, :email)
  end


  def render_unprocessable_entity_response(invalid)
    render json: { errors: invalid.record.errors.full_messages }, status: :unprocessable_entity
  end

  def render_not_found_response(invalid)
    render json: { error: invalid.message }, status: :not_found
  end
end

And here's Concert:

class ConcertsController < ApplicationController


  def index
    concerts = Concert.all
    render json: concerts
  end

  def show
    concert = Concert.find_by!(id: params[:id])
    render json: concert, status: 200
  end

  ## finish after the duplicates issue
  def create
    ## find the proper artist, and link the proper artist
  end
end

Here's the Serializers:

class ConcertSerializer < ActiveModel::Serializer
  attributes :id, :date, :location, :image, :artist_id
  
  belongs_to :artist, serializer: ArtistSerializer
  has_many :posts, serializer: PostSerializer
  has_many :users, through: :posts, serializer: UserSerializer
end

class PostSerializer < ActiveModel::Serializer
  attributes :id, :body, :for_sale, :tickets, :concert_id, :user_id

  belongs_to :user, serializer: UserSerializer
  belongs_to :concert, serializer: ConcertSerializer
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :email

  has_many :posts, serializer: PostSerializer
  has_many :concerts, through: :posts, serializer: ConcertSerializer
end

Here's routes.rb:

Rails.application.routes.draw do
  #& Defines the root path route ("/")
  #& root "articles#index"

  ##~ FOR THE ARTIST-CONCERTS-VENUES DISPLAYS
  #& getting all the artists-concerts-users
  get '/artists', to: "artists#index"
  get '/artists/:id', to: "artists#show"
  get '/concerts', to: "concerts#index"
  get "/users", to: "users#index"

  ##~ FOR THE POSTS GET/CREATION/EDITS/DELETION
  get '/posts', to: "posts#index"
  post '/new_post', to: "posts#create"
  patch '/update_post/:id', to: "posts#update"
  delete '/delete_post/:id', to: "posts#destroy"

  ##~ THE LOGIN/LOGOUT ROUTES
  #& to create a new user outright
  post "/new_user", to: "users#create"
  #& to login our user
  post "/login", to: "sessions#create"
  #& to keep the user logged in
  get "/me", to: "users#show"
  #& to log the user out
  delete "/logout", to: "sessions#destroy"

  ##~ SESSION & COOKIES INFO
  #& shows session_id and sessions info
  get "/show_session", to: "application#show_session"
  #& displays cookies
  get "/cookies", to: "application#show_cookies"
  
  # Routing logic: fallback requests for React Router.
  # Leave this here to help deploy your app later!
  get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? }
end

CodePudding user response:

The fact that posts: [] is shown I am going to assume is for brevity becuase there can be no users for a Concert without posts having elements.

Your issue is that you join User and Concert through Post, and it is reasonable to assume that a User may post more than once about a Concert is it not?

Given your current relationships and the fact that you are using ActiveModel::Serializer you are going to have to Override the Association method to return only distinct User/Concerts.

For Example:

class ConcertSerializer < ActiveModel::Serializer
  attributes :id, :date, :location, :image, :artist_id
  
  belongs_to :artist, serializer: ArtistSerializer
  has_many :posts, serializer: PostSerializer
  has_many :users, through: :posts, serializer: UserSerializer do 
    object.users.distinct
  end 
end

Note: I am not sure how this does not end up in a circular dependency as it appears it should (I don't use this library for APIs)

CodePudding user response:

I managed to solve this by using the distinct property when defining my models.

class Concert < ApplicationRecord
  belongs_to :artist
  
  has_many :posts
  has_many :users, -> { distinct }, through: :posts

end

By using --> {distinct}, I only got distinct (i.e. no repeats) objects rendered back in the JSON. Whether or not this is the most optimal way, I can't speak on but it definitely solved my original problem so I'm answering this question myself. You can read more here if you're stuck in the same boat.

  • Related