439 votes

Rails : Comment définir des valeurs par défaut dans ActiveRecord ?

Comment puis-je définir une valeur par défaut dans ActiveRecord ?

Je vois un message de Pratik qui décrit un morceau de code compliqué et laid : 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 en cherchant sur Internet :

  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érerais qu'il soit défini dans le code du modèle.

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

0 votes

On dirait que vous avez répondu à la question vous-même, dans deux variantes différentes :)

22 votes

Notez que l'idiome Ruby "standard" pour "self.status = ACTIVE unless self.status" est "self.status ||= ACTIVE".

1 votes

La réponse de Jeff Perrin est bien meilleure que celle qui est actuellement marquée comme acceptée. default_scope est une solution inacceptable pour définir des valeurs par défaut, car elle a l'ÉNORME EFFET SECONDAIRE de modifier également le comportement des requêtes.

579voto

Jeff Perrin Points 4603

Chacune des méthodes disponibles pose plusieurs problèmes, mais je pense que la définition d'une after_initialize est la meilleure solution pour les raisons suivantes :

  1. default_scope initialisera les valeurs pour les nouveaux modèles, mais cela deviendra alors la portée sur laquelle vous trouverez le modèle. Si vous voulez juste initialiser certains nombres à 0, alors c'est pas ce que vous voulez.
  2. Définir des valeurs par défaut dans votre migration fonctionne aussi une partie du temps... Comme cela a déjà été mentionné, cela pas fonctionne lorsque vous appelez simplement Model.new.
  3. Remplacement de initialize peut fonctionner, mais n'oubliez pas d'appeler super !
  4. Utiliser un plugin comme celui de Phusion devient un peu ridicule. C'est ruby, avons-nous vraiment besoin d'un plugin juste pour initialiser quelques valeurs par défaut ?
  5. Remplacement de after_initialize est déprécié à partir de Rails 3. Lorsque je passe outre after_initialize dans rails 3.0.3, j'obtiens l'avertissement suivant dans la console :

AVERTISSEMENT DE DEPRECATION : Base#after_initialize a été déprécié, veuillez utiliser la méthode Base.after_initialize :à la place. (appelé depuis /Users/me/myapp/app/models/my_model:15)

Je dirais donc qu'il faut écrire un after_initialize qui vous permet de définir des attributs par défaut en plus de vous permettant de définir des valeurs par défaut pour les associations, par exemple :

  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    

Maintenant, vous avez juste un endroit où chercher 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'est-à-dire si vous utilisez la fonction select dans une requête comme Person.select(:firstname, :lastname).all ), vous obtiendrez un MissingAttributeError si votre init accède à une colonne qui n'a pas été incluse dans le fichier select clause. Vous pouvez vous prémunir contre ce cas comme suit :

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

    et pour une colonne booléenne...

    self.bool_field = true if (self.has_attribute? :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)

7 votes

Cela semble être la meilleure façon d'y parvenir. Ce qui est vraiment étrange et malheureux. Une méthode préférée raisonnable pour établir les attributs par défaut des modèles lors de leur création semble être quelque chose que Rails devrait déjà avoir intégré. La seule autre méthode (fiable), en surchargeant initialize J'ai passé des heures à parcourir la documentation avant de chercher ici. J'ai passé des heures à parcourir la documentation avant de chercher ici parce que je supposais que cette fonctionnalité existait déjà quelque part et que je n'en étais pas conscient.

114 votes

Une remarque à ce sujet : si vous avez un champ booléen que vous voulez utiliser par défaut, ne faites pas l'opération suivante self.bool_field ||= true car cela forcera le champ à être vrai même si vous l'initialisez explicitement à faux. Faites plutôt self.bool_field = true if self.bool_field.nil? .

2 votes

En ce qui concerne le point 2, Model.new fonctionne effectivement (seulement pour moi ?) avec les valeurs par défaut définies dans les migrations, ou plus exactement avec les valeurs par défaut des colonnes de la table. Mais je reconnais que la méthode de Jeff basée sur le callback after_initialize est probablement la meilleure façon de faire. Juste une question : est-ce que cela fonctionne avec des objets sales mais non sauvegardés ? Dans votre exemple, Person.new.number_was renverra-t-il 0.0 ?

50voto

Laurent Farcy Points 452

Nous plaçons les valeurs par défaut dans la base de données par le biais de migrations (en spécifiant l'attribut :default sur chaque définition de colonne) et laisser Active Record utiliser ces valeurs pour définir la valeur par défaut de chaque attribut.

À mon avis, cette approche est conforme aux principes de la RA : la convention prime sur la configuration, la méthode DRY, la définition de la table détermine le modèle, et non l'inverse.

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

3 votes

Un autre problème se pose lorsque vous voulez une valeur par défaut pour une clé étrangère. Vous ne pouvez pas coder en dur une valeur d'identification dans le champ de la clé étrangère, car l'identification peut être différente selon les bases de données.

2 votes

Un autre problème est que de cette façon, vous ne pouvez pas initialiser les accesseurs non persistants (attributs qui ne sont pas des colonnes de la base de données).

2 votes

Un autre problème est que vous ne pouvez pas nécessairement voir toutes les valeurs par défaut en un seul endroit. elles peuvent être dispersées dans différentes migrations.

41voto

Joseph Lord Points 250

Certains cas simples peuvent être traités en définissant une valeur par défaut dans le schéma de la base de données, mais cela ne permet pas de traiter un certain nombre de cas plus délicats, 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 la fonction after_initialize mais je ne veux pas qu'elle soit appliquée aux objets trouvés, seulement à ceux qui sont nouveaux ou créés. Je pense qu'il est presque choquant qu'un callback after_new ne soit pas fourni pour ce cas d'utilisation évident mais j'ai fait avec en confirmant si l'objet est déjà persistant indiquant qu'il n'est pas nouveau.

Après avoir vu la réponse de Brad Murray, c'est encore plus propre 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

4 votes

C'est un point très important. Je dois imaginer que, dans la plupart des cas, la définition de la valeur par défaut d'un enregistrement ne doit être effectuée qu'avant la persistance d'un nouvel enregistrement, et non lors du chargement d'un enregistrement persistant.

0 votes

Thx mec, tu as sauvé ma journée.

2 votes

Et si :before_create ?

18voto

Brad Murray Points 158

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

after_initialize :some_method_goes_here, :if => :new_record?

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

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end

16voto

Milan Novota Points 10892

Les gars de Phusion ont de belles plugin pour ça.

0 votes

Notez que ce plugin permet au :default dans les migrations de schémas pour "fonctionner" avec Model.new .

0 votes

Je peux obtenir le :default dans les migrations pour qu'elles "fonctionnent" simplement avec Model.new contrairement à ce que Jeff a dit dans son message. Vérifié en fonctionnement dans Rails 4.1.16.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X