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

108

J'essaie de trouver le meilleur moyen de définir des valeurs par défaut pour les objets dans Rails.

Le mieux que je puisse penser est de définir la valeur par défaut dans la newméthode dans le contrôleur.

Quelqu'un a-t-il quelque chose à dire si cela est acceptable ou s'il existe une meilleure façon de le faire?

biagidp
la source
1
Quels sont ces objets; comment sont-ils consommés / utilisés? Sont-ils utilisés lors du rendu des vues ou pour la logique du contrôleur?
Gishu
3
Si vous parlez d'un objet ActiveRecord, je dois vous dire qu'il n'y a pas de solution sensée au problème des «valeurs par défaut». Seuls des hacks insensés, et les auteurs de rails ne semblent pas penser que la fonctionnalité en vaut la peine (incroyable car seule la communauté des rails l'est ...)
Mauricio
Étant donné que la plupart des réponses acceptées se concentrent sur ActiveRecords, nous supposons que la question initiale concernait AcitveRecords. Par conséquent duplication possible de stackoverflow.com/questions/328525/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

98

«Correct» est un mot dangereux en Ruby. Il y a généralement plus d'une façon de faire quoi que ce soit. Si vous savez que vous voudrez toujours cette valeur par défaut pour cette colonne sur cette table, les définir dans un fichier de migration de base de données est le moyen le plus simple:

class SetDefault < ActiveRecord::Migration
  def self.up
    change_column :people, :last_name, :type, :default => "Doe"
  end

  def self.down
    # You can't currently remove default values in Rails
    raise ActiveRecord::IrreversibleMigration, "Can't remove the default"
  end
end

Étant donné qu'ActiveRecord détecte automatiquement les propriétés de votre table et de votre colonne, la même valeur par défaut sera définie dans tout modèle l'utilisant dans n'importe quelle application Rails standard.

Cependant, si vous ne voulez que des valeurs par défaut définies dans des cas spécifiques - par exemple, c'est un modèle hérité qui partage une table avec d'autres - alors une autre manière élégante est de le faire directement dans votre code Rails lorsque l'objet de modèle est créé:

class GenericPerson < Person
  def initialize(attributes=nil)
    attr_with_defaults = {:last_name => "Doe"}.merge(attributes)
    super(attr_with_defaults)
  end
end

Ensuite, lorsque vous faites un GenericPerson.new(), il va toujours ruisseler l'attribut "Doe" jusqu'à Person.new()moins que vous ne le remplaciez par autre chose.

SFEley
la source
2
Essayez-le. Cela fonctionnera sur de nouveaux objets de modèle appelés avec la .newméthode de classe. La discussion de ce billet de blog sur l'appel direct d'ActiveRecord .allocateportait sur les objets de modèle chargés avec les données existantes de la base de données. ( Et c'est une idée terrible pour ActiveRecord de fonctionner de cette façon, OMI. Mais ce n'est pas la question.)
SFEley
2
Si vous prenez du recul pour relire la question de l'affiche originale, Nikita, puis mes commentaires dans l'ordre, cela peut avoir plus de sens pour vous. Sinon ... Eh bien, on a répondu à la question. Bonne journée.
SFEley
8
Mieux pour la migration descendante: change_column_default :people, :last_name, nil stackoverflow.com/a/1746246/483520
Nolan Amy
9
Veuillez noter que pour les versions de rails plus récentes, vous avez besoin d'un paramètre de type supplémentaire avant le paramètre par défaut. Rechercher: tapez sur cette page.
Ben Wheeler
3
@JoelBrewer J'ai rencontré ceci aujourd'hui - pour Rails 4, vous devez également spécifier le type de colonne:change_column :people, :last_name, :string, default: "Doe"
GoBusto
56

Sur la base de la réponse de SFEley, en voici une mise à jour / corrigée pour les nouvelles versions de Rails:

