Intelligente ActiveRecord-Modelle

ActiveRecord-Modelle in Rails erledigen bereits viel Gewicht, was Datenbankzugriffe und Modellbeziehungen angeht. Mit etwas Arbeit können sie jedoch mehr Aufgaben automatisch erledigen. Lass uns herausfinden wie!


Schritt 1 - Erstellen Sie eine Base Rails-App

Diese Idee funktioniert für alle Arten von ActiveRecord-Projekten. Da Rails jedoch am häufigsten verwendet wird, verwenden wir dies für unsere Beispiel-App. Die App, die wir verwenden werden, hat viele Benutzer, Jeder von ihnen kann eine Reihe von Aktionen ausführen Projekte .

Wenn Sie noch nie zuvor eine Rails-App erstellt haben, lesen Sie zuerst dieses Tutorial oder diesen Lehrplan. Starten Sie andernfalls die alte Konsole und geben Sie den Typ ein Schienen neu example_app um die App zu erstellen und wechseln Sie dann die Verzeichnisse in Ihre neue App cd example_app.


Schritt 2 - Erstellen Sie Ihre Modelle und Beziehungen

Zunächst generieren wir den Benutzer, der Eigentümer sein wird:

 Schienen erzeugen Gerüst Benutzername: Text E-Mail: Zeichenfolge Passwort_Hash: Text

In einem realen Projekt hätten wir wahrscheinlich noch ein paar weitere Felder, aber das reicht für den Moment. Lassen Sie uns als nächstes unser Projektmodell generieren:

 Schienen erzeugen ein Gerüst. Projektname: text started_at: datetime started_by_id: integer complet_at: datetime completed_by_id: integer

Wir bearbeiten dann das generierte project.rb Datei zur Beschreibung der Beziehung zwischen Benutzern und Projekten:

 Klasse Projekt < ActiveRecord::Base belongs_to :starter, :class_name =>"User",: foreign_key => "started_by_id" gehört zu_to: completer,: class_name => "User",: foreign_key => "complet_by_id"

und die umgekehrte Beziehung in user.rb:

 Klasse Benutzer < ActiveRecord::Base has_many :started_projects, :foreign_key =>"started_by_id" has_many: completed_projects,: foreign_key => "complet_by_id" end

Führen Sie als Nächstes eine schnelle aus rake db: migrieren, und wir sind bereit, mit diesen Modellen intelligent zu werden. Wenn es nur so einfach wäre, Beziehungen zu Modellen aufzubauen? Wenn Sie das Rails-Framework schon einmal verwendet haben, haben Sie wahrscheinlich noch nichts gelernt… noch nicht!


Schritt 3 - Faux-Attribute sind kühler als Kunstleder

Als erstes verwenden wir einige Felder, die automatisch generiert werden. Sie haben bemerkt, dass wir beim Erstellen des Modells einen Kennwort-Hash und kein Kennwortfeld erstellt haben. Wir werden ein Faux-Attribut für ein Passwort erstellen, das es in einen Hash umwandelt, wenn es vorhanden ist.

In Ihrem Modell fügen wir also eine Definition für dieses neue Kennwortfeld hinzu.

 def password = neues_kennwort) write_attribute (: password_hash, SHA1 :: hexdigest (neues_kennwort)) ende def kennwort "" ende

Wir speichern nur einen Hash gegen den Benutzer, so dass wir die Passwörter nicht ohne einen Streit abgeben.

Die zweite Methode bedeutet, dass wir etwas zurückgeben, damit Formulare verwendet werden können.

Wir müssen auch sicherstellen, dass die Sha1-Verschlüsselungsbibliothek geladen ist. hinzufügen benötige 'sha1' zu deinem application.rb Datei nach Zeile 40: config.filter_parameters + = [: Kennwort].

Da wir die App auf der Konfigurationsebene geändert haben, laden Sie sie schnell wieder Berühren Sie tmp / restart.txt in deiner Konsole.

