web-dev-qa-db-fra.com

Comment définir des 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 complexe et moche: 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 googler autour de:

  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 l'inclure dans leur migration, mais je préférerais que cela soit 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 dans le modèle ActiveRecord?

397
ryw

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 meilleure solution pour les raisons suivantes:

  1. _default_scope_ initialisera les valeurs pour les nouveaux modèles, mais cela deviendra la portée sur laquelle vous trouverez le modèle. Si vous voulez simplement initialiser des nombres à 0, alors c'est pas ce que vous voulez.
  2. La définition de 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. Remplacer 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 pour initialiser certaines valeurs par défaut?
  5. Remplacer _after_initialize_ est déconseillé à partir de Rails 3. Lorsque je remplace _after_initialize_ dans Rails 3.0.3, je reçois l'avertissement suivant dans la console:

AVERTISSEMENT CONCERNANT LA DEPRECATION: La base # after_initialize est obsolète. Veuillez utiliser Base.after_initialize: method à la place. (appelé depuis/Users/me/myapp/app/models/my_model: 15)

Par conséquent, je dirais d'écrire un callback _after_initialize_, qui vous permet d'attribuer des attributs par défaut en plus de vous permettant de définir des valeurs par défaut pour les associations, comme ceci:

_  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 n seul endroit pour rechercher l’initialisation de vos modèles. J'utilise cette méthode jusqu'à ce que quelqu'un en propose une meilleure.

Mises en garde:

  1. Pour les champs booléens:

    _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 (par exemple, en utilisant select dans une requête telle que Person.select(:firstname, :lastname).all), vous obtiendrez un MissingAttributeError si votre méthode init accède à une colonne qui n'a pas pas été inclus dans la clause select. Vous pouvez vous prémunir contre ce cas de la manière suivante:

    _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)

545
Jeff Perrin

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

IMHO, cette approche est alignée sur les principes de l'AR: convention sur la configuration, DRY, la définition de la table dirige 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.

45
Laurent Farcy

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 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 que ce soit appliqué aux objets qui se trouvent uniquement ceux qui sont nouveaux ou créés. Je pense qu'il est presque choquant qu'un rappel after_new ne soit pas fourni pour ce cas d'utilisation évident, mais je me suis contenté de confirmer si l'objet est déjà conservé, ce qui indique qu'il n'est pas nouveau.

Ayant vu la réponse de Brad Murray, cela est encore plus clair 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
39
Joseph Lord

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

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

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 trivial si votre code init doit traiter avec des associations, car le code suivant déclenche un n + 1 subtil si vous lisez l'enregistrement initial sans inclure celui qui est associé.

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end
17
Brad Murray

Les gars de Phusion ont quelques Nice plugin pour ça.

16
Milan Novota

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 "Remplacement des accesseurs par défaut" dans la documentation ActiveRecord :: Base et de StackOverflow à l’aide de self .

8
peterhurford

J'utilise le attribute-defaults gem

Dans la documentation: Lancez Sudo gem install attribute-defaults et ajoutez require 'attribute_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"
8
aidan

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

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

Si vous voulez que chaque objet commence par une valeur: use after_initialize :init

Vous voulez que le formulaire new.html ait une valeur par défaut lors de l'ouverture de la page? utiliser 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 voulez que chaque objet ait une valeur calculée à partir de l'entrée de l'utilisateur: utilisez before_save :default_valuesVous voulez que l'utilisateur entre X et Y = X+'foo'? utilisation:

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

Tout d'abord, je ne suis pas en désaccord avec la réponse de Jeff. Cela a du sens lorsque votre application est petite et votre logique simple. Je suis ici pour essayer de comprendre en quoi cela peut être un problème lors de la création et de la maintenance d’une application plus large. Je ne recommande pas d'utiliser cette approche en premier lors de la construction de quelque chose de petit, mais gardez-le à l'esprit comme une approche alternative:


