Intelligent ActiveRecord-modeller

ActiveRecord-modeller i Rails gjør allerede mye tung løft, når det gjelder databasetilgang og modellrelasjoner, men med litt arbeid kan de gjøre flere ting automatisk. La oss finne ut hvordan!


Trinn 1 - Opprett en Base Rails App

Denne ideen fungerer for alle typer ActiveRecord-prosjekter; Siden Rails er den vanligste, bruker vi det for eksempeleksemplet vårt. Appen vi skal bruke har mange brukere, hver av dem kan utføre en rekke handlinger på prosjekter .

Hvis du aldri har opprettet en Rails-app før, så les denne veiledningen eller pensum først. Ellers brann opp den gamle konsollen og skriv inn skinner nytt eksempel_app å opprette appen og deretter endre kataloger til den nye appen din med cd example_app.


Trinn 2 - Lag dine modeller og relasjoner

Først genererer vi brukeren som vil eie:

 skinner generere stillas Brukernavn: tekst e-post: streng password_hash: text

Sannsynligvis, i et ekte verdensprosjekt, ville vi ha noen flere felt, men dette vil gjøre for nå. La oss neste generere vår prosjektmodell:

 skinner generere stillas Prosjektnavn: tekst startet_at: datetime startet_by_id: heltall completed_at: datetime completed_by_id: heltall

Vi redigerer deretter den genererte project.rb fil for å beskrive forholdet mellom brukere og prosjekter:

 klasseprosjekt < ActiveRecord::Base belongs_to :starter, :class_name =>"User",: foreign_key => "started_by_id" belongs_to: completer,: class_name => "Bruker",: foreign_key => "completed_by_id" slutten

og det omvendte forholdet i user.rb:

 klassen bruker < ActiveRecord::Base has_many :started_projects, :foreign_key =>"started_by_id" har_many: completed_projects,: foreign_key => "completed_by_id" slutten

Deretter kjør en rask rake db: migrere, og vi er klare til å begynne å bli intelligente med disse modellene. Hvis bare å få forhold til modeller, var det like lett i den virkelige verden! Nå, hvis du noen gang har brukt Rails-rammeverket før, har du sikkert ikke lært noe ... ennå!


Trinn 3 - Faux Egenskaper er kjøligere enn Faux Leather

Det første vi skal gjøre er å bruke noen auto genererende felt. Du har lagt merke til at når vi opprettet modellen, opprettet vi en passord hash og ikke et passordfelt. Vi skal lage en fauxattributt for et passord som vil konvertere det til en hash hvis det er til stede.

Så, i modellen din, legger vi til en definisjon for dette nye passordfeltet.

 def passord = new_password) write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) avslutte def passord

Vi lagrer bare en hash mot brukeren, slik at vi ikke gir ut passordene uten litt kamp.

Den andre metoden betyr at vi returnerer noe for skjemaer som skal brukes.

Vi må også sikre at vi har Sha1-krypteringsbiblioteket lastet; Legg til krever 'sha1' til din application.rb fil etter linje 40: config.filter_parameters + = [: passord].

Når vi har endret appen på konfigureringsnivået, legger du det på nytt med en rask trykk tmp / restart.txt i konsollen din.

La oss nå endre standardskjemaet for å bruke dette i stedet for password_hash. Åpen _form.html.erb i app / modeller / brukere mappen:

 
<%= f.label :password_hash %>
<%= f.text_area :password_hash %>

blir

 
<%= f.label :password %>
<%= f.text_field :password %>

Vi gjør det til et faktisk passordfelt når vi er fornøyd med det.

Nå, last http: // localhost / brukere og ha et spill med å legge til brukere. Det skal se litt ut som bildet nedenfor; flott, er det ikke!

Vent, hva er det? Det overskriver passord hash hver gang du redigerer en bruker? La oss fikse det.

Åpne opp user.rb igjen, og endre det slik:

 write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) hvis new_password.present?

På denne måten blir feltet bare oppdatert når du oppgir et passord.


Trinn 4 - Automatisk datagaranti Nøyaktighet eller pengene tilbake

Den siste delen handlet om å endre dataene som modellen din får, men hva med å legge til mer informasjon basert på ting som allerede er kjent uten å måtte spesifisere dem? La oss se på det med prosjektmodellen. Begynn med å se på http: // localhost / prosjekter.

Gjør følgende endringer raskt.

* app / controllers / projects_controler.rb * linje 24

 # GET / prosjekter / nytt # GET /projects/new.json def new @project = Project.new @users = ["-", null) + User.all.collect | u | [u.name, u.id] reply_to | format | format.html # new.html.erb format.json gjør: json => @ prosjekt slutten # GET / prosjekter / 1 / rediger def edit @project = Project.find (params [: id]) @users = [ "-", null] + User.all.collect | u | [u.name, u.id] slutt

* app / visninger / prosjekter / _form.html.erb * linje 24

 <%= f.select :started_by_id, @users %>

