Intelligenta ActiveRecord-modeller

ActiveRecord-modeller i Rails gör redan mycket tunga lyft, när det gäller databasåtkomst och modellrelationer, men med lite arbete kan de göra fler saker automatiskt. Låt oss ta reda på hur!


Steg 1 - Skapa en basskenor App

Denna idé fungerar för alla typer av ActiveRecord-projekt. Men eftersom Rails är den vanligaste använder vi det för vår exemplarapp. Appen vi använder har massor av användare, var och en kan utföra ett antal åtgärder på projekt .

Om du aldrig har skapat en Rails-app tidigare läser du den här handledningen eller kursplanen först. I annat fall skjuter du upp den gamla konsolen och skriver skenar nytt exempel_app att skapa appen och sedan byta kataloger till din nya app med cd example_app.


Steg 2 - Skapa dina modeller och relationer

Först genererar vi användaren som kommer att äga:

 skenor generera ställning Användarnamn: text email: string password_hash: text

Sannolikt, i ett verkligt världsprojekt, skulle vi ha några fler fält, men det kommer att göra för nu. Låt oss nu generera vår projektmodell:

 rails generera ställning Projektnamn: text startat: datetime startat_by_id: heltal completed_at: datetime completed_by_id: heltal

Vi redigerar sedan den genererade project.rb fil för att beskriva förhållandet mellan användare och projekt:

 klassprojekt < ActiveRecord::Base belongs_to :starter, :class_name =>"User",: foreign_key => "started_by_id" belongs_to: completer,: class_name => "Användare",: foreign_key => "completed_by_id" slut

och det omvända förhållandet i user.rb:

 klass användare < ActiveRecord::Base has_many :started_projects, :foreign_key =>"started_by_id" har_many: completed_projects,: foreign_key => "completed_by_id" slutar

Kör sedan snabbt rake db: migrera, och vi är redo att börja bli intelligenta med dessa modeller. Om bara att få relationer med modeller var lika lätt i den verkliga världen! Nu, om du någonsin har använt Rails-ramen tidigare har du nog inte lärt dig någonting ... än!


Steg 3 - Faux attribut är kallare än Faux Leather

Det första vi ska göra är att använda några autogenererande fält. Du har märkt att när vi skapade modellen skapade vi ett lösenords hash och inte ett lösenordsfält. Vi ska skapa en fauxattribut för ett lösenord som konverterar det till en hash om det är närvarande.

Så, i din modell lägger vi till en definition för det här nya lösenordsfältet.

 def password = new_password) write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) slut def passord "" slutet

Vi lagrar bara en hash mot användaren så att vi inte ger ut lösenorden utan lite kamp.

Den andra metoden innebär att vi returnerar något för formulär att använda.

Vi måste också se till att vi har Sha1-krypteringsbiblioteket laddat Lägg till kräver "sha1" till din application.rb fil efter rad 40: config.filter_parameters + = [: password].

Eftersom vi har ändrat appen på konfigurationsnivån, ladda om den med en snabb tryck på tmp / restart.txt i din konsol.

Låt oss nu ändra standardformuläret för att använda det istället för password_hash. Öppna _form.html.erb i appen / modellerna / användarnas mapp:

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

blir

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

Vi gör det till ett faktiskt lösenordsfält när vi är nöjda med det.

Nu ladda http: // localhost / användare och spela med att lägga till användare. Det ska se lite ut som bilden nedan; bra, är det inte!

Vänta, vad är det där? Det skriver över ditt lösenordshastighet varje gång du redigerar en användare? Låt oss fixa det.

Öppna user.rb igen och ändra det som så:

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

På så sätt blir fältet uppdaterat endast när du anger ett lösenord.


Steg 4 - Automatiska datagarantier Noggrannhet eller pengarna tillbaka

Det sista avsnittet handlade om att ändra de data som din modell får, men hur är det med att lägga till mer information baserat på saker som redan är kända utan att behöva ange dem? Låt oss ta en titt på det med projektmodellen. Börja med att titta på http: // localhost / projects.

Gör följande ändringar snabbt.

* app / controllers / projects_controler.rb * linje 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 | format | format.html # new.html.erb format.json render: json => @ projekt slutänden # GET / projects / 1 / redigera def edit @project = Project.find (params [: id]) @users = [ "-", noll] + User.all.collect | u | [u.namn, u.id] slut

