2016-10-12 38 views
6

Mam powiązanie, które wymaga kilku sprzężeń/niestandardowych zapytań. Podczas próby ustalenia, jak to wdrożyć, powtarzana odpowiedź to finder_sql. Jednak w Rails 4.2 (i nowszych):Ma wiele "finder_sql" Zamiennik w Railsach 4.2

ArgumentError: Unknown key: :finder_sql

mojej kwerendy zrobić wygląd złączyć tak:

'SELECT DISTINCT "tags".*' \ 
' FROM "tags"' \ 
' JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"' \ 
' JOIN "articles" ON "article_tags"."article_id" = "articles"."id"' \ 
' WHERE articles"."user_id" = #{id}' 

Rozumiem, że można to osiągnąć poprzez:

has_many :tags, through: :articles 

jednak jeśli liczność łączenia jest duża (np. użytkownik ma tysiące artykułów - ale system ma tylko kilka znaczników), to wymaga załadowania wszystkich artykułów/tagów:

SELECT * FROM articles WHERE user_id IN (1,2,...) 
SELECT * FROM article_tags WHERE article_id IN (1,2,3...) -- a lot 
SELECT * FROM tags WHERE id IN (1,2,3) -- a few 

I oczywiście ciekawy również ogólny przypadek.

Uwaga: próbował również przy użyciu składni proc, ale nie może się zorientować, że spośród:

has_many :tags, -> (user) { 
    select('DISTINCT "tags".*') 
    .joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"') 
    .joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"') 
    .where('"articles"."user_id" = ?', user.id) 
}, class_name: "Tag" 

ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column tags.user_id does not exist

SELECT DISTINCT "tags".* FROM "tags" JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id" JOIN "articles" ON "article_tags"."article_id" = "articles"."id" WHERE "tags"."user_id" = $1 AND ("articles"."user_id" = 1)

To wygląda na to, że stara się wstrzyknąć user_id na znaczniki automatycznie (i że kolumna istnieje tylko w artykułach). Uwaga: Wstępnie ładuję dla wielu użytkowników, więc nie mogę używać user.tags bez innych poprawek (wklejony kod SQL jest tym, co widzę, używając tego samego!). Myśli?

+0

gdzie piszesz to 'has_many: tags, -> (użytkownik) ...'? W user.rb? – Sajan

+0

@sajan correct - in 'user.rb'. – Stussa

Odpowiedz

3

Mimo to nie rozwiąże problemu bezpośrednio - jeśli trzeba tylko podzbiór swoich danych można potencjalnie przekręceniem go poprzez podselekcji:

users = User.select('"users".*"').select('COALESCE((SELECT ARRAY_AGG(DISTINCT "tags"."name") ... WHERE "articles"."user_id" = "users"."id"), '{}') AS tag_names') 
users.each do |user| 
    puts user[:tag_names].join(' ') 
end 

Powyższe jest specyficzne dla bazy danych dla PostgreSQL (z powodu ARRAY_AGG), ale istnieje równoważne rozwiązanie dla innych baz danych.

Alternatywnym rozwiązaniem może być w konfiguracji widokiem jak stół fałszywy join (znowu wymaga wsparcia bazy danych):

CREATE OR REPLACE VIEW tags_users AS (
    SELECT 
    "users"."id" AS "user_id", 
    "tags"."id" AS "tag_id" 
    FROM "users" 
    JOIN "articles" ON "users"."id" = "articles"."user_id" 
    JOIN "articles_tags" ON "articles"."id" = "articles_tags"."article_id" 
    JOIN "tags" ON "articles_tags"."tag_id" = "tags"."id" 
    GROUP BY "user_id", "tag_id" 
) 

Następnie można użyć has_and_belongs_to_many :tags (nie testowany - mogą chcieć ustawić readonly i może usunąć niektóre sprzężenia i użyć, jeśli masz odpowiednie ustawienia ograniczeń klucza obcego).

+0

Wygląda na to, że widok działa (chociaż jest tylko do odczytu - ale ma to sens). Nie zrzuca do mojego schematu (prawdopodobnie oddzielny problem) i tylko do odczytu niczego nie zmienia (ale wywołuje wyjątek przy próbie przypisania). To może być najlepszy wysiłek (choć nieco denerwujący wymagający widoku). – Stussa

0

Zgaduję, że otrzymujesz błąd podczas próby uzyskania dostępu do @user.tags, ponieważ masz to powiązanie z user.rb.

Więc myślę, że to, co się dzieje, gdy staramy się uzyskać dostęp do @user.tags, staramy się sprowadzić tags użytkownika i do szyny wyszuka Tags których user_id mecze z identyfikatorem zaopatrywanych obecnie użytkownika. Ponieważ szyny domyślnie przyjmują nazwę asocjacyjną jako format modelname_id, nawet jeśli nie masz user_id, spróbujesz wyszukać w tej kolumnie i będzie wyszukiwać (lub dodać WHERE "tags"."user_id") bez względu na to, czy chcesz, czy nie, ponieważ ostatecznym celem jest znalezienie tags, które należą do bieżącego użytkownika.