* app / visninger / prosjekter / _form.html.erb * linje 24

 <%= f.select :completed_by , @users%>

I MVC-rammene er rollene klart definert. Modeller representerer dataene. Visninger viser dataene. Kontrollører får data og sender dem til visningen.

Hvem nyter å fylle ut dato / klokkeslettfelt?

Vi har nå en fullstendig fungerende form, men det bugser meg at jeg må sette start ved tid manuelt. Jeg vil gjerne ha det satt når jeg tildeler en started_by bruker. Vi kunne sette den i kontrolleren, men hvis du noen gang har hørt frasen "fete modeller, tynne kontrollere", vet du at dette gir dårlig kode. Hvis vi gjør dette i modellen, vil det fungere hvor som helst vi setter en startpakke eller komplett. La oss gjøre det.

Først redigere app / modeller / project.rb, og legg til følgende metode:

 def start_by = (bruker) hvis (user.present?) user = user.id hvis user.class == Bruker write_attribute (: startet_by_id, bruker) write_attribute (: startet_at, Time.now) slutten

Denne koden sikrer at noe faktisk har blitt bestått. Så, hvis det er en bruker, henter den sin ID og til slutt skriver både brukeren * og * tiden det skjedde - hellige røyker! La oss legge til det samme for Fullført av felt.

 def completed_by = (bruker) hvis (user.present?) user = user.id hvis user.class == Bruker write_attribute (: completed_by_id, bruker) write_attribute (: startet_at, Time.now) slutten

Rediger nå skjemavisningen slik at vi ikke har den aktuelle tiden. I app / visninger / prosjekter / _form.html.erb, fjern linjene 26-29 og 18-21.

Åpne opp http: // localhost / prosjekter og ta en tur!

Spot den forsettlige feilen

Whoooops! Noen (jeg tar varmen siden det er koden min) kutt og lim, og glemte å endre : started_at til : completed_at i den andre stort sett identiske (hint) attributtmetoden. Ikke biggie, endre det og alt er gå ... riktig?


Trinn 5 - Hjelp din fremtid selv ved å gjøre tillegg enklere

Så bortsett fra en liten kutte-og-lim-forvirring, tror jeg vi gjorde ganske god jobb, men det glir opp og koden rundt det plager meg litt. Hvorfor? Vel, la oss få en tanke:

  • Det er kuttet og lim inn duplisering: Tørk (Ikke repeter selv) er et prinsipp å følge.
  • Hva om noen vil legge til en annen somethingd_at og somethingd_by til vårt prosjekt, som, si, authorised_at og autorisert av>
  • Jeg kan forestille meg at noen av disse feltene blir lagt til.

Se og se, langs kommer en kjekk haired sjef og ber om, drumroll, authorised_at / by field og en suggested_at / by felt! Greit da; la oss få de klippe og lim inn fingrene klar da ... eller er det en bedre måte?

The Scary Art of Meta-progamming!

Det er riktig! Den hellige gral; de skummelene dine mødre advarte deg om. Det virker komplisert, men det kan faktisk være ganske enkelt - spesielt hva vi skal forsøke. Vi skal ta en rekke navn på stadier vi har, og deretter bygge disse metodene automatisk på fly. Spent? Flott.

Selvfølgelig må vi legge til feltene; så la oss legge til en migrering skinner genererer migrasjon additional_workflow_stages og legg til de feltene i den nylig genererte db / migrerer / TODAYSTIMESTAMP_additional_workflow_stages.rb.

 klasse 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

Overfør databasen din med rake db: migrere, og erstatt prosjektklassen med:

 klasseprosjekt < ActiveRecord::Base # belongs_to :starter, :class_name =>"User" # def started_by = (bruker) # if (user.present?) # User = user.id hvis user.class == Bruker # write_attribute (: startet_by_id, bruker) # write_attribute (: startet_tid, Time.now) # slutt # slutt # # def startet av # read_attribute (: completed_by_id) # slutten

Jeg har forlatt started_by der inne, så du kan se hvordan koden var før.

 [: start,: complete,: authorize,: suggeste] .each do | arg | ... MER ... slutten

