Preventing duplicates in a has_many through association? - ruby-on-rails
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 (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
Need help validating query parameters
I have multiple methods within my controller that takes in query parameters. How can I validate that I am being passed in valid parameters? For example, for the index method, how can I make sure that I am getting an array of authorIds. def index author_ids_array = params[:authorIds].to_s.split(',') posts = Post .get_posts_by_user_id(author_ids_array) .order(sort_column => sort_direction) if posts render json: { posts: posts }, status: :ok else render json: {error: posts.errors}, status: :unprocessable_entity end end Or in this update method. How can I validate that I am getting a valid postId def update post = current_user.posts.find_by(id: params[:id]) if post.update!(post_params) post_hash = post.as_json post_hash.merge!(authorIds: params[:authorIds]) render json: {post: post_hash}, status: :ok else render json: {error: post.errors}, status: :unprocessable_entity end end Update: Post Model: class Post < ApplicationRecord # Associations has_many :user_posts has_many :users, through: :user_posts, dependent: :destroy # Validations validates :text, presence: true, length: { minimum: 3 } validates :popularity, inclusion: { in: 0.0..1.0 } def tags if super super.split(",") end end def tags=(value) if value.kind_of? Array super value.join(",") else super value end end def self.get_posts_by_user_id(user_id) Post.joins(:user_posts).where(user_posts: { user_id: user_id }) end end User Model: class User < ApplicationRecord has_secure_password # Associations has_many :user_posts has_many :posts, through: :user_posts, dependent: :destroy # Validations validates :username, :password, presence: true validates :password, length: { minimum: 6 } validates :username, uniqueness: true end User_post Model: class UserPost < ApplicationRecord belongs_to :user belongs_to :post end
You can use specific render like below user this in any method like def index return render body: params.inspect . . end user below code return render body: params.inspect so when you use index it will give you params which is passing OR you can user below code in your application.html.erb above <%= yield%> <%= debug(params) if Rails.env.development? %>
After your clarifications, your question remains unclear to me and it is difficult to guess what you're doing. But I understood that you want to ensure that params[:authorIds] or anything else is an array. You can see if a given variable is an array the following way: a = ["1","2"] if a.is_a?(Array) puts "is an array" end With params: params[:authorIds].is_a?(Array) You can use byebug (before Rails 7) or debugger (for Rails 7) to inspect what a param is. As an example: (ruby#whatever: cluster worker 1: 42779 [MyApp]#42793) params[:ids].class Array
RoR API how to use the post for asociations
I am string an API project using RoR 6. I have this models: class Province < ApplicationRecord validates :province, presence: true, length: {minimum:5}, uniqueness: true has_many :cities, dependent: :destroy end class City < ApplicationRecord validates :city, presence: true, length: {minimum: 5}, uniqueness: true belongs_to :province end the migrations: class CreateProvinces < ActiveRecord::Migration[6.1] def change create_table :provinces do |t| t.string :province t.timestamps end end end class CreateCities < ActiveRecord::Migration[6.1] def change create_table :cities do |t| t.string :city t.references :province, null: false, foreign_key: true t.timestamps end end end the controller method: def create #city = City.new(city_params) if #city.save render json: #city, status: :created, location: #city else render json: #city.errors, status: :unprocessable_entity end end private def city_params params.require(:city).permit(:city, :province_id) end and the routes: Rails.application.routes.draw do resources :provinces, defaults: { format: :json } do resources :cities, defaults: { format: :json } end end the thing is this, when running: curl --location --request POST 'http://127.0.0.1:3000/provinces/1/cities' \ --header 'Content-Type: application/json' \ --data-raw '{ "city": "New City", "province": 1 }' { "province": [ "must exist" ] } The province exists. But there is no way to insert it. If I use the rails console and run: p=Province.find(1) c=City.new({city: 'New City', province: p}) c.save it works as expected. How to solve this?
When dealing with a nested route you want to find the parent resource first and then create the nested resource off it: class ProvincesController < ApplicationController # POST /provinces/1/cities def create #province = Province.find(params[:province_id]) #city = #province.cities.new(city_params) if #city.save render json: #city, status: :created, location: #city else render json: #city.errors, status: :unprocessable_entity end end private def city_params params.require(:city).permit(:city) end end Also calling the columns provinces.province and cities.city makes a lot less sense then using provinces.name and cities.name.
"password_digest can't be blank" and "password can't be blank" errors
I'm trying to create a user via Postman as shown in the screenshot, but getting errors: This is a rails app created with an --api option. user.rb class User < ApplicationRecord validates :email, uniqueness: true validates_format_of :email, with: /#/ validates :password_digest, presence: true has_secure_password end users_controller.rb class Api::V1::UsersController < ApplicationController # GET /users/1 def show render json: User.find(params[:id]) end # POST /users def create #user = User.new(user_params) if #user.save render json: #user, status: :created else render json: #user.errors, status: :unprocessable_entity end end private def user_params params.require(:user).permit(:email, :password) end end routes.rb Rails.application.routes.draw do namespace :api, defaults: { format: :json } do namespace :v1 do resources :users, only: [:show, :create] end end end
Your user_params method expects the attributes nested in a user hash. Change the response to: { "user": { "email": "...", "password": "..." } } Btw the validates :password_digest, presence: true line is not needed because has_secure_password has validations build in and ensures internally that a password is present.
Access to associations from polymorphic model
I have next models: class Post < ActiveRecord::Base has_many :comments, as: :commentable end class Article < ActiveRecord::Base has_many :comments, as: :commentable end class Comment < ActiveRecord::Base belongs_to :commentable, polymorphic: true end Now I can get posts and articles via #comment.commentable, but I need via #comment.post or #comment.article. I found some solutions, but it's not working with nested attributes: accepts_nested_attributes_for :article accepts_nested_attributes_for :post My comments_controller: class CommentsController < ApplicationController def create #comment = Comment.new(comment_params) if #comment.save! head :ok, location: #comment else render json: #comment.errors, status: :unprocessable_entity end end private def comment_params params.require(:comment).permit( :text, text: {}, post_attributes: [:id, text: {}], article_attributes: [:id, text: {}] ) end end Post params from form like: const params = { comment: { text, post_attributes: [{ id: post ? post.id : null, text, }] } } If post or article not exists it must be created. Or add one more model like here https://gist.github.com/runemadsen/1242485 — but I don't want to do that :(
Rails 3.2 Associations and :include
I have some troubles to make an association between 2 tables. I have Users who can write Posts Here is my migration file : class LinkUsersAndPosts < ActiveRecord::Migration def change add_column :posts, :user_id, :integer add_index :posts, :user_id end end My models : class Post < ActiveRecord::Base attr_accessible :content, :title belongs_to :user end class User < ActiveRecord::Base attr_accessible :email, :login, :password, :password_confirmation, :rights has_many :posts end My controller : class PostsController < ApplicationController respond_to :json def index #posts = Post.includes(:user).all respond_with #posts end def create #post = Post.new(params[:post]) #post.user = current_user if #post.save render json: #post, status: :created, location: #post else render json: #post.errors, status: :unprocessable_entity end end end The posts is correctly created, the user_id is set all is fine. The problem is when i want to retrieve the list of posts including user, the list of posts is retrieve, but i havn't any data on the user except his id. Response : [ { "content":"jksdjd", "created_at":"2013-08-31T09:03:01Z", "id":11,"title":"kdjs", "updated_at":"2013-08-31T09:03:01Z", "user_id":4 }, { "content":"tez2", "created_at":"2013-08-31T09:16:45Z", "id":12, "title":"test2", "updated_at":"2013-08-31T09:16:45Z", "user_id":4 } ]
By default a JSON response won't return any associated models. You need to specify the other models you want returned. So in your case, you can do this: render json: #post => #post.to_json(:include => :user), status: :created, location: #post Also see: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html Rails Object Relationships and JSON Rendering