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?
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:
-
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. - 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.
- Substitution
initialize
peut fonctionner, mais n'oubliez pas d'appelersuper
! - 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?
- Substitution
after_initialize
est déprécié à partir des Rails 3. Lorsque je remplaceafter_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:
-
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
-
si vous sélectionnez seulement un sous-ensemble de colonnes pour un modèle (i.e.; en utilisant
select
dans une requête commePerson.select(:firstname, :lastname).all
) vous obtiendrez unMissingAttributeError
si votre méthodeinit
accède à une colonne qui n'a pas été incluse dans la clauseselect
. 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)
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).
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
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
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
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 .
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"
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
C'est à ça que servent les constructeurs! Outrepasser la méthode initialize
du modèle.
utiliser 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è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?
-
Ç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.
-
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.
-
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.
-
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 = {})
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
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
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.
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
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
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
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 .
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.
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
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.
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end
utiliser default_scope dans les rails 3
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.
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