* app / views / projects / _form.html.erb * rad 24

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

* app / views / projects / _form.html.erb * rad 24

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

I MVC-ramar är rollerna tydligt definierade. Modeller representerar data. Visningar visar data. Controllers får data och skickar dem till vyn.

Vem trivs med att fylla i datum / tidfält?

Vi har nu en fullständig fungerande form, men det gör mig ojämn att jag måste ställa in börja på tid manuellt. Jag skulle vilja ha den inställd när jag tilldelar en startad av användare. Vi skulle kunna placera den i regulatorn, men om du någonsin har hört frasen "feta modeller, skinniga kontroller" vet du att det här är en dålig kod. Om vi ​​gör det i modellen, kommer det att fungera var som helst vi ställer in startaren eller komplettaren. Låt oss göra det.

Först redigera app / modeller / project.rb, och lägg till följande metod:

 def start_by = (användare) om (user.present?) user = user.id om user.class == Använd write_attribute (: started_by_id, användare) write_attribute (: started_at, Time.now) slutet

Denna kod garanterar att någonting faktiskt har passerat. Då, om det är en användare, hämtar den sitt ID och slutligen skriver både användaren * och * tiden det hände - heliga röker! Låt oss lägga till samma för fullgjord av fält.

 def completed_by = (användaren) om (user.present?) user = user.id om user.class == Användare write_attribute (: completed_by_id, användare) write_attribute (: started_at, Time.now) slutet

Redigera nu formulärvyn så att vi inte har den aktuella tiden. I app / views / projekt / _form.html.erb, ta bort linjerna 26-29 och 18-21.

Öppna http: // localhost / projekt och ha en gå!

Spot det avsiktliga misstaget

Whoooops! Någon (jag tar värmen eftersom det är min kod) klippa och klistra in och glömt att byta : started_at till : completed_at i den andra i stort sett identiska (hint) attributmetoden. Ingen biggie, ändra det och allt går ... rätt?


Steg 5 - Hjälp din framtid själv genom att göra tillägg enklare

Så, förutom en liten förvirring, tycker jag att vi gjorde ganska bra jobb, men det glider upp och koden runt det stör mig lite. Varför? Tja, låt oss tänka:

  • Det är klippt och klistra in dubbelarbete: Torka (Repetera inte dig själv) är en princip att följa.
  • Vad händer om någon vill lägga till en annan somethingd_at och somethingd_by till vårt projekt, som, säg, authorised_at och auktoriserad av>
  • Jag kan föreställa mig att några av dessa fält läggs till.

Titta och se, längs kommer en spetsig haig chef och ber om drumroll, authorised_at / by field och ett suggested_at / by fält! Okej då; låt oss få dem att klippa och klistra in fingrarna redo då ... eller finns det ett bättre sätt?

The Scary Art of Meta-progamming!

Det är rätt! Den heliga gralen; de läskiga sakerna som din mamma varnade för. Det verkar komplicerat, men det kan faktiskt vara ganska enkelt - särskilt vad vi ska försöka. Vi ska ta en uppsättning av namnen på de stadier vi har, och bygga sedan automatiskt dessa metoder på flugan. Upphetsad? Bra.

Självklart måste vi lägga till fälten; så låt oss lägga till en migrering rails generera migration additional_workflow_stages och lägg till de fälten i den nybildade db / migrera / TODAYSTIMESTAMP_additional_workflow_stages.rb.

 klass 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

Migrera din databas med rake db: migrera, och ersätt projektklassen med:

 klassprojekt < ActiveRecord::Base # belongs_to :starter, :class_name =>"User" # def started_by = (användare) # om (user.present?) # User = user.id om user.class == Användare # write_attribute (: started_by_id, användare) # write_attribute (: started_at, Time.now) # slutet # slut # # def start_by # read_attribute (: completed_by_id) # änden slut

Jag har lämnat startad av där inne så kan du se hur koden var förut.

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

