Połącz dwa obiekty ActiveRecord :: Relation

174

Załóżmy, że mam następujące dwa obiekty:

first_name_relation = User.where(:first_name => 'Tobias') # ActiveRecord::Relation
last_name_relation  = User.where(:last_name  => 'Fünke') # ActiveRecord::Relation

czy można połączyć te dwie relacje w celu uzyskania jednego ActiveRecord::Relationobiektu zawierającego oba warunki?

Uwaga: zdaję sobie sprawę, że mogę połączyć miejsca, aby uzyskać takie zachowanie, naprawdę interesuje mnie przypadek, w którym mam dwa oddzielne ActiveRecord::Relationobiekty.

Patrick Klingemann
źródło

Odpowiedzi:

202

Jeśli chcesz połączyć używając AND(przecięcie), użyj merge:

first_name_relation.merge(last_name_relation)

Jeśli chcesz połączyć za pomocą OR(union), użyj :or

first_name_relation.or(last_name_relation)

Tylko w ActiveRecord 5+; dla 4.2 zainstaluj gdzie-lub backport.

Andrew Marshall
źródło
a co z tym, że chcę, aby wynik był ActiveRecord::Relationtypem?
Nowa Aleksandria
1
@NewAlexandria Tak, we wszystkich przypadkach, Rails okłamują Cię co do klasy obiektu.
Andrew Marshall
77
Czy istnieje wersja scalania „LUB”?
Arcolye,
8
@minohimself Prawdopodobnie dlatego, że jest to rzeczywisty wynik połączenia twoich dwóch relacji. Zauważ, że mergejest to skrzyżowanie, a nie związek.
Andrew Marshall,
5
W przyszłości #ormetoda została dodana do ActiveRecord :: Relation w styczniu 2015 r. I będzie częścią Rails 5.0 , która zostanie udostępniona pod koniec 2015 r. Pozwala na użycie operatora OR do łączenia klauzul WHERE lub HAVING. Możesz kupić HEAD, jeśli potrzebujesz go przed oficjalną premierą. Zobacz Merge Pull Request # 16052 @Arcolye @AndrewMarshall @ Aldo-xoen-Giambelluca
Claudio Floreani
37

Obiekty relacji można konwertować na tablice. To neguje możliwość późniejszego użycia na nich jakichkolwiek metod ActiveRecord, ale nie musiałem. Ja to zrobiłem:

name_relation = first_name_relation + last_name_relation

Ruby 1.9, szyny 3.2

thatmiddleway
źródło
Aktualizacja: szkoda, że ​​to nie łączy się według daty
Daniel Morris
68
Nie działa jako tablica, konwertuje twoją relację na tablicę. Wtedy nie możesz już używać na nim metod ActiveRecord, takich jak .where () ...
Augustin Riedinger
1
Jeśli nie zależy Ci na tym, aby typ zwracany był relacją, możesz połączyć wiele wyników za pomocą argumentu first_name_relation | last_name_relation. „|” operator działa również na wielu relacjach. Wartość wynikowa to tablica.
MichaelZ
11
Pierwotne pytanie brzmiało: „czy można połączyć te dwie relacje w celu utworzenia jednego obiektu ActiveRecord :: Relation zawierającego oba warunki?” Ta odpowiedź zwraca tablicę ...
courimas
To DOSKONAŁE rozwiązanie w wielu przypadkach. Jeśli potrzebujesz tylko wyliczenia rekordów do wykonania pętli (np. Nie obchodzi cię, czy jest to Array czy ActiveRecord :: Relation) i nie przeszkadza ci narzut dwóch zapytań, rozwiązuje to problem dzięki oczywistej, prostej składni. Chciałbym móc +2 to!
David Hempy
21

mergewłaściwie nie działa jak OR. To po prostu przecięcie ( AND)

Zmagałem się z tym problemem, aby połączyć obiekty ActiveRecord :: Relation w jeden i nie znalazłem dla mnie działającego rozwiązania.

Zamiast szukać właściwej metody tworzenia unii z tych dwóch zbiorów, skupiłem się na algebrze zbiorów. Możesz to zrobić w inny sposób, korzystając z prawa De Morgana

