Выберите родителя на основе атрибута самого последнего дочернего элемента - PullRequest
1 голос
/ 11 сентября 2011

Я пишу простую заявку на членство.У меня есть две модели, Member и Membership.

class Member < ActiveRecord::Base
   has_many :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :member
end

A member содержит такую ​​информацию, как имя и дата рождения, membership содержит дату, на которую было подано заявление, дату его начала, дату истечения срока действия и т. Д. По истечении срока действия членстваПользователь может продлить его, создав новое членство.Это означает, что каждый участник может иметь несколько членств, хотя их действительным действительным членством может быть только самое последнее.

Теперь я хочу получить, например, всех участников, срок действия которых истек.Я не могу просто сделать что-то вроде

@members = Member.joins(:memberships).where('memberships.expires < ?', Time.now) 

, поскольку это будет включать в себя любых текущих членов, которые имели членство в прошлом.Что мне действительно нужно сделать, так это иметь возможность присоединиться к only самым последним членствам членов и основывать запрос на этом.Я новичок в Rails, хотя и немного борюсь с этим - помощь или идеи очень ценятся.

РЕДАКТИРОВАТЬ: Очевидно, что такого рода вещи нетрудно сделать в простом старом SQL, но я надеялсябыл бы хороший способ сделать это с помощью Rails (кроме просто вставки SQL в).

Я бы также не стал добавлять другой столбец в одну из таблиц, чтобы сделать запросы, которые я хочу сделать, возможными.,Это грязно, мне не нужно это делать, и, скорее всего, это вызовет проблемы в будущем.

Ответы [ 5 ]

2 голосов
/ 11 сентября 2011

Подход 1 - группа МАКС.

Вы можете получить лучшие результаты, используя 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 - СЛЕДУЮЩЕЕ СОЕДИНЕНИЕ

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 голосов
/ 11 сентября 2011

Давайте подумаем об этом логически.

все участники, чье последнее членство истекло

Фактически совпадает с

всеучастники, которые не имеют активного членства

В последнем случае вы можете сделать это в 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 голос
/ 11 сентября 2011

Другой запрос SQL, который решает эту проблему типа [great-n-per-group], будет следующим.Он объединяет таблицы 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
                )
1 голос
/ 11 сентября 2011

Спасибо за все ваши предложения. Я проголосовал за них, а не принял их, потому что я не чувствую, что они вполне охватывают то, что я хочу, но то, что я собираюсь сделать, безусловно, принесло им пользу.

Я все еще чувствую, что по возможности следует избегать 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-запросом, но держи его только в одном месте.

1 голос
/ 11 сентября 2011

Если вы добавляете столбец past_expired в модели членства и становится истинным, когда участник добавляет новое членство, вы можете легко получить последнее членство.

...