简体   繁体   中英

Preventing duplicates in a has_many through association?

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": "eusebia_larson@wilderman.co"
      },
     {
      "id": 257,
      "username": "onlineguy1",
      "email": "eusebia_larson@wilderman.co"
      },
     {
      "id": 273,
      "username": "L0V3MUSIC",
      "email": "lulu_lemke@johns.name"
      }
   ]
},

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

{
   "id": 257,
   "username": "onlineguy1",
   "email": "eusebia_larson@wilderman.co",
   "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

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)

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 (ie 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM