Pregunta

¿Cómo puedo establecer el valor predeterminado en ActiveRecord?

Veo una publicación de Pratik que describe un fragmento de código feo y complicado: 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

He visto los siguientes ejemplos buscando en Google:

  def initialize 
    super
    self.status = ACTIVE unless self.status
  end

y

  def after_initialize 
    return unless new_record?
    self.status = ACTIVE
  end

También he visto a personas ponerlo en su migración, pero prefiero verlo definido en el código del modelo.

¿Existe una forma canónica de establecer el valor predeterminado para los campos en el modelo ActiveRecord?

¿Fue útil?

Solución

Hay varios problemas con cada uno de los métodos disponibles, pero creo que definir una devolución de llamada after_initialize es el camino a seguir por las siguientes razones:

  1. default_scope inicializará los valores para los nuevos modelos, pero luego se convertirá en el ámbito en el que encontrará el modelo. Si solo desea inicializar algunos números a 0, entonces esto es no lo que desea.
  2. Definir valores predeterminados en su migración también funciona parte del tiempo ... Como ya se mencionó, esto no funcionará cuando llame a Model.new.
  3. Anular initialize puede funcionar, ¡pero no olvides llamar a super !
  4. Usar un complemento como el de phusion se está volviendo un poco ridículo. Esto es ruby, ¿realmente necesitamos un complemento solo para inicializar algunos valores predeterminados?
  5. Reemplazar after_initialize está en desuso a partir de Rails 3. Cuando anulo after_initialize en rails 3.0.3, aparece la siguiente advertencia en la consola :
  

ADVERTENCIA DE DEPRECACIÓN: Base # after_initialize ha quedado en desuso, utilice Base.after_initialize: método en su lugar. (llamado desde / Users / me / myapp / app / models / my_model: 15)

Por lo tanto, diría que escriba una devolución de llamada after_initialize , que le permite los atributos predeterminados además de permitiéndole establecer valores predeterminados en las asociaciones de esta manera:

  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    

Ahora tiene solo un lugar para buscar la inicialización de sus modelos. Estoy usando este método hasta que alguien encuentre uno mejor.

Advertencias:

  1. Para campos booleanos hacer:

    self.bool_field = true si self.bool_field.nil?

    Vea el comentario de Paul Russell sobre esta respuesta para más detalles

  2. Si solo está seleccionando un subconjunto de columnas para un modelo (es decir, usando select en una consulta como Person.select (: firstname,: lastname) .all ) obtendrá un MissingAttributeError si su método init accede a una columna que no se ha incluido en la cláusula select . Puede protegerse contra este caso así:

    self.number || = 0.0 if self.has_attribute? : número

    y para una columna booleana ...

    self.bool_field = true if (self.has_attribute?: bool_value) & amp; & amp; self.bool_field.nil?

    También tenga en cuenta que la sintaxis es diferente a Rails 3.2 (vea el comentario de Cliff Darling a continuación)

Otros consejos

Ponemos los valores predeterminados en la base de datos mediante migraciones (especificando la opción : default en cada definición de columna) y permitimos que Active Record use estos valores para establecer el valor predeterminado para cada atributo.

En mi humilde opinión, este enfoque está alineado con los principios de AR: convención sobre la configuración, DRY, la definición de la tabla impulsa el modelo, no al revés.

Tenga en cuenta que los valores predeterminados todavía están en el código de la aplicación (Ruby), aunque no en el modelo sino en las migraciones.

Rieles 5+

Puede usar el atributo método dentro de sus modelos, por ejemplo:

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

También puede pasar una lambda al parámetro default . Ejemplo:

attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }

Algunos casos simples pueden manejarse definiendo un valor predeterminado en el esquema de la base de datos, pero eso no maneja una cantidad de casos más complicados, incluidos los valores calculados y las claves de otros modelos. Para estos casos hago esto:

after_initialize :defaults

def defaults
   unless persisted?
    self.extras||={}
    self.other_stuff||="This stuff"
    self.assoc = [OtherModel.find_by_name('special')]
  end
end

Decidí usar after_initialize pero no quiero que se aplique a los objetos que se encuentran solo aquellos nuevos o creados. Creo que es casi impactante que no se proporcione una devolución de llamada after_new para este caso de uso obvio, pero lo hice confirmando si el objeto ya persiste, lo que indica que no es nuevo.

