Союз запитів ActiveRecord


90

Я написав кілька складних запитів (принаймні для мене) за допомогою інтерфейсу запитів Ruby on Rail:

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})

Обидва ці запити працюють самі по собі. Обидва повертають об'єкти Post. Я хотів би об’єднати ці повідомлення в єдину ActiveRelation. Оскільки в якийсь момент може бути сотні тисяч повідомлень, це потрібно робити на рівні бази даних. Якби це був запит MySQL, я міг би просто користуватися UNIONоператором. Хтось знає, чи можу я зробити щось подібне з інтерфейсом запитів RoR?


Ви повинні мати можливість використовувати область . Створіть 2 сфери, а потім назвіть їх обома лайками Post.watched_news_posts.watched_topic_posts. Можливо, вам доведеться надіслати параметри до областей для таких речей, як :user_idі :topic.
— Zabba

6
Дякую за пропозицію. Згідно з документами, "Обсяг представляє звуження запиту до бази даних". У моєму випадку я не шукаю публікацій, які містяться як у поглядах_нових_постів, так і у поглядах_тег_постів. Швидше, я шукаю публікації, які містяться у поглядах_нових_постів або поглядах_теп_постів, без дублікатів. Чи можливо це ще зробити за допомогою сфери застосування?
— LandonSchropp

1
Насправді це неможливо нестандартно. Існує плагін на github, який називається union, але він використовує синтаксис старої школи (метод класу та параметри запитів у стилі хеш), якщо це круто з вами, я б сказав, що йдіть з ним ... інакше випишіть це довгий шлях у find_by_sql у вашій області.
— jenjenut233

1
Я погоджуюсь з jenjenut233, і я думаю, ви могли б зробити щось подібне find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}"). Я не тестував цього, тому дайте мені знати, як це відбувається, якщо ви спробуєте. Крім того, можливо, є якась функціональність ARel, яка б працювала.
— Wizard of Ogz

2
Ну, я переписав запити як запити SQL. Вони працюють зараз, але, на жаль, find_by_sqlне можуть бути використані з іншими запрошуваними запитами, а це означає, що мені тепер доведеться також переписати свої фільтри та запити. Чому ActiveRecord не підтримує unionоперацію?
— LandonSchropp

Відповіді:


93

Ось короткий маленький модуль, який я написав, що дозволяє об’єднати кілька областей. Він також повертає результати як екземпляр ActiveRecord :: Relation.

module ActiveRecord::UnionScope
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    def union_scope(*scopes)
      id_column = "#{table_name}.id"
      sub_query = scopes.map { |s| s.select(id_column).to_sql }.join(" UNION ")
      where "#{id_column} IN (#{sub_query})"
    end
  end
end

Ось суть: https://gist.github.com/tlowrimore/5162327

Редагувати:

За запитом, ось приклад того, як працює UnionScope:

class Property < ActiveRecord::Base
  include ActiveRecord::UnionScope

  # some silly, contrived scopes
  scope :active_nearby,     -> { where(active: true).where('distance <= 25') }
  scope :inactive_distant,  -> { where(active: false).where('distance >= 200') }

  # A union of the aforementioned scopes
  scope :active_near_and_inactive_distant, -> { union_scope(active_nearby, inactive_distant) }
end

2
Це справді шлях більш повної відповіді на інші, перераховані вище. Чудово працює!
— ghayes

Приклад використання було б непогано.
— ciembor

За запитом я додав приклад.
— Тім Лоурімор

3
Рішення "майже" правильне, і я дав йому +1, але я зіткнувся з проблемою, яку я вирішив
— Лоуренс І. Сіден

7
Швидке попередження: цей метод є дуже проблематичним з точки зору продуктивності з MySQL, оскільки підзапит буде зараховуватися як залежний і виконуватися для кожного запису в таблиці (див. Percona.com/blog/2010/10/25/mysql-limitations-part -3-підзапити ).
— шості

71

Я також зіткнувся з цією проблемою, і зараз моєю стратегією є створення SQL (вручну або за to_sqlдопомогою існуючої області), а потім вставити його в fromпункт. Я не можу гарантувати, що він є більш ефективним, ніж ваш прийнятий метод, але це відносно легко для очей і повертає вам звичайний об'єкт ARel.

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})