Oczywiście moja odpowiedź może nie wyjaśnić tego w 100%. Nie krępuj się komentować swojej myśli lub Jeśli znajdziesz coś nie tak, daj mi znać.

+0

Zgadza się - używam 'user.tags'. Popraw także, czy dane wyjściowe pasują do tego, co powiedziałeś. Czy masz odpowiedź, którą uważasz, że rozwiąże? Naprawdę szukam zamiennika dla 'finder_sql' (z' finder_sql' można to zrobić). Dzięki. – Stussa

0

Krótka odpowiedź

Ok, jeśli dobrze rozumiem to poprawnie Chyba mam rozwiązanie, które po prostu korzysta z podstawowych narzędzi ActiveRecord i nie używa finder_sql.

potencjalnie może używać:

user.tags.all.distinct 

lub alternatywnie, w modelu użytkownika zmienić tagów HAS_MANY do

has_many :tags, -> {distinct}, through: :articles 

Można utworzyć metody pomocnika w użytkownikiem, aby pobrać ten:

def distinct_tags 
    self.tags.all.distinct 
end 

Dowód

Od swoje pytanie wierzę masz następujący scenariusz:

  1. Użytkownik może mieć wiele artykułów.
  2. Artykuł należy do jednego użytkownika.
  3. Tagi mogą należeć do wielu artykułów.
  4. Artykuły mogą zawierać wiele tagów.
  5. Chcesz odzyskać wszystkie odrębne znaczniki, które użytkownik skojarzył z tworzonymi przez siebie artykułami.

Mając to na uwadze, stworzyłem następujące migracje:

class CreateUsers < ActiveRecord::Migration 
    def change 
    create_table :users do |t| 
     t.string :name, limit: 255 

     t.timestamps null: false 
    end 
    end 
end 

class CreateArticles < ActiveRecord::Migration 
    def change 
    create_table :articles do |t| 
     t.string :name, limit: 255 
     t.references :user, index: true, null: false 

     t.timestamps null: false 
    end 

    add_foreign_key :articles, :users 
    end 
end 

class CreateTags < ActiveRecord::Migration 
    def change 
    create_table :tags do |t| 
     t.string :name, limit: 255 

     t.timestamps null: false 
    end 
    end 
end 

class CreateArticlesTagsJoinTable < ActiveRecord::Migration 
    def change 
    create_table :articles_tags do |t| 
     t.references :article, index: true, null:false 
     t.references :tag, index: true, null: false 
    end 

    add_index :articles_tags, [:tag_id, :article_id], unique: true 
    add_foreign_key :articles_tags, :articles 
    add_foreign_key :articles_tags, :tags 
    end 
end 

a modele:

class User < ActiveRecord::Base 
    has_many :articles 
    has_many :tags, through: :articles 

    def distinct_tags 
    self.tags.all.distinct 
    end 
end 

class Article < ActiveRecord::Base 
    belongs_to :user 
    has_and_belongs_to_many :tags 
end 

class Tag < ActiveRecord::Base 
    has_and_belongs_to_many :articles 
end 

Następny nasienie baza z dużą ilością danych:

10.times do |tagcount| 
    Tag.create(name: "tag #{tagcount+1}") 
end 

5.times do |usercount| 
    user = User.create(name: "user #{usercount+1}") 

    1000.times do |articlecount| 
    article = Article.new(user: user) 
    5.times do |tagcount| 
     article.tags << Tag.find(tagcount+usercount+1) 
    end 
    article.save 
    end 
end 

Wreszcie w konsoli szyn:

user = User.find(3) 
user.distinct_tags 

wyniki w następujący wynik:

Tag Load (0.4ms) SELECT DISTINCT `tags`.* FROM `tags` INNER JOIN `articles_tags` ON `tags`.`id` = `articles_tags`.`tag_id` INNER JOIN `articles` ON `articles_tags`.`article_id` = `articles`.`id` WHERE `articles`.`user_id` = 3 
=> #<ActiveRecord::AssociationRelation [#<Tag id: 3, name: "tag 3", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 4, name: "tag 4", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 5, name: "tag 5", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 6, name: "tag 6", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">, #<Tag id: 7, name: "tag 7", created_at: "2016-10-18 22:00:52", updated_at: "2016-10-18 22:00:52">]> 
+0

Wypróbuj 'User.preload (: tags) .each {| user | puts user.tags.inspect} ', a zobaczysz złe kwerendy liczności. – Stussa

+0