Después de ver la respuesta de Brad Murray, esto es aún más claro si la condición se traslada a la solicitud de devolución de llamada:

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

El patrón de devolución de llamada after_initialize se puede mejorar simplemente haciendo lo siguiente

after_initialize :some_method_goes_here, :if => :new_record?

Esto tiene un beneficio no trivial si su código de inicio necesita tratar con asociaciones, ya que el siguiente código activa un sutil n + 1 si lee el registro inicial sin incluir el asociado.

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end

Los chicos de Phusion tienen un buen plugin para esto.

Una forma potencial aún mejor / más limpia que las respuestas propuestas es sobrescribir el descriptor de acceso, de esta manera:

def status
  self['status'] || ACTIVE
end

Ver " Sobrescribir accesos predeterminados " en el ActiveRecord :: Documentación base y más de StackOverflow al usar self .

Uso la atributo-valores predeterminados gem

De la documentación: ejecute sudo gem install attribute-defaults y agregue require 'attribute_defaults' a su aplicación.

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"

Preguntas similares, pero todas tienen un contexto ligeramente diferente: - ¿Cómo creo un valor predeterminado para atributos en el modelo de Rails activerecord?

La mejor respuesta: ¡Depende de lo que quieras!

Si desea que cada objeto comience con un valor: use after_initialize: init

¿Desea que el formulario new.html tenga un valor predeterminado al abrir la página? 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 desea que cada objeto tenga un valor calculado a partir de la entrada del usuario: use before_save: default_values ?? ¿Desea que el usuario ingrese X y luego Y = X + 'foo' ? uso:

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

¡Para eso están los constructores! Anule el método initialize del modelo.

Utilice el método after_initialize .

Sup chicos, terminé haciendo lo siguiente:

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

¡Funciona como un encanto!

Primero lo primero: no estoy en desacuerdo con la respuesta de Jeff. Tiene sentido cuando su aplicación es pequeña y su lógica simple. Estoy tratando de dar una idea de cómo puede ser un problema al crear y mantener una aplicación más grande. No recomiendo usar este enfoque primero al construir algo pequeño, pero para tenerlo en cuenta como un enfoque alternativo:


Una pregunta aquí es si este valor predeterminado en los registros es lógica empresarial. Si es así, sería prudente incluirlo en el modelo ORM. Como el campo que menciona ryw es activo , esto suena como una lógica de negocios. P.ej. el usuario está activo.

¿Por qué sería cauteloso de poner preocupaciones comerciales en un modelo ORM?

  1. Se rompe SRP . Cualquier clase que herede de ActiveRecord :: Base ya está haciendo un lote de cosas diferentes, la principal de ellas es la consistencia de los datos (validaciones) y la persistencia (guardar). Poniendo lógica de negocios, por pequeña que sea, con AR :: Base rompe SRP.

  2. Es más lento para probar. Si quiero probar cualquier forma de lógica que ocurra en mi modelo ORM, mis pruebas deben inicializar Rails para poder ejecutarse. Esto no será un gran problema al comienzo de su aplicación, pero se acumulará hasta que las pruebas unitarias demoren mucho tiempo en ejecutarse.

  3. Romperá SRP aún más en el futuro, y de manera concreta. ¿Digamos que nuestro negocio ahora requiere que enviemos correos electrónicos a los usuarios cuando haya un elemento activo? Ahora estamos agregando lógica de correo electrónico al modelo ORM del artículo, cuya responsabilidad principal es modelar un artículo. No debería importarle la lógica del correo electrónico. Este es un caso de efectos secundarios comerciales . Estos no pertenecen al modelo ORM.

  4. Es difícil diversificar. He visto aplicaciones Rails maduras con cosas como una base de datos respaldada por init_type: string field, cuyo único propósito es controlar la lógica de inicialización. Esto está contaminando la base de datos para solucionar un problema estructural. Hay mejores formas, creo.

La forma PORO: Si bien se trata de un código un poco más, le permite mantener separados sus Modelos ORM y su Lógica empresarial. El código aquí está simplificado, pero debería mostrar la idea:

