2009-07-14 23 views
31

Vedere i commenti per gli aggiornamenti.Rotaie: molte o molte relazioni polimorfiche

Ho faticato per ottenere una risposta chiara e diretta su questo, spero che questa volta lo prendo! : D Ho ancora molto da imparare ancora con Rails, tuttavia capisco il problema che sto affrontando e apprezzerei davvero un ulteriore aiuto.

  • Ho un modello chiamato "Attività".
  • Ho un modello astratto chiamato "Target".
  • Vorrei mettere in relazione più istanze di sottoclassi di Destinazione su Attività.
  • Non utilizzo l'ereditarietà di una tabella singola.
  • Vorrei interrogare la relazione polimorfica per restituire un set di risultati misto di sottoclassi di Target.
  • Vorrei interrogare singole istanze di sottoclassi di Target per ottenere attività con le quali sono in relazione.

Quindi, immagino che una relazione polimorfa da molti a molti tra Attività e sottoclassi di Obiettivi sia in ordine. Più in dettaglio, sarò in grado di fare cose come questa nella console (e ovviamente altrove):

task = Task.find(1) 
task.targets 
[...array of all the subclasses of Target here...] 

Ma! Supponendo modelli "Store", "Software", "Office", "veicolo", che sono tutte le sottoclassi di "Target" esistere, sarebbe bello per attraversare anche il rapporto nella direzione opposta:

store = Store.find(1) 
store.tasks 
[...array of all the Tasks this Store is related to...] 
software = Software.find(18) 
software.tasks 
[...array of all the Tasks this Software is related to...] 

Il tabelle di database implicite da rapporti polimorfi sembra essere in grado di fare questo attraversamento, ma vedo alcuni temi ricorrenti nel tentativo di trovare una risposta che mi sconfiggere lo spirito di relazioni polimorfici:

  • Utilizzando ancora, compare il mio esempio persone volere definire Store, Software, Office, Vehicle in Task, che possiamo dire subito non è un rapporto polimorfo come lo restituisce solo un tipo di modello.
  • Analogamente all'ultimo punto, le persone desiderano ancora definire Store, Software, Office e Vehicle in Task in una forma o forma. Il punto importante qui è che la relazione è cieca rispetto alla sottoclasse. Inizialmente, i miei polimorfi verranno interagiti solo come Target, non come singoli tipi di sottoclassi. La definizione di ciascuna sottoclasse nell'attività ricomincia a consumare lo scopo della relazione polimorfica.
  • Vedo che un modello per la tabella di join potrebbe essere in ordine, a me sembra un po 'corretto, tranne per il fatto che aggiunge una certa complessità. Presumo che Rails sarebbe disposto a farla finita. Mi sento inesperto su questo.

Sembra essere un piccolo buco nella funzionalità di binari o nella conoscenza collettiva della comunità. Quindi speriamo che StackOverflow possa cronologicamente la mia ricerca della risposta!

Grazie a tutti coloro che aiutano!

+0

Nei tuoi sei punti elenco, cinque di loro sono banali da realizzare se si lascia cadere la sesta, "Io non sto usando l'ereditarietà singola tabella". Per quanto riguarda il punto su STI di seguito, dal momento che le colonne in più ti infastidiscono, considera l'utilizzo della delega per distribuire i dati e il comportamento aggiuntivi ad altri modelli. – austinfromboston

+2

Spingendola verso l'esterno è ciò che ha provocato questo. STI però non è un'opzione. Vorrei che fosse perché, sì ... Ognuno è un grande fan di esso. Ma voglio che i dati archiviati siano coerenti e ci saranno diversi tipi di target. Ancora mi sorprende che non ci sia un modo per estrarre una raccolta mista come questa. Il mio design sembra abbastanza solido. –

+0

Sono stato in grado di realizzare la maggior parte delle funzionalità che desidero attraverso l'utilizzo di has_many_polymorphs. L'unica limitazione rimanente è che sono ancora bloccato definendo ogni tipo polimorfico nel mio genitore (Attività). Altre soluzioni sono le benvenute, ma non sono sicuro che una soluzione ci raggiungerà fino a quando una nuova versione di binari o un aggiornamento di has_many_polymorphs! –

risposta

0

Questa potrebbe non essere una risposta particolarmente utile, ma ha affermato semplicemente, non penso che ci sia un modo facile o automatico per farlo. Almeno, non così facile come con le associazioni più semplici a uno o molti.