ActiveRecord zapewnia metodę scalania (AND), a także możesz użyć not method lub none_of (NOT).

search.where.none_of(search.where.not(id: ids_to_exclude).merge(search.where.not("title ILIKE ?", "%#{query}%")))

Masz tutaj (A u B) '= A' ^ B '

AKTUALIZACJA: powyższe rozwiązanie jest dobre w bardziej złożonych przypadkach. W twoim przypadku coś takiego wystarczy:

User.where('first_name LIKE ? OR last_name LIKE ?', 'Tobias', 'Fünke')
Tomasz Jaśkiewicz
źródło
15

Udało mi się to osiągnąć, nawet w wielu dziwnych sytuacjach, używając wbudowanego Arel Railsów .

User.where(
  User.arel_table[:first_name].eq('Tobias').or(
    User.arel_table[:last_name].eq('Fünke')
  )
)

Spowoduje to scalenie obu relacji ActiveRecord przy użyciu metody Arel lub .


Merge, jak tu zasugerowano, nie działa dla mnie. Usunął z wyników drugi zestaw obiektów relacji.

6 stóp Dan
źródło
2
To najlepsza odpowiedź, IMO. Działa bezbłędnie w przypadku zapytań typu OR.
thekingoftruth
Dziękuję za zwrócenie uwagi, bardzo mi pomogli. Inne odpowiedzi działają tylko dla niewielkiego podzbioru pól, ale dla ponad dziesięciu tysięcy identyfikatorów w górę w instrukcji IN wydajność może być bardzo niska.
onetwopunch
1
@KeesBriggs - to nieprawda. Użyłem tego we wszystkich wersjach Rails 4.
6 stóp Dan
14

Istnieje klejnot o nazwie active_record_union, który może być tym, czego szukasz.

Oto przykładowe zastosowania:

current_user.posts.union(Post.published)
current_user.posts.union(Post.published).where(id: [6, 7])
current_user.posts.union("published_at < ?", Time.now)
user_1.posts.union(user_2.posts).union(Post.published)
user_1.posts.union_all(user_2.posts)
Echo
źródło
Dzięki za udostępnienie. Nawet cztery lata po swoim poście to było jedynym rozwiązaniem dla mnie do utworzenia unii z ransacki acts-as-taggable-onutrzymując swoje ActiveRecordinstancje nienaruszone.
Benjamin Bojko
Używając tego klejnotu, możesz nie otrzymać ActiveRecord :: Relation zwróconego przez wywołanie union (), jeśli masz zdefiniowane zakresy w modelu. Zobacz State of the Union w ActiveRecord w pliku Readme.
dechimp
9

Oto jak sobie z tym poradziłem, jeśli użyjesz pluck, aby uzyskać identyfikator dla każdego z rekordów, połączyć tablice razem, a następnie wykonać zapytanie o te połączone identyfikatory:

  transaction_ids = @club.type_a_trans.pluck(:id) + @club.type_b_transactions.pluck(:id) + @club.type_c_transactions.pluck(:id)
  @transactions = Transaction.where(id: transaction_ids).limit(100)
daveomcd
źródło
6

Jeśli masz tablicę aktywnych relacji rekordów i chcesz je wszystkie scalić, możesz to zrobić

array.inject(:merge)
stevenspiel
źródło
array.inject(:merge)nie działał dla tablicy aktywnych relacji rekordów w szynach 5.1.4. Ale array.flatten!zrobił.
zhisme
0

Brute force it:

first_name_relation = User.where(:first_name => 'Tobias') # ActiveRecord::Relation
last_name_relation  = User.where(:last_name  => 'Fünke') # ActiveRecord::Relation

all_name_relations = User.none
first_name_relation.each do |ar|
  all_name_relations.new(ar)
end
last_name_relation.each do |ar|
  all_name_relations.new(ar)
end
Richarda Grossmana
źródło
"NoMethodError (undefined method` stringify_keys 'for # <MyModel: 0x ...) w Rails3, nawet po zmianie MyModel.none na MyModel.where (1 = 2), ponieważ Rails3 nie ma metody' none '.
JosephK