8

Moja aplikacja ma model Job. Każde zadanie w systemie ma contact. To jest jak osoba, do której chcesz zadzwonić, jeśli chcesz zadać pytanie dotyczące pracy. Kontaktem może być client lub pracownik klienta (ClientEmployee).Łączenie ActiveRecord :: Relacje zapytane przez skojarzenie polimorficzne

class Job < ActiveRecord::Base 
    belongs_to :contact, polymorphic: true 
end 

class Client < ActiveRecord::Base 
    has_many :jobs, as: :contact 
    has_many :employees, class_name: 'ClientEmployee' 
end 

class ClientEmployee < ActiveRecord::Base 
    belongs_to :client 
    has_many :jobs, as: :contact 
end 

Klienci mają pomysł na commissioned_jobs. Klienci, którym zlecono zadania, to te, dla których klient jest kontaktem LUB jednym z pracowników klienta jest kontakt.

class Client < ActiveRecord::Base 
    has_many :jobs, as: :contact 
    has_many :employee_jobs, through: :employees, source: :jobs 

    def commissioned_jobs 
    jobs << employee_jobs 
    end 
end 

bok: Ta metoda jest nieco hack, ponieważ zwraca tablicę raczej niż ActiveRecord::Relation. Interesujące jest również to, że wysadza się, gdy próbuję konkatentować pracę w employee_jobs. To może, ale nie musi, zrobić dla moich celów.

Chciałbym dodać zakres do Client o nazwie with_commissioned_jobs. Powinno to zwrócić wszystkich klientów w systemie, którzy mają pracę lub mają pracowników, którzy mają pracę.

class Client < ActiveRecord::Base 
    def self.with_commissioned_jobs 
    # I can get clients with jobs using: joins(:jobs). How do 
    # I also include clients with employees who have jobs? 
    end 
end 

Jak zastosować tę metodę?

Używam Rails 3.2.9.

Aktualizacja:

Zrobiłem pewne postępy i teraz mam dwa sposoby, z których każdy robi połowę tego, co mi potrzeba.

class Client < ActiveRecord::Base 
    # Return all clients who have an employee with at least one job. 
    def self.with_employee_jobs 
    joins(employees: :jobs) 
    # SQL: SELECT "clients".* FROM "clients" INNER JOIN "client_employees" ON "client_employees"."employer_id" = "clients"."id" INNER JOIN "jobs" ON "jobs"."contact_id" = "client_employees"."id" AND "jobs"."contact_type" = 'ClientEmployee' 
    end 

    # Return all clients who have at least one job. 
    def self.with_jobs 
    joins(:jobs) 
    # SQL: SELECT "clients".* FROM "clients" INNER JOIN "jobs" ON "jobs"."contact_id" = "clients"."id" AND "jobs"."contact_type" = 'Client' 
    end 
end 

Teraz wszystko, co muszę zrobić, to połączyć te dwie wywołania metod w jeden ActiveRecord::Relation. Mogę oczywiście to zrobić:

def self.with_commissioned_jobs 
    with_jobs + with_employee_jobs 
    end 

Problem polega na tym, że zwraca tablicę zamiast instancję Relation i nie mogę łańcuchu zasięgów na nim.

Aktualizacja 2:

Korzystanie merge wydaje się nie działa. Oto zapytanie AR i wynikowy kod SQL.

joins(:jobs).merge(joins(employees: :jobs)) 

SELECT "clients".* FROM "clients" INNER JOIN "jobs" 
    ON "jobs"."contact_id" = "clients"."id" 
    AND "jobs"."contact_type" = 'Client' 
    INNER JOIN "client_employees" 
    ON "client_employees"."employer_id" = "clients"."id" 
    INNER JOIN "jobs" "jobs_client_employees" 
    ON "jobs_client_employees"."contact_id" = "client_employees"."id" 
    AND "jobs_client_employees"."contact_type" = 'ClientEmployee' 

Przy okazji, oto testy, które próbuję przekazać. Pierwszy test kończy się niepowodzeniem, ponieważ przy scalaniu uzyskuję zerowe wyniki.