class SetDefault < ActiveRecord::Migration
  def change
    change_column :table_name, :column_name, :type, default: "Your value"
  end
end
J -_- L
la source
2
Attention, cela ne fonctionne PAS sur Rails 4.2.x (non testé sur les versions ultérieures). comme cela change_columnpeut être tout à fait «structurant», l'opération inverse ne peut être déduite. Si vous n'êtes pas sûr, testez-le simplement en exécutant db:migrateet db:rollbackjuste après. Réponse acceptée comme le même résultat mais au moins c'est supposé!
gfd le
1
C'est la bonne réponse pour moi .. Cela fonctionne avec les rails 5.1 mais vous devrez faire upet downcomme décrit ci-dessus de @GoBusto
Fabrizio Bertoglio
22

Tout d'abord, vous ne pouvez pas surcharger initialize(*args)car il n'est pas appelé dans tous les cas.

Votre meilleure option est de mettre vos valeurs par défaut dans votre migration:

add_column :accounts, :max_users, :integer, :default => 10

Le deuxième mieux est de placer des valeurs par défaut dans votre modèle, mais cela ne fonctionnera qu'avec des attributs initialement nuls. Vous pouvez avoir des problèmes comme je l'ai fait avec les booleancolonnes:

def after_initialize
  if new_record?
    max_users ||= 10
  end
end

Vous en avez besoin new_record?pour que les valeurs par défaut ne remplacent pas les valeurs chargées à partir de la base de données.

Vous devez ||=empêcher Rails de remplacer les paramètres passés dans la méthode d'initialisation.

Ian Purton
la source
12
note mineure - vous voulez faire deux choses: .... 1) n'appelez pas votre méthode after_initialize. Vous voulez after_initiation :your_method_name.... 2 utilisationself.max_users ||= 10
Jesse Wolgamott
5
Pour les booléens, faites simplement ceci: prop = true si prop.nil?
Francis Potter
2
ou after_initialize doau lieu dedef after_initialize
fotanus
self.max_users pour être sûr.
Ken Ratanachai S.
15

Vous pouvez également essayer change_column_defaultdans vos migrations (testé dans Rails 3.2.8):

class SetDefault < ActiveRecord::Migration
  def up
    # Set default value
    change_column_default :people, :last_name, "Smith"
  end

  def down
    # Remove default
    change_column_default :people, :last_name, nil
  end
end

Documentation sur l'API change_column_default Rails

Sebastiaan Pouyet
la source
1
La réponse acceptée est plus complète mais j'aime cette réponse propre et documentée ... Merci!
gfd le
7

Si vous faites référence à des objets ActiveRecord, vous avez (plus de) deux façons de procéder:

1. Utilisez un: paramètre par défaut dans le DB

PAR EXEMPLE

class AddSsl < ActiveRecord::Migration
  def self.up
    add_column :accounts, :ssl_enabled, :boolean, :default => true
  end

  def self.down
    remove_column :accounts, :ssl_enabled
  end
end

Plus d'informations ici: http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

2. Utilisez un rappel

PAR EXEMPLE before_validation_on_create

Plus d'informations ici: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147

Vlad Zloteanu
la source
2
Le problème avec l'utilisation des valeurs par défaut dans la base de données n'est-il pas qu'ils ne sont pas définis tant que l'objet n'est pas enregistré? Si je veux construire un nouvel objet avec les valeurs par défaut remplies, je dois les définir dans l'initialiseur.
Rafe
Les booléens ne peuvent pas être définis par défaut sur 1 ou 0 - ils doivent être définis sur true ou false (voir la réponse de Silasj).
Jamon Holmgren
@JamonHolmgren Merci pour la remarque, j'ai corrigé la réponse :)
Vlad Zloteanu
Pas de problème @VladZloteanu
Jamon Holmgren
7

Dans Ruby on Rails v3.2.8, en utilisant le after_initializerappel ActiveRecord, vous pouvez appeler une méthode dans votre modèle qui attribuera les valeurs par défaut pour un nouvel objet.

