ActiveRecord-modellen in Rails doen al veel van het zware werk, in termen van databasetoegang en modelrelaties, maar met een beetje werk kunnen ze meer dingen automatisch doen. Laten we kijken hoe!
Dit idee werkt voor elk type ActiveRecord-project; Omdat Rails de meest voorkomende is, gebruiken we dat echter voor onze voorbeeld-app. De app die we gaan gebruiken heeft veel gebruikers, van elk kan een aantal acties worden uitgevoerd projecten .
Als u nog nooit eerder een Rails-app hebt gemaakt, leest u eerst deze zelfstudie of syllabus. Anders start u de oude console op en typt u rails nieuw voorbeeld_app
om de app te maken en verander vervolgens de mappen naar uw nieuwe app met cd example_app
.
Eerst genereren we de gebruiker die eigenaar is van:
rails genereren steiger Gebruikersnaam: sms email: string wachtwoord_hash: tekst
Waarschijnlijk hebben we in een project in de echte wereld nog een paar velden, maar dit zal voorlopig wel gebeuren. Laten we vervolgens ons projectmodel genereren:
rails genereren steiger Projectnaam: text started_at: datetime started_by_id: integer completed_at: datetime completed_by_id: integer
Vervolgens bewerken we de gegenereerde project.rb
bestand om de relatie tussen gebruikers en projecten te beschrijven:
klasse Project < ActiveRecord::Base belongs_to :starter, :class_name =>"Gebruiker",: foreign_key => "started_by_id" belong_to: completer,: class_name => "User",: foreign_key => "completed_by_id" end
en de omgekeerde relatie in user.rb
:
klasse Gebruiker < ActiveRecord::Base has_many :started_projects, :foreign_key =>"started_by_id" has_many: completed_projects,: foreign_key => "completed_by_id" end
Voer vervolgens een snel uit rake db: migreren
, en we zijn klaar om te beginnen intelligent te worden met deze modellen. Als het verkrijgen van relaties met modellen net zo makkelijk was in de echte wereld! Als u ooit eerder het Rails-framework hebt gebruikt, heeft u waarschijnlijk nog niets geleerd ...!
Het eerste dat we gaan doen is gebruik maken van een aantal auto-genererende velden. Het zal je zijn opgevallen dat we bij het maken van het model een wachtwoordhash en geen wachtwoordveld hebben gemaakt. We gaan een faux-attribuut maken voor een wachtwoord dat het naar een hash omzet als het aanwezig is.
In uw model voegen we een definitie toe voor dit nieuwe wachtwoordveld.
def password = new_password) write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) end def password "" end
We slaan alleen een hash op tegen de gebruiker, dus we geven de wachtwoorden niet uit zonder een beetje te vechten.
De tweede methode betekent dat we iets terugsturen voor formulieren om te gebruiken.
We moeten er ook voor zorgen dat de Sha1-coderingsbibliotheek geladen is; toevoegen vereisen 'sha1'
aan jouw application.rb
bestand na regel 40: config.filter_parameters + = [: wachtwoord]
.
Omdat we de app op configuratieniveau hebben gewijzigd, laadt u deze snel opnieuw raak tmp / restart.txt aan
in je console.
Laten we nu het standaardformulier wijzigen om dit te gebruiken in plaats van password_hash
. Open _form.html.erb
in de map app / models / users:
<%= f.label :password_hash %>
<%= f.text_area :password_hash %>
wordt
<%= f.label :password %>
<%= f.text_field :password %>
We zullen er een echt wachtwoordveld van maken als we er blij mee zijn.
Nu, laad http: // localhost / gebruikers
en speel met het toevoegen van gebruikers. Het zou een beetje moeten lijken op de onderstaande afbeelding; geweldig, is het niet!
Wacht, wat is dat? Het overschrijft je wachtwoordhash elke keer dat je een gebruiker bewerkt? Laten we dat oplossen.
Doe open user.rb
nogmaals, en verander het als volgt:
write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) als new_password.present?
Op deze manier wordt het veld alleen bijgewerkt wanneer u een wachtwoord opgeeft.
Het laatste deel ging over het wijzigen van de gegevens die uw model krijgt, maar hoe zit het met het toevoegen van meer informatie op basis van dingen die al bekend zijn zonder ze te moeten opgeven? Laten we daar eens naar kijken met het projectmodel. Begin met het bekijken van http: // localhost / projects.
Breng snel de volgende wijzigingen aan.
* app / controllers / projects_controler.rb * regel 24
# GET / projects / new # GET /projects/new.json def new @project = Project.new @users = ["-", nil] + User.all.collect | u | [u.name, u.id] response_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 = [ "-", nihil] + User.all.collect | u | [u.name, u.id] einde
* app / views / projects / _form.html.erb * regel 24
<%= f.select :started_by_id, @users %>
* app / views / projects / _form.html.erb * regel 24
<%= f.select :completed_by , @users%>
In MVC-frameworks zijn de rollen duidelijk gedefinieerd. Modellen vertegenwoordigen de gegevens. Weergaven geven de gegevens weer. Controllers krijgen gegevens en geven deze door aan de weergave.
We hebben nu een volledig functionerende vorm, maar het irriteert me dat ik de begin bij
tijd handmatig. Ik zou het graag willen laten instellen als ik een gestart door
gebruiker. We zouden het in de controller kunnen stoppen, maar als je ooit de uitdrukking "dikke modellen, magere controllers" hebt gehoord, weet je dat dit slechte code oplevert. Als we dit in het model doen, werkt het overal waar we een starter of completer instellen. Laten we dat doen.
Eerste bewerking app / modellen / project.rb
, en voeg de volgende methode toe:
def started_by = (user) if (user.present?) user = user.id if user.class == Gebruiker write_attribute (: started_by_id, user) write_attribute (: started_at, Time.now) end end
Deze code zorgt ervoor dat er daadwerkelijk iets is gepasseerd. Dan, als het een gebruiker is, haalt het zijn ID op en schrijft tenslotte zowel de gebruiker * als * de tijd dat het gebeurde - heilige rookt! Laten we hetzelfde toevoegen voor de Afgemaakt door
veld-.
def completed_by = (user) if (user.present?) user = user.id if user.class == Gebruiker write_attribute (: completed_by_id, user) write_attribute (: started_at, Time.now) end end
Bewerk de formulierweergave nu, zodat we die tijd niet hebben geselecteerd. In app / views / projecten / _form.html.erb
, verwijder regels 26-29 en 18-21.
Doe open http: // localhost / projecten
en probeer het!
Whoooops! Iemand (ik neem het vuur omdat het mijn code is) knippen en plakken, en vergat het te veranderen :begon bij
naar : completed_at
in de tweede grotendeels identieke (hint) attribuutmethode. Geen buistelevisie, verander dat en alles gaat ... juist?
Dus afgezien van een beetje knip-en-plak verwarring, denk ik dat we het redelijk goed gedaan hebben, maar dat is een vergissing en de code eromheen stoort me een beetje. Waarom? Laten we nadenken:
somethingd_at
en somethingd_by
naar ons project, laten we zeggen, authorised_at
en geautoriseerd door
>Lo en zie, langs komt een puntige haired werkgever en vraagt, drumroll, authorised_at / door veld en een gesuggereerd_at / door veld! Goed dan; laten we die knip- en plakvingers klaar maken ... of is er een betere manier?
Dat is juist! De Heilige graal; de enge dingen waar je moeders je voor waarschuwden. Het lijkt ingewikkeld, maar eigenlijk kan het vrij eenvoudig zijn - vooral wat we gaan proberen. We nemen een reeks namen van podia die we hebben en bouwen deze methoden vervolgens meteen op. Opgewonden? Super goed.
Natuurlijk moeten we de velden toevoegen; dus laten we een migratie toevoegen rails genereren migratie additional_workflow_stages
en voeg die velden toe aan de nieuw gegenereerde velden db / migrate / TODAYSTIMESTAMP_additional_workflow_stages.rb
.
class 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
Migreer uw database met rake db: migreren
, en vervang de projectklasse door:
klasse Project < ActiveRecord::Base # belongs_to :starter, :class_name =>"Gebruiker" # def started_by = (gebruiker) # if (user.present?) # User = user.id if user.class == User # write_attribute (: started_by_id, user) # write_attribute (: started_at, Time.now) # end # end # # def started_by # read_attribute (: completed_by_id) # end end
Ik heb de gestart door
daar in zodat je kunt zien hoe de code ervoor stond.
[: starte,: complete,: authorize,: suggeste] .each do | arg | ... MORE ... end
Leuk en zachtaardig - doorloopt de namen (ish) van de methoden die we willen creëren:
[: starte,: 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 ... MEER ... einde
Voor elk van deze namen berekenen we de twee modelkenmerken die we bijvoorbeeld instellen started_by_id
en begon bij
en de naam van de associatie, b.v.. beginner
[: starte,: 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 belong_to object_method_name,: class_name => "User",: foreign_key => attr_by end
Dit lijkt redelijk bekend. Dit is eigenlijk al een Rails-bit van metaprogrammering dat een aantal methoden definieert.
[: starte,: 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 belong_to object_method_name,: class_name => "Gebruiker",: foreign_key => attr_by get_method_name = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) einde
Ok, we komen nu bij een echte metaprogrammering die de 'get methode' naam berekent - bijv. gestart door
, en maakt vervolgens een methode, net zoals we doen als we schrijven def methode
, maar in een andere vorm.
[: starte,: 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 belong_to object_method_name,: class_name => "Gebruiker",: 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 | user | als user.present? user = user.id if user.class == Gebruiker write_attribute (attr_by, user) write_attribute (attr_at, Time.now) end end end
Een beetje ingewikkelder nu. We doen hetzelfde als voorheen, maar dit is het reeks methode naam. We definiëren die methode, met behulp van define (methode_naam) do | param | einde
, liever dan def method_name = (param)
.
Dat was niet zo erg, toch??
Laten we kijken of we nog steeds projecten kunnen bewerken zoals voorheen. Het blijkt dat we dat kunnen! Dus we voegen de extra velden toe aan het formulier, en, hé, presto!
app / views / project / _form.html.erb
regel 20
<%= f.label :suggested_by %>
<%= f.select :suggested_by, @users %><%= f.label :authorised_by %>
<%= f.select :authorised_by, @users %>
En naar de showweergave ... zodat we het kunnen zien werken.
* app / views-project / show.html.erb * regel 8
Voorgesteld bij: <%= @project.suggested_at %>
Gesuggereerd door: <%= @project.suggested_by_id %>
Geautoriseerd op: <%= @project.authorised_at %>
Geautoriseerd door: <%= @project.authorised_by_id %>
Speel nog een keer met http: // localhost / projecten
, en je kunt zien dat we een winnaar hebben! Je hoeft niet bang te zijn als iemand om een andere workflowstap vraagt; voeg simpelweg de migratie voor de database toe en plaats deze in de reeks methoden ... en deze wordt aangemaakt. Tijd voor rust? Misschien, maar ik heb nog twee dingen om op te merken.
Die reeks methoden lijkt mij heel nuttig. Zouden we er meer mee kunnen doen?
Laten we eerst de lijst met methode-namen constant maken, zodat we deze van buitenaf kunnen bekijken.
WORKFLOW_METHODS = [: starte,: complete,: authorize,: suggeste] WORKFLOW_METHODS.each do | arg | ...
Nu kunnen we ze gebruiken om automatisch vorm en weergaven te maken. Open de _form.html.erb
voor projecten, en laten we het proberen door regels 19 -37 te vervangen door het onderstaande fragment:
<% Project::WORKFLOW_METHODS.each do |workflow| %><%= f.label "#workflowd_by" %><% end %>
<%= f.select "#workflowd_by", @users %>
Maar app / views-project / show.html.erb
is waar de echte magie is:
<%= notice %>
Naam:: <%= @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 %>
Dit moet vrij duidelijk zijn, hoewel, als u niet bekend bent met sturen()
, het is een andere manier om een methode te noemen. Zo object.send ( "name_of_method")
is hetzelfde als object.name_of_method
.
We zijn bijna klaar, maar ik heb twee bugs opgemerkt: de ene is aan het formatteren en de andere is een beetje serieuzer.
De eerste is dat, terwijl ik een project bekijk, de hele methode een lelijke Ruby-objectuitvoer vertoont. In plaats van een methode aan het einde toe te voegen, zoals deze
@ Project.send (who_method) .name
Laten we aanpassen Gebruiker
om een te hebben to_s
methode. Bewaar dingen in het model als je kunt en voeg dit toe aan de top van de user.rb
, en doe hetzelfde voor project.rb
ook. Het is altijd zinvol om een standaardrepresentatie voor een model als een tekenreeks te hebben:
def to_s naam einde
Voelt een beetje alledaagse schrijfmethoden op de gemakkelijke manier nu, eh? Nee? Hoe dan ook, op tot meer serieuze dingen.
Wanneer we een project bijwerken omdat we alle workflowfasen verzenden die eerder zijn toegewezen, worden al onze tijdstempels gemixt. Gelukkig, omdat al onze code op één plaats staat, zal een enkele wijziging ze allemaal oplossen.
define_method (set_method_name) do | user | als user.present? user = user.id if user.class == Gebruiker # ADDITION HERE # Dit zorgt ervoor dat het van de opgeslagen waarde wordt veranderd voordat het wordt ingesteld als read_attribute (attr_by) .to_i! = user.to_i write_attribute (attr_by, user) write_attribute (attr_at, Time .now) einde end end
Wat hebben we geleerd??
Heel erg bedankt voor het lezen en laat het me weten als je nog vragen hebt.