class SellableItemFactory
  def self.new(attributes = {})
    record = Item.new(attributes)
    record.active = true if record.active.nil?
    record
  end
end

Entonces, con esto en su lugar, la forma de crear un nuevo elemento sería

SellableItemFactory.new

Y mis pruebas ahora podrían simplemente verificar que ItemFactory se active en Item si no tiene un valor. No se necesita inicialización de Rails, no se rompe el SRP. Cuando la inicialización del elemento se hace más avanzada (por ejemplo, establecer un campo de estado, un tipo predeterminado, etc.), ItemFactory puede agregar esto. Si terminamos con dos tipos de valores predeterminados, podemos crear un nuevo BusinesCaseItemFactory para hacer esto.

NOTA: También podría ser beneficioso usar la inyección de dependencia aquí para permitir que la fábrica construya muchas cosas activas, pero lo dejé por simplicidad. Aquí está: self.new (klass = Item, atributos = {})

Esto ha sido respondido durante mucho tiempo, pero necesito valores predeterminados con frecuencia y prefiero no ponerlos en la base de datos. Creo una preocupación de 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

Y luego usarlo en mis modelos así:

class Widget < ApplicationRecord
  include DefaultValues

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

También he visto a personas ponerlo en su migración, pero prefiero verlo   definido en el código del modelo.

     

¿Existe una forma canónica de establecer el valor predeterminado para los campos en   ¿Modelo ActiveRecord?

La forma canónica de Rails, antes de Rails 5, era en realidad establecerlo en la migración, y solo mirar en el db / schema.rb cada vez que quiera ver qué valores predeterminados están siendo establecidos por El DB para cualquier modelo.

Al contrario de lo que dice la respuesta de @Jeff Perrin (que es un poco viejo), el enfoque de migración incluso aplicará el valor predeterminado cuando se usa Model.new , debido a la magia de Rails. Trabajo verificado en Rails 4.1.16.

Lo más simple es a menudo lo mejor. Menos deuda de conocimiento y posibles puntos de confusión en la base de código. Y 'simplemente funciona'.

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

El null: false no permite valores NULL en la base de datos y, como beneficio adicional, también actualiza todos los registros de bases de datos preexistentes con el valor predeterminado para este campo también. Si lo desea, puede excluir este parámetro en la migración, ¡pero lo encontré muy útil!

La forma canónica en Rails 5+ es, como dijo @Lucas Caton:

class Item < ActiveRecord::Base
  attribute :scheduler_type, :string, default: 'hotseat'
end

El problema con las soluciones after_initialize es que debe agregar after_initialize a cada objeto que busque fuera de la base de datos, independientemente de si accede a este atributo o no. Sugiero un enfoque cargado de pereza.

Los métodos de atributo (getters) son, por supuesto, métodos en sí mismos, por lo que puede anularlos y proporcionar un valor predeterminado. Algo así como:

Class Foo < ActiveRecord::Base
  # has a DB column/field atttribute called 'status'
  def status
    (val = read_attribute(:status)).nil? ? 'ACTIVE' : val
  end
end

A menos que, como alguien señaló, debe hacer Foo.find_by_status ('ACTIVE'). En ese caso, creo que realmente necesitaría establecer el valor predeterminado en las restricciones de su base de datos, si el DB lo admite.

Me encontré con problemas con after_initialize al dar ActiveModel :: MissingAttributeError errores al hacer búsquedas complejas:

por ejemplo:

@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)

" buscar " en .where es hash de condiciones

Así que terminé haciéndolo anulando initialize de esta manera:

def initialize
  super
  default_values
end

private
 def default_values
     self.date_received ||= Date.current
 end

La llamada super es necesaria para asegurarse de que el objeto se inicialice correctamente desde ActiveRecord :: Base antes de hacer mi código de personalización, es decir: valores_predeterminados

class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end

Recomiendo encarecidamente usar el " default_value_for " gema: https://github.com/FooBarWidget/default_value_for

Hay algunos escenarios difíciles que requieren anular el método de inicialización, lo que hace esa gema.

Ejemplos:

Su valor predeterminado de db es NULL, su valor predeterminado definido por modelo / ruby ??es " alguna cadena " ;, pero en realidad desea establecer el valor en nil por cualquier razón: MyModel.new (my_attr: nil)

