Пересечение двух отношений

Скажем, у меня есть два отношения, которые содержат записи в одной и той же модели, например:

@companies1 = Company.where(...)
@companies2 = Company.where(...)

Как мне найти пересечение этих двух отношений, т.е. только те компании, которые существуют внутри обоих?


person sscirrus    schedule 22.06.2011    source источник
comment
Кажется, вам не нужен sql здесь. Таким образом, вы можете использовать метод пересечения массивов: ruby-doc.org/core /classes/Array.html#M000274   -  person apneadiving    schedule 22.06.2011


Ответы (5)


По умолчанию соединение этих where вместе создает И, что вы и хотите.

Так много быть:

class Company < ActiveRecord::Base
  def self.where_1
    where(...)
  end
  def self.where_2
    where(...)
  end
end

@companies = Company.where_1.where_2

====== ОБНОВЛЕНО ======

Есть два случая:

# case 1: the fields selecting are different
Company.where(:id => [1, 2, 3, 4]) & Company.where(:other_field => true)
# a-rel supports &, |, +, -, but please notice case 2

# case 2
Company.where(:id => [1, 2, 3]) & Company.where(:id => [1, 2, 4, 5])

# the result would be the same as
Company.where(:id => [1, 2, 4, 5])
# because it is &-ing the :id key, instead of the content inside :id key

Так что, если вы находитесь в случае 2, вам нужно будет сделать то, что прокомментировал @apneadiving.

Company.where(...).all & Company.where(...).all

Конечно, при этом отправляются два запроса и, скорее всего, запрашивается больше результатов, чем вам нужно.

person PeterWong    schedule 22.06.2011
comment
Питер, предположим, что эти отношения достигаются различными способами, которые не включают оба where. Есть ли ручной способ расчета этого пересечения? - person sscirrus; 22.06.2011
comment
@sscirrus Обновлен ответ, пожалуйста, посмотрите. - person PeterWong; 22.06.2011
comment
Разве это не возвращает массив? Как я могу заставить его возвращать ресурс ActiveRecord? - person Mohamed El Mahallawy; 20.06.2014
comment
Я только что попробовал это с символом &, и это сработало для меня. - person adg; 21.07.2016

Я решаю подобную проблему так

Company.connection.unprepared_statement do
  Company.find_by_sql "#{@companies1.to_sql} INTERSECT #{@companies2.to_sql}"
end

Здесь нам нужен блок unprepared_statement, потому что последние версии Rails используют подготовленные операторы для ускорения запросов arel, но нам нужен чистый SQL.

person mikdiet    schedule 28.08.2013
comment
Это так мило. Это может быть реализовано с помощью string::join для пересечения набора операторов sql. - person jgomo3; 15.03.2020

Используйте ключевое слово sql INTERSECT.

params1 = [1,2,4]
params2 = [1,3,4]
query = "
SELECT companies.* FROM companies
WHERE id in (?,?,?)
INTERSECT
SELECT companies.* FROM companies
WHERE id in (?,?,?)
"
Company.find_by_sql([query, *params1, *params2])

это будет быстрее, чем предыдущее решение.

person Michael    schedule 02.07.2012
comment
Спасибо за Ваш ответ! Что произойдет, если params1 и params2 будут массивами неопределенной длины? - person sscirrus; 03.07.2012
comment
1. Вы можете изменить условие where, если вы нашли не по идентификаторам о. 2. Что-то вроде: ...ГДЕ id IN (#{params1.map{'?'}.join(',')})... - person Michael; 04.07.2012
comment
Я обнаружил, что #{Array.new(params1.size, '?').join(',')} быстрее. Но это не так важно в данном случае. - person Michael; 06.07.2012

Вы можете использовать ActiveRecord::SpawnMethods#merge

Пример:

Company.where(condition: 'value').merge(Company.where(other_condition: 'value'))
person KARASZI István    schedule 07.10.2015
comment
Он просто добавляет условие where. Это не альтернатива INTERSECT - person Atul Yadav; 26.02.2017

Для тех, кто застрял в Rails4 и не может использовать синтаксис Rails5 .or:

У меня было динамическое количество больших запросов с похожими условиями (и, следовательно, с похожими результатами). У моего рейк-сервера были бы проблемы, когда все они одновременно создавались бы, преобразовывались в массивы, а затем объединялись.

Мне нужен был ActiveRecord::Relation (еще не запущенный) для использования с find_each.

Выглядело примерно так:

Class Conditions
  def initialize
    self.where_arr = []
    self.joins_arr = []
  end

  def my_condition1
    where_arr << 'customers.status = "active"'
    joins_arr << :customer
  end

  def my_condition2
    where_arr << 'companies.id = 1'
  end
end

conditions = []    
joins = []
# probably call them in a .each block with .send
conditions1 = Conditions.new
conditions1.my_condition1
conditions1.my_condition2
conditions << "(#{conditions1.where_arr.join(' AND ')})"
joins << conditions1.joins_arr

conditions2 = Conditions.new
conditions2.my_condition1
joins << conditions2.joins_arr

Company.joins(joins).where(conditions.join(' OR '))

=> SELECT companies.* FROM companies 
INNER JOIN customers ON companies.customer_id = customers.id 
WHERE (customers.status = 'active' AND companies.id = 1) OR
(customers.status = 'active')

Это немного хакерски, но это работает, пока вы, надеюсь, не сможете перейти на Rails 5.

person lennart_dev    schedule 17.04.2018