Une question ici est de savoir si cette valeur par défaut sur les enregistrements est une logique métier. Si c'est le cas, je serais prudent de le mettre dans le modèle ORM. Puisque le champ indiqué ci-dessus est actif, cela ressemble à de la logique métier. Par exemple. l'utilisateur est actif.

Pourquoi devrais-je me méfier des problèmes d’entreprise dans un modèle ORM?

  1. Il casse SRP . Toute classe héritant de ActiveRecord :: Base effectue déjà un lot de différentes choses, dont la cohérence des données (validations) et la persistance (sauvegarde). Ajouter la logique métier, si petite soit-elle, à AR :: Base rompt SRP.

  2. C'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 vos tests unitaires prennent beaucoup de temps.

  3. Cela divisera SRP encore plus en profondeur et de manière concrète. Supposons que notre entreprise exige maintenant que nous envoyions un courrier électronique aux utilisateurs lorsque les éléments deviennent actifs? Nous ajoutons maintenant une logique de messagerie au modèle Item ORM, dont la responsabilité principale consiste à modéliser un élément. Il ne devrait pas se soucier de la logique de messagerie. C'est un cas de 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 éléments tels qu'un champ init_type: string sauvegardé par une base de données, dont le seul but est de contrôler la logique d'initialisation. Cela pollue la base de données pour résoudre un problème structurel. Il y a de meilleures façons, je crois.

La méthode PORO: Bien qu'il s'agisse d'un peu plus de code, cela vous permet de garder vos modèles ORM et votre logique applicative séparés. Le code ici est simplifié, mais devrait montrer l'idée:

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

Ensuite, avec cela en place, la façon de créer un nouvel élément serait

SellableItemFactory.new

