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!
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
.
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!
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.
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.
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å!
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?
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:
somethingd_at
och somethingd_by
till vårt projekt, som, säg, authorised_at
och auktoriserad av
>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?
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?
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.
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" %><% end %>
<%= f.select "#workflowd_by", @users %>
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
.
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.
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
Vad har vi lärt oss?
Tack så mycket för att läsa, och låt mig veta om du har några frågor.