Modèles intelligents ActiveRecord

Les modèles ActiveRecord dans Rails font déjà beaucoup de travail, en termes d'accès aux bases de données et de relations entre les modèles, mais avec un peu de travail, ils peuvent faire plus de choses automatiquement. Voyons comment!


Étape 1 - Créer une application Rails de base

Cette idée fonctionne pour tout type de projet ActiveRecord; Cependant, comme Rails est le plus courant, nous l'utilisons pour notre exemple d'application. L'application que nous allons utiliser a beaucoup de Utilisateurs, chacun d'entre eux peut effectuer un certain nombre d'actions sur Projets .

Si vous n'avez jamais créé d'application Rails auparavant, lisez d'abord ce didacticiel, ou programme. Sinon, lancez l'ancienne console et tapez rails new exemple_app pour créer l’application, puis changez de répertoire pour votre nouvelle application avec cd exemple_app.


Étape 2 - Créez vos modèles et vos relations

Tout d'abord, nous générons l'utilisateur qui possédera:

 rails générer un échafaudage Nom d'utilisateur: text e-mail: chaîne password_hash: text

Probablement, dans un projet réel, nous aurions quelques champs supplémentaires, mais cela suffira pour le moment. Générons ensuite notre modèle de projet:

 rails générer l'échafaudage Nom du projet: texte started_at: date-heure id_débarreté: entier date_atteinte: date-heure terminée id_base_de: entier

On édite ensuite le généré project.rb Fichier décrivant la relation entre les utilisateurs et les projets:

 projet de classe < ActiveRecord::Base belongs_to :starter, :class_name =>"User",: foreign_key => "started_by_id" appartient_à: compléteur,: class_name => "User",: foreign_key => "completed_by_id" end

et la relation inverse dans user.rb:

 classe utilisateur < ActiveRecord::Base has_many :started_projects, :foreign_key =>"started_by_id" has_many: completed_projects,: foreign_key => "completed_by_id" end

Ensuite, lancez un rapide rake db: migrer, et nous sommes prêts à devenir intelligents avec ces modèles. Si seulement nouer des relations avec des modèles était aussi facile dans le monde réel! Maintenant, si vous avez déjà utilisé le framework Rails auparavant, vous n’avez probablement rien appris… pour le moment.!


Étape 3 - Les faux attributs sont plus froids que le faux cuir

La première chose que nous allons faire est d'utiliser des champs générant automatiquement. Vous aurez remarqué que lorsque nous avons créé le modèle, nous avons créé un hachage de mot de passe et non un champ de mot de passe. Nous allons créer un faux attribut pour un mot de passe qui le convertira en un hachage s'il est présent.

Donc, dans votre modèle, nous allons ajouter une définition pour ce nouveau champ de mot de passe.

 def password = new_password) write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) end def password "" end

Nous ne stockons qu'un hachage contre l'utilisateur afin que nous ne communiquions pas les mots de passe sans nous battre..

La deuxième méthode signifie que nous retournons quelque chose pour que les formulaires soient utilisés.

Nous devons également nous assurer que la bibliothèque de chiffrement Sha1 est chargée; ajouter besoin de 'sha1' à ton application.rb fichier après la ligne 40: config.filter_parameters + = [: mot de passe].

Comme nous avons modifié l’application au niveau de la configuration, rechargez-la rapidement. touchez tmp / restart.txt dans votre console.

Maintenant, changeons le formulaire par défaut pour utiliser ceci au lieu de password_hash. Ouvrir _form.html.erb dans le dossier app / models / users:

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

devient

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

Nous en ferons un mot de passe lorsque nous en serons satisfaits.

Maintenant, chargez http: // localhost / users et avoir un jeu avec l'ajout d'utilisateurs. Cela devrait ressembler un peu à l'image ci-dessous; super, n'est ce pas!

Attends, c'est quoi ça? Il écrase votre mot de passe hash chaque fois que vous modifiez un utilisateur? Corrigeons ça.

S'ouvrir user.rb encore une fois, et changez-le comme ceci:

 write_attribute (: password_hash, SHA1 :: hexdigest (new_password)) si new_password.present?

De cette façon, le champ n'est mis à jour que lorsque vous fournissez un mot de passe..


Étape 4 - La garantie automatique des données est exacte ou votre argent est remboursé

La dernière section était entièrement consacrée à la modification des données de votre modèle, mais quid de l'ajout d'informations supplémentaires basées sur des éléments connus sans avoir à les spécifier? Voyons cela avec le modèle de projet. Commencez par regarder http: // localhost / projects.

Apportez les modifications suivantes rapidement.

* app / controllers / projects_controler.rb * ligne 24

 # GET / projects / new # GET /projects/new.json def new @project = Project.new @users = ["-", nil] + User.all.collect | u | [u.name, u.id] respond_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 = [ "-", nil] + User.all.collect | u | [u.name, u.id] end

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

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

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

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

