ruby-on-rails rails - Comment puis-je définir des valeurs par défaut dans ActiveRecord?




generate model (21)

Comment puis-je 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 googler:

  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 une méthode canonique pour définir la valeur par défaut des champs dans le modèle ActiveRecord?


Answers

Premières choses d'abord: je ne suis pas en désaccord avec la réponse de Jeff. Il est logique lorsque votre application est petite et votre logique simple. Je suis ici pour essayer de donner un aperçu de la façon dont cela peut être un problème lors de la construction et du maintien d'une application plus grande. Je ne recommande pas d'utiliser cette approche d'abord lors de la construction de quelque chose de petit, mais de le garder à 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. Puisque le champ ryw mentions est actif , cela ressemble à une logique métier. Par exemple, l'utilisateur est actif.

Pourquoi serais-je prudent de mettre les préoccupations d'affaires dans un modèle ORM?

  1. Cela brise la SRP . Toute classe héritant de ActiveRecord :: Base fait déjà beaucoup de choses différentes, dont la cohérence des données (validations) et la persistance (sauvegarde). Mettre la logique métier, aussi petite soit-elle, avec AR :: Base rompt la SRP.

  2. Il est plus lent à tester. Si je veux tester toute forme de logique qui se passe dans mon modèle ORM, mes tests doivent initialiser Rails pour fonctionner. Ce ne sera pas trop un problème au début de votre application, mais s'accumulera jusqu'à ce que vos tests unitaires prennent beaucoup de temps à courir.

  3. Cela va briser le SRP encore plus bas et de façon concrète. Supposons que notre entreprise nous oblige maintenant à envoyer un courriel aux utilisateurs lorsque les articles deviennent actifs? Nous ajoutons maintenant la logique de messagerie au modèle ORM Item, dont la responsabilité principale consiste à modéliser un élément. Il ne devrait pas se soucier de la logique d'email. Ceci est un cas d' effets secondaires commerciaux . Ceux-ci n'appartiennent pas au modèle ORM.

  4. C'est difficile de diversifier. J'ai vu des applications Rails matures avec des choses comme une base de données init_type: string, 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 meilleurs moyens, je crois.

La façon PORO: Bien que ce soit un peu plus de code, cela 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(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 article 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 d'initialisation Rails nécessaire, pas de rupture SRP. Lorsque l'initialisation d'élément devient plus avancée (par exemple, définir un champ d'état, un type par défaut, etc.), ItemFactory peut ajouter ceci. Si nous nous retrouvons avec deux types de valeurs par défaut, nous pouvons créer un nouveau BusinesCaseItemFactory pour le faire.

NOTE: Il pourrait également être avantageux d'utiliser l'injection de dépendance ici pour permettre à l'usine de construire beaucoup de choses actives, mais je l'ai laissé pour plus de simplicité. Le voici: self.new (klass = Item, attributs = {})


Cela a été répondu depuis longtemps, mais j'ai besoin de valeurs par défaut fréquemment et préfèrent 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 utilisez-le dans mes modèles comme ça:

class Widget < ApplicationRecord
  include DefaultValues

  defaults :category, to: 'uncategorized'
  defaults :token, to: -> { SecureRandom.uuid }
end


C'est ce que les constructeurs sont pour! Remplacer la méthode d' initialize du modèle.

Utilisez la méthode after_initialize .


Le problème avec les solutions after_initialize est que vous devez ajouter une 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 elles-mêmes des méthodes, 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, comme quelqu'un l'a souligné, vous devez faire Foo.find_by_status ('ACTIVE'). Dans ce cas, je pense que vous devez vraiment définir la valeur par défaut dans vos contraintes de base de données, si la base de données le supporte.


J'utilise le attribute-defaults gem

À partir de la documentation: exécutez 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"

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

Il y a quelques scénarios compliqués qui requièrent à peu près le remplacement de la méthode initialize, ce que fait cette gemme.

Exemples:

Votre db par défaut est NULL, votre modèle / ruby-defined par défaut est "some string", mais vous voulez en fait mettre la valeur à zéro pour une raison quelconque: MyModel.new(my_attr: nil)