describe "with_commissioned_jobs" do 
    # A client with a job. 
    let!(:client_with) { create :client } 
    let!(:job) { create :job, contact: client_with } 
    # A client who does not himself have a job, but who has an employee 
    # with a job. 
    let!(:client_with_emp) { create :client } 
    let!(:employee) { create :client_employee, employer: client_with_emp } 
    let!(:emp_job) { create :job, contact: employee } 
    # A client with nothing. Should not show up. 
    let!(:client_without) { create :client } 

    it "should return clients with jobs and clients with employee jobs" do 
    Client.with_commissioned_jobs.should == [client_with, client_with_emp] 
    end 

    it "should return a relation" do 
    Client.with_commissioned_jobs.should be_instance_of(ActiveRecord::Relation) 
    end 
end 
+0

Czy obejrzałeś arel? https://github.com/rails/arel Zawiera warunek dla zapytań i dobrze obsługuje połączenia złożone. – John

+0

Z pewnością przyjmuję również rozwiązanie oparte na Arel. Spędziłem kilka godzin próbując wymyślić jeden i wydaje mi się, że nie jestem w stanie nim zarządzać. –

Odpowiedz

1

Czy rozważał gem meta_where? Najważniejsze wydaje się, że chcesz zwrócić obiekt ActiveRecord:Relation w celu dalszego łączenia.

UPDATE 2 got to dwukrotnie pracy z LEFT OUTER JOIN pracy z aliasing

# scope for ::Client 
    def self.with_commissioned_jobs 
    self.joins("LEFT OUTER JOIN client_employees ON clients.id =client_employees.client_id"). 
     joins("LEFT OUTER JOIN jobs AS cjobs ON clients.id = cjobs.contact_id AND cjobs.contact_type = 'Client'"). 
     joins("LEFT OUTER JOIN jobs AS ejobs ON client_employees.id = ejobs.contact_id AND ejobs.contact_type = 'ClientEmployee'"). 
     where("cjobs.id IS NOT NULL OR ejobs.id IS NOT NULL") 
    end 

sprawdzając, czy to działa:

#c1 has no job 
    c1 = Client.create 

    #c2 has a job 
    c2 = Client.create 
    c2.jobs.create 

    #c3 has no job, but has an employee with a job 
    c3 = Client.create 
    c3.employees.create 
    c3.employees.first.jobs.create 

    puts Client.all.inspect    #=> [#<Client id: 1>, #<Client id: 2>, #<Client id: 3>] 
    puts Client.with_commissioned_jobs #=> [#<Client id: 2>, #<Client id: 3>] 

    puts [c2,c3] == Client.with_commissioned_jobs.all #=> true 
+0

Nie obsługuje Railsów 3.2 Obawiam się. Próbowałem [Squeel] (https://github.com/ernie/squeel), który jest podobny do MetaWhere, ale nie mogę tego zmusić do scalenia 'AR :: Relation's. –

+0

Twoja aktualizacja działa z jedną niewielką modyfikacją, która jest potrzebna, ponieważ klucz obcy moich pracowników klienta to 'employer_id', w przeciwieństwie do' client_id'. Świetna robota, dziękuję! –

1

Spróbuj tego:

joins(:jobs, {employees: :jobs}) 

Należy dołączyć do oferty klienta, jak i pracy pracowników klienckich. Aby uzyskać bardziej szczegółowe informacje, patrz the guides.

Edit

W twoim przypadku można użyć Relation.merge:

joins(:jobs).merge(joins(employees: :jobs)) 
+0

Czytam przewodniki (dzięki za link btw) i o ile rozumiem, kod, który podałeś powinien powrócić "klienci, którzy mają pracę I mają pracowników, którzy mają pracę". Naprawdę szukam warunku OR i nie widzę sposobu, aby to zrobić. –

+0

Scalenie nie działa, albo Obawiam się. Dodałem więcej informacji do pytania wraz ze specyfikacjami, z którymi pracuję. –

0
class Client < ActiveRecord::Base 
    has_many :jobs, as: :contact 
    has_many :employees, class_name: 'ClientEmployee' 

    scope :with_commissioned_jobs, lambda do 
    includes(:jobs, {:employees => :jobs}).where("jobs.contact_type IS NOT NULL AND jobs.contact_id IS NOT NULL") 
    end 
end 

Ok, moja kolejna decyzja od rzeczywistej aplikacji roboczej. Stara szkoła ci pomoże. :)

Ta metoda pozwala utworzyć warunek szeregowy dla AR: Relation dla rzeczy polimorficznych.