Nie sądzę, że to rozwiąże problem - czy widzisz, że masywne wybiera się, jeśli wkleisz fragment snippet powyżej? – Stussa

+0

ah po prostu przesunąłeś bramki XD. Będę miał szybki rzut oka. – David

0

Pomocne może być użycie eager_load, aby zmusić ActiveRecord do wykonywania połączeń.To działa jak includes(:tags).references(:tags)

Oto fragment kodu:

users.eager_load(:tags).map { |user| user.tag.inspect } 
# equal to 
users.includes(:tags).references(:tags).map { |user| user.tag.inspect } 

Gdzie users - jest relacją ActiveRecord.

Kod ten trafi bazę przynajmniej dwukrotnie:

  1. Wybierz tylko użytkownicy identyfikatory (oby nie za dużo)
  2. Wybierz użytkowników z dołącza tagów poprzez article_tags unikając

SELECT * FROM article_tags WHERE article_id IN (1,2,3...) -- a lot

0

Jesteś na dobrej drodze z has_many :tags, through: :articles (lub jeszcze lepiej has_many :tags, -> {distinct}, through: :articles, jak sugeruje Kevin). Ale powinieneś przeczytać nieco o includes vs preload vs eager_load. Robisz to:

User.preload(:tags).each {|u| ... } 

Ale trzeba to zrobić:

User.eager_load(:tags).each {|u| ... } 

lub to:

User.includes(:tags).references(:tags).each {|u| ... } 

Kiedy robię, że mam takie zapytanie:

SELECT "users"."id" AS t0_r0, 
     "tags"."id" AS t1_r0, 
     "tags"."name" AS t1_r1 
FROM "users" 
LEFT OUTER JOIN "articles" 
ON  "articles"."user_id" = "users"."id" 
LEFT OUTER JOIN "articles_tags" 
ON  "articles_tags"."article_id" = "articles"."id" 
LEFT OUTER JOIN "tags" 
ON  "tags"."id" = "articles_tags"."tag_id" 

Ale to nadal będzie wysyłało dużo zbędnych s tuf z bazy danych do aplikacji. To będzie szybciej:

User.eager_load(:tags).distinct.each {|u| ... } 

Dawanie:

SELECT DISTINCT "users"."id" AS t0_r0, 
     "tags"."id" AS t1_r0, 
     "tags"."name" AS t1_r1 
FROM "users" 
LEFT OUTER JOIN "articles" 
ON  "articles"."user_id" = "users"."id" 
LEFT OUTER JOIN "articles_tags" 
ON  "articles_tags"."article_id" = "articles"."id" 
LEFT OUTER JOIN "tags" 
ON "tags"."id" = "articles_tags"."tag_id" 

robi tylko User.first.tags.map &:name dostaje mnie dołączy też:

SELECT DISTINCT "tags".* 
FROM "tags" 
INNER JOIN "articles_tags" 
ON  "tags"."id" = "articles_tags"."tag_id" 
INNER JOIN "articles" 
ON  "articles_tags"."article_id" = "articles"."id" 
WHERE "articles"."user_id" = ? 

Aby uzyskać więcej informacji, proszę zobaczyć ten github repo z rspec testu, aby zobaczyć jakie SQL Rails używa.

0

Są trzy możliwe rozwiązania:

1) są nadal w użyciu has_many związki

Fałszywe user_id kolumnę przez dodanie go do wybranych kolumn.

class User < ActiveRecord::Base 
    has_many :tags, -> (user) { 
     select(%Q{DISTINCT "tags".*, #{user_id} AS user_id }) 
     .joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"') 
     .joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"') 
     .where('"articles"."user_id" = ?', user.id) 
    }, class_name: "Tag" 
    end 

2) Dodaj metodę instancji na klasy User

Jeśli używasz tags tylko dla zapytań i nie zostały wykorzystane w przyłącza można użyć tego podejścia:

class User 
    def tags 
    select(%Q{DISTINCT "tags".*}) 
     .joins('JOIN "articles_tags" ON "articles_tags"."tag_id" = "tags"."id"') 
     .joins('JOIN "articles" ON "article_tags"."article_id" = "articles"."id"') 
     .where('"articles"."user_id" = ?', id) 
    end 
end 

Teraz user.tags zachowuje się jak stowarzyszenie dla wszystkich praktycznych celów.

3) OTOH korzystając EXISTS może być wydajnych niż przy użyciu odrębnego

class User < ActiveRecord::Base 
    def tags 
     exists_sql = %Q{ 
     SELECT 1 
     FROM articles, 
       articles_tags 
     WHERE "articles"."user_id" = #{id} AND 
       "articles_tags"."article_id" = "article"."id" AND 
       "articles_tags"."tag_id" = "tags.id" 
     } 
     Tag.where(%Q{ EXISTS (#{exists_sql}) }) 
    end 
    end