根据最近的子元素的属性选择父级

时间:2011-09-11 00:07:00

标签: mysql ruby-on-rails activerecord greatest-n-per-group

我正在编写一个简单的会员申请表。我有两个模型MemberMembership

class Member < ActiveRecord::Base
   has_many :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :member
end

member包含姓名和出生日期等信息,membership包含申请会员资格的日期,开始日期,到期日期等。一旦会员资格到期,会员可以续订,从而创建会员资格。这意味着每个成员可以拥有多个成员资格,尽管只有最新成员才是他们当前有效的成员资格。

我现在想要检索其成员资格已过期的所有成员。我不能做像

这样的事情
@members = Member.joins(:memberships).where('memberships.expires < ?', Time.now) 

因为这将包括过去拥有会员资格的所有现有会员。我真正需要做的是能够加入最近的成员会员资格并将查询作为基础。我是Rails的新手,虽然对此有点挣扎 - 非常感谢帮助或想法。

编辑:显然这是在普通的旧SQL中不难做到的事情,但我希望有一些不错的Rails方法(除了粘贴SQL之外)在)。

我还不想在其中一个表中添加另一个列,只是为了使我想要的查询成为可能。它很乱,我不应该这样做,并且很可能会引起问题。

5 个答案:

答案 0 :(得分:2)

让我们从逻辑上思考它。

  

最近成员资格已过期的所有成员

实际上与

相同
  

所有没有有效会员资格的会员

在后一种情况下,您可以在SQL中执行此操作

SELECT * FROM members
WHERE NOT EXISTS (
  SELECT * FROM memberships
  WHERE members.id = memberships.member_id
  AND   memberships.expires > #{Time.now}
)

您可以使用Active Record实现相同的目标

Member.where(["NOT EXISTS (
  SELECT * FROM memberships
  WHERE members.id = memberships.member_id
  AND   memberships.expires > ?
)", Time.now])

现在非常讨厌, 但这正是你所要求的。

答案 1 :(得分:2)

方法1-组MAX

使用JOIN可能会获得更好的结果。

Member.joins("JOIN (
     SELECT a.member_id, MAX(a.expires) expires_at
     FROM   memberships a
     GROUP BY a.member_id
     WHERE a.paid = 1 AND  a.expires IS NOT NULL
   ) b ON b.member_id = members.id
  ").
  where("b.expires < ?", Time.now) 

方法2 - LEFT JOIN

Member.joins(
  "
    JOIN
    ( 
      SELECT m1.membership_id
      FROM   membership m1
      LEFT OUTER JOIN membership m2 
             ON m2.membership_id = m1.membership_id AND
                m2.expires IS NOT NULL AND
                m2.expires < m1.expires 
      WHERE  m2.expires IS NULL AND 
             m1.expires < #{sanitize(Time.now)}
    ) m ON m.membership_id = members.id
 "
)

方法3 - 非规范化

更好的解决方案是在is_current表上添加一个名为memberships的标记,并将默认值设置为true

class Membership

  after_create :reset_membership

  # set the old current to false    
  def reset_membership
    Membership.update_all({:is_current => false}, 
      ["member_id = ? AND id != ?", member_id, id])
  end
end

class Member
  has_many :memberships
  scope :recently_expired, lambda {
   { 
     :joins      => :memberships, 
     :conditions => [ "memberships.is_current = ? AND memberships.expires < ?", 
                       true, Time.now]
   }
  }
end

现在,您可以将最近过期的成员视为:

Member.recently_expired

答案 2 :(得分:1)

如果您在会员模型中添加了past_expired列,并在会员添加新会员资格时变为true,那么您可以轻松获得最后一个会员资格。

答案 3 :(得分:1)

感谢您的所有建议。我投了他们而不是接受他们,因为我觉得他们并没有完全覆盖我想要的东西,但我要做的事情肯定会受益于他们。

我仍然觉得应该尽可能避免使用SQL(由于我在评论中已经提到的所有原因),但我认为在这种情况下它不能,所以我决定为它定义一个命名范围会员模型如下:

编辑:我最初将此定义为默认范围 - 但是我已经决定在复杂的默认范围内注意KandadaBoggu的警告,因此将其作为命名范围。当存在当前活动的成员资格时,查询也比其他描述的更复杂,以应对排除更新的成员资格(开始日期在将来)。再次感谢KandadaBoggu查询的骨骼,并提示避免N + 1选择。

scope :with_membership, lambda { 
select('members.*, m.applied AS applied, m.paid AS paid, m.start AS start, m.expiry as expiry').
joins("INNER JOIN (
  SELECT m3.*
  FROM memberships m3
  LEFT OUTER JOIN memberships m5 
  ON m3.member_id = m5.member_id 
  AND m5.created_at > m3.created_at
  AND m5.expiry > #{sanitize(Time.now)}
  WHERE m3.expiry > #{sanitize(Time.now)}
  AND m5.id IS NULL
  UNION
  SELECT m1.*
  FROM   memberships m1
  LEFT OUTER JOIN memberships m2 
  ON m1.member_id = m2.member_id 
  AND m2.created_at > m1.created_at 
  LEFT OUTER JOIN memberships m4 
  ON m1.member_id = m4.member_id 
  AND m4.expiry > #{sanitize(Time.now)}
  WHERE  m2.id IS NULL AND m4.id IS NULL
  ) m 
  on (m.member_id = members.id)") }         

我见过更漂亮的代码。但我的理由是 - 如果你不得不像这样拥有一些可怕的SQL,你可能只在一个地方拥有它,而不是为你可能需要的每个不同的查询重复遍地(比如过期)会员,尚未付款的会员等)。

有了上述范围,我可以将额外的成员资格列视为Member上的普通列,并编写如此简单的简单查询:

#expired
Member.with_membership.find :all, :conditions => ['expires < ?', Time.now ]

#current
Member.with_membership.find :all, :conditions => ['started < ? AND expires > ?', Time.now, Time.now ]

#pending payment
Member.with_membership.find :all, :conditions => ['applied < ? AND paid IS NULL', Time.now ]

为了简单地接受我自己的答案,而不是其他一个非常有用的答案,我想指出,这绝不是一个关于如何获得'每组最大n'的问题(尽管这是一个组成部分)它)。它是关于如何在Rails的特定环境中最好地处理这种查询,具有多个成员资格的成员的特定问题,其中一次只有一个是活动的,并且有许多类似的查询都需要来自这个单一的活跃会员。

这就是为什么我认为以这种方式使用命名范围最终是问题的最佳答案。忍受可怕的SQL查询,但只能在一个地方使用它。

答案 4 :(得分:1)

另一个解决此[最大n组]类型问题的SQL查询如下。它使用复杂条件连接membersmemberships表,该条件将连接限制为具有最新到期日期的一行成员资格:

SELECT members.*
     , m.applied AS applied
     , m.paid    AS paid
     , m.started AS started
     , m.expires AS expires
FROM 
    members
  INNER JOIN 
    memberships AS m
      ON m.id = ( SELECT m1.id
                  FROM memberships m1 
                  WHERE m1.member_id = members.id
                  ORDER BY m1.expires DESC
                  LIMIT 1
                )