Writing a scope for multiple associations - Rails 4 - ruby-on-rails

I am having challenges writing a scope to display:
all cards belonging to events that have payments that belong to a specific user
i am currently able to display, all events that have payments that belong to a specific user using the scope scope :booked_events, -> (user) { joins(payments: :user).where(users: { id: user.id }) } in the event.rb file
some events have a card and some don't
could one kindly advise me how i display all events with a card that
have payments that belong to a specific user
event.rb
has_many :payments
has_one :card
scope :booked_events_with_cards, -> (user) { joins(payments: :user).where(users: { id: user.id }) }
card.rb
belongs_to :event
payment.rb
belongs_to :event
belongs_to :user
user.rb
has_many :payments
i tried the below in the card.rb file but i am unsure
belongs_to :event
has_many :payments, through: :event
scope :cards_belonging_to_booked_events, -> (user) { joins(payments: :event).where(users: { id: user.id }) }
but got the below error:
2.3.0 :012 > cards.cards_belonging_to_booked_events(user)
Card Load (0.6ms) SELECT "cards".* FROM "cards" INNER JOIN "events" ON "events"."id" = "cards"."event_id" INNER JOIN "payments" ON "payments"."event_id" = "events"."id" INNER JOIN "events" "events_payments" ON "events_payments"."id" = "payments"."event_id" WHERE "users"."id" = 4
SQLite3::SQLException: no such column: users.id: SELECT "cards".* FROM "cards" INNER JOIN "events" ON "events"."id" = "cards"."event_id" INNER JOIN "payments" ON "payments"."event_id" = "events"."id" INNER JOIN "events" "events_payments" ON "events_payments"."id" = "payments"."event_id" WHERE "users"."id" = 4
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.id: SELECT "cards".* FROM "cards" INNER JOIN "events" ON "events"."id" = "cards"."event_id" INNER JOIN "payments" ON "payments"."event_id" = "events"."id" INNER JOIN "events" "events_payments" ON "events_payments"."id" = "payments"."event_id" WHERE "users"."id" = 4
or, am i to write the scope in event.rb file if i want to display all cards with events that have payments that have been made by a user?

You just need to include card association in joins. It removes events without card associated from the query result:
scope :booked_events_with_cards, -> (user) { joins(:card, payments: :user).where(users: { id: user.id }) }

Related

postgres - distinct order_by a jsonb key throws a rails PG::InvalidColumnReference

