Reputation: 113
I'm building e-commerce application in which you can set user specific prices for products. If price for specific product for specific user is not set, it will show default product price.
It works fine, but I'm looking for more efficient solution since I'm not happy with current one.
Tables:
Models:
class User < ApplicationRecord
has_many :prices
end
class Product < ApplicationRecord
has_many :prices
validates :name, presence: true
def self.with_user_prices(current_user)
Product.joins(
Product.sanitize_sql_array(['LEFT OUTER JOIN prices ON prices.user_id = ?
AND products.id = prices.product_id', current_user])
).select('products.*, prices.user_price')
end
end
class Price < ApplicationRecord
belongs_to :product
belongs_to :user
end
How I get all products with user specific prices in controller:
@products = Product.with_user_prices(current_user)
How I display them in view:
<% @products.each do |product| %>
<%= product.user_price ? product.user_price : product.regular_price %>
<% end %>
As you see I'm currently joining prices table and then in view I display user_price (prices table) if it exists, otherwise regular_price (products table).
I would love to solve everything in a single query keeping only one price column with appropriate value according to the current_user
Upvotes: 2
Views: 247
Reputation: 6603
please do not mark this answer as correct because I just basically extended Marek's code and yours into mine, as after several attempts I came to what you've already done anyway, but I'm just putting it here in case it helps anyone:
app/models/product.rb
class Product < ApplicationRecord
def self.with_user_prices(user)
joins(
sanitize_sql_array([
"LEFT OUTER JOIN prices on prices.product_id = products.id AND prices.user_id = ?", user.id
])
).select(
'products.*',
'COALESCE(prices.user_price, products.regular_price) AS price_for_user'
)
end
end
controller:
@products = Product.with_user_prices(current_user)
view:
<% @products.each do |product| %>
<%= product.price_for_user %>
<% end %>
Untested but can you try the following? (Not sure if this is more or less efficient than your approach)
app/models/product.rb
class Product < ApplicationRecord
has_many :prices
def price_for_user(user)
prices.includes(:user).where(
users: { id: user.id }
).first&.user_price || regular_price
end
end
controller:
# will perform LEFT OUTER JOIN (to eager load both `prices` and `prices -> user`) preventing N+1 queries
@products = Product.eager_load(prices: :user)
view:
<% @products.each do |product| %>
<%= product.price_for_user(current_user) %>
<% end %>
Upvotes: 1
Reputation: 51151
You can make use of SQL COALESCE function:
class Product < ApplicationRecord
# ...
def self.with_user_prices(user)
includes(:prices).where(prices: { user_id: user.id }).select(
'products.*, COALESCE(prices.user_price, products.regular_price) as price'
)
end
end
Then you can use it simply by:
<%= product.price %>
Note that I simplified Product.with_user_prices
method a little bit by using includes
, which is gonna generate SQL LEFT JOIN
query since there's condition on prices
.
Upvotes: 2