8

La mia applicazione ha un modello di lavoro. Ogni lavoro nel sistema ha un contact. È come una persona che chiameresti se devi fare una domanda sul lavoro. Un contatto può essere un client o un dipendente di un client (ClientEmployee).Concatenazione di ActiveRecord :: Relazioni interrogate tramite associazione polimorfica

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 

I clienti hanno l'idea di commissioned_jobs. I lavori commissionati dai clienti sono quei lavori per i quali il cliente è il contatto O uno dei dipendenti del cliente è il contatto.

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 

parte: Tale metodo è un po 'di un hack, perché restituisce un array piuttosto che un ActiveRecord::Relation. È anche interessante che esploda se provo a concatenare i lavori in employee_jobs. Può o non può fare per i miei scopi.

Vorrei aggiungere un ambito a Client chiamato with_commissioned_jobs. Questo dovrebbe restituire tutti i client nel sistema che hanno un lavoro o che hanno dipendenti che hanno un lavoro.

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 

Come si implementa questo metodo?

Sto usando Rails 3.2.9.

Aggiornamento:

ho fatto qualche progresso e ora ho due metodi, ciascuno dei quali fa la metà di quello che mi serve.

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 

Adesso tutto quello che devo fare è combinare questi due chiamate di metodo in uno ActiveRecord::Relation. Posso ovviamente fare questo:

def self.with_commissioned_jobs 
    with_jobs + with_employee_jobs 
    end 

Il problema è che che restituisce un array anziché un'istanza di Relation e non posso catena più ambiti sopra.

Update 2:

Utilizzando merge non sembra funzionare. Ecco la query AR e l'SQL risultante.

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' 

A proposito, ecco i test che sto cercando di superare. Il primo test fallisce perché non ci sono risultati zero quando utilizzo l'unione.

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

Hai guardato in arel? https://github.com/rails/arel Ha una o una condizione per le query e gestisce bene i join complessi. – John

+0

Accetterò certamente anche una soluzione basata su Arel. Ho passato un bel po 'di ore a cercare di inventarne uno e non riesco a gestirlo. –

risposta

1

avete considerato la gemma meta_where? La cosa principale sembra essere che si desidera restituire un oggetto ActiveRecord:Relation per un ulteriore concatenamento.

UPDATE 2: ottenuto che funziona con LEFT OUTER JOIN lavori due volte con 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 

Vedendo se funziona:

#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

Non supporta Rails 3.2 Ho paura. Ho provato [Squeel] (https://github.com/ernie/squeel) che è simile a MetaWhere, ma non riesco a capire come si possa unire 'AR :: Relation'. –

+0

L'aggiornamento funziona con una leggera modifica necessaria poiché la chiave esterna dei dipendenti del mio cliente è 'employer_id' invece di' client_id'. Ottimo lavoro, grazie! –

1

Prova questo:

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

Occorre unire lavoro del cliente, nonché di posti di lavoro dei dipendenti del cliente. Vedi the guides per informazioni più approfondite.

Modifica

Nel tuo caso, è possibile utilizzare Relation.merge:

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

Sto leggendo le guide (grazie per il collegamento btw) e per quanto posso capire il codice che hai fornito dovrebbe restituire "i clienti che hanno un lavoro e hanno dipendenti che hanno un lavoro". In realtà sto cercando una condizione OR e non vedo un modo per farlo. –

+0

L'unione non sembra funzionare o ho paura. Ho aggiunto ulteriori informazioni alla domanda insieme alle specifiche con cui lavoro. –

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, il mio un'altra decisione dall'applicazione di lavoro vero e proprio. La vecchia scuola ti aiuta. :)

Questo metodo crea semplicemente una condizione array per AR: Relazione per materiale polimorfico.

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 

Poi estendere il lavoro polimorfa:

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 

Ora chiamano:

Job.commissioned_by() # pass client instance 

godere. Se sono necessari dettagli, digitami.

+0

Questo restituisce una relazione vuota quando viene eseguito sui test che ho incluso nella domanda, temo. –

+0

odio la sintassi stile Arel.) –

1

Hai una ragione importante per attaccare al polimorfismo?

Se un ClientEmployee ha sempre un Cliente, forse dovresti avere Job.belongs_to :client. Questo rende la tua relazione semplicemente semplice. Ho scoperto che l'aggiunta di alcune associazioni ridondanti può essere anche un'ottimizzazione delle prestazioni, a patto che non renda più difficile mantenere coerenti i record (cioè le relazioni Client/ClientEmployee sincronizzate con assegnazioni Job.Client/Job.ClientEmployee quando entrambi sono presenti).

Mi piace molto il polimorfismo in rotaie ma può diventare complicato quando si tenta di unirsi a loro come in questo caso. Anche se si disponessero di id separati per Client e ClientEmployee, ciò sarebbe più efficiente nel db (due inte rispetto a int e stringa).

+0

Buon suggerimento. Il punto della configurazione attuale è che sebbene solo i clienti possano essere fatturati per lavoro, nella gestione quotidiana, un dipendente di un cliente potrebbe essere il "go-to-guy" per il lavoro, piuttosto che il cliente stesso. Quello che vedrò è che i dipendenti del client devono essere solo record nella tabella del client e quindi utilizzare l'auto-referenziazione all'interno di quella tabella per simulare l'occupazione. –

+0

In questo caso 'def Job.contact; client_employee || cliente; end' fornisce questo manterrà il tuo schema ottimizzato per le tue esigenze. – aceofspades

0

Hai provato a eseguire un join personalizzato?

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

Questo non sembra estrarre il cliente che è direttamente il contatto di un lavoro nei test che ho incluso nella mia domanda. Restituisce solo 'client_with_emp'. –

Problemi correlati