【问题标题】:Joining three tables (2 level) and finding sum based on conditions - SQL Rails连接三个表(2级)并根据条件求和 - SQL Rails
【发布时间】:2013-12-10 12:06:31
【问题描述】:

我有以下架构:

User:
---
ID
---

Tasks:
-------------------
ID | classification
-------------------

Timesheets:
------------------------
ID  |  task_id | user_id
------------------------

TimesheetItem:
--------------------------------
ID | timesheet_id | hours | date
--------------------------------

协会:

class User 
  has_many :timesheets
end

class Task 
  has_many :timesheets
end

class Timesheet 
  has_many :timesheet_items
  belongs_to :user
  belongs_to :task
end

class TimesheetItem
  belongs_to :timesheet
end

分类可以是“计费”或“不可计费”。 现在我需要为每个用户找到可计费和不可计费小时的总和,如下所示:

-----------------------------------
|user_id | billable | nonbillable |
-----------------------------------

我在 Rails 中所做的是:

User.joins(:timesheets, :timesheets => :task, :timesheets => :timesheet_items)
    .select("SUM(CASE 
                   WHEN tasks.task_classification = 'Billable' 
                      THEN timesheet_items.hours 
                   ELSE 
                      0 
                 END) as billable, 
             SUM(CASE 
                   WHEN tasks.task_classification = 'Non-Billable' 
                     THEN timesheet_items.hours 
                   ELSE 0 
                 END) as nonbillable")

但 MySQL 给出错误“tasks.classification”是未知列。看看正在生成的查询,这是可以理解的:

SELECT SUM(CASE WHEN tasks.classification = 'Billable' THEN hours ELSE 0 END) as billable, SUM(CASE WHEN tasks.classification = 'Non-Billable' THEN hours ELSE 0 END) as nonbillable FROM `users` INNER JOIN `timesheets` ON `timesheets`.`user_id` = `users`.`id` INNER JOIN `timesheet_items` ON `timesheet_items`.`timesheet_id` = `timesheets`.`id`

如您所见,未加入任务表。

我如何实现这一目标?谢谢。

编辑:

我有点继续使用一个简单的 sql 查询,它加入了任务表以获得结果,因为这些数据只在一个地方使用并且很少使用。

但现在我需要按月对小时数进行分组,并找出每个用户记录计费和非计费小时的小时数。例如。

结果:

-----------------------------------------------
user_id | month | BillableHrs | NonBillableHRS|
-----------------------------------------------

我试过group(user_id, MONTH(date)) 但是.. 结果很奇怪。我怎样才能获得这种信息?

顺便说一句,将连接更改为:

joins(:timesheets, :timesheets => [:task, :timesheet_items])

解决了未找到列的问题:)

我终于找到了这个解决方案。任何优化的想法?

SELECT 
  users.id as user_id,
  users.name as user_name,
  CONCAT(MONTHNAME(date)," ",YEAR(date)) as month,
  SUM( CASE 
        WHEN tasks.task_classification = "Billable" 
          THEN hours 
        ELSE 
          0 
        END ) as blb_sum, 
  SUM( CASE 
        WHEN tasks.task_classification = "Non-Billable"
          THEN hours 
        ELSE 
          0 
        END ) as nblb_sum
FROM `users` 
  INNER JOIN `timesheets` ON `timesheets`.`user_id` = `users`.`id` 
  INNER JOIN `timesheet_items` ON `timesheet_items`.`timesheet_id` = `timesheets`.`id` 
  INNER JOIN `tasks` ON `timesheets`.`task_id` = `tasks`.`id` 
WHERE 
  timesheet_items.date >= '2013-11-1' AND
  timesheet_items.date <= '2013-11-31'

【问题讨论】:

  • 这是一个丑陋的 activerecord 和 SQL 组合——我只是将它作为 SQL 语句运行。
  • 哈哈。是啊,你说得对。我正在考虑这样做。但是AR中有什么方法可以做案例吗?顺便说一句,我将为此使用普通 SQL。
  • 嗯,我不知道。不过,我会提出另一个建议作为答案,只是看看人们对此有何看法。
  • 感谢你们的解决方案!!你能帮我找到如何按月对总和进行分组吗?谢谢!

标签: mysql ruby-on-rails join


【解决方案1】:

由于如果将其表示为 activerecord 和 SQL 的混合,这是一些非常丑陋的代码,因此一种想法是将复杂查询定义为视图并将其引用为模型。

这很简单——如果这是一组用户级别的指标,那么按照以下方式构建一个视图:

create view user_billing_metrics
as
select  user.id user_id,
        sum(case ... blah blah) billable_hours,
        sum(case ... blah blah) unbillable_hours
from    ...

然后创建一个只读模型...

class UserBillingMetric < ActiveRecord::Base

  belongs_to :user, :inverse_of => :user_billing_metric

  def read_only?
    true
  end

end

然后……

class User < ActiveRecord::Base

  has_one :user_billing_metric, :inverse_of => :user

  delegate :billable_hours  , :to => :user_billing_metric
  delegate :unbillable_hours, :to => :user_billing_metric

  def read_only?
    true
  end
end

然后你可以:

u = User.find( ...)

u.billable_hours

...或...

u = User.find( ...)
hours= u.user_billing_metric

等等

这里可能是一个愚蠢的错字。

这样做的一个很好的功能是您可以,例如:

users_to_fire = User.joins(:user_billing_metric).
                     where(:user_billing_metrics => {:billable_hours = 0})

再一次,可能是愚蠢的错字。

将其推送到数据库非常有效,比尝试通过 Rails 运行要高效得多。一个体面的查询优化器不会评估视图中不需要的表达式,甚至不会执行逻辑上冗余的连接。

无论如何,只是将这个浮出水面以供 cmets 使用。我知道将业务逻辑放在数据库层并不符合每个人的口味,但它会让它保持干燥,而且我在自己的应用程序中也有一些情况,出于性能原因,它绝对是唯一的选择。

【讨论】:

  • 哇。这个解决方案让我有点震惊。
【解决方案2】:

我会这样做:

#id
class User 
  has_many :timesheets
  has_many :tasks, :through => :timesheets

  def billable_hours
    self.tasks.billable.collect{|task| task.timesheet_items_for(self)}.flatten.collect(&:hours).sum
  end

  def non_billable_hours
    self.tasks.non_billable.collect{|task| task.timesheet_items_for(self)}.flatten.collect(&:hours).sum
  end  
end

#id, classification
class Task 
  has_many :timesheets

  def timesheet_items_for(user)
    self.timesheets.for_user(user).collect(&:timesheet_items).flatten
  end

  named_scope :billable, :conditions => ["classification = ?", "Billable"]
  named_scope :non_billable, :conditions => ["classification = ?", "Non-Billable"]
end

#id, task_id, user_id
class Timesheet 
  has_many :timesheet_items
  belongs_to :user
  belongs_to :task

  named_scope :for_user, lambda {|user| {:conditions => ["user_id = ?", user.id]} }
end

#id, timesheet_id, hours
class TimesheetItem
  belongs_to :timesheet
end

然后在您的控制器中,假设您定义了@user,您可以调用@user.billable_hours@user.non_billable_hours

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-30
    • 1970-01-01
    • 2023-03-06
    • 2019-11-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多