Jak wyrazić zapytanie NOT IN za pomocą ActiveRecord / Rails?

207

Po prostu zaktualizuj to, ponieważ wydaje się, że wiele osób przychodzi do tego, jeśli używasz Rails 4, spójrz na odpowiedzi Trung Lê` i VinniVidiVicci.

Topic.where.not(forum_id:@forums.map(&:id))

Topic.where(published:true).where.not(forum_id:@forums.map(&:id))

Mam nadzieję, że istnieje proste rozwiązanie, które nie wymaga find_by_sql, jeśli nie, to chyba będzie musiało zadziałać.

Znalazłem ten artykuł, który odwołuje się do tego:

Topic.find(:all, :conditions => { :forum_id => @forums.map(&:id) })

który jest taki sam jak

SELECT * FROM topics WHERE forum_id IN (<@forum ids>)

Zastanawiam się, czy istnieje sposób, aby to zrobić NOT IN, na przykład:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)
Toby Joiner
źródło
3
Jako FYI firma Datamapper miała specjalne wsparcie dla NOT IN. Przykład:Person.all(:name.not => ['bob','rick','steve'])
Mark Thomas
1
przepraszam, że jestem ignorantem, ale czym jest Datamapper? czy to część szyn 3?
Toby Joiner
2
maper danych to alternatywny sposób przechowywania danych, zastępuje Active Record inną strukturą, a następnie piszesz różne rzeczy związane z modelem, takie jak zapytania.
Michael Durrant,

Odpowiedzi:

313

Szyny 4+:

Article.where.not(title: ['Rails 3', 'Rails 5']) 

Szyny 3:

Topic.where('id NOT IN (?)', Array.wrap(actions))

Gdzie actionsjest tablica z:[1,2,3,4,5]

José Castro
źródło
1
Jest to właściwe podejście w przypadku najnowszego modelu zapytań Active Record
Nevir
5
@ Nowa Aleksandria ma rację, więc musisz zrobić coś takiego Topic.where('id NOT IN (?)', (actions.empty? ? '', actions). Nadal by się zepsuł na zero, ale uważam, że tablica, którą przekazujesz, jest zwykle generowana przez filtr, który zwróci []co najmniej, a nigdy zero. Polecam sprawdzenie Squeel, DSL na Active Record. Następnie możesz zrobić Topic.where{id.not_in actions}:, zero / puste / lub w inny sposób.
danneu
6
@danneu tylko zamiana .empty?na .blank?i jesteś zero-proof
colllin
(akcje. puste?? '', akcje) autorstwa @daaneu powinny być (akcje. puste?? '': akcje)
marcel salathe
3
idź do notacji na szynach 4: Article.where.not (tytuł: ['Rails 3', 'Rails 5'])
Tal
152

Do Twojej wiadomości, w Rails 4 możesz użyć notskładni:

Article.where.not(title: ['Rails 3', 'Rails 5'])
Trung Lê
źródło
11
Wreszcie! co zajęło im tyle czasu, żeby to uwzględnić? :)
Dominik Goltermann
50

Możesz spróbować czegoś takiego:

Topic.find(:all, :conditions => ['forum_id not in (?)', @forums.map(&:id)])

Być może będziesz musiał zrobić @forums.map(&:id).join(',') . Nie pamiętam, czy Railsy umieszczą argument na liście CSV, jeśli jest ona wyliczalna.

Możesz także to zrobić:

# in topic.rb
named_scope :not_in_forums, lambda { |forums| { :conditions => ['forum_id not in (?)', forums.select(&:id).join(',')] }

# in your controller 
Topic.not_in_forums(@forums)
jonnii
źródło
50

Korzystanie z Arel:

topics=Topic.arel_table
Topic.where(topics[:forum_id].not_in(@forum_ids))

lub, jeśli preferowane:

topics=Topic.arel_table
Topic.where(topics[:forum_id].in(@forum_ids).not)

i od szyn 4 na:

topics=Topic.arel_table
Topic.where.not(topics[:forum_id].in(@forum_ids))

Zauważ, że ostatecznie nie chcesz, aby forum_ids była listą identyfikatorów, a raczej podzapytaniem, jeśli tak, to powinieneś zrobić coś takiego przed uzyskaniem tematów:

@forum_ids = Forum.where(/*whatever conditions are desirable*/).select(:id)

w ten sposób otrzymujesz wszystko w jednym zapytaniu: coś takiego:

select * from topic 
where forum_id in (select id 
                   from forum 
                   where /*whatever conditions are desirable*/)

Zauważ też, że ostatecznie nie chcesz tego robić, a raczej dołączyć - co może być bardziej wydajne.

Pedro Rolo
źródło
2
Łączenie może być bardziej wydajne, ale niekoniecznie. Pamiętaj, aby użyć EXPLAIN!
James
20

Aby rozwinąć odpowiedź @Trung Lê, w Rails 4 możesz wykonać następujące czynności:

Topic.where.not(forum_id:@forums.map(&:id))

I możesz pójść o krok dalej. Jeśli musisz najpierw odfiltrować tylko opublikowane tematy, a następnie odfiltrować niepotrzebne identyfikatory, możesz to zrobić:

Topic.where(published:true).where.not(forum_id:@forums.map(&:id))

Rails 4 sprawia, że ​​jest to o wiele łatwiejsze!

Vincent Cadoret
źródło
12

Akceptowane rozwiązanie kończy się niepowodzeniem, jeśli @forumsjest puste. Aby to obejść, musiałem to zrobić

Topic.find(:all, :conditions => ['forum_id not in (?)', (@forums.empty? ? '' : @forums.map(&:id))])

Lub, jeśli używasz Rails 3+:

Topic.where( 'forum_id not in (?)', (@forums.empty? ? '' : @forums.map(&:id)) ).all
Filipe Giusti
źródło
4

Większość powyższych odpowiedzi powinna ci wystarczyć, ale jeśli robisz dużo więcej takich predykatów i złożonych kombinacji, sprawdź Squeel . Będziesz mógł zrobić coś takiego:

Topic.where{{forum_id.not_in => @forums.map(&:id)}}
Topic.where{forum_id.not_in @forums.map(&:id)} 
Topic.where{forum_id << @forums.map(&:id)}
jake
źródło
2

Możesz zajrzeć do wtyczki meta_where autorstwa Ernie Millera. Twoja instrukcja SQL:

SELECT * FROM topics WHERE forum_id NOT IN (<@forum ids>)

... można wyrazić w następujący sposób:

Topic.where(:forum_id.nin => @forum_ids)

Ryan Bates of Railscasts stworzył fajny screencast wyjaśniający MetaWhere .

Nie jestem pewien, czy tego właśnie szukasz, ale moim zdaniem z pewnością wygląda to lepiej niż wbudowane zapytanie SQL.

Marcin Wyszyński
źródło
2

Oryginalny post wspomina konkretnie o numerycznych identyfikatorach, ale przyszedłem tutaj, szukając składni do wykonywania NOT IN z tablicą ciągów.

ActiveRecord poradzi sobie również z tym dobrze:

Thing.where(['state NOT IN (?)', %w{state1 state2}])
Andy Triggs
źródło
1

Czy te identyfikatory forum można opracować w sposób pragmatyczny? np. czy możesz jakoś znaleźć te fora - w takim przypadku powinieneś zrobić coś takiego

Topic.all(:joins => "left join forums on (forums.id = topics.forum_id and some_condition)", :conditions => "forums.id is null")

Co byłoby bardziej wydajne niż wykonywanie SQL not in

Omar Qureshi
źródło
1

W ten sposób optymalizuje się pod kątem czytelności, ale nie jest tak wydajna pod względem zapytań do bazy danych:

# Retrieve all topics, then use array subtraction to
# find the ones not in our list
Topic.all - @forums.map(&:id)
evanrmurphy
źródło
0

Możesz używać sql w swoich warunkach:

Topic.find(:all, :conditions => [ "forum_id NOT IN (?)", @forums.map(&:id)])
Tjeden
źródło
0

Kiedy przeszukujesz pustą tablicę, dodaj „<< 0” do tablicy w bloku where, aby nie zwracała „NULL” i nie przerywała zapytania.

Topic.where('id not in (?)',actions << 0)

Jeśli akcje mogą być pustą lub pustą tablicą.

itsEconomics
źródło
1
Ostrzeżenie: to faktycznie dodaje 0 do tablicy, więc nie jest już pusta. Ma również efekt uboczny polegający na modyfikacji tablicy - podwójne niebezpieczeństwo, jeśli użyjesz go później. Znacznie lepiej jest zawinąć go w if-else i użyć Topic.none / all w przypadku skrzynek
Ted Pennings
Bezpieczniejszym sposobem jest:Topic.where("id NOT IN (?)", actions.presence || [0])
Weston Ganger,
0

Oto bardziej złożone zapytanie „nie w”, wykorzystujące podzapytanie w szynach 4 za pomocą squeel. Oczywiście bardzo wolno w porównaniu do równoważnej sql, ale hej, to działa.

    scope :translations_not_in_english, ->(calmapp_version_id, language_iso_code){
      join_to_cavs_tls_arr(calmapp_version_id).
      joins_to_tl_arr.
      where{ tl1.iso_code == 'en' }.
      where{ cavtl1.calmapp_version_id == my{calmapp_version_id}}.
      where{ dot_key_code << (Translation.
        join_to_cavs_tls_arr(calmapp_version_id).
        joins_to_tl_arr.    
        where{ tl1.iso_code == my{language_iso_code} }.
        select{ "dot_key_code" }.all)}
    }

Pierwsze 2 metody w zakresie to inne zakresy, które deklarują aliasy cavtl1 i tl1. << to operator in in squeel.

Mam nadzieję, że to komuś pomoże.

dukha
źródło