我正在编写一个简单的会员申请表。我有两个模型Member
和Membership
。
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之外)在)。我还不想在其中一个表中添加另一个列,只是为了使我想要的查询成为可能。它很乱,我不应该这样做,并且很可能会引起问题。
答案 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查询如下。它使用复杂条件连接members
和memberships
表,该条件将连接限制为具有最新到期日期的一行成员资格:
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
)