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:
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. initialize
peut fonctionner, mais n’oubliez pas d’appeler super
! 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:
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
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?
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.
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.
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.
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