Intelligente ActiveRecord-modellen

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!


Stap 1 - Maak een Base Rails-app

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.


Stap 2 - Maak uw modellen en relaties

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


Stap 3 - Fauxattributen zijn cooler dan kunstleer

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.


Stap 4 - Automatische gegevens garanderen nauwkeurigheid of uw geld terug

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.

Wie geniet van de invuldatum / tijdvelden?

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!

Zoek de opzettelijke fout

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?


Stap 5 - Help je toekomstige zelf door toevoegingen gemakkelijker te maken

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:

  • Het is knippen en plakken van duplicatie: DROOG (jezelf niet herhalen) is een te volgen principe.
  • Wat als iemand er nog een wil toevoegen? somethingd_at en somethingd_by naar ons project, laten we zeggen, authorised_at en geautoriseerd door>
  • Ik kan me voorstellen dat nogal wat van deze velden worden toegevoegd.

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?

The Scary Art of Meta-progamming!

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

Probeer het uit in het formulier

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.


Stap 6 - Automatiseer de automatisering

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

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.

Laatste sprint

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.

Een echte bug

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

Conclusie

Wat hebben we geleerd??

  • Het toevoegen van functionaliteit aan het model kan de rest van uw code aanzienlijk verbeteren
  • Metaprogrammering is niet onmogelijk
  • Het voorstellen van een project kan worden vastgelegd
  • Slim schrijven betekent in de eerste plaats minder werk later
  • Niemand houdt van knippen, plakken en bewerken en het veroorzaakt bugs
  • Slimme modellen zijn sexy in alle lagen van de bevolking

Heel erg bedankt voor het lezen en laat het me weten als je nog vragen hebt.