module ActiveRecordHelper 

    def self.polymorphic_sql(*args) 
    conditions = [] 
    table = args.first.table_name 
    stack = args.extract_options! 
    sql_queries = stack.collect do |as_resource, hash| 
     resource_queries = hash.collect do |name, find_options| 
     resource_class = name.to_s.classify.constantize 
     resource_table = resource_class.table_name 
     conditions << resource_class.name 
     if find_options[:conditions].present? 
      conditions += find_options[:conditions][1..-1] 
     end 
     joins_clause = 
     Array.wrap(find_options[:join]).collect do |association| 
      reflection = resource_class.reflections[association]    
      if reflection.macro == :belongs_to && reflection.options[:polymorphic] != true 
      "INNER JOIN #{reflection.klass.table_name} ON #{reflection.active_record.table_name}.#{reflection.foreign_key} = #{reflection.klass.table_name}.id" 
      elsif reflection.macro.in?([:has_many, :has_one]) && reflection.options[:as].nil? 
      "INNER JOIN #{reflection.klass.table_name} ON #{reflection.klass.table_name}.#{reflection.foreign_key} = #{reflection.active_record.table_name}.id" 
      end 
     end.compact.join(" ").strip 
     "(#{table}.#{as_resource}_type = ? AND EXISTS(#{["SELECT 1 FROM #{resource_table}#{joins_clause.left_indent(1) if joins_clause.present?} WHERE #{resource_table}.id = #{table}.#{as_resource}_id", find_options[:conditions].first].compact.join(" AND ")}))" 
     end 
     "CASE WHEN #{table}.#{as_resource}_type IS NOT NULL AND #{table}.#{as_resource}_id IS NOT NULL THEN #{resource_queries.join(" OR ")} ELSE TRUE END" 
    end 
    conditions.insert(0, "(#{sql_queries.join(" OR ")})") 
    end 

end 

Następnie przedłużyć polimorficzny praca:

def self.comissioned_by(client) 
    conditions = ActiveRecordHelper.polymorphic_sql(self, :contact => {:client => {:conditions => ["clients.id = ?", client.id]}, :client_employee => {:conditions => ["client_employees.client_id = ?", client.id]}} 
    where(conditions) 
end 

Teraz zadzwonić:

Job.commissioned_by() # pass client instance 

Enjoy. Jeśli potrzebne są jakieś szczegóły, wpisz mnie.

+0

Zwraca pustą relację po uruchomieniu na testy, które zawarłem w odpowiedzi na pytanie. –

+0

Po prostu nienawidzę składni stylów.) –

1

Czy masz ważny powód, aby trzymać się polimorfizmu?

Jeśli ClientEmployee zawsze ma klienta, być może powinieneś mieć Job.belongs_to :client. To sprawia, że ​​twoja relacja jest prosta. Zauważyłem, że dodanie kilku zbędnych skojarzeń może również być świetną optymalizacją wydajności, o ile nie utrudnia to zachowania spójności danych (tj. Relacje klient/klient są zsynchronizowane z zadaniami Job.Client/Job.ClientEmployee, gdy oba są obecne).

Bardzo lubię polimorfizm w szynach, ale może to być trudne, gdy próbujesz połączyć się z nimi, tak jak w tym przypadku. Nawet jeśli masz oddzielne identyfikatory klienta i klienta, będzie to bardziej wydajne w db (dwa ints vs. int i string).

+0

Dobra sugestia. Celem obecnej konfiguracji jest to, że chociaż tylko klienci mogą być rozliczani za pracę, w codziennym zarządzaniu, pracownik klienta może być klientem do pracy, a nie samym klientem. Rozważę, że klienci-klienci po prostu będą rekordami w tabeli klienta, a następnie użyli samoindeksowania w tej tabeli, aby symulować zatrudnienie. –

+0

W tym przypadku 'def Job.contact; client_employee || klient; end' zapewnia to, że twój schemat będzie zoptymalizowany do twoich potrzeb. – aceofspades

0

Czy próbowałeś niestandardowego sprzężenia?

def self.with_commissioned_jobs 
    query = <<-QUERY 
    INNER JOIN client_employees 
    ON client_employees.employer_id = clients.id 
    INNER JOIN jobs 
    ON ((jobs.contact_id = client_employees.id AND jobs.contact_type = 'ClientEmployee') 
     OR (jobs.contact_id = clients.id AND jobs.contact_type = 'Client')) 
    QUERY 

    joins(query) 
end 
+0

To nie wydaje się wyodrębnić klienta, który jest bezpośrednio kontaktem pracy w testach, które zawarłem w moim pytaniu. Zwraca tylko 'client_with_emp'. –

Powiązane problemy