Et mes tests pourraient maintenant simplement vérifier que ItemFactory définit les éléments actifs sur Item s'il n'a pas de valeur. Aucune initialisation de Rails nécessaire, aucune rupture de SRP. Lorsque l'initialisation d'élément devient plus avancée (par exemple, définissez un champ d'état, un type par défaut, etc.), cela peut être ajouté à ItemFactory. Si nous nous retrouvons avec deux types de valeurs par défaut, nous pouvons créer une nouvelle BusinesCaseItemFactory pour le faire.

REMARQUE: Il pourrait également être avantageux d’utiliser l’injection de dépendance ici pour permettre à l’usine de créer beaucoup d’activités actives, mais j’ai laissé de côté pour des raisons de simplicité. La voici: self.new (klass = Item, attributs = {})

4
Houen

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!

4
Tony

C'est à cela que servent les constructeurs! Remplacez la méthode initialize du modèle.

Utilisez la méthode after_initialize.

4
John Topley

Cela fait longtemps qu'on y répond, mais j'ai souvent besoin de valeurs par défaut et je préfère ne pas les mettre dans la base de données. Je crée un problème 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 l'utiliser dans mes modèles comme suit:

class Widget < ApplicationRecord
  include DefaultValues

  defaults :category, to: 'uncategorized'
  defaults :token, to: -> { SecureRandom.uuid }
end
3
clem

J'ai aussi vu des gens l'inclure dans leur migration, mais je préférerais 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 dans Modèle ActiveRecord?

Avant Rails 5, la méthode la plus classique était de la définir dans la migration et de rechercher dans le db/schema.rb chaque fois que vous souhaitiez connaître les valeurs par défaut définies par la base de données pour n'importe quel modèle. 

Contrairement à ce que dit @Jeff Perrin (qui est un peu ancien), l'approche de migration appliquera même la valeur par défaut lors de l'utilisation de Model.new, en raison de la magie de 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 la base de code. Et ça marche juste.

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

null: false interdit les valeurs NULL dans la base de données et, comme avantage supplémentaire, il met également à jour tous les enregistrements de base de données préexistants et définit également 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 @Lucas Caton a déclaré:

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

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

par exemple:

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

"search" dans le .where est un hash de conditions

J'ai donc fini par le faire en redéfinissant l'initialisation de la manière suivante:

def initialize
  super
  default_values
end

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

L’appel super est nécessaire pour s’assurer que l’objet s’initialise correctement à partir de ActiveRecord::Base avant de personnaliser mon code, c’est-à-dire: default_values

1
Sean

Je suggère fortement d'utiliser la gem "default_value_for": https://github.com/FooBarWidget/default_value_for

Il existe des scénarios difficiles qui nécessitent à peu près de surcharger la méthode d'initialisation, ce que fait cette gemme.

Exemples:

La valeur par défaut de votre base de données est NULL, celle de votre modèle/Ruby est "une chaîne", mais vous avez en fait voulez pour définir la valeur sur nil, quelle qu'en soit la raison: MyModel.new(my_attr: nil).

La plupart des solutions ne parviendront pas à définir la valeur sur nil, mais à la valeur par défaut.

OK, alors au lieu d'adopter l'approche ||=, vous passez à my_attr_changed?...

MAIS maintenant, imaginez que votre base de données par défaut est "une chaîne", votre modèle/Ruby défini par défaut est "une autre chaîne", mais dans un certain scénario, vous voulez pour définir la valeur vers "une chaîne" (la base de données par défaut): MyModel.new(my_attr: 'some_string')

Cela aura pour résultat que my_attr_changed? sera false car la valeur correspond à la valeur par défaut de la base de données, ce qui déclenchera à son tour le 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 correctement accompli avec un simple 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

1
etipton
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end
1
Mike Breen

Le problème des solutions after_initialize est que vous devez ajouter un after_initialize à chaque objet que vous recherchez dans la base de données, que vous accédiez ou non à cet attribut. Je suggère une approche paresseuse. 

Les méthodes d'attribut (getters) sont bien sûr des méthodes elles-mêmes, vous pouvez donc les remplacer et fournir une valeur 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 exécuter Foo.find_by_status ('ACTIVE'). Dans ce cas, je pense que vous auriez vraiment besoin de définir la valeur par défaut dans les contraintes de votre base de données, si la base de données le prend en charge.

1
Jeff Gran

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 reste le moyen le plus propre.

0
Greg

Voici une solution que j'ai utilisée et qui m'a un peu surprise n'a pas encore été ajoutée. 

Il y a deux parties. La première partie consiste à définir la valeur par défaut dans la migration réelle et la seconde à ajouter une validation dans le modèle afin de s’assurer que la présence est vraie. 

add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'

Donc, vous verrez ici que la valeur par défaut est déjà définie. Maintenant, dans la validation, vous voulez vous assurer qu'il y a toujours une valeur pour la chaîne, alors faites juste

 validates :new_team_signature, presence: true

Ce que cela va faire est de définir la valeur par défaut pour vous. (pour moi, j'ai "Welcome to the Team"), et cela ira un peu plus loin pour s'assurer qu'il y a toujours une valeur présente pour cet objet. 

J'espère que cela pourra aider!

0
kdweber89

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

0
skalee

J'ai constaté que l'utilisation d'une méthode de validation permet de beaucoup contrôler les paramètres par défaut. Vous pouvez même définir des valeurs par défaut (ou des échecs de validation) pour les mises à jour. Vous pouvez même définir une valeur par défaut différente pour les insertions par rapport aux mises à jour si vous le souhaitiez vraiment. Notez que la valeur par défaut ne sera pas définie avant #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

En ce qui concerne la définition d'une méthode after_initialize, il peut y avoir des problèmes de performances, car after_initialize est également appelé par chaque objet renvoyé par: find: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and -after_find

0
Kelvin

S'il se trouve que la colonne est du type "statut" et que votre modèle se prête bien à l'utilisation de machines à états, envisagez d'utiliser le aasm gem , après quoi vous pourrez simplement le faire. 

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

Cela n'initialise toujours pas la valeur pour les enregistrements non sauvegardés, mais c'est un peu plus propre que de faire rouler la vôtre avec init ou quoi que ce soit, et vous bénéficiez des autres avantages d'un désastre, tels que des portées pour tous vos statuts.

0
Bad Request

https://github.com/keithrowell/Rails_default_value

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