Jetzt ändern wir das Standardformular, um dieses anstelle von zu verwenden password_hash. Öffnen _form.html.erb im app / models / users Ordner:

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

wird

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

Wir machen es zu einem echten Passwortfeld, wenn wir damit zufrieden sind.

Laden Sie jetzt http: // localhost / users und spielen mit dem Hinzufügen von Benutzern. Es sollte ein bisschen wie das Bild unten aussehen; großartig, nicht wahr?!

Warten Sie, was ist das? Wird der Kennwort-Hash jedes Mal überschrieben, wenn Sie einen Benutzer bearbeiten? Lass uns das reparieren.

Aufmachen user.rb wieder und ändern Sie es wie folgt:

 write_attribute (: password_hash, SHA1 :: hexdigest (neues_Kennwort)), wenn neues_Kennwort.present?

Auf diese Weise wird das Feld nur aktualisiert, wenn Sie ein Kennwort eingeben.


Schritt 4 - Automatische Daten garantiert Genauigkeit oder Geld zurück

Im letzten Abschnitt ging es nur darum, die Daten zu ändern, die Ihr Modell erhält, aber wie würden Sie weitere Informationen hinzufügen, die auf bereits bekannten Informationen basieren, ohne sie angeben zu müssen? Schauen wir uns das mit dem Projektmodell an. Beginnen Sie mit einem Blick auf http: // localhost / projects.

Nehmen Sie die folgenden Änderungen schnell vor.

* app / controller / projects_controler.rb * zeile 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 | 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 = [ "-", nil] + User.all.collect | u | [u.name, u.id] end

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

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

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

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

In MVC-Frameworks sind die Rollen klar definiert. Modelle repräsentieren die Daten. Ansichten zeigen die Daten an. Controller erhalten Daten und geben sie an die Ansicht weiter.

Wer genießt es, Datum / Uhrzeit-Felder auszufüllen?

Wir haben jetzt eine voll funktionsfähige Form, aber es nervt mich, dass ich das einstellen muss anfangen bei Zeit manuell. Ich möchte es einstellen lassen, wenn ich eine started_by Nutzer. Wir könnten es in den Controller stecken, aber wenn Sie jemals die Phrase "fette Modelle, dünne Controller" gehört haben, wissen Sie, dass dies für schlechten Code sorgt. Wenn wir dies im Modell tun, funktioniert es überall, wo wir den Starter oder Komplementär setzen. Lass uns das tun.

Erste Bearbeitung app / models / project.rb, und fügen Sie die folgende Methode hinzu:

 def started_by = (Benutzer) if (user.present?) user = user.id if user.class == Benutzer write_attribute (: started_by_id, user) write_attribute (: started_at, Time.now) end end

Dieser Code stellt sicher, dass tatsächlich etwas übergeben wurde. Wenn es sich um einen Benutzer handelt, ruft er seine ID ab und schreibt schließlich sowohl den Benutzer * als auch den Zeitpunkt, zu dem es passiert ist - heilig raucht! Fügen wir das gleiche für das hinzu vervollständigt von Feld.

 def complet_by = (Benutzer) if (user.present?) user = user.id if user.class == Benutzer write_attribute (: complet_by_id, user) write_attribute (: started_at, Time.now) end end

Bearbeiten Sie nun die Formularansicht, damit diese Zeit nicht ausgewählt wird. Im app / views / projects / _form.html.erb, Zeilen 26-29 und 18-21 entfernen.

Aufmachen http: // localhost / projects und versuchen Sie es!

Finde den absichtlichen Fehler

Whoooops! Jemand (ich nehme die Hitze, da es mein Code ist), schneide und füge ein und vergaß das zu ändern :fing an bei zu : complet_at in der zweiten weitgehend identischen Attributmethode. Kein Biggie, ändere das und alles ist gut ... richtig?


Schritt 5 - Helfen Sie Ihrem zukünftigen Selbst, indem Sie Ergänzungen einfacher machen