Trevlig och mild - går igenom namnen (ish) av de metoder vi vill skapa:

 [: 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 ... än

För var och en av dessa namn utarbetar vi de två modellattributen vi ställer in t.ex. started_by_id och started_at och föreningsnamnet t ex. förrätt

 [: 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 => "Användare": foreign_key => attr_by end

Detta verkar ganska bekant. Det här är faktiskt en Rails bit av metaprogrammering som redan definierar en massa 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 => "Användare": foreign_key => attr_by get_method_name = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) avsluta

Ok, vi kommer till en verklig metaprogrammering nu som beräknar namnet 'get method' - t ex. startad av, och skapar sedan en metod, precis som vi gör när vi skriver def metod, men i en annan 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 => "Användare": 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) gör | användare | om user.present? user = user.id om user.class == Användar write_attribute (attr_by, user) write_attribute (attr_at, Time.now) slutet änden

Lite mer komplicerat nu. Vi gör detsamma som tidigare, men det här är uppsättning metodnamn. Vi definierar den metoden, med hjälp av define (method_name) do | param | slutet, hellre än def method_name = (param).

Det var inte så illa, var det?

Prova det i formuläret

Låt oss se om vi fortfarande kan redigera projekt som tidigare. Det visar sig att vi kan! Så vi lägger till de ytterligare fälten i formuläret, och hej, presto!

app / views / projekt / _form.html.erb linje 20

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

Och till showvisningen ... så kan vi se det fungera.

* app / views-project / show.html.erb * rad 8

 

Föreslagna på: <%= @project.suggested_at %>

Föreslaget av: <%= @project.suggested_by_id %>

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

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

Ha en annan lek med http: // localhost / projekt, och du kan se vi har en vinnare! Inget behov av att frukta om någon frågar efter ett annat arbetsflödessteg; Lägg helt enkelt till migreringen för databasen och sätt den i en rad metoder ... och det skapas. Dags för vila? Kanske, men jag har bara två saker att notera.


Steg 6 - Automatisera automationen

Den sortimentet av metoder verkar ganska användbart för mig. Kan vi göra mer med det?

Låt oss först göra listan över metodnamn en konstant så att vi kan komma åt det från utsidan.

 WORKFLOW_METHODS = [: start,: complete,: authorize,: suggeste] WORKFLOW_METHODS.each do | arg | ... 

Nu kan vi använda dem för att automatiskt skapa formulär och visningar. Öppna upp _form.html.erb för projekt, och låt oss försöka genom att ersätta raderna 19 -37 med nedanstående kod:

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

Men app / vyer-projekt / show.html.erb är där den verkliga magiken är:

 

<%= notice %>

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

Detta borde vara ganska tydligt, men om du inte är bekant med skicka(), det är ett annat sätt att ringa en metod. Så object.send ( "name_of_method") är det samma som object.name_of_method.

Final Sprint

Vi är nästan färdiga, men jag har märkt två fel: en är formatering och den andra är lite mer allvarlig.

Det första är att medan jag tittar på ett projekt visar hela metoden en grym Ruby-objektutmatning. Snarare än att lägga till en metod till slutet, så här

 @ Project.send (who_method) .name

Låt oss ändra Användare att ha en to_s metod. Håll saker i modellen om du kan, och lägg till detta till toppen av user.rb, och gör detsamma för project.rb också. Det är alltid meningsfullt att ha en standardrepresentation för en modell som en sträng:

 def till_s namn slut

Känner sig lite vardagliga skrivningsmetoder det enkla sättet nu, va? Nej? Hur som helst, på mer allvarliga saker.

En verklig bugg

När vi uppdaterar ett projekt eftersom vi skickar alla de arbetsflödesstadier som har tilldelats tidigare, är alla våra frimärken uppblandade. Lyckligtvis, eftersom all vår kod är på ett ställe, kommer en enda ändring att fixa dem alla.

 define_method (set_method_name) do | user | om user.present? user = user.id if user.class == Användare # ADDITION HERE # Detta säkerställer att det ändras från det lagrade värdet innan det ställs in om read_attribute (attr_by) .to_i! = user.to_i write_attribute (attr_by, user) write_attribute (attr_at, Time .now) slutet änden

Slutsats

Vad har vi lärt oss?

  • Att lägga till funktionalitet till modellen kan på ett allvarligt sätt förbättra resten av din kod
  • Metaprogrammering är inte omöjlig
  • Föreslå ett projekt kan bli loggad
  • Att skriva smart i första hand betyder mindre arbete senare
  • Ingen gillar att klippa, klistra och redigera och det orsakar buggar
  • Smarta Modeller är sexiga i alla samhällsskikt

Tack så mycket för att läsa, och låt mig veta om du har några frågor.