Fint og forsiktig - går gjennom navnene (ish) av metodene vi ønsker å skape:

 [: start,: complete,: authorize,: 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 ... MER ... ende

For hvert av disse navnene trener vi ut de to modellattributtene vi setter inn f.eks started_by_id og started_at og navnet på foreningen f.eks. starter

 [: start,: complete,: authorize,: 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 belongs_to object_method_name,: class_name => "Bruker",: foreign_key => attr_by end

Dette virker ganske kjent. Dette er faktisk en Rails bit av metaprogrammering som allerede definerer en rekke metoder.

 [: start,: complete,: authorize,: 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 belongs_to object_method_name,: class_name => "Bruker",: foreign_key => attr_by get_method_name = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) avslutte

Ok, vi kommer til noen ekte metaprogrammering nå som beregner navnet på "få metode" - f.eks. started_by, og skaper da en metode, akkurat som vi gjør når vi skriver def metode, men i en annen form.

 [: start,: complete,: authorize,: 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 belongs_to object_method_name,: class_name => "Bruker",: 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) gjør | bruker | hvis user.present? user = user.id hvis user.class == Bruker write_attribute (attr_by, bruker) write_attribute (attr_at, Time.now) slutten slutten

Litt mer komplisert nå. Vi gjør det samme som før, men dette er sett Metodenavn. Vi definerer den metoden, ved hjelp av define (method_name) do | param | slutt, heller enn def method_name = (param).

Det var ikke så ille, var det?

Prøv det ut i skjemaet

La oss se om vi fortsatt kan redigere prosjekter som tidligere. Det viser seg at vi kan! Så vi legger til de ekstra feltene i skjemaet, og hei, presto!

app / visninger / prosjekt / _form.html.erb linje 20

 
<%= f.label :suggested_by %>
<%= f.select :suggested_by, @users %>
<%= f.label :authorised_by %>
<%= f.select :authorised_by, @users %>

Og til showvisningen ... så kan vi se det som fungerer.

* app / visninger-prosjekt / show.html.erb * linje 8

 

Foreslått på: <%= @project.suggested_at %>

Foreslått av: <%= @project.suggested_by_id %>

Godkjent på: <%= @project.authorised_at %>

Autorisert av: <%= @project.authorised_by_id %>

Ha en annen lek med http: // localhost / prosjekter, og du kan se vi har en vinner! Du trenger ikke å frykte hvis noen ber om et annet arbeidsflytstrinn; bare legg til migrering for databasen, og sett den i en rekke metoder ... og det blir opprettet. Tid til hvile? Kanskje, men jeg har bare to ting å merke seg.


Trinn 6 - Automatiser automasjonen

Det utvalg av metoder virker ganske nyttig for meg. Kan vi gjøre mer med det?

Først må vi gjøre listen over metodenavn konstant, slik at vi kan få tilgang til det fra utsiden.

 WORKFLOW_METHODS = [: start,: fullfør,: autoriser,: suggeste] WORKFLOW_METHODS.each do | arg | ... 

Nå kan vi bruke dem til å automatisk lage skjema og visninger. Åpne opp _form.html.erb for prosjekter, og la oss prøve det ved å erstatte linjene 19 -37 med koden nedenfor:

 <% Project::WORKFLOW_METHODS.each do |workflow| %> 
<%= f.label "#workflowd_by" %>
<%= f.select "#workflowd_by", @users %>
<% end %>

Men app / views-prosjekt / show.html.erb er der den virkelige magien er:

 

<%= notice %>

Navn:: <%= @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 %>

Dette bør være ganske klart, selv om du ikke er kjent med sende(), det er en annen måte å ringe en metode på. Så object.send ( "name_of_method") er det samme som object.name_of_method.

Final Sprint

Vi er nesten ferdige, men jeg har lagt merke til to feil: den ene er formatering, og den andre er litt mer alvorlig.

Den første er at mens jeg ser på et prosjekt, viser hele metoden en stygg Ruby-objektutgang. Snarere enn å legge til en metode til slutt, slik som dette

 @ Project.send (who_method) .name

La oss endre Bruker å ha en to_s metode. Hold ting i modellen hvis du kan, og legg dette til toppen av user.rb, og gjør det samme for project.rb også. Det er alltid fornuftig å ha en standardrepresentasjon for en modell som en streng:

 def to_s navn slutt

Føler litt dagligdagse skrivemetoder på den enkle måten nå, eh? Nei? Uansett, videre til mer alvorlige ting.

En faktisk feil

Når vi oppdaterer et prosjekt fordi vi sender alle arbeidsflytstadiene som tidligere er tildelt, blir alle tidsstemplene våre blandet sammen. Heldigvis, fordi all vår kode er på ett sted, vil en enkelt endring fikse dem alle.

 define_method (set_method_name) do | user | hvis user.present? user = user.id if user.class == Bruker # ADDITION HERE # Dette sikrer at den er endret fra den lagrede verdien før du setter den inn hvis read_attribute (attr_by) .to_i! = user.to_i write_attribute (attr_by, bruker) write_attribute (attr_at, Time .now) slutten slutten

Konklusjon

Hva har vi lært?

  • Hvis du legger til funksjonalitet i modellen, kan du forbedre resten av koden din på alvor
  • Metaprogrammering er ikke umulig
  • Foreslå et prosjekt kan bli logget
  • Å skrive smart i utgangspunktet betyr mindre arbeid senere
  • Ingen liker å kutte, lime og redigere og det forårsaker insekter
  • Smarte Modeller er sexy i alle samfunnslag

Takk så mye for å lese, og gi meg beskjed hvis du har noen spørsmål.