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?
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:
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.initialize
peut fonctionner, mais n'oubliez pas d'appeler super
!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:
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
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)
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.
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
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
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
Les gars de Phusion ont quelques Nice plugin pour ça.
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 .
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"
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_values
Vous 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
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?
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.
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.
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.
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 = {})
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!
C'est à cela que servent les constructeurs! Remplacez la méthode initialize
du modèle.
Utilisez la méthode after_initialize
.
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
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
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
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
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
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.
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.
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!
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 .
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
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.
https://github.com/keithrowell/Rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end