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

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

je vois un post 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 googler autour:

  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, voir plutôt il défini dans le code modèle.

y a-t-il une façon canonique de définir la valeur par défaut des champs dans le modèle ActiveRecord?

389
demandé sur meagar 2008-11-30 09:27:29

25 réponses

il y a plusieurs problèmes avec chacune des méthodes disponibles, mais je crois que la définition d'un rappel after_initialize est la voie à suivre pour les raisons suivantes:

  1. default_scope initialisera les valeurs pour les nouveaux modèles, mais alors cela deviendra la portée sur laquelle vous trouvez le modèle. Si vous voulez juste initialiser quelques nombres à 0 alors c'est pas ce que vous voulez.
  2. définir les valeurs par défaut dans votre la migration fonctionne aussi en partie... Comme cela a déjà été mentionné, cela fonctionnera et non lorsque vous appelez simplement Model.nouveau.
  3. Substitution initialize peut fonctionner, mais n'oubliez pas d'appeler super !
  4. utiliser un plugin comme phusion's devient un peu ridicule. C'est ruby, avons-nous vraiment besoin d'un plugin juste pour initialiser des valeurs par défaut?
  5. Substitution after_initialize est déprécié à partir des Rails 3. Lorsque je remplace after_initialize dans les rails 3.0.3, j'obtiens l'avertissement suivant dans la console:

avertissement de dépréciation: la Base # after_initialize a été dépréciée, veuillez utiliser la Base.after_initialize :méthode à la place. (appelée à partir de /Users/moi/myapp/app/models/my_model:15)

donc je dirais d'écrire un after_initialize callback, qui vous laisse les attributs par défaut en plus de vous permettant de définir des associations par défaut comme cela:

  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 pour 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:

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

    voir Paul Russell's commenter cette réponse pour plus de détails

  2. si vous sélectionnez seulement un sous-ensemble de colonnes pour un modèle (i.e.; en utilisant select dans une requête comme Person.select(:firstname, :lastname).all ) vous obtiendrez un MissingAttributeError si votre méthode init accède à une colonne qui n'a pas été incluse dans la clause select . Vous pouvez vous prémunir contre cette affaire comme ceci:

    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 aussi que la syntaxe est différente avant les Rails 3.2 (voir le commentaire de Cliff Darling ci-dessous)

528
répondu Jeff Perrin 2017-07-11 02:14:21