Post.from("(#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}) AS posts")

Ви можете зробити це з двома різними моделями, але вам потрібно переконатися, що вони обидва "виглядають однаково" всередині UNION - ви можете використовувати selectобидва запити, щоб переконатися, що вони будуть створювати однакові стовпці.

topics = Topic.select('user_id AS author_id, description AS body, created_at')
comments = Comment.select('author_id, body, created_at')

Comment.from("(#{comments.to_sql} UNION #{topics.to_sql}) AS comments")

припустимо, якщо у нас є дві різні моделі, будь ласка, дайте мені знати, яким буде запит на unoin.
— Chitra

Дуже корисна відповідь. Майбутнім читачам запам’ятайте останню частину «Як коментарі», оскільки activerecord конструює запит як «ВИБЕРІТЬ» коментарі ».« * «ВІД« ... якщо ви не вкажете ім'я унізованого набору АБО вкажете інше ім'я, наприклад "AS foo", остаточне виконання sql не вдасться.
— HeyZiko,

1
Це було саме те, що я шукав. Я розширив ActiveRecord :: Relation для підтримки #orу своєму проекті Rails 4. Припускаючи ту саму модель:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
— M. Wyatt

11

Спираючись на відповідь Оліви, я дійсно придумав інше рішення цієї проблеми. Це трохи схоже на хакерство, але воно повертає екземпляр ActiveRelation, що я спочатку й шукав.