Penso che la creazione di un modello ActiveRecord per la tabella di join sia il modo giusto per affrontare il problema. Una normale relazione has_and_belongs_to_many presuppone un join tra due tabelle specificate, mentre nel tuo caso sembra che tu voglia unirti tra tasks e uno qualsiasi di stores, softwares, offices o vehicles (a proposito, c'è un motivo per non usare STI qui? Sembra che contribuirebbe a ridurre la complessità limitando il numero di tabelle che hai). Quindi nel tuo caso, la tabella di join dovrebbe anche conoscere il nome della sottoclasse Target coinvolta. Qualcosa di simile

create_table :targets_tasks do |t| 
    t.integer :target_id 
    t.string :target_type 
    t.integer :task_id 
end 

Poi, nella tua classe Task, i tuoi Target sottoclassi, e la classe TargetsTask, si potrebbe istituire has_many associazioni utilizzando la parola chiave :through come documentato sul ActiveRecord::Associations::ClassMethods rdoc pages.

Tuttavia, questo ti porta solo a metà strada, perché :through non saprà di utilizzare il campo target_type come nome della sottoclasse Target. Per questo, potresti essere in grado di scrivere alcuni frammenti SQL select/finder personalizzati, anche documentati in ActiveRecord::Associations::ClassMethods.

Speriamo che questo ti spinga nella giusta direzione. Se trovi una soluzione completa, mi piacerebbe vederla!

+0

Fidati di me, ho battuto la testa su questo per un po '. Non appena avrò in mente qualcosa, sarà diffuso molto ampiamente. Sono un po 'sorpreso dal fatto che non sia stato affrontato prima dato che i set di risultati misti sono di gran moda al giorno d'oggi! :) –

+0

Inoltre, non sto usando STI perché è inefficiente e non nel modo in cui voglio memorizzare i miei dati. Probabilmente associerò comportamenti addizionali con sottoclassi di Target sopra e oltre i valori predefiniti.Grandi tavoli con un sacco di colonne non mi piacciono molto :) –

0

Sono d'accordo con gli altri, opterei per una soluzione che utilizza una combinazione di STI e la delega sarebbe molto più facile da implementare.

Il cuore del problema è dove memorizzare un record di tutte le sottoclassi di Target. ActiveRecord sceglie il database tramite il modello STI.

È possibile memorizzarli in una variabile di classe nella destinazione e utilizzare il callback ereditato per aggiungerne di nuovi. Quindi puoi generare dinamicamente il codice che ti servirà dal contenuto di quell'array e sfruttare method_missing.

0

Vi siete conseguire che bruta approccio forza:

class Task 
    has_many :stores 
    has_many :softwares 
    has_many :offices 
    has_many :vehicles 

    def targets 
    stores + softwares + offices + vehicles 
    end 
    ... 

Esso non può essere che elegante, ma ad essere onesti non è che verbose, e non c'è nulla di intrinsecamente inefficiente sul codice.

+0

Giusto, ma l'attività deve essere a conoscenza di ogni tipo di targetable. Mi piacerebbe essere in grado di creare un compito selezionabile e non dover modificare ogni volta che ne viene aggiunto uno nuovo. –

+0

È rubino. Si potrebbe scrivere un metodo "belongs_to_task" classe (o un modulo) da inserire nei vostri negozi, uffici ecc potrebbero modificare la classe Task, fare ciò che è fatto in precedenza (l'aggiunta di un has_many e patch gli obiettivi per includere ciò che la classe) . Non sto raccomandando questo approccio, sto solo dicendo ... – ndp

53

È possibile combinare il polimorfismo e has_many :through per ottenere una mappatura flessibile:

class Assignment < ActiveRecord::Base 
    belongs_to :task 
    belongs_to :target, :polymorphic => true 
end 

class Task < ActiveRecord::Base 
    has_many :targets, :through => :assignment 
end 

class Store < ActiveRecord::Base 
    has_many :tasks, :through => :assignment, :as => :target 
end 

class Vehicle < ActiveRecord::Base 
    has_many :tasks, :through => :assignment, :as => :target 
end 

... E così via.

+2

Questo sembra risolvere il problema molto semplicemente ed è supportato al 100% da Rails. –

+0

pulito e semplice – brettish

+1

Si noti che ': come => target' dovrebbe essere': come =>: target' in entrambi i casi. Bella soluzione –

1

La soluzione has_many_polymorphs che hai menzionato non è poi così male.

class Task < ActiveRecord::Base 
    has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle] 
end 

Sembra fare tutto quello che vuoi.

Esso fornisce i seguenti metodi:

a Task:

t = Task.first 
t.targets # Mixed collection of all targets associated with task t 
t.stores # Collection of stores associated with task t 
t.softwares # same but for software 
t.offices # same but for office 
t.vehicles # same but for vehicles 

