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!
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
.
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!
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.
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.
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!
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?
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:
somethingd_at
und somethingd_by
zu unserem Projekt, sagen wir, authorised_at
und authorisiert von
>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??
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??
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.
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" %><% end %>
<%= f.select "#workflowd_by", @users %>
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
.
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.
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
Was haben wir gelernt??
Vielen Dank für das Lesen und lassen Sie mich wissen, wenn Sie Fragen haben.