2011-09-11 124 views
1

我在寫一個簡單的會員申請。我有兩個型號,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之外)。

我也寧願不添加另一列到任何一個表,只是爲了讓我想做的查詢成爲可能。這很麻煩,我不應該這樣做,並且很可能會導致問題。

+0

檢查這個答案:HTTP://計算器。com/questions/1844037/activerecord-nested-select-can-i-do-it-without-manual-sql –

+0

感謝您的鏈接。正如我所說,在SQL中這樣做不是問題。我希望能夠以類似Rails的方式實現它,但仍然可以獲得將SQL抽象出來的好處。從各種答案開始看起來這是不可能的。 – Russell

+0

抽象很好。這種情況下的問題是ActiveRecord(以及其他類似的抽象層)似乎只支持非常有限的SQL功能子集。 –

回答

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 ] 

要簡要證明接受我自己的答案,而不是一個另一方面,非常有用的答案,我想指出,這不是一個關於如何獲得'最大的每個組'的問題(儘管這是它的一個組成部分)。這是關於如何在Rails的特定環境中最好地處理這種查詢,以及具有多個成員的成員的具體問題,其中只有一個成員在任何時候都處於活動狀態,並且有許多類似的查詢都需要來自這個活躍的會員資格。

這就是爲什麼我認爲以這種方式使用一個命名範圍,最終,最好的回答這個問題。忍受可怕的SQL查詢,但只能在一個地方。

+1

'default_scope'應該是最簡單的,它最多可以包含'order'或者可以是一些狀態標誌的過濾器(例如:默認情況下你想過濾刪除的行)。當你將'連接'和'選擇'子句添加到'default_scope'時,你是在尋找麻煩。當你試圖調試某個問題時(或者你的團隊成員)會忘記在'Member'類中有一個默認的作用域,幾個月後就沒有了。至少這就是我的團隊發生的事情。 –

+1

'default_scope'中使用的查詢具有相關的子查詢。服務器將運行到N + 1查詢問題。 –

+0

感謝關於保持'default_scope'簡單的警告 - 我可以看到它是如何導致問題的。但是,我仍然認爲在這種情況下,使用它會獲得很多簡單性(與在整個代碼中重複相同的查詢相比)。我也認爲會員的會員日期是這種系統的基本部分,因此期望開發人員理解並記住這種情況是合理的。 – Russell

2

讓我們從邏輯上思考它。

,最新的成員資格已過期

實際上是一樣的

誰沒有積極成員

在你可以在後一種情況下所有成員所有成員在SQL中這樣做就像這樣

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

可以達到同樣與活動記錄

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

現在是相當討厭, 但是這是你問什麼了。

+0

請注意,我不打算討論子查詢的優點。這取決於你的數據庫但是對於簡單的使用情況來說,它的意圖很明顯,而且工作正常。 –

+0

這很骯髒,你說得對。我想我希望有一些很好,乾淨,Railsy的方式來做到這一點。 – Russell

+0

我會用另一種方式來看待它。使用AASM,併爲會員設置「過期」狀態。然後找到所有具有過期日期的「活動」會員,並加入他們的會員。 –

1

如果您在成員身份模型中添加了past_expired列,並且在成員添加新成員時成爲true,那麼您可以輕鬆獲得最後的成員資格。

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 - 非規範化

更好的解決方案是在memberships表添加一個名爲is_current標誌並設置默認值,以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 
+0

我不確定我喜歡添加「is_current」標誌的想法。這樣可以有效地對錶格進行非規範化處理,只要有更新失敗或引入了一個錯誤,導致標誌設置停止,它們就會失去同步,導致數據不良,從而導致各種問題,並且需要手動清理。 – Russell

+0

表更新在一個事務中完成,因此不存在幻影記錄的問題。規範化表格後,非規範化是一種有效的技術。像Observer這樣的Rails函數通常用於更新從屬表。 –

+0

我對反標準化本身沒有任何反應 - 當它得到適當的應用 - 我只是認爲這不是必要的。是的,交易將在您描述的情況下防止出現問題,但簡單的事實是,如果您在兩個不同的地方擁有相同的數據,他們將*失去同步。不知何故他們總是這樣做。一個地方的數據永遠不會這樣做。所以我在必要時使用反規範化,以及數據不同步的時候合理安全。 – Russell

1

另一個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 
       ) 
+0

不幸的是,這不會在MySQL中工作(這是我的第一次嘗試),因爲您似乎無法像子查詢中的父查詢中引用表一樣,因爲您在此處使用WHERE m1.member_id = members.id'。來自甲骨文這感覺相當嚴格。 – Russell

+0

@Russell:這個**可以在MySQL中工作。你使用什麼版本的MySQL? –

+0

不行的是IN(SELECT ... LIMIT 1)'。也許你嘗試過這條路。 –