nous mettons les valeurs par défaut dans la base de données par le biais de migrations (en spécifiant l'option :default sur 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 avec les principes de AR : convention sur la configuration, DRY, la définition de la table pilote le modèle, pas 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 migration(s).

43
répondu Laurent Farcy 2008-11-30 13:45:52

quelques cas simples peuvent être traités en définissant un défaut dans le schéma de base de données, mais qui ne traite pas un certain nombre de cas plus compliqués, y compris 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 le after_initialize mais je ne veux pas qu'il soit appliqué aux objets qui ne se trouvent que ceux qui sont nouveaux ou créés. Je pense qu'il est presque choquant qu'un rappel after_new ne soit pas prévu pour ce cas d'utilisation évidente mais J'ai fait faire en confirmant que l'objet est déjà persistantes indiquant qu'il n'est pas nouveau.

Avoir vu Brad Murray répondre à cette question est encore plus propre si la condition est déplacé à 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
38
répondu Joseph Lord 2015-05-28 15:05:36

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

class Account < ApplicationRecord
  attribute :locale, :string, default: 'en'
end
25
répondu Lucas Caton 2017-04-19 01:40:29

le modèle de rappel after_initialize peut être amélioré en faisant simplement ce qui suit

after_initialize :some_method_goes_here, :if => :new_record?

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

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end
17
répondu Brad Murray 2012-09-14 05:37:50

les gars de Phusion ont quelques nice plugin pour cela.

16
répondu Milan Novota 2008-11-30 09:42:56

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

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

Voir "l'Écrasement par défaut accesseurs" dans la ActiveRecord::Base de la documentation et plus de StackOverflow sur l'utilisation de l'auto .

8
répondu peterhurford 2017-05-23 12:26:37

j'utilise le attribute-defaults gem

de 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
répondu aidan 2014-10-16 22:46:39

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 chaque objet à commencer par une valeur: utilisez after_initialize :init

vous voulez que le formulaire new.html ait un valeur par défaut à l'ouverture de la page? use 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 de ait une valeur calculée à partir de l'entrée de l'Utilisateur: utiliser before_save :default_values Vous voulez que l'utilisateur entre X et ensuite Y = X+'foo' ? use:

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status ||= 'P'
  end
end
6
répondu Blair Anderson 2017-05-23 12:10:47

C'est à ça que servent les constructeurs! Outrepasser la méthode initialize du modèle.

utiliser la méthode after_initialize .

4
répondu John Topley 2009-11-13 09:56:19

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
répondu Tony 2010-07-27 06:36:21

première chose: Je ne suis pas en désaccord avec la réponse de Jeff. Cela a du sens quand votre application est petite et votre logique simple. Je suis ici essayer de donner un aperçu de la façon dont il peut être un problème lors de la construction et le maintien d'une application plus grande. Je ne recommande pas d'utiliser cette approche d'abord pour construire quelque chose de petit, mais de garder à l'esprit comme une approche alternative:


une question ici est de savoir si ce défaut sur les documents est logique. Si c'est le cas, je serais prudent de le mettre dans le modèle ORM. Puisque le champ mentionné par ryw est actif , cela ressemble à une logique d'affaires. E. g. l'utilisateur est actif.

pourquoi hésiterais-je à mettre des préoccupations commerciales dans un modèle ORM?

  1. Ça casse SRP . Toute classe héritant de ActiveRecord:: Base fait déjà un lot de choses différentes, les principaux sont la cohérence des données (validations) et la persistance (sauvegarde). Mettre la logique des affaires, aussi petite soit-elle, avec AR::Base breaks SRP.

  2. il est plus lent à tester. Si je veux tester n'importe quelle forme de logique se produisant dans mon modèle ORM, mes tests doivent initialiser les Rails afin de courir. Cela ne sera pas trop un problème au début de votre application, mais s'accumulera jusqu'à ce que les tests de votre unité prennent beaucoup de temps à exécuter.

  3. il cassera encore plus de SRP le long de la ligne, et de manière concrète. Disons que notre entreprise nous oblige maintenant à envoyer des e-mails aux Utilisateurs quand il élément est devenu actif? Maintenant, nous ajoutons la logique d'email au modèle ORM D'Item, dont la responsabilité principale est la modélisation d'un Item. Il ne devrait pas se soucier de la logique de courrier électronique. C'est un cas de "151970920 d'affaires" effets secondaires . Ceux-ci n'ont pas leur place dans le modèle ORM.

  4. c'est dur diversifier. J'ai vu des applications de Rails matures avec des choses comme un champ init_type: string appuyé sur 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 manières, je crois.

the PORO way: bien qu'il s'agisse d'un peu plus de code, il vous permet de garder vos modèles ORM et logique D'Affaires séparés. Le code ici est simplifié, mais doit montrer l'idée:

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

alors avec ceci 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 est actif sur Item s'il n'a pas de valeur. Pas besoin d'initialisation des Rails, pas de rupture de la PRS. Lorsque l'initialisation D'un élément est plus avancée (par exemple, définir un champ d'état, un type par défaut, etc.) L'ItemFactory peut avoir ceci ajouté. Si nous nous retrouvons avec deux types de défaut, nous pouvons créer un nouveau BusinesCaseItemFactory pour ce faire.

NOTE: il pourrait également être bénéfique d'utiliser l'injection de dépendances ici pour permettre à l'usine de construire beaucoup de choses actives, mais je l'ai laissé dehors pour la simplicité. Le voici: moi.nouveau(klass = Point, attributes = {})

4
répondu Houen 2015-07-27 09:40:57

Cela a été répondu depuis longtemps, mais j'ai besoin de valeurs par défaut fréquemment et préfère ne pas les mettre dans la base de données. Je crée un DefaultValues concern:

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 ainsi:

class Widget < ApplicationRecord
  include DefaultValues

  defaults :category, to: 'uncategorized'
  defaults :token, to: -> { SecureRandom.uuid }
end
3
répondu clem 2016-02-14 21:35:41

j'ai aussi vu des gens le mettre dans leur migration, mais je préférerais le voir défini dans le code type.

y a-t-il une façon canonique de définir la valeur par défaut pour les champs dans Modèle ActiveRecord?

le chemin canonique des Rails, avant les Rails 5, était en fait de le régler dans la migration, et il suffit de regarder dans le db/schema.rb pour chaque fois que vous voulez voir quelles valeurs par défaut sont définies par le DB pour n'importe quel modèle.

contrairement à ce que déclare la réponse de @Jeff Perrin (qui est un peu vieille), l'approche de migration va même appliquer la valeur par défaut en utilisant Model.new , en raison de la magie de certains Rails. Vérification du travail sur Rails 4.1.16.

la chose La plus simple est souvent la meilleure. Moins de dettes de connaissances et de points potentiels de confusion dans la base de données. Et il fonctionne.

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

le null: false rejette les valeurs nulles dans le DB, et, en tant qu'ajout avantage, il met également à jour tous les enregistrements DB préexistants est défini avec la valeur par défaut pour ce champ aussi bien. Vous pouvez exclure ce paramètre de la migration si vous le souhaitez, mais je l'ai trouvé très pratique!

la voie canonique dans les Rails 5+ est, comme l'a dit @Lucas Caton:

class Item < ActiveRecord::Base
  attribute :scheduler_type, :string, default: 'hotseat'
end
3
répondu Magne 2017-05-22 14:46:34