La mayoría de las soluciones aquí no podrán establecer el valor en nulo, y en su lugar lo establecerán en el valor predeterminado.

OK, así que en lugar de adoptar el enfoque || = , cambie a my_attr_changed? ...

PERO ahora imagine que su valor predeterminado de base de datos es " alguna cadena " ;, su valor predeterminado definido por modelo / ruby ??es " alguna otra cadena " ;, pero bajo un escenario determinado, quiere para establecer el valor en " alguna cadena " (el valor predeterminado de db): MyModel.new (my_attr: 'some_string')

Esto dará como resultado que my_attr_changed? sea falso porque el valor coincide con el valor predeterminado de db, que a su vez activará el código predeterminado definido por ruby ??y establecerá el valor en " ; alguna otra cuerda " - de nuevo, no es lo que deseabas.


Por esas razones, no creo que esto se pueda lograr correctamente con solo un gancho after_initialize.

Nuevamente, creo que el " valor_predeterminado_para " gem está tomando el enfoque correcto: https://github.com/FooBarWidget/default_value_for

Aunque hacer eso para establecer los valores predeterminados es confuso e incómodo en la mayoría de los casos, también puede usar : default_scope . Consulte comentario de squil aquí .

el método after_initialize está en desuso, use la devolución de llamada en su lugar.

after_initialize :defaults

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

sin embargo, usar : default en sus migraciones sigue siendo la forma más limpia.

Descubrí que el uso de un método de validación proporciona mucho control sobre la configuración predeterminada. Incluso puede establecer valores predeterminados (o fallar la validación) para las actualizaciones. Incluso puede establecer un valor predeterminado diferente para inserciones vs actualizaciones si realmente lo desea. Tenga en cuenta que el valor predeterminado no se establecerá hasta #valid? se llama.

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

Con respecto a la definición de un método after_initialize, podría haber problemas de rendimiento porque after_initialize también es llamado por cada objeto devuelto por: find: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initfterize-andand-inafter_

Si la columna es una columna de tipo 'estado' y su modelo se presta para el uso de máquinas de estado, considere usar el gema de asas , después de lo cual puedes simplemente hacer

  aasm column: "status" do
    state :available, initial: true
    state :used
    # transitions
  end

Todavía no inicializa el valor de los registros no guardados, pero es un poco más limpio que rodar el tuyo con init o lo que sea, y obtienes los otros beneficios del aasmo, como los ámbitos para todos tus estados.

https://github.com/keithrowell/rails_default_value

class Task < ActiveRecord::Base
  default :status => 'active'
end

Aquí hay una solución que he usado y que me sorprendió un poco que aún no se ha agregado.

Hay dos partes. La primera parte es establecer el valor predeterminado en la migración real, y la segunda parte es agregar una validación en el modelo para garantizar que la presencia sea verdadera.

add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'

Entonces verá aquí que el valor predeterminado ya está configurado. Ahora, en la validación, desea asegurarse de que siempre haya un valor para la cadena, así que solo haga

 validates :new_team_signature, presence: true

Lo que esto hará es establecer el valor predeterminado para usted. (para mí tengo '' Bienvenido al equipo ''), y luego irá un paso más allá para asegurarme de que siempre haya un valor presente para ese objeto.

¡Espero que eso ayude!

use default_scope en rails 3

api doc

ActiveRecord oculta la diferencia entre el valor predeterminado definido en la base de datos (esquema) y el valor predeterminado realizado en la aplicación (modelo). Durante la inicialización, analiza el esquema de la base de datos y anota cualquier valor predeterminado especificado allí. Más tarde, al crear objetos, asigna los valores predeterminados especificados por el esquema sin tocar la base de datos.

discusión

De los documentos de la api http://api.rubyonrails.org/classes/ActiveRecord/ Callbacks.html Use el método before_validation en su modelo, le brinda las opciones de crear una inicialización específica para crear y actualizar llamadas p.ej. en este ejemplo (nuevamente el código tomado del ejemplo de api docs) el campo de número se inicializa para una tarjeta de crédito. Puede adaptar esto fácilmente para establecer los valores que desee

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

Sorprendido de que el suyo no haya sido sugerido aquí

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top