Comment définir les valeurs par défaut dans ActiveRecord?

Comment définir la valeur par défaut dans ActiveRecord?

Je vois un article de Pratik qui décrit un morceau de code laid et compliqué: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model

class Item < ActiveRecord::Base def initialize_with_defaults(attrs = nil, &block) initialize_without_defaults(attrs) do setter = lambda { |key, value| self.send("#{key.to_s}=", value) unless !attrs.nil? && attrs.keys.map(&:to_s).include?(key.to_s) } setter.call('scheduler_type', 'hotseat') yield self if block_given? end end alias_method_chain :initialize, :defaults end 

J’ai vu les exemples suivants sur Google:

  def initialize super self.status = ACTIVE unless self.status end 

et

  def after_initialize return unless new_record? self.status = ACTIVE end 

J’ai aussi vu des gens le mettre dans leur migration, mais je préfère le voir défini dans le code du modèle.

Existe-t-il un moyen canonique de définir la valeur par défaut pour les champs du modèle ActiveRecord?

Il existe plusieurs problèmes avec chacune des méthodes disponibles, mais je pense que la définition d’un rappel after_initialize est la voie à suivre pour les raisons suivantes:

  1. default_scope initialisera les valeurs des nouveaux modèles, mais cela deviendra la scope sur laquelle vous trouverez le modèle. Si vous voulez juste initialiser des nombres à 0, ce n’est pas ce que vous voulez.
  2. Définir les valeurs par défaut dans votre migration fonctionne également une partie du temps … Comme cela a déjà été mentionné, cela ne fonctionnera pas lorsque vous appelez simplement Model.new.
  3. Inverser l’ initialize peut fonctionner, mais n’oubliez pas d’appeler super !
  4. Utiliser un plugin comme phusion devient un peu ridicule. Ceci est ruby, avons-nous vraiment besoin d’un plugin juste pour initialiser certaines valeurs par défaut?
  5. Le after_initialize est obsolète à partir de Rails 3. Lorsque je remplace after_initialize dans rails 3.0.3, je reçois l’avertissement suivant dans la console:

AVERTISSEMENT DE DEPRECATION: Base # after_initialize est obsolète, utilisez plutôt la méthode Base.after_initialize: (appelé depuis / Utilisateurs / moi / myapp / app / models / my_model: 15)

Par conséquent, je dirais écrire un after_initialize , qui vous permet de after_initialize des atsortingbuts par défaut en plus de vous permettre de définir des valeurs par défaut pour des associations comme celles-ci:

  class Person < ActiveRecord::Base has_one :address after_initialize :init def init self.number ||= 0.0 #will set the default value only if it's nil self.address ||= build_address #let's you set a default association end end 

Vous n'avez plus qu'un endroit pour rechercher l'initialisation de vos modèles. J'utilise cette méthode jusqu'à ce que quelqu'un en trouve une meilleure.

Mises en garde:

  1. Pour les champs booléens, faites:

    self.bool_field = true if self.bool_field.nil?

    Voir le commentaire de Paul Russell sur cette réponse pour plus de détails

  2. Si vous ne sélectionnez qu'un sous-ensemble de colonnes pour un modèle (c.-à-d. En utilisant select dans une requête comme Person.select(:firstname, :lastname).all ), vous obtiendrez une MissingAtsortingbuteError si votre méthode init accède à une colonne t été inclus dans la clause select . Vous pouvez vous prémunir contre cette affaire comme suit:

    self.number ||= 0.0 if self.has_atsortingbute? :number

    et pour une colonne booléenne ...

    self.bool_field = true if (self.has_atsortingbute? :bool_value) && self.bool_field.nil?

    Notez également que la syntaxe est différente avant Rails 3.2 (voir le commentaire de Cliff Darling ci-dessous)

Nous avons placé les valeurs par défaut dans la firebase database via les migrations (en spécifiant l’option :default sur chaque définition de colonne) et laissez Active Record utiliser ces valeurs pour définir la valeur par défaut pour chaque atsortingbut.

