I modelli ActiveRecord in Rails fanno già molti lavori pesanti, in termini di accesso al database e relazioni tra modelli, ma con un po 'di lavoro, possono fare più cose automaticamente. Scopriamo come!
Questa idea funziona per qualsiasi tipo di progetto ActiveRecord; tuttavia, poiché Rails è il più comune, lo useremo per la nostra app di esempio. L'app che useremo ha un sacco di utenti, ognuno dei quali può eseguire un numero di azioni su progetti .
Se non hai mai creato un'app di Rails prima, leggi prima questo tutorial o sillabo. Altrimenti, attiva la vecchia console e digita rota nuovo esempio_app
per creare l'app e quindi cambiare le directory con la tua nuova app cd esempio_app
.
Innanzitutto, generiamo l'utente che possiederà:
rails genera scaffold Nome utente: email di testo: stringa password_hash: testo
Probabilmente, in un progetto del mondo reale, avremmo alcuni campi in più, ma per ora lo farò. Iniziamo a generare il nostro modello di progetto:
rails genera scaffold Nome del progetto: text started_at: datetime started_by_id: intero completed_at: datetime completed_by_id: intero
Quindi modifichiamo il generato project.rb
file per descrivere la relazione tra utenti e progetti:
progetto di classe < ActiveRecord::Base belongs_to :starter, :class_name =>"Utente",: foreign_key => "started_by_id" appartiene a: completer,: class_name => "Utente",: foreign_key => "completed_by_id" fine
e la relazione inversa in user.rb
:
utente di classe < ActiveRecord::Base has_many :started_projects, :foreign_key =>"started_by_id" has_many: completed_projects,: foreign_key => "completed_by_id" fine
Quindi, esegui un rapido rake db: migrate
, e siamo pronti per iniziare a diventare intelligenti con questi modelli. Se solo ottenere relazioni con modelli fosse facile nel mondo reale! Ora, se hai mai usato il framework Rails, probabilmente non hai ancora imparato nulla ...!
La prima cosa che faremo è utilizzare alcuni campi di generazione automatica. Avrai notato che quando abbiamo creato il modello, abbiamo creato un hash della password e non un campo password. Creeremo un attributo faux per una password che lo convertirà in un hash se presente.
Quindi, nel tuo modello, aggiungeremo una definizione per questo nuovo campo password.
def password = new_password) write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) end def password "" end
Archiviamo solo un hash contro l'utente, quindi non stiamo dando le password senza un po 'di lotta.
Il secondo metodo significa che restituiamo qualcosa per i moduli da utilizzare.
Dobbiamo anche assicurarci di aver caricato la libreria di crittografia Sha1; Inserisci richiedere 'sha1'
alla tua application.rb
file dopo la riga 40: config.filter_parameters + = [: password]
.
Come abbiamo cambiato l'app a livello di configurazione, ricaricalo con un rapido tocca tmp / restart.txt
nella tua console.
Ora, cambiamo il modulo predefinito per usarlo al posto di password_hash
. Aperto _form.html.erb
nella cartella app / models / users:
<%= f.label :password_hash %>
<%= f.text_area :password_hash %>
diventa
<%= f.label :password %>
<%= f.text_field :password %>
Lo renderemo un vero campo per le password quando saremo soddisfatti.
Ora, carica http: // localhost / utenti
e giocare con l'aggiunta di utenti. Dovrebbe assomigliare un po 'all'immagine qui sotto; fantastico, non è vero?!
Aspetta, cos'è? Sovrascrive l'hash della password ogni volta che modifichi un utente? Risolviamolo.
Aprire user.rb
di nuovo, e cambialo in questo modo:
write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) se new_password.present?
In questo modo, solo quando si fornisce una password, il campo viene aggiornato.
L'ultima sezione riguardava la modifica dei dati ottenuti dal modello, ma che dire dell'aggiunta di ulteriori informazioni basate su cose già note senza doverle specificare? Diamo un'occhiata a questo con il modello del progetto. Inizia dando un'occhiata a http: // localhost / projects.
Apporta rapidamente le seguenti modifiche.
* app / controller / projects_controler.rb * riga 24
# GET / projects / new # GET /projects/new.json def new @project = Project.new @users = ["-", nil] + User.all.collect | u | [u.name, u.id] answer_to do | format | format.html # new.html.erb format.json render: json => @ project end end # GET / projects / 1 / edit def edit @project = Project.find (params [: id]) @users = [ "-", nil] + User.all.collect | u | [u.name, u.id] fine
* app / views / projects / _form.html.erb * line 24
<%= f.select :started_by_id, @users %>
* app / views / projects / _form.html.erb * line 24
<%= f.select :completed_by , @users%>
Nei framework MVC, i ruoli sono chiaramente definiti. I modelli rappresentano i dati. Le visualizzazioni mostrano i dati. I controllori ricevono i dati e li passano alla vista.
Ora abbiamo un modulo funzionante, ma mi infastidisce che devo impostare il iniziare a
tempo manualmente. Mi piacerebbe averlo impostato quando assegno un iniziato da
utente. Potremmo metterlo nel controller, tuttavia, se hai mai sentito la frase "modelli grassi, controller skinny", saprai che questo rende il codice errato. Se lo facciamo nel modello, funzionerà ovunque impostiamo l'iniziatore o il completamento. Facciamolo.
Prima modifica app / modelli / project.rb
, e aggiungere il seguente metodo:
def started_by = (utente) if (user.present?) user = user.id se user.class == Utente write_attribute (: started_by_id, utente) write_attribute (: started_at, Time.now) end end
Questo codice garantisce che qualcosa è stato effettivamente passato. Quindi, se si tratta di un utente, recupera il suo ID e alla fine scrive sia l'utente * che * l'ora in cui è successo: Holy Smokes! Aggiungiamo lo stesso per il completato da
campo.
def completed_by = (utente) if (user.present?) user = user.id se user.class == Utente write_attribute (: completed_by_id, utente) write_attribute (: started_at, Time.now) end end
Ora modifica la visualizzazione del modulo in modo da non avere quei selezionare il tempo. Nel app / views / progetti / _form.html.erb
, rimuovere le righe 26-29 e 18-21.
Aprire http: // localhost / progetti
e provate!
Whoooops! Qualcuno (prenderò il calore dal momento che è il mio codice) taglia e incolla, e ha dimenticato di cambiare il : started_at
a : completed_at
nel secondo metodo di attributo in gran parte identico (suggerimento). No biggie, cambia tutto e tutto è andato ... giusto?
Quindi, a parte una piccola confusione di tipo "taglia e incolla", penso che abbiamo fatto un buon lavoro, ma questo scivola e il codice intorno mi infastidisce un po '. Perché? Bene, pensiamo:
somethingd_at
e somethingd_by
al nostro progetto, come, per esempio, authorised_at
e autorizzato da
>Ecco, arriva un capo dai capelli a punta e chiede, drumroll, authorised_at / by field e un campo proposto / da campo! Bene allora; prendiamo quelle dita tagliate e incollate allora ... o c'è un modo migliore?
Giusto! Il Sacro Graal; la roba spaventosa di cui ti hanno avvertito le madri. Sembra complicato, ma in realtà può essere piuttosto semplice, specialmente quello che tenteremo. Prenderemo una serie di nomi di stadi che abbiamo, e quindi auto-costruiremo questi metodi al volo. Eccitato? grande.
Certo, dovremo aggiungere i campi; quindi aggiungiamo una migrazione le rotaie generano la migrazione additional_workflow_stages
e aggiungi quei campi all'interno dei nuovi generati db / migrate / TODAYSTIMESTAMP_additional_workflow_stages.rb
.
classe AdditionalWorkflowStages < ActiveRecord::Migration def up add_column :projects, :authorised_by_id, :integer add_column :projects, :authorised_at, :timestamp add_column :projects, :suggested_by_id, :integer add_column :projects, :suggested_at, :timestamp end def down remove_column :projects, :authorised_by_id remove_column :projects, :authorised_at remove_column :projects, :suggested_by_id remove_column :projects, :suggested_at end end
Migrazione del database con rake db: migrate
, e sostituire la classe dei progetti con:
progetto di classe < ActiveRecord::Base # belongs_to :starter, :class_name =>"Utente" # def start_by = (utente) # if (utente.presente?) # Utente = user.id se user.class == Utente # write_attribute (: started_by_id, utente) # write_attribute (: started_at, Time.now) # end # end # # def started_by # read_attribute (: completed_by_id) # end end
Ho lasciato il iniziato da
lì puoi vedere come era il codice prima.
[: starte,: complete,: autorize,: suggeste] .each do | arg | ... MORE ... end
Bello e gentile - passa attraverso i nomi (ish) dei metodi che vogliamo creare:
[: starte,: complete,: autorize,: suggeste] .each do | arg | attr_by = "# arg d_by_id" .to_sym attr_at = "# arg d_at" .to_sym object_method_name = "# arg r" .to_sym ... MORE ... end
Per ognuno di questi nomi, elaboriamo i due attributi del modello che stiamo impostando ad es started_by_id
e started_at
e il nome dell'associazione, ad es. antipasto
[: starte,: complete,: autorize,: suggeste] .each do | arg | attr_by = "# arg d_by_id" .to_sym attr_at = "# arg d_at" .to_sym object_method_name = "# arg r" .to_sym appartiene_to oggetto_metodo_nome,: class_name => "Utente",: foreign_key => attr_by fine
Questo sembra abbastanza familiare. Questo è in realtà un po 'Rails di metaprogrammazione che definisce una serie di metodi.
[: starte,: complete,: autorize,: suggeste] .each do | arg | attr_by = "# arg d_by_id" .to_sym attr_at = "# arg d_at" .to_sym object_method_name = "# arg r" .to_sym appartiene_to object_method_name,: class_name => "Utente",: foreign_key => attr_by get_method_name = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) fine
Ok, arriviamo ad una vera meta programmazione ora che calcola il nome 'get method' - ad es. iniziato da
, e poi crea un metodo, proprio come facciamo quando scriviamo metodo def
, ma in una forma diversa.
[: starte,: complete,: autorize,: suggeste] .each do | arg | attr_by = "# arg d_by_id" .to_sym attr_at = "# arg d_at" .to_sym object_method_name = "# arg r" .to_sym appartiene_to object_method_name,: class_name => "Utente",: foreign_key => attr_by get_method_name = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) set_method_name = "# arg d_by =". to_sym define_method (set_method_name) do | utente | se user.present? utente = user.id se user.class == Utente write_attribute (attr_by, utente) write_attribute (attr_at, Time.now) end end end
Un po 'più complicato ora. Facciamo lo stesso di prima, ma questo è il impostato nome del metodo. Definiamo quel metodo, usando define (method_name) do | param | fine
, piuttosto che def method_name = (param)
.
Non era poi così male, vero??
Vediamo se possiamo ancora modificare i progetti come prima. Si scopre che possiamo! Quindi aggiungeremo i campi aggiuntivi al modulo e, hey, presto!
app / views / progetto / _form.html.erb
riga 20
<%= f.label :suggested_by %>
<%= f.select :suggested_by, @users %><%= f.label :authorised_by %>
<%= f.select :authorised_by, @users %>
E per la vista spettacolo ... quindi possiamo vederlo funzionare.
* app / views-project / show.html.erb * riga 8
Suggerito a: <%= @project.suggested_at %>
Suggerito da: <%= @project.suggested_by_id %>
Autorizzato a: <%= @project.authorised_at %>
Autorizzato da: <%= @project.authorised_by_id %>
Fai un altro gioco con http: // localhost / progetti
, e puoi vedere che abbiamo un vincitore! Non c'è bisogno di temere se qualcuno chiede un altro passaggio del flusso di lavoro; semplicemente aggiungi la migrazione per il database e inseriscile nella serie di metodi ... e viene creata. È ora di riposare? Forse, ma ho solo altre due cose da prendere in considerazione.
Questa serie di metodi mi sembra abbastanza utile. Potremmo fare di più con esso?
Innanzitutto, rendiamo costante la lista dei nomi dei metodi in modo che possiamo accedervi dall'esterno.
WORKFLOW_METHODS = [: starte,: complete,: autorize,: suggeste] WORKFLOW_METHODS.each do | arg | ...
Ora, possiamo usarli per creare automaticamente forme e viste. Apri il _form.html.erb
per i progetti, proviamo sostituendo le righe 19 -37 con lo snippet seguente:
<% Project::WORKFLOW_METHODS.each do |workflow| %><%= f.label "#workflowd_by" %><% end %>
<%= f.select "#workflowd_by", @users %>
Ma app / views-progetto / show.html.erb
è dove la vera magia è:
<%= notice %>
Nome:: <%= @project.name %>
<% Project::WORKFLOW_METHODS.each do |workflow| at_method = "#workflowd_at" by_method = "#workflowd_by_id" who_method = "#workflowr" %><%= at_method.humanize %>:: <%= @project.send(at_method) %>
<%= who_method.humanize %>:: <%= @project.send(who_method) %>
<%= by_method.humanize %>:: <%= @project.send(by_method) %>
<% end %> <%= link_to 'Edit', edit_project_path(@project) %> | <%= link_to 'Back', projects_path %>
Questo dovrebbe essere abbastanza chiaro, anche se non hai familiarità con inviare()
, è un altro modo per chiamare un metodo. Così object.send ( "name_of_method")
equivale a object.name_of_method
.
Abbiamo quasi finito, ma ho notato due bug: uno è formattato e l'altro è un po 'più serio.
Il primo è che, mentre visualizzo un progetto, l'intero metodo mostra un brutto output di oggetti Ruby. Piuttosto che aggiungere un metodo alla fine, in questo modo
@ Project.send (who_method) .name
Modifichiamo Utente
avere un to_s
metodo. Mantieni le cose nel modello se puoi, e aggiungi questo in cima al user.rb
, e fare lo stesso per project.rb
anche. Ha sempre senso avere una rappresentazione predefinita per un modello come una stringa:
def to_s name end
Senti metodi di scrittura banali in un modo facile ora, eh? No? Comunque, su cose più serie.
Quando aggiorniamo un progetto perché inviamo tutte le fasi del flusso di lavoro che sono state assegnate in precedenza, tutti i nostri timestamp sono confusi. Fortunatamente, poiché tutto il nostro codice è in un posto, un singolo cambiamento li risolverà tutti.
define_method (set_method_name) do | user | se user.present? user = user.id if user.class == User # ADDITION HERE # Questo assicura che sia cambiato dal valore memorizzato prima di impostarlo se read_attribute (attr_by) .to_i! = user.to_i write_attribute (attr_by, user) write_attribute (attr_at, Time .now) end end end
Cosa abbiamo imparato?
Grazie mille per la lettura e fammi sapere se hai qualche domanda.