Abgesehen von einer kleinen Verwirrung, bei der es nur wenige Schnitte gibt, glaube ich, dass wir ziemlich gute Arbeit geleistet haben, aber das rutscht ab und der Code stört mich ein bisschen. Warum? Nun, denken wir mal:

  • Es ist eine Kopie von Kopieren und Einfügen: TROCKEN (sich nicht wiederholen) ist ein Prinzip, dem zu folgen ist.
  • Was ist, wenn jemand einen anderen hinzufügen möchte somethingd_at und somethingd_by zu unserem Projekt, sagen wir, authorised_at und authorisiert von>
  • Ich kann mir vorstellen, dass einige dieser Felder hinzugefügt werden.

Sieh mal, ein spitzhaariger Boss kommt und fragt nach drumroll, authorised_at / by field und ein suggest_at / by Feld! Also gut; lasst uns dann die Finger schneiden und einfügen ... oder gibt es einen besseren Weg??

Die unheimliche Kunst der Metaprogrammierung!

Stimmt! Der Heilige Gral; das gruselige Zeug, vor dem deine Mütter gewarnt haben. Es scheint kompliziert zu sein, kann aber ziemlich einfach sein - vor allem was wir versuchen werden. Wir werden ein Array mit den Namen der einzelnen Stufen nehmen und diese Methoden dann automatisch erstellen. Aufgeregt? Großartig.

Natürlich müssen wir die Felder hinzufügen. Fügen wir also eine Migration hinzu Schienen generieren die Migration additional_workflow_stages und fügen Sie diese Felder in das neu generierte Feld ein db / migrate / 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

Migrieren Sie Ihre Datenbank mit rake db: migrieren, und ersetzen Sie die Projektklasse durch:

 Klasse Projekt < ActiveRecord::Base # belongs_to :starter, :class_name =>"User" # def started_by = (Benutzer) # if (user.present?) # User = user.id if user.class == Benutzer # write_attribute (: started_by_id, Benutzer) # write_attribute (: started_at, Time.now) # end # end # # def started_by # read_attribute (: complet_by_id) # end end

Ich habe die verlassen started_by Dort kann man sehen, wie der Code vorher war.

 [: starte,: complete,: authorise,: suggeste] .each do | arg |… MORE… ende