IMHO, cette approche est alignée avec les principes de l’AR: convention sur la configuration, DRY, la définition de la table pilote le modèle, et non l’inverse.

Notez que les valeurs par défaut sont toujours dans le code d’application (Ruby), mais pas dans le modèle mais dans la ou les migrations.

Certains cas simples peuvent être traités en définissant une valeur par défaut dans le schéma de la firebase database, mais cela ne gère pas un certain nombre de cas plus complexes, notamment les valeurs calculées et les clés d’autres modèles. Pour ces cas, je fais ceci:

 after_initialize :defaults def defaults unless persisted? self.extras||={} self.other_stuff||="This stuff" self.assoc = [OtherModel.find_by_name('special')] end end 

J’ai décidé d’utiliser after_initialize, mais je ne veux pas qu’il soit appliqué à des objects que l’on trouve uniquement ceux nouveaux ou créés. Je pense qu’il est presque choquant qu’un call_new ne soit pas fourni pour ce cas d’utilisation évident, mais je l’ai fait en confirmant si l’object est déjà persistant, indiquant qu’il n’est pas nouveau.

Ayant vu la réponse de Brad Murray, cela est encore plus net si la condition est déplacée vers la demande de rappel:

 after_initialize :defaults, unless: :persisted? # ":if => :new_record?" is equivalent in this context def defaults self.extras||={} self.other_stuff||="This stuff" self.assoc = [OtherModel.find_by_name('special')] end 

Dans Rails 5+, vous pouvez utiliser la méthode d’ atsortingbut dans vos modèles, par exemple:

 class Account < ApplicationRecord attribute :locale, :string, default: 'en' end 

Le modèle de rappel after_initialize peut être amélioré simplement en procédant comme suit

 after_initialize :some_method_goes_here, :if => :new_record? 

Cela présente un avantage non négligeable si votre code d’initialisation doit traiter des associations, car le code suivant déclenche une subtilité n + 1 si vous lisez l’enregistrement initial sans inclure le code associé.

 class Account has_one :config after_initialize :init_config def init_config self.config ||= build_config end end 

Les gars de Phusion ont un joli plugin pour cela.

Un moyen encore meilleur / plus propre que les réponses proposées est d’écraser l’accesseur, comme ceci:

 def status self['status'] || ACTIVE end 

Voir “Ecraser les accesseurs par défaut” dans la documentation ActiveRecord :: Base et plus encore sur StackOverflow lors de l’utilisation de self .

J’utilise l’ atsortingbute-defaults gem

À partir de la documentation: exécutez sudo gem install atsortingbute-defaults et ajoutez require 'atsortingbute_defaults' à votre application.

 class Foo < ActiveRecord::Base attr_default :age, 18 attr_default :last_seen do Time.now end end Foo.new() # => age: 18, last_seen => "2014-10-17 09:44:27" Foo.new(:age => 25) # => age: 25, last_seen => "2014-10-17 09:44:28" 

Des questions similaires, mais toutes ont un contexte légèrement différent: – Comment créer une valeur par défaut pour les atsortingbuts du modèle de Rails activerecord?

Meilleure réponse: dépend de ce que vous voulez!

Si vous voulez que chaque object commence par une valeur: utilisez after_initialize :init

Vous souhaitez que le new.html formulaire.html ait une valeur par défaut lors de l’ouverture de la page? utilisez https://stackoverflow.com/a/5127684/1536309

 class Person < ActiveRecord::Base has_one :address after_initialize :init def init self.number ||= 0.0 #will set the default value only if it's nil self.address ||= build_address #let's you set a default association end ... end 

Si vous souhaitez que chaque object ait une valeur calculée à partir de l'entrée utilisateur: utilisez before_save :default_values Vous voulez que l'utilisateur entre X , puis Y = X+'foo' ? utilisation:

 class Task < ActiveRecord::Base before_save :default_values def default_values self.status ||= 'P' end end 

C’est à quoi servent les constructeurs! Remplacez la méthode d’ initialize du modèle.

Utilisez la méthode after_initialize .

