Rails has_and_belongs_to_many找到共同的唯一对象

时间:2013-08-10 15:12:38

标签: sql ruby-on-rails postgresql relational-division

我有两个模型,Conversation和Phones,两个模型彼此都有_and_belongs_to_many。电话可以有很多对话,对话可以有很多电话(两个或更多)。

class Conversation < ActiveRecord::Base
  has_and_belongs_to_many :phones
end

class Phone < ActiveRecord::Base
  has_and_belongs_to_many :conversations
end

当然,还有一个conversations_phones连接表。

如果我有两个或更多电话对象,我如何找到他们共享的所有对话的列表?问题:对话不能包括任何其他电话(IE电话号码的数量等于我们搜索的号码)。

我已经能够使用纯Rails做到这一点,但它涉及循环每个对话并依靠数据库。不好。

我不介意做纯SQL;使用模型ID应该有助于阻止注入攻击。

我最接近的是:

SELECT conversations.* FROM conversations 
INNER JOIN conversations_phones AS t0_r0 ON conversations.id = t0_r0.conversation_id 
INNER JOIN conversations_phones AS t0_r1 ON conversations.id = t0_r1.conversation_id 
WHERE (t0_r0.phone_id = ? AND t0_r1.phone_id = ?), @phone_from.id, @phone_to.id

但它包括与外部手机的对话。我有一种感觉GROUP BY和HAVING COUNT会有所帮助,我对SQL太新了。

1 个答案:

答案 0 :(得分:2)

我想你差不多了。只需使用额外的NOT EXISTS反半连接来排除与外人的对话:

SELECT c.*
FROM   conversations c
JOIN   conversations_phones AS cp1 ON cp1.conversation_id = c.id
                                  AND cp1.phone_id = ?
JOIN   conversations_phones AS cp2 ON cp2.conversation_id = c.id
                                  AND cp2.phone_id = ?
...
WHERE NOT EXISTS (
   SELECT 1
   FROM   conversations_phones cp
   WHERE  cp.conversation_id = c.id
   AND    cp.phone_id NOT IN (cp1.phone_id, cp2.phone_id, ...) -- or repeat param
   )
, @phone1.id, @phone2.id, ...

为简单起见,我将条件引入JOIN子句,不会更改查询计划 不用说conversations(id)conversations_phones(conversation_id, phone_id)上需要索引

替代品(慢得多):

很简单,但很慢:

SELECT cp.conversation_id
FROM  (
   SELECT conversation_id, phone_id
   FROM   conversations_phones
   ORDER  BY 1,2
   ) cp
GROUP  BY 1
HAVING array_agg(phone_id) = ?

..其中?是像'{559,12801}'::int[]

这样的有序数组

在快速测试中慢了30倍

为了完整性,(简化)提议的alternative by @BroiSatse in the comments在类似的快速测试中执行 20x

...
JOIN (
   SELECT conversation_id, COUNT(*) AS phone_count
   FROM   conversations_phones
   GROUP  BY prod_id
   ) AS pc ON pc.conversation_id = c.id AND phone_count = 2

或者,更简单,更快:

...
JOIN (
   SELECT conversation_id
   FROM   conversations_phones
   GROUP  BY prod_id
   HAVING COUNT(*) = 2
   ) AS pc ON pc.conversation_id = c.id