My user model has the following definition for the follower association:
has_many :passive_follow_actions, -> { where("actions.activity_verb = 'follow'").uniq }, class_name: 'Action', foreign_key: :activity_object_id
has_many :followers, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
#
User table has a jsonb field called follow_stats which I use to order the follower results.
On its own, a distinct operation works
has_many :followers, -> { distinct }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT DISTINCT "users".* FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User'
#
Also on its own, an order operation works
has_many :followers, -> { order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT "users".* FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
However, put together,
has_many :followers, -> { distinct.order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT DISTINCT "users".* FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
it throws the following exception:
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
#
I tried adding the jsonb query onto the select clause to prevent the error, but that results in an empty response:
has_many :followers, -> { select("follow_stats->'followers_count'").uniq.order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
# SELECT DISTINCT follow_stats->'followers_count' FROM "users" INNER JOIN "actions" ON "users"."id" = "actions"."activity_actor_id" WHERE "actions"."activity_object_id" = 1 AND (actions.activity_verb = 'follow') AND "actions"."activity_actor_type" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
Response:
[#<User id: nil, ?column?: 42>, #<User id: nil, ?column?: 4>, #<User id: nil, ?column?: 2>, #<User id: nil, ?column?: 1>, #<User id: nil, ?column?: 0>]
#
When I add a * in the select clause, it returns the same exception PG::InvalidColumnReference
has_many :followers, -> { select("*, follow_stats->'followers_count'").uniq.order("follow_stats->'followers_count' DESC") }, through: :passive_follow_actions, source: :activity_actor, source_type: 'User'
#SELECT DISTINCT *, follow_stats->'followers_count' FROM \"users\" INNER JOIN \"actions\" ON \"users\".\"id\" = \"actions\".\"activity_actor_id\" WHERE \"actions\".\"activity_object_id\" = 1 AND (actions.activity_verb = 'follow') AND \"actions\".\"activity_actor_type\" = 'User' ORDER BY follow_stats->'followers_count' DESC
#
How select distinct and order by a jsonb key at the same time?

prevent different user to see a model through the show method

In my index method for every model I make sure to query starting with current user: current_user.model.all, in order to only show models that belong to the current user.
My show method for all models is quite simple and standard, without current_user.
def show
#logica = Logica.find params[:id]
authorize #logica
end
This does open the chance of a user entering a random id in the url and see the model from a different user. What is the best way to prevent this from happening?
The has_many association has a number of different methods available including find
current_user.model.find(params[:id])
which is similar to
Model.where(user_id: current_user.id).find(params[:id])
If you have to go through several models in order to reach your User model, for instance
class User < ApplicationRecord
has_one :test_one
end
class TestOne < ApplicationRecord
has_one :test_three
belongs_to :user
end
class TestThree < ApplicationRecord
has_many :test_fours
belongs_to :test_one
end
class TestFour < ApplicationRecord
belongs_to :test_three
end
you can set it up in a single query by doing something like
TestFour.joins(:test_three => { :test_one => :user }).where(test_threes: { test_ones: { users: { id: current_user.id}}}).find(1)
# TestFour Load (1.2ms) SELECT "test_fours".* FROM "test_fours" INNER JOIN "test_threes" ON "test_threes"."id" = "test_fours"."test_three_id" INNER JOIN "test_ones" ON "test_ones"."id" = "test_threes"."test_one_id" INNER JOIN "users" ON "users"."id" = "test_ones"."user_id" WHERE "users"."id" = $1 AND "test_fours"."id" = $2 LIMIT $3 [["id", 1], ["id", 1], ["LIMIT", 1]]
#=> #<TestFour id: 1, test_three_id: 1, created_at: "2017-07-12 21:06:51", updated_at: "2017-07-12 21:06:51">
and then if you did this with a user id/test four id that don't match:
TestFour.joins(:test_three => { :test_one => :user }).where(test_threes: { test_ones: { users: { id: current_user.id + 1}}}).find(1)
# TestFour Load (0.8ms) SELECT "test_fours".* FROM "test_fours" INNER JOIN "test_threes" ON "test_threes"."id" = "test_fours"."test_three_id" INNER JOIN "test_ones" ON "test_ones"."id" = "test_threes"."test_one_id" INNER JOIN "users" ON "users"."id" = "test_ones"."user_id" WHERE "users"."id" = $1 AND "test_fours"."id" = $2 LIMIT $3 [["id", 2], ["id", 1], ["LIMIT", 1]]
#=> ActiveRecord::RecordNotFound: Couldn't find TestFour with 'id'=1 [WHERE "users"."id" = $1]

Convert Complex SQL to Activerecord

Hi I am trying to work out if its possible to convert this SQL statement to something I can use within a controller on my rails project
SELECT "products".name, properties.display_name,variant_properties.description, variants.price FROM "products"
INNER JOIN "product_properties" ON "product_properties"."product_id" = "products"."id"
INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id
INNER JOIN "properties" ON "properties"."id" = "product_properties"."property_id" AND properties.id = variant_properties.property_id
INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id
GROUP BY products.name, variant_properties.description, properties.display_name, variants.price
ORDER BY 1,2,4,3
The simplest way to do it is cheat with the active record join method that accepts raw SQL:
products = Product.select("products.name, properties.display_name, variant_properties.description, variants.price")
.joins("INNER JOIN product_properties ON product_properties.product_id = products.id")
.joins("INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id")
.joins("INNER JOIN properties ON properties.id = product_properties.property_id AND properties.id = variant_properties.property_id")
.joins("INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id")
.group("products.name, variant_properties.description, properties.display_name, variants.price")
.order("products.name, properties.display_name, variants.price, variant_properties.description")
With proper associations in your models, you can simplify it to:
products = Product.select("products.name, properties.display_name, variant_properties.description, variants.price")
.joins(:product_properties)
.joins(:variant_properties)
.joins(:properties)
.joins(:variants)
.group("products.name, variant_properties.description, properties.display_name, variants.price")
.order("products.name, properties.display_name, variants.price, variant_properties.description")
Assume you have models:
class Product
has_many :product_properties
has_many :variants
has_many :properties, through: :product_properties
LIST = 'products.name, variant_properties.description, properties.display_name, variants.price'
scope :spec, -> {
joins(:product_properties, :properties, :variants).
merge(Property.strikt).
merge(Variant.strikt).
select(LIST).
group(LIST).
order('1,2,3,4')
}
end
class ProductProperty
belongs_to :product
belongs_to :property
end
class VariantProperty
belongs_to :variant
belongs_to :property
has_many :product_properties, foreign_key: :property_id, primary_key: :property_id
end
class Property
has_many :variant_properties
has_many :product_properties
scope :strikt, -> { joins(:variant_properties).joins(:product_properties) }
end
class Variant
belongs_to :product
has_many :variant_properties
scope :strikt, -> { joins(:product).joins(:variant_properties) }
end
so your SQL you should get as of:
Product.spec.to_sql

Rails 4 ActsAsTenant complex has_many through doesn't work with setted current_tenant

I have these models in my multi-tenant app:
class Tenant < ActiveRecord::Base
has_many :userroles
has_many :users, through: :userroles
has_many :roles, through: :userroles
has_many :admins, -> { joins(:roles).where("roles.name = 'admin'").uniq }, through: :userroles, class_name: 'User', source: :user
end
class Role < ActiveRecord::Base
has_paper_trail
acts_as_paranoid
has_many :userroles, :dependent => :destroy
has_many :users, :through => :userroles
end
class Userrole < ActiveRecord::Base
acts_as_tenant(:tenant)
has_paper_trail
belongs_to :user
belongs_to :role
end
I use gem ActsAsTenant written by Erwin (source code at github). When current_tenant doesn't set my code work right, but if I set current_tenant, I got errors.
In console I got those errors:
2.1.0 :001 > ActsAsTenant.current_tenant
=> nil
2.1.0 :002 > t = Tenant.first
2.1.0 :004 > t.admins.count
(1.5ms) SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (roles.name = 'admin') [["tenant_id", 1]]
=> 1
2.1.0 :005 > ActsAsTenant.current_tenant = t
2.1.0 :006 > t.admins.count
(2.6ms) SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" AND (userroles.tenant_id = 1) INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (userroles.tenant_id = 1) AND (roles.name = 'admin') [["tenant_id", 1]]
PG::UndefinedTable: ERROR: invalid reference to FROM-clause entry for table "userroles"
LINE 1: ...erroles_users_join"."user_id" = "users"."id" AND (userroles....
^
HINT: Perhaps you meant to reference the table alias "userroles_users_join".
: SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" AND (userroles.tenant_id = 1) INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (userroles.tenant_id = 1) AND (roles.name = 'admin')
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: invalid reference to FROM-clause entry for table "userroles"
Problem is when I set current_tenant. All works right before setting it. What could be the problem? In gem's code I can't find anything strange.
I change has_many condition:
has_many :admins, -> { for_tenanted_roles.where("roles.name = 'admin'").uniq }, through: :userroles, class_name: 'User', source: :user
And in user.rb
scope :for_tenanted_roles, -> { joins('INNER JOIN "roles" ON "roles"."id" = "userroles"."role_id" AND "roles"."deleted_at" IS NULL') }
It's just handy overrided joins(:roles)

How to use ActiveRecord's INCLUDES when looping over an Object

I have the following:
#rooms = current_user.rooms
Then I need to build a JSON object so I do:
render :json => room_json_index(#rooms)
Then to build the JSON object:
def room_json_index(rooms)
#roomList = Array.new
rooms.each do |room|
#roomList << {
:id => room.id,
:user_id => room.user_id,
:user_count => room_members.length,
:user_photos => room_members.collect { |room_member|
{
:id => room_member.user.id,
:photo => room_member.user.photo(:thumb),
:name => room_member.user.full_name
}
}
}
end
#roomList.to_json
end
Problem here is that in every loop of rooms.each, rails keeps hitting the data for the same user objects. Is that necessary. I see in the logs that it is caching but I'd rather rails not even have to thinking about it.
roomMember Load (1.1ms) SELECT "room_members".* FROM "room_members" INNER JOIN "users" ON "users"."id" = "room_members"."user_id" WHERE ("room_members".room_id = 143) AND (users.guest = false OR users.guest = true AND users.fname IS NOT NULL OR users.id = 3)
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 3 LIMIT 1
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 69 LIMIT 1
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = 70 LIMIT 1
Room
Rails in the logs is repeating the request above over and over, looking for the same records over and over. Any ideas on how this can be optimized?
Thanks
seems your data model is like
class User << AR::Base
has_many :room_members
has_many :rooms,:through=>:room_members,:include=>:users
......
end
class Room << AR::Base
has_many :room_members
has_many :users,:through=>:room_members
......
end
class RoomMember << AR::Base
belongs_to :room
belongs_to :user
......
end
you can load user when load room
class User << AR::Base
has_many :room_members
has_many :rooms,:through=>:room_members,:include=>:users
......
end

Resources