【问题标题】:How to override table column value with column value from second table if it exist?如果存在,如何用第二个表中的列值覆盖表列值?
【发布时间】:2019-06-18 21:33:29
【问题描述】:

我正在构建电子商务应用程序,您可以在其中为产品设置用户特定的价格。如果没有为特定用户设置特定产品的价格,它将显示默认产品价格。

它工作正常,但我正在寻找更有效的解决方案,因为我对当前的解决方案不满意。

表格:

  • 用户
  • 产品(所有产品信息 + 常规价格)
  • 价格(user_id、product_id、user_price)

型号:

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

如何在控制器中获取所有具有用户特定价格的产品:

@products = Product.with_user_prices(current_user)

我如何在视图中显示它们:

<% @products.each do |product| %>

  <%= product.user_price ? product.user_price : product.regular_price %>

<% end %>

如您所见,我目前正在加入价格表,然后在视图中显示 user_price(价格表)(如果存在),否则显示常规价格(产品表)。

我很想在一个查询中解决所有问题,根据 current_user 只保留一个具有适当值的价格列

【问题讨论】:

  • 你的数据库引擎是什么?
  • @MarekLipka 目前我正在使用开发 SQLite3 数据库
  • 那我的回答应该是有效的。

标签: ruby-on-rails


【解决方案1】:

你可以使用SQL COALESCE函数:

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

然后你可以简单地使用它:

<%= product.price %>

请注意,我通过使用includes 稍微简化了Product.with_user_prices 方法,因为prices 上有条件,这将生成SQL LEFT JOIN 查询。

【讨论】:

  • 如果用户没有设置任何自定义价格,它将返回 nil,否则:未定义方法 `price' for #<0x000000000d600588>
【解决方案2】:

新答案:

请不要将此答案标记为正确,因为我基本上只是将 Marek 的代码和您的代码扩展到我的代码中,因为经过几次尝试后,我还是得出了您已经完成的内容,但我只是把它放在这里如果它可以帮助任何人:

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

控制者:

@products = Product.with_user_prices(current_user)

查看:

<% @products.each do |product| %>
  <%= product.price_for_user %>
<% end %>

旧答案(低效代码):

未经测试,但您可以尝试以下方法吗? (不确定这是否比您的方法效率更高或更低)

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

控制者:

# will perform LEFT OUTER JOIN (to eager load both `prices` and `prices -> user`) preventing N+1 queries
@products = Product.eager_load(prices: :user)

查看:

<% @products.each do |product| %>
  <%= product.price_for_user(current_user) %>
<% end %>

【讨论】:

  • 技术上,Product.includes(prices: :user) 默认情况下不会生成left join,而是生成三个单独的数据库查询。此外,末尾的all 是不必要的。此外,Product#price_for_user 也不起作用,因为在 Product 实例上没有定义方法 includes
  • @MarekLipka 嗨,是的。谢谢!你是绝对正确的。我现在正在更新它以改用eager_load
  • 另外,你正在为每个Product 执行单独的数据库查询:prices.includes(:user).where (...),所以这里有 N+1 个问题。
  • @MarekLipka ^ 我还不确定它是否会为每个产品执行 SQL,因为我急于加载它。但是,正如你所说,我可能错了。我将在本地 Rails 应用程序上试用它,并相应地进行调整。谢谢:)
  • 谢谢!我感谢你的努力。它工作正常。在我想要的方向上绝对是对我的解决方案的一个很好的升级。我相信@MarekLipka 会完善它:)
猜你喜欢
  • 2020-04-08
  • 1970-01-01
  • 1970-01-01
  • 2014-07-13
  • 1970-01-01
  • 2019-09-27
  • 2022-10-03
  • 2020-06-27
  • 2020-10-28
相关资源
最近更新 更多