Sup les gars, j’ai fini par faire ce qui suit:

 def after_initialize self.extras||={} self.other_stuff||="This stuff" end 

Fonctionne comme un charme!

Premièrement, je ne suis pas en désaccord avec la réponse de Jeff. Cela a du sens lorsque votre application est petite et que votre logique est simple. Je suis ici pour essayer de comprendre comment cela peut poser problème lors de la création et du maintien d’une application plus grande. Je ne recommande pas d’utiliser cette approche en premier lors de la construction de quelque chose de petit, mais de garder cela à l’esprit comme une approche alternative:


Une question ici est de savoir si ce défaut sur les enregistrements est la logique métier. Si c’est le cas, je serais prudent de le mettre dans le modèle ORM. Comme le champ ryw est actif , cela ressemble à la logique métier. Par exemple, l’utilisateur est actif.

Pourquoi serais-je prudent de mettre les préoccupations commerciales dans un modèle ORM?

  1. Il casse SRP . Toute classe héritant d’ActiveRecord :: Base fait déjà beaucoup de choses, la principale étant la cohérence des données (validations) et la persistance (save). Mettre la logique métier, si petite soit-elle, avec AR :: Base rompt SRP.

  2. Il est plus lent à tester. Si je veux tester toute forme de logique se produisant dans mon modèle ORM, mes tests doivent initialiser Rails pour pouvoir s’exécuter. Cela ne posera pas trop de problèmes au début de votre application, mais s’accumulera jusqu’à ce que les tests de votre unité prennent beaucoup de temps.

  3. Cela brisera encore plus la SRP et de manière concrète. Disons que notre entreprise exige maintenant que nous envoyions des e-mails aux utilisateurs lorsque ceux-ci deviennent actifs? Nous ajoutons maintenant une logique de courrier électronique au modèle Item ORM, dont la principale responsabilité consiste à modéliser un élément. Il ne devrait pas se soucier de la logique du courrier électronique. Ceci est un cas d’ effets secondaires commerciaux . Ceux-ci n’appartiennent pas au modèle ORM.

  4. Il est difficile de se diversifier. J’ai vu des applications Rails matures avec des choses comme un champ init_type: ssortingng sauvegardé dans une firebase database, dont le seul but est de contrôler la logique d’initialisation. Cela pollue la firebase database pour résoudre un problème structurel. Il y a de meilleures façons, je crois.

La méthode PORO: bien que le code soit un peu plus volumineux, il vous permet de séparer vos modèles ORM et votre logique métier. Le code ici est simplifié, mais devrait montrer l’idée:

 class SellableItemFactory def self.new(atsortingbutes = {}) record = Item.new(atsortingbutes) record.active = true if record.active.nil? record end end 

Alors, avec cela en place, le moyen de créer un nouvel élément serait

 SellableItemFactory.new 

Et mes tests peuvent maintenant simplement vérifier que ItemFactory est actif sur Item s’il n’a pas de valeur. Aucune initialisation de Rails requirejse, pas de rupture de SRP. Lorsque l’initialisation de l’object devient plus avancée (par exemple, définir un champ d’état, un type par défaut, etc.), ItemFactory peut l’append. Si nous nous retrouvons avec deux types de valeurs par défaut, nous pouvons créer un nouveau BusinesCaseItemFactory pour ce faire.

REMARQUE: Il pourrait également être utile d’utiliser l’dependency injection pour permettre à l’usine de créer de nombreux éléments actifs, mais je l’ai laissé pour des raisons de simplicité. La voici: self.new (klass = Item, atsortingbutes = {})

Cela a été répondu depuis longtemps, mais j’ai souvent besoin de valeurs par défaut et je préfère ne pas les mettre dans la firebase database. Je crée une préoccupation par DefaultValues :

 module DefaultValues extend ActiveSupport::Concern class_methods do def defaults(attr, to: nil, on: :initialize) method_name = "set_default_#{attr}" send "after_#{on}", method_name.to_sym define_method(method_name) do if send(attr) send(attr) else value = to.is_a?(Proc) ? to.call : to send("#{attr}=", value) end end private method_name end end end 

