Modelli ActiveRecord intelligenti

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!


Passaggio 1: crea un'app per le barre di base

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.


Passaggio 2: crea i tuoi modelli e le tue relazioni

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 ...!


Passo 3 - Gli attributi di Faux sono più belli di Faux Leather

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.


Passaggio 4: la garanzia automatica dei dati garantisce la precisione o il rimborso

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.

Chi si diverte a compilare i campi Data / ora?

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!

Trova l'errore deliberato

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?


Passaggio 5: aiuta il tuo futuro a rendere più facili le aggiunte

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:

  • È una copia taglia e incolla: DRY (Non ripeterti) è un principio da seguire.
  • Cosa succede se qualcuno vuole aggiungerne un altro somethingd_at e somethingd_by al nostro progetto, come, per esempio, authorised_at e autorizzato da>
  • Posso immaginare che alcuni di questi campi vengano aggiunti.

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?

The Scary Art di Meta-progamming!

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??

Provalo nella forma

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.


Passaggio 6: automazione dell'automazione

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" %>
<%= f.select "#workflowd_by", @users %>
<% end %>

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.

Sprint finale

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.

Un bug reale

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

Conclusione

Cosa abbiamo imparato?

  • L'aggiunta di funzionalità al modello può seriamente migliorare il resto del codice
  • La meta-programmazione non è impossibile
  • Il suggerimento di un progetto potrebbe essere registrato
  • Scrivere in modo intelligente in primo luogo significa lavorare meno dopo
  • Nessuno si diverte a tagliare, incollare e modificare e causa bug
  • I modelli intelligenti sono sexy in tutti i settori della vita

Grazie mille per la lettura e fammi sapere se hai qualche domanda.