Dans les frameworks MVC, les rôles sont clairement définis. Les modèles représentent les données. Les vues affichent les données. Les contrôleurs obtiennent des données et les transmettent à la vue.

Qui aime remplir les champs de date / heure?

Nous avons maintenant un formulaire qui fonctionne, mais cela me gêne que je doive définir le commencer à temps manuellement. Je voudrais l'avoir réglé quand j'attribue un commencé par utilisateur. Nous pourrions le mettre dans le contrôleur, cependant, si vous avez déjà entendu l'expression "gros modèles, contrôleurs maigres", vous saurez que cela crée un mauvais code. Si nous faisons cela dans le modèle, cela fonctionnera n'importe où nous définirons un démarreur ou un compléteur. Faisons cela.

Première édition app / models / project.rb, et ajoutez la méthode suivante:

 def started_by = (utilisateur) si (utilisateur.present?) utilisateur = utilisateur.id si utilisateur.classe == Utilisateur write_attribute (: started_by_id, utilisateur) write_attribute (: started_at, Time.now) end

Ce code garantit que quelque chose a réellement été passé. Ensuite, s'il s'agit d'un utilisateur, il récupère son identifiant et écrit finalement l'utilisateur * et * l'heure à laquelle cela s'est produit - Saint Fume! Ajoutons la même chose pour le terminé par champ.

 def completed_by = (utilisateur) si (utilisateur.present?) utilisateur = utilisateur.id si utilisateur.classe == Utilisateur write_attribute (: completed_by_id, utilisateur) write_attribute (: started_at, Time.now) end

Maintenant, éditez la vue formulaire afin que nous n'ayons pas ces sélections de temps. Dans app / views / projects / _form.html.erb, supprimer les lignes 26-29 et 18-21.

S'ouvrir http: // localhost / projects et essayer!

Repérer l'erreur délibérée

Whoooops! Quelqu'un (je vais prendre la chaleur car c'est mon code) couper et coller, et a oublié de changer le :commencé à à : complete_at dans la seconde méthode d'attribut en grande partie identique (indice). Pas grave, changez ça et tout est parti… c'est ça?


Étape 5 - Aidez votre futur moi en facilitant les ajouts

Donc, mis à part un peu de confusion copier-coller, je pense que nous avons fait du bon travail, mais cette erreur et le code qui l’entoure me dérange un peu. Pourquoi? Eh bien, réfléchissons:

  • C'est la duplication couper / coller: DRY (Ne te répète pas) est un principe à suivre.
  • Et si quelqu'un veut en ajouter un autre quelque chose et quelque chose d_by à notre projet, comme, disons, authorised_at et autorisé par>
  • J'imagine qu'un certain nombre de ces champs sont ajoutés.

Et voilà qu'un patron aux cheveux pointus arrive et demande: drumroll, authorised_at / by field et un suggestion_at / par champ! Juste alors; préparons ces doigts coupés et collés alors… ou existe-t-il un meilleur moyen?

L'art effrayant de la méta-programmation!

C'est vrai! Le Saint-Graal; le truc effrayant que votre mère vous a mis en garde. Cela semble compliqué, mais peut en fait être assez simple - surtout ce que nous allons essayer. Nous allons prendre un tableau des noms des étapes que nous avons, puis créer automatiquement ces méthodes à la volée. Excité? Génial.

Bien sûr, nous devrons ajouter les champs; alors ajoutons une migration les rails génèrent la migration additional_workflow_stages et ajouter ces champs à l'intérieur de la nouvelle génération db / migrate / TODAYSTIMESTAMP_additional_workflow_stages.rb.

 classe 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

Migrez votre base de données avec rake db: migrer, et remplacez la classe de projets par:

 projet de classe < ActiveRecord::Base # belongs_to :starter, :class_name =>"Utilisateur" # def started_by = (utilisateur) # si (utilisateur.present?) # Utilisateur = utilisateur.id si utilisateur.classe == Utilisateur # write_attribute (: started_by_id, utilisateur) # write_attribute (: started_at, Time.now) # end # end # # def started_by # read_attribute (: completed_by_id) # end end

J'ai quitté le commencé par afin que vous puissiez voir comment le code était avant.

 [: starte,: complete,: authorize,: suggeste] .each do | arg |… PLUS… fin