le problème avec les solutions after_initialize est que vous devez ajouter un after_initialize à chaque objet que vous recherchez à partir de la base de données, peu importe si vous accédez à cet attribut ou non. 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 outrepasser 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 souligné, vous devez faire Foo.find_by_status ("ACTIVE"). Dans ce cas, je pense que vous avez 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 supporte.

1
répondu Jeff Gran 2012-09-08 00:43:17

j'ai rencontré des problèmes avec after_initialize donner ActiveModel::MissingAttributeError erreurs lors de la réalisation de finds complexes:

par exemple:

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

"recherche" dans le .where est de hachage de conditions

donc j'ai fini par le faire en outrepassant initialize de cette façon:

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 initialise correctement à partir de ActiveRecord::Base avant de faire mon code personnalisé, c'est à dire: default_values

1
répondu Sean 2012-11-30 03:24:52
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end
1
répondu Mike Breen 2013-07-15 07:59:14

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

il y a quelques scénarios délicats qui exigent à peu près la suppression de la méthode initialize, ce que fait gem.

exemples:

votre db par défaut est NULL, votre model / ruby défini par défaut est "une chaîne de caractères", mais vous en fait voulez pour définir la valeur à zéro pour quelque raison que ce soit: MyModel.new(my_attr: nil)

la plupart des solutions ici ne parviendront pas à définir la valeur à zéro, et la placeront plutôt à la valeur par défaut.

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

mais imaginez maintenant que votre db par défaut est "une chaîne", votre modèle / définition ruby par défaut est "une autre chaîne", mais dans un certain scénario, vous voulez pour définissez la valeur à "une chaîne de caractères" (la valeur par défaut db): MyModel.new(my_attr: 'some_string')

il en résultera que my_attr_changed? sera false parce que la valeur correspond à la valeur par défaut de la base de données, qui à son tour allumera votre code par défaut ruby-défini et définira la valeur à" une autre chaîne de caractères " -- encore une fois, pas ce que vous vouliez.


pour ces raisons, je ne pense pas que cela puisse être correctement accompli avec un simple crochet d'after_initialisation.

encore une fois, je pense que la" default_value_for "gem prend la bonne approche: https://github.com/FooBarWidget/default_value_for

1
répondu etipton 2016-08-20 04:35:02

bien que faire cela pour définir les valeurs par défaut soit déroutant et embarrassant dans la plupart des cas, vous pouvez utiliser :default_scope aussi. Découvrez squil commentaire ici .

0
répondu skalee 2010-07-21 12:57:32

la méthode after_initialize est dépréciée, utilisez le callback à la place.

after_initialize :defaults

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

cependant, en utilisant : par défaut dans vos migrations est toujours le moyen le plus propre.

0
répondu Greg 2010-10-17 01:59:13

j'ai trouvé que l'utilisation d'une méthode de validation fournit beaucoup de contrôle sur le réglage par défaut. Vous pouvez même définir les valeurs par défaut (ou la validation par défaut) pour les mises à jour. Vous avez même défini une valeur par défaut différente pour inserts vs mises à jour si vous le vouliez 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

concernant la définition d'une méthode after_initialize, il pourrait y avoir des problèmes de performance car after_initialize est aussi appelé par chaque objet retourné par :trouver : http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find

0
répondu Kelvin 2011-03-30 21:24:23

si la colonne se trouve être une colonne de type "status", et que votre modèle se prête à l'utilisation de machines d'état, envisagez d'utiliser le AASM gem , après quoi vous pouvez simplement faire

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

il n'initialise toujours pas la valeur pour les enregistrements Non gravés, mais il est un peu plus propre que rouler votre propre avec init ou peu importe, et vous récoltez les autres avantages d'aasm tels que des lunettes pour tous vos statuts.

0
répondu Bad Request 2013-11-11 22:30:44

https://github.com/keithrowell/rails_default_value

class Task < ActiveRecord::Base
  default :status => 'active'
end
0
répondu Keith Rowell 2016-03-24 12:39:31

utiliser default_scope dans les rails 3

doc api

ActiveRecord masque la différence entre défaut défini dans la base de données (schéma) et défaut fait dans l'application (modèle). Lors de l'initialisation, il analyse le schéma de la base de données et Note les valeurs par défaut qui y sont spécifiées. Plus tard, lors de la création d'objets, il assigne les valeurs par défaut spécifiées dans le schéma sans toucher à la base de données.

discussion

-1
répondu Viktor Trón 2017-05-23 12:34:45

de l'api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html Utilisez la méthode before_validation dans votre model, elle vous donne les options de création d'initialisation spécifique pour créer et mettre à jour les appels par exemple dans cet exemple (encore une fois du code tiré de l'exemple api docs) le champ nombre est initialisé pour une carte de crédit. Vous pouvez facilement l'adapter 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 attribute_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

-2
répondu jamesc 2012-05-31 18:43:21