Schön und sanft - geht durch die Namen (ish) der Methoden, die wir erstellen möchten:

 [: starte,: complete,: authorise,: 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… MORE… end

Für jeden dieser Namen ermitteln wir die zwei Modellattribute, die wir setzen, z started_by_id und fing an bei und der Name der Vereinigung, z.B.. Anlasser

 [: starte,: complete,: authorise,: 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 gehört zu to_method_name,: class_name => "user",:

Das kommt mir ziemlich bekannt vor. Dies ist eigentlich ein Rails-Bit der Metaprogrammierung, das eine Reihe von Methoden definiert.

 [: starte,: complete,: authorise,: suggeste] .each do | arg | attr_by = "# arg d_by_id" .to_sym attr_at = "# arg d_at" .to_sym object_method_name = "# arg" .r_ = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) end

Ok, wir kommen jetzt zu einer echten Meta-Programmierung, die den Namen der Get-Methode berechnet - z. started_by, und erstellt dann eine Methode, genau wie wir es beim Schreiben tun def Methode, aber in einer anderen Form.

 [: starte,: complete,: authorise,: suggeste] .each do | arg | attr_by = "# arg d_by_id" .to_sym attr_at = "# arg d_at" .to_sym object_method_name = "# arg" .r_ = "# 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 | wenn user.present? user = user.id if user.class == Benutzer write_attribute (attr_by, user) write_attribute (attr_at, Time.now) end end end

Jetzt etwas komplizierter. Wir machen das gleiche wie vorher, aber das ist das einstellen Methodenname. Wir definieren diese Methode mit define (Methodenname) do | param | Ende, eher, als def method_name = (param).

Das war nicht so schlimm, oder??

Probieren Sie es im Formular aus

Mal sehen, ob wir Projekte nach wie vor bearbeiten können. Es stellt sich heraus, dass wir können! Also fügen wir die zusätzlichen Felder zum Formular hinzu und, hey, presto!

app / views / project / _form.html.erb Zeile 20

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

Und zur Showansicht… damit wir sehen können, wie es funktioniert.

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

 

Vorgeschlagen bei: <%= @project.suggested_at %>

Vorgeschlagen von: <%= @project.suggested_by_id %>

Zugelassen bei: <%= @project.authorised_at %>

Authorisiert von: <%= @project.authorised_by_id %>

Habe ein anderes Spiel mit http: // localhost / projects, und du kannst sehen, wir haben einen Gewinner! Keine Angst, wenn jemand nach einem anderen Workflow-Schritt fragt; Fügen Sie einfach die Migration für die Datenbank hinzu und fügen Sie sie in das Array der Methoden ein. Dann wird sie erstellt. Zeit für eine Pause? Vielleicht, aber ich muss nur noch zwei Dinge beachten.


Schritt 6 - Automatisieren Sie die Automatisierung

Diese Reihe von Methoden erscheint mir sehr nützlich. Könnten wir mehr damit machen??

Lassen Sie uns zunächst die Liste der Methodennamen zu einer Konstanten machen, sodass wir von außen darauf zugreifen können.

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

Jetzt können wir sie zum automatischen Erstellen von Formularen und Ansichten verwenden. Öffne die _form.html.erb für Projekte, und versuchen wir es, indem Sie die Zeilen 19 bis 37 durch den folgenden Ausschnitt ersetzen:

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

Aber app / views-project / show.html.erb ist die wahre Magie:

 

<%= notice %>

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

Dies sollte ziemlich klar sein, auch wenn Sie nicht vertraut sind senden(), Es ist eine andere Methode, eine Methode aufzurufen. So object.send ("name_of_method") ist das gleiche wie object.name_of_method.

Final Sprint

Wir sind fast fertig, aber mir sind zwei Fehler aufgefallen: der eine formatiert und der andere ist etwas schwerwiegender.

Die erste ist, dass, während ich ein Projekt sehe, die gesamte Methode eine unschöne Ruby-Objektausgabe anzeigt. Anstatt eine Methode am Ende hinzuzufügen, wie hier

 @ project.send (who_method) .name

Lass uns modifizieren Nutzer Ein ... Haben to_s Methode. Behalten Sie die Dinge im Modell, wenn Sie können, und fügen Sie dies oben in das Modell ein user.rb, und mache dasselbe für project.rb auch. Es ist immer sinnvoll, eine Standarddarstellung für ein Modell als Zeichenfolge zu haben:

 def to_s Namensende

Fühlt sich jetzt ein bisschen banale Schreibmethoden an, oder? Nein? Jedenfalls zu ernsteren Dingen.

Ein tatsächlicher Fehler

Wenn wir ein Projekt aktualisieren, weil wir alle Workflowstufen senden, die zuvor zugewiesen wurden, werden alle Zeitstempel gemischt. Da sich unser Code an einem Ort befindet, werden sie alle durch eine einzige Änderung behoben.

 define_method (set_method_name) do | user | wenn user.present? user = user.id if user.class == User # ADDITION HERE # Dies stellt sicher, dass der gespeicherte Wert geändert wird, bevor read_attribute (attr_by) .to_i! = user.to_i write_attribute (attr_by, user) write_attribute (attr_at, Time) festgelegt wird .now) Ende Ende Ende

Fazit

Was haben wir gelernt??

  • Das Hinzufügen von Funktionen zum Modell kann den Rest des Codes erheblich verbessern
  • Meta-Programmierung ist nicht unmöglich
  • Wenn Sie ein Projekt vorschlagen, wird dies möglicherweise protokolliert
  • Intelligentes Schreiben bedeutet zunächst weniger Arbeit
  • Niemand genießt das Schneiden, Einfügen und Bearbeiten und es verursacht Fehler
  • Smart Models sind in allen Lebensbereichen sexy

Vielen Dank für das Lesen und lassen Sie mich wissen, wenn Sie Fragen haben.