Le rappel after_initialize est déclenché pour chaque objet trouvé et instancié par un chercheur, after_initialize étant également déclenché après l'instanciation de nouveaux objets ( voir Rappels ActiveRecord ).

Donc, IMO, cela devrait ressembler à quelque chose comme:

class Foo < ActiveRecord::Base
  after_initialize :assign_defaults_on_new_Foo
  ...
  attr_accessible :bar
  ...
  private
  def assign_defaults_on_new_Foo
    # required to check an attribute for existence to weed out existing records
    self.bar = default_value unless self.attribute_whose_presence_has_been_validated
  end
end

Foo.bar = default_valuepour cette instance, sauf si l'instance contient une attribute_whose_presence_has_been_validatedsauvegarde / mise à jour précédemment en cours. Le default_valuesera ensuite utilisé avec votre vue pour rendre le formulaire en utilisant default_valuepour l' barattribut.

Au mieux, c'est du piratage ...

EDIT - utilisez 'new_record?' pour vérifier si l'instanciation à partir d'un nouvel appel

Au lieu de vérifier une valeur d'attribut, utilisez la new_record?méthode intégrée avec des rails. Ainsi, l'exemple ci-dessus devrait ressembler à:

class Foo < ActiveRecord::Base
  after_initialize :assign_defaults_on_new_Foo, if: 'new_record?'
  ...
  attr_accessible :bar
  ...
  private
  def assign_defaults_on_new_Foo
    self.bar = default_value
  end
end

C'est beaucoup plus propre. Ah, la magie de Rails - c'est plus intelligent que moi.