Sympa et gentil - passe par les noms (ish) des méthodes que nous souhaitons créer:

 [: 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… PLUS… fin

Pour chacun de ces noms, nous élaborons les deux attributs de modèle que nous définissons par exemple. started_by_id et commencé à et le nom de l'association, par exemple. entrée

 [: 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 appartient_to object_method_méthode,: nom_ class => "Utilisateur",: nom_utilisateur ">: utilisateur",: foreign_key => attr_by end

Cela semble assez familier. C’est déjà un peu de métaprogrammation de Rails qui définit déjà un tas de méthodes.

 [: 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 appartient_to object_method_méthode,: nom_classe => "Utilisateur",: nom_utilisateur => "Utilisateur",: mot_étrangère_tranger => "utilisateur": = "# arg d_by" .to_sym define_method (get_method_name) read_attribute (attr_by) end

Ok, nous arrivons maintenant à une véritable méta-programmation qui calcule le nom de la méthode "get" - par exemple. commencé par, et crée ensuite une méthode, comme nous le faisons lorsque nous écrivons méthode def, mais sous une forme différente.

 [: 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 appartient_to object_method_méthode,: nom_classe => "Utilisateur",: nom_utilisateur => "Utilisateur",: mot_étrangère_tranger => "utilisateur": = "# 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 | utilisateur | si user.present? user = user.id si user.class == User write_attribute (attr_by, user) write_attribute (attr_at, Time.now) end end end

Un peu plus compliqué maintenant. Nous faisons comme avant, mais c’est le ensemble nom de la méthode. Nous définissons cette méthode en utilisant define (nom_méthode) do | param | fin, plutôt que def nom_methode = (param).

Ce n'était pas si mal, était-ce?

Essayez-le dans le formulaire

Voyons si nous pouvons toujours éditer les projets comme avant. Il s'avère que nous pouvons! Nous allons donc ajouter les champs supplémentaires au formulaire, et hop!

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

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

Et à la vue du spectacle… pour que nous puissions le voir fonctionner.

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

 

Suggéré à: <%= @project.suggested_at %>

Suggéré par: <%= @project.suggested_by_id %>

Autorisé à: <%= @project.authorised_at %>

Autorisé par: <%= @project.authorised_by_id %>

Avoir une autre pièce avec http: // localhost / projects, et vous pouvez voir que nous avons un gagnant! Pas besoin de craindre si quelqu'un demande une autre étape du flux de travail; ajoutez simplement la migration pour la base de données et mettez-la dans le tableau des méthodes… et elle est créée. Temps de repos? Peut-être, mais je n'ai que deux autres choses à noter.


Étape 6 - Automatiser l'automatisation

Cet éventail de méthodes me semble très utile. Pourrions-nous faire plus avec cela?

Premièrement, faisons de la liste des noms de méthodes une constante pour pouvoir y accéder de l'extérieur.

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

Nous pouvons maintenant les utiliser pour créer automatiquement des formulaires et des vues. Ouvrez le _form.html.erb pour les projets, et essayons en remplaçant les lignes 19 à 37 par l'extrait ci-dessous:

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

Mais app / views-project / show.html.erb C'est là que réside la vraie magie:

 

<%= notice %>

Prénom:: <%= @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 %>

Cela devrait être assez clair, bien que, si vous n'êtes pas familier avec envoyer(), c'est une autre façon d'appeler une méthode. Alors object.send ("name_of_method") est le même que object.name_of_method.

Sprint final

Nous avons presque terminé, mais j'ai remarqué deux bugs: l'un est le formatage et l'autre est un peu plus sérieux.

La première est que, lorsque je visualise un projet, toute la méthode montre une sortie d'objet Ruby moche. Plutôt que d'ajouter une méthode à la fin, comme ceci

 @ project.send (who_method) .name

Modifions Utilisateur avoir un to_s méthode. Conservez les éléments dans le modèle si vous le pouvez et ajoutez-les en haut de la liste. user.rb, et faire la même chose pour project.rb ainsi que. Il est toujours judicieux d’avoir une représentation par défaut pour un modèle sous forme de chaîne:

 def to_s name end

Se sent un peu banal méthodes d'écriture de la manière facile maintenant, hein? Non? Quoi qu'il en soit, passons aux choses plus sérieuses.

Un bug réel

Lorsque nous mettons à jour un projet parce que nous envoyons toutes les étapes de flux de travail qui ont été attribuées précédemment, tous nos horodatages sont mélangés. Heureusement, parce que tout notre code est au même endroit, un seul changement les corrigera tous..

 define_method (set_method_name) do | utilisateur | si user.present? user = user.id if user.class == User # ADDITION HERE # Cela garantit sa modification de la valeur stockée avant de la définir si read_attribute (attr_by) .to_i! = user.to_i write_attribute (attr_by, utilisateur) write_attribute (attr_at, Time .now) fin fin fin

Conclusion

Qu'avons-nous appris?

  • L'ajout de fonctionnalités au modèle peut sérieusement améliorer le reste de votre code.
  • La méta programmation n'est pas impossible
  • Proposer un projet peut être connecté
  • D'abord, écrire intelligemment signifie moins de travail plus tard
  • Personne n'aime couper, coller et éditer et cela cause des bugs
  • Les modèles intelligents sont sexy dans tous les domaines

Merci beaucoup d'avoir lu, et laissez-moi savoir si vous avez des questions.