La plupart des solutions ne parviendront pas à définir la valeur à zéro et la définiront par défaut.

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

MAIS imaginez que votre db default est "une chaîne", votre modèle / ruby-defined est "une autre chaîne", mais dans un certain scénario, vous voulez définir la valeur "some string" (la valeur par défaut): MyModel.new(my_attr: 'some_string')

Cela entraînera my_attr_changed? étant faux car la valeur correspond à la valeur par défaut de db, qui à son tour lancera votre code par défaut défini par ruby ​​et définira la valeur à "une autre chaîne" - encore une fois, pas ce que vous vouliez.

Pour ces raisons, je ne pense pas que cela puisse être accompli avec juste un hook after_initialize.

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


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)

"rechercher" dans le .where est un hachage de conditions

J'ai donc fini par le faire en redéfinissant initialiser de cette façon:

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


Bien que cela soit déroutant et gênant dans la plupart des cas, vous pouvez également utiliser :default_scope . Découvrez le commentaire de Squil ici .


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 une méthode canonique pour définir la valeur par défaut des champs dans le modèle ActiveRecord?

La méthode canonique Rails, avant Rails 5, était en fait de le définir dans la migration, et il suffit de regarder dans le db/schema.rb pour savoir quand les valeurs par défaut sont définies par la base de données pour n'importe quel modèle.

Contrairement à la réponse de @Jeff Perrin (qui est un peu ancienne), 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 potentiels points de confusion dans le code de base. Et ça "marche".

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

La valeur null: false n'autorise pas les valeurs NULL dans la base de données et, en plus, elle met également à jour tous les enregistrements de base de données préexistants avec 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

La méthode after_initialize est obsolète, utilisez plutôt le callback.

after_initialize :defaults

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

Cependant, utiliser : default dans vos migrations est toujours le moyen le plus propre.


Certains cas simples peuvent être traités en définissant une valeur par défaut dans le schéma de base de données, mais cela ne gère pas un certain nombre de cas plus complexes, 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 l'after_initialize mais je ne veux pas qu'il soit appliqué aux objets qui ne sont trouvés que ceux qui sont nouveaux ou créés. Je pense qu'il est presque choquant qu'un after_new callback ne soit pas fourni pour ce cas d'utilisation évident mais je l'ai fait en confirmant si l'objet est déjà persistant en indiquant qu'il n'est pas nouveau.

Ayant vu la réponse de Brad Murray, c'est encore plus propre 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

Nous mettons les valeurs par défaut dans la base de données via les 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.

À mon humble avis, cette approche est alignée avec les principes de AR: convention sur la configuration, DRY, la définition de la table conduit 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 (les) migration (s).


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 a un avantage non-trivial si votre code init doit gérer les associations, car le code suivant déclenche un subtil n + 1 si vous lisez l'enregistrement initial sans inclure l'associé.

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end

Sup gars, j'ai fini par faire ce qui suit:

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

Fonctionne comme un charme!


utilise default_scope dans les rails 3

api doc

ActiveRecord masque la différence entre la valeur par défaut définie dans la base de données (schéma) et la valeur par défaut définie dans l'application (modèle). Au cours 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 affecte les valeurs par défaut spécifiées par le schéma sans toucher à la base de données.

discussion


A partir de l'api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html Utilisez la méthode before_validation dans votre modèle, elle 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 (code pris à partir de l'exemple api docs) le champ de numéro est initialisé pour une carte de crédit. Vous pouvez facilement l'adapter pour définir les valeurs souhaitées

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 été suggéré ici


Dans Rails 5+, vous pouvez utiliser la méthode d' attribute dans vos modèles, par exemple:

class Account < ApplicationRecord
  attribute :locale, :string, default: 'en'
end


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? utilisez https://.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 ensuite Y = X+'foo' ? utilisation:

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status ||= 'P'
  end
end

Pour autant que je sache, jruby est simplement une implémentation ruby ​​écrite en java, ce qui permet une intégration facile avec le runtime java.





ruby-on-rails rails-activerecord