erroné
la source
Cela ne fonctionne pas comme je m'y attendais - il était censé définir la valeur par défaut uniquement sur new, mais il définit également l'attribut par défaut pour chaque objet trouvé et instancié (c'est-à-dire les enregistrements chargés à partir de la base de données). Vous pouvez vérifier un attribut d'objet à la recherche de valeurs avant d'attribuer la valeur par défaut, mais ce n'est pas une solution très élégante ou robuste.
erroric
1
Pourquoi recommandez-vous le after_initializerappel ici? La documentation Rails sur les rappels a un exemple de définition de la valeur par défaut before_createsans vérification de condition supplémentaire
Dfr
@Dfr - J'ai dû manquer ça, pouvez-vous me jeter un lien pour revoir et je mettrai à jour la réponse ...
erroric
Oui, ici api.rubyonrails.org/classes/ActiveRecord/Callbacks.html premier exemple, classeSubscription
Dfr
5
@Dfr - La raison pour laquelle nous utilisons le after_initialize, au lieu du before_createrappel, est que nous voulons définir une valeur par défaut pour l' utilisateur (à utiliser dans une vue) lorsqu'il crée de nouveaux objets. Le before_createrappel est appelé après que l' utilisateur a reçu un nouvel objet, fourni son entrée et soumis l'objet pour la création au contrôleur. Le contrôleur vérifie ensuite les éventuels before_createrappels. Cela semble contre-intuitif, mais c'est une chose de nomenclature - se before_createréfère à l' createaction. L'instanciation d'un nouvel objet ne fait pas createl'objet.
erroric du
5

Pour les champs booléens dans Rails 3.2.6 au moins, cela fonctionnera dans votre migration.

def change
  add_column :users, :eula_accepted, :boolean, default: false
end

Mettre un 1ou 0pour une valeur par défaut ne fonctionnera pas ici, car il s'agit d'un champ booléen. Ce doit être une valeur trueou false.

Silasj
la source
2

Générer une migration et une utilisation change_column_default, c'est succinct et réversible:

class SetDefaultAgeInPeople < ActiveRecord::Migration[5.2]
  def change
    change_column_default :people, :age, { from: nil, to: 0 }
  end
end
Père Joan Martorell
la source
1

Si vous définissez simplement des valeurs par défaut pour certains attributs d'un modèle basé sur une base de données, j'envisagerais d'utiliser les valeurs de colonne par défaut SQL - pouvez-vous clarifier les types de valeurs par défaut que vous utilisez?

Il existe un certain nombre d'approches pour le gérer, ce plugin ressemble à une option intéressante.

paulthenerd
la source
1
Je pense que vous ne devriez pas dépendre de la base de données pour gérer les valeurs par défaut et les contraintes, tout cela devrait être trié dans la couche de modèle. Ce plugin ne fonctionne que pour les modèles ActiveRecord, ce n'est pas un moyen générique de définir les valeurs par défaut des objets.
Lukas Stejskal
Je dirais que cela dépend des types de valeurs par défaut que vous essayez d'utiliser, ce n'est pas quelque chose que j'ai dû faire très souvent, mais je dirais que les contraintes sont en fait beaucoup mieux définies à la fois dans la base de données et le modèle - pour éviter que vos données ne deviennent invalides dans la base de données.
paulthenerd
1

La suggestion de remplacer new / initialize est probablement incomplète. Rails appellera (fréquemment) allocate pour les objets ActiveRecord, et les appels à allouer n'entraîneront pas d'appels à initialiser.

Si vous parlez d'objets ActiveRecord, jetez un œil à la substitution de after_initialize.

Ces articles de blog (pas les miens) sont utiles:

Valeurs par défaut Constructeurs par défaut non appelés

[Edit: SFEley souligne que Rails regarde réellement la valeur par défaut dans la base de données quand il instancie un nouvel objet en mémoire - je ne l'avais pas réalisé.]

James Moore
la source
1

J'avais besoin de définir une valeur par défaut comme si elle était spécifiée comme valeur de colonne par défaut dans DB. Donc ça se comporte comme ça

a = Item.new
a.published_at # => my default value

a = Item.new(:published_at => nil)
a.published_at # => nil

Comme le rappel after_initialize est appelé après avoir défini des attributs à partir d'arguments, il n'y avait aucun moyen de savoir si l'attribut est nul car il n'a jamais été défini ou parce qu'il a été intentionnellement défini comme nul. J'ai donc dû fouiller un peu à l'intérieur et suis venu avec cette solution simple.

class Item < ActiveRecord::Base
  def self.column_defaults
    super.merge('published_at' => Time.now)
  end
end

Fonctionne très bien pour moi. (Rails 3.2.x)

Petr '' Bubák '' Šedivý
la source
0

J'ai répondu à une question similaire ici .. une façon simple de le faire est d'utiliser Rails attr_accessor_with_default

class SOF
  attr_accessor_with_default :is_awesome,true
end

sof = SOF.new
sof.is_awesome

=> true

METTRE À JOUR

attr_accessor_with_default est obsolète dans Rails 3.2 .. vous pouvez le faire à la place avec du Ruby pur

class SOF
  attr_writer :is_awesome

  def is_awesome
    @is_awesome ||= true
  end
end

sof = SOF.new
sof.is_awesome

#=> true
Orlando
la source
attr_accessor_with_defaultest obsolète à partir de rails> 3.1.0
flynfish
Dans votre exemple, is_awesome sera toujours vrai même lorsque @is_awesome == false.
Ritchie
-3

Vous pouvez remplacer le constructeur du modèle ActiveRecord.

Comme ça:

def initialize(*args)
  super(*args)
  self.attribute_that_needs_default_value ||= default_value
  self.attribute_that_needs_another_default_value ||= another_default_value
  #ad nauseum
end
Terry
la source
2
C'est un endroit terrible pour changer la fonctionnalité des rails de base. Il existe d'autres moyens beaucoup plus stables, moins susceptibles de casser d'autres choses, des moyens de le faire mentionnés dans d'autres réponses. Le patching de singe ne devrait être qu'un dernier recours pour des choses qui ne peuvent pas être faites autrement.
Volonté du