Et puis utilisez-le dans mes modèles comme ceci:

 class Widget < ApplicationRecord include DefaultValues defaults :category, to: 'uncategorized' defaults :token, to: -> { SecureRandom.uuid } end 

J’ai aussi vu des gens le mettre dans leur migration, mais je préfère le voir défini dans le code du modèle.

Existe-t-il un moyen canonique de définir la valeur par défaut pour les champs du modèle ActiveRecord?

La méthode Rails canonique, avant Rails 5, était en fait de la définir dans la migration, et il suffit de regarder dans la db/schema.rb pour savoir quelles valeurs par défaut sont définies par le DB pour chaque modèle.

Contrairement à ce que dit la réponse de @Jeff Perrin (qui est un peu ancienne), l’approche de la migration appliquera même la valeur par défaut lors de l’utilisation de Model.new , à cause de la magie des Rails. Travail vérifié dans Rails 4.1.16.

La chose la plus simple est souvent la meilleure. Moins de dette de connaissances et de points de confusion potentiels dans le code. Et ça marche simplement.

 class AddStatusToItem < ActiveRecord::Migration def change add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" } end end 

Le null: false n'autorise pas les valeurs NULL dans la firebase database et, en tant qu'avantage supplémentaire, met également à jour tous les enregistrements de firebase database préexistants, ainsi que la valeur par défaut pour ce champ. Vous pouvez exclure ce paramètre dans la migration si vous le souhaitez, mais je l'ai trouvé très pratique!

La manière canonique dans Rails 5+ est, comme l'a dit @Lucas Caton:

 class Item < ActiveRecord::Base attribute :scheduler_type, :string, default: 'hotseat' end 

Le problème avec les solutions after_initialize est que vous devez append un after_initialize à chaque object que vous recherchez dans la firebase database, que vous accédiez ou non à cet atsortingbut. Je suggère une approche paresseuse.

Les méthodes d’atsortingbut (getters) sont bien entendu des méthodes, vous pouvez donc les remplacer et fournir un paramètre par défaut. Quelque chose comme:

 Class Foo < ActiveRecord::Base # has a DB column/field atttribute called 'status' def status (val = read_attribute(:status)).nil? ? 'ACTIVE' : val end end 

Sauf si, comme quelqu'un l'a fait remarquer, vous devez faire Foo.find_by_status ('ACTIVE'). Dans ce cas, je pense que vous devriez vraiment définir la valeur par défaut dans vos contraintes de firebase database, si la firebase database le prend en charge.

J’ai rencontré des problèmes avec after_initialize donnant des erreurs ActiveModel::MissingAtsortingbuteError lors de recherches complexes:

par exemple:

 @bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no) 

“chercher” dans le ” .where est le hachage des conditions”

J’ai donc fini par le faire en écrasant l’initialisation de cette manière:

 def initialize super default_values end private def default_values self.date_received ||= Date.current end 

Le super appel est nécessaire pour s’assurer que l’object s’initialise correctement depuis ActiveRecord::Base avant de faire mon code de personnalisation, à savoir: default_values

 class Item < ActiveRecord::Base def status self[:status] or ACTIVE end before_save{ self.status ||= ACTIVE } end 

Je suggère fortement d’utiliser le gem “default_value_for”: https://github.com/FooBarWidget/default_value_for

Il y a des scénarios difficiles qui nécessitent de surcharger la méthode d’initialisation, ce que fait cette gem.

Exemples:

Votre db par défaut est NULL, votre modèle / ruby ​​défini par défaut est “some ssortingng”, mais vous voulez en fait définir la valeur à nil pour une raison quelconque: MyModel.new(my_attr: nil)

La plupart des solutions ici ne parviendront pas à définir la valeur à nil, et au lieu de cela définir à la valeur par défaut.

OK, donc au lieu de prendre l’approche ||= , vous passez à my_attr_changed?