al Software, Negozio, Ufficio, Veicolo:

s = Software.first # works for any of the subtargets. 
s.tasks    # lists tasks associated with s 

Se sto seguendo i commenti in modo corretto, il l'unico problema rimanente è che non si desidera modificare app/models/task.rb ogni volta che si crea un nuovo tipo di subtarget. Il modo Rails sembra richiedere la modifica di due file per creare un'associazione bidirezionale. has_many_polymorphs richiede solo di modificare il file delle attività. Sembra una vittoria per me. O almeno lo sarebbe se non fosse necessario modificare il nuovo file Model in ogni caso.

Ci sono alcuni modi per aggirare questo, ma sembrano troppo lavoro per evitare di cambiare un file ogni tanto. Ma se sei un set morto contro la modifica di Task da aggiungere alla relazione polimorfa, ecco il mio suggerimento:

Mantieni un elenco di sottoargomenti, ho intenzione di suggerire in lib/subtarget formattati una voce per riga che è essenzialmente table_name.underscore. (Le lettere maiuscole hanno un carattere di sottolineatura prefissato e quindi tutto è fatto in minuscolo)

store 
software 
office 
vehicle 

Crea config/inizializzatori/subtargets.rb e riempirlo con questo:

SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym) 

Avanti si sta andando a voler o crea un generatore personalizzato o una nuova attività rake. Per generare il nuovo subargomento e aggiungere il nome del modello al file dell'elenco dei subtarget, definito sopra. Probabilmente finirai per fare qualcosa di nudo che apporta la modifica e passa gli argomenti al generatore standard.

Mi dispiace, non ho davvero voglia di accompagnare il lettore attraverso che in questo momento, ma qui ci sono someresources

Infine sostituire l'elenco nella dichiarazione has_many_polymorphs con SubtargetList

class Task < ActiveRecord::Base 
    has_many_polymorphs :targets, :from => SubtargetList 
end 

Da questo punto in poi potresti aggiungere un nuovo subargomento con

$ script/generate subtarget_model home 

E questo aggiornerà automaticamente il tuo polimorfo lista ic una volta ricaricata la console o riavviato il server di produzione.

Come ho detto, è molto lavoro aggiornare automaticamente l'elenco dei subtarget. Tuttavia, se segui questa strada, puoi modificare il generatore personalizzato assicurandoti che tutte le parti necessarie del modello di subtarget siano presenti al momento della generazione.

1

Utilizzando STI:

class Task < ActiveRecord::Base 
end 

class StoreTask < Task 
    belongs_to :store, :foreign_key => "target_id" 
end 

class VehicleTask < Task 
    belongs_to :vehicle, :foreign_key => "target_id" 
end 

class Store < ActiveRecord::Base 
    has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id" 
end 

class Vehicle < ActiveRecord::Base 
    has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id" 
end 

Nel vostro databse che vi serve: Task type:string e Task target_id:integer

Il vantaggio è che ora avete un bel passaggio modello per ogni tipo di attività che può essere specifico.

Vedi anche STI and polymorphic model together

Cheers!

10

Anche se la soluzione proposta da da SFEley è grande, c'è un alcuni difetti:

  • Il recupero di attività da bersaglio (Store/veicolo) funziona, ma i all'indietro Wont. Questo è fondamentalmente perché non è possibile attraversare a: attraverso l'associazione a un tipo di dati polimorfo perché l'SQL non è in grado di indicare in quale tabella si trova.
  • Ogni modello con associazione: tramite ha bisogno di un'associazione diretta con la tabella intermedia
  • l': attraverso l'associazione assegnazione dovrebbe essere al plurale
  • l': come dichiarazione non funzionerà insieme a: attraverso, è necessario specificare in primo luogo con l'associazione diretta necessario con la tabella intermedia

con questo in mente, la mia soluzione più semplice sarebbe:

class Assignment < ActiveRecord::Base 
    belongs_to :task 
    belongs_to :target, :polymorphic => true 
end 

class Task < ActiveRecord::Base 
    has_many :assignments 
    # acts as the the 'has_many targets' needed 
    def targets 
    assignments.map {|x| x.target} 
    end 
end 

class Store < ActiveRecord::Base 
    has_many :assignments, as: :target 
    has_many :tasks, :through => :assignment 
end 

class Vehicle < ActiveRecord::Base 
    has_many :assignments, as: :target 
    has_many :tasks, :through => :assignment, :as => :target 
end 

Riferimenti: http://blog.hasmanythrough.com/2006/4/3/polymorphic-through

Problemi correlati