Post.where('posts.id IN 
      (
        SELECT post_topic_relationships.post_id FROM post_topic_relationships
          INNER JOIN "watched" ON "watched"."watched_item_id" = "post_topic_relationships"."topic_id" AND "watched"."watched_item_type" = "Topic" WHERE "watched"."user_id" = ?
      )
      OR posts.id IN
      (
        SELECT "posts"."id" FROM "posts" INNER JOIN "news" ON "news"."id" = "posts"."news_id" 
        INNER JOIN "watched" ON "watched"."watched_item_id" = "news"."id" AND "watched"."watched_item_type" = "News" WHERE "watched"."user_id" = ?
      )', id, id)

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


Як я міг зробити те саме з цим: gist.github.com/2241307 Так, щоб він створив клас AR :: Relation, а не клас Array?
— Марк

10

Ви можете також використовувати Brian Hempel «s active_record_union дорогоцінний камінь , який проходить ActiveRecordз unionметодом для областей.

Ваш запит буде таким:

Post.joins(:news => :watched).
  where(:watched => {:user_id => id}).
  union(Post.joins(:post_topic_relationships => {:topic => :watched}
    .where(:watched => {:user_id => id}))

Сподіваємось, це з часом буде об’єднано в ActiveRecordякийсь день.


8

Як на рахунок...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end

15
Зауважте, що це буде виконувати три запити, а не один, оскільки кожен pluckвиклик сам по собі є запитом.
— JacobEvelyn

3
Це дійсно хороше рішення, оскільки воно не повертає масив, тож ви можете використовувати .orderабо .paginateметоди ... Він зберігає класи
— orm

Корисно, якщо сфери застосування однієї моделі, але це призведе до двох запитів через зриви.
— jmjm

6

Чи не могли б ви використати АБО замість СОЮЗУ?

Тоді ви можете зробити щось на зразок:

Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)

(Оскільки ви приєднуєтеся до переглянутої таблиці двічі, я не надто впевнений, якими будуть назви таблиць для запиту)

Оскільки об’єднань багато, це може також бути досить важким для бази даних, але його можна буде оптимізувати.


2
Вибачте, що зв’язався з вами так пізно, але я був у відпустці останні пару днів. Проблема, з якою я зіткнувся, коли спробував отримати відповідь, полягала в тому, що метод об’єднання спричинив об’єднання обох таблиць, а не двох окремих запитів, які потім можна було порівняти. Однак ваша ідея була обґрунтованою і все-таки дала мені ще одну ідею. Дякую за допомогу.
— LandonSchropp

вибрати, використовуючи АБО, повільніше, ніж UNION, цікавлячись будь-яким рішенням для UNION
— Ніч

5

Можливо, це покращує читабельність, але не обов'язково продуктивність:

def my_posts
  Post.where <<-SQL, self.id, self.id
    posts.id IN 
    (SELECT post_topic_relationships.post_id FROM post_topic_relationships
    INNER JOIN watched ON watched.watched_item_id = post_topic_relationships.topic_id 
    AND watched.watched_item_type = "Topic" 
    AND watched.user_id = ?
    UNION
    SELECT posts.id FROM posts 
    INNER JOIN news ON news.id = posts.news_id 
    INNER JOIN watched ON watched.watched_item_id = news.id 
    AND watched.watched_item_type = "News" 
    AND watched.user_id = ?)
  SQL
end

Цей метод повертає ActiveRecord :: Relation, тому ви можете назвати його так:

my_posts.order("watched_item_type, post.id DESC")

звідки ви отримуєте posts.id?
— berto77

Є два параметри self.id, оскільки на self.id посилається двічі в SQL - див. Два знаки запитання.
— richardsun

Це був корисний приклад того, як зробити запит UNION і повернути ActiveRecord :: Relation. Дякую.
— Монтажер

чи є у вас інструмент для генерування цих типів запитів SDL - як ви це зробили без орфографічних помилок тощо?
— BKSpurgeon

2

Існує камінь active_record_union. Може бути корисним

https://github.com/brianhempel/active_record_union

За допомогою ActiveRecordUnion ми можемо:

поточні повідомлення користувача (чернетка) та всі опубліковані повідомлення від кого-небудь, current_user.posts.union(Post.published) що еквівалентно наступному SQL:

SELECT "posts".* FROM (
  SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" = 1
  UNION
  SELECT "posts".* FROM "posts"  WHERE (published_at < '2014-07-19 16:04:21.918366')
) posts

1

Я б просто виконав два необхідні запити та об’єднав масиви записів, які повертаються:

@posts = watched_news_posts + watched_topics_posts

Або, принаймні, протестуйте це. Думаєте, комбінація масивів у рубіні буде занадто повільною? Переглядаючи запропоновані запити, щоб обійти проблему, я не впевнений, що буде така суттєва різниця в продуктивності.


Насправді робити @ posts = спостерігали_нові_пості та спостерігали_теми_постів могли б бути краще, оскільки це перехрестя і уникне обману.
— Джеффрі Алан Лі,

1
У мене склалося враження, що ActiveRelation ліниво завантажує свої записи. Чи не втратите ви цього, якби перетинали масиви в Ruby?
— LandonSchropp

Очевидно, союз, який повертає відношення, знаходиться під розробкою в рейках, але я не знаю, в якій версії він буде.
— Джеффрі Алан Лі,

1
цей масив повернення замість цього, два різні результати запиту зливаються.
— alexzg

1

У подібному випадку я підсумував два масиви та використав Kaminari:paginate_array(). Дуже приємне та робоче рішення. Я не зміг використати where(), оскільки мені потрібно підсумувати два результати з різними order()в одній таблиці.


1

Менше проблем і легше виконувати:

    def union_scope(*scopes)
      scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) }
    end

Отже, врешті-решт:

union_scope(watched_news_posts, watched_topic_posts)

1
Я трохи змінив його на: scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
— eikes

0

Елліот Нельсон відповів добре, за винятком випадку, коли деякі стосунки порожні. Я б зробив щось подібне:

def union_2_relations(relation1,relation2)
sql = ""
if relation1.any? && relation2.any?
  sql = "(#{relation1.to_sql}) UNION (#{relation2.to_sql}) as #{relation1.klass.table_name}"
elsif relation1.any?
  sql = relation1.to_sql
elsif relation2.any?
  sql = relation2.to_sql
end
relation1.klass.from(sql)

кінець


0

Ось як я приєднався до запитів SQL за допомогою UNION на власному додатку ruby ​​on rails.

Ви можете використати наведене нижче як натхнення для власного коду.

class Preference < ApplicationRecord
  scope :for, ->(object) { where(preferenceable: object) }
end

Нижче СОЮЗ, де я приєднався до сфери діяльності.

  def zone_preferences
    zone = Zone.find params[:zone_id]
    zone_sql = Preference.for(zone).to_sql
    region_sql = Preference.for(zone.region).to_sql
    operator_sql = Preference.for(Operator.current).to_sql

    Preference.from("(#{zone_sql} UNION #{region_sql} UNION #{operator_sql}) AS preferences")
  end
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.