MAIS maintenant, imaginez que votre db par défaut soit “une chaîne de caractères”, votre modèle / ruby ​​défini par défaut est “une autre chaîne”, mais dans un certain scénario, vous voulez définir la valeur “une chaîne” (la db par défaut): MyModel.new(my_attr: 'some_ssortingng')

Cela se traduira par my_attr_changed? étant faux car la valeur correspond à la valeur par défaut de la firebase database, qui à son tour déclenchera votre code par défaut défini par Ruby et définira la valeur sur “une autre chaîne” – encore une fois, pas ce que vous souhaitiez.


Pour ces raisons, je ne pense pas que cela puisse être accompli avec un hook after_initialize.

Encore une fois, je pense que le joyau “default_value_for” adopte la bonne approche: https://github.com/FooBarWidget/default_value_for

Bien que faire cela pour définir des valeurs par défaut soit déroutant et difficile dans la plupart des cas, vous pouvez également utiliser :default_scope . Découvrez le commentaire de squil ici .

La méthode after_initialize est obsolète, utilisez plutôt le rappel.

 after_initialize :defaults def defaults self.extras||={} self.other_stuff||="This stuff" end 

cependant, utiliser : default dans vos migrations rest la méthode la plus propre.

J’ai trouvé que l’utilisation d’une méthode de validation permet de contrôler les parameters par défaut. Vous pouvez même définir les valeurs par défaut (ou échec de la validation) pour les mises à jour. Vous pouvez même définir une valeur par défaut différente pour les insertions et les mises à jour si vous le souhaitiez vraiment. Notez que la valeur par défaut ne sera pas définie jusqu’à #valid? est appelé.

 class MyModel validate :init_defaults private def init_defaults if new_record? self.some_int ||= 1 elsif some_int.nil? errors.add(:some_int, "can't be blank on update") end end end 

Concernant la définition d’une méthode after_initialize, il pourrait y avoir des problèmes de performance car after_initialize est également appelé par chaque object renvoyé par: find: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find

Si la colonne se trouve être une colonne de type «status» et que votre modèle se prête à l’utilisation de machines à états, envisagez d’utiliser le gem aasm , après quoi vous pouvez simplement faire

  aasm column: "status" do state :available, initial: true state :used # transitions end 

Cela n’initialise toujours pas la valeur pour les enregistrements non enregistrés, mais c’est un peu plus propre que de lancer votre propre avec init ou autre, et vous récoltez les autres avantages de aasm tels que les étendues pour tous vos statuts.

https://github.com/keithrowell/rails_default_value

 class Task < ActiveRecord::Base default :status => 'active' end 

utiliser default_scope dans les rails 3

api doc

ActiveRecord masque la différence entre la définition par défaut définie dans la firebase database (schéma) et la définition par défaut effectuée dans l’application (modèle). Lors de l’initialisation, il parsing le schéma de la firebase database et note les valeurs par défaut spécifiées. Plus tard, lors de la création d’objects, il affecte les valeurs par défaut spécifiées pour le schéma sans toucher à la firebase database.

discussion

A partir des api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html Utilisez la méthode before_validation dans votre modèle, cela vous donne la possibilité de créer une initialisation spécifique pour les appels de création et de mise à jour, par exemple dans cet exemple (encore une fois le code extrait de l’exemple api docs), le champ numérique est initialisé pour une carte de crédit. Vous pouvez facilement adapter cela pour définir les valeurs que vous voulez

 class CreditCard < ActiveRecord::Base # Strip everything but digits, so the user can specify "555 234 34" or # "5552-3434" or both will mean "55523434" before_validation(:on => :create) do self.number = number.gsub(%r[^0-9]/, "") if atsortingbute_present?("number") end end class Subscription < ActiveRecord::Base before_create :record_signup private def record_signup self.signed_up_on = Date.today end end class Firm < ActiveRecord::Base # Destroys the associated clients and people when the firm is destroyed before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } before_destroy { |record| Client.destroy_all "client_of = #{record.id}" } end 

Surpris que son n'a pas été suggéré ici