ruby on rails - rails - ¿Cómo puedo configurar los valores por defecto en ActiveRecord?
rails update (25)
También he visto a personas ponerlo en su migración, pero prefiero verlo definido en el código del modelo.
¿Hay una forma canónica de establecer el valor predeterminado para los campos en el modelo ActiveRecord?
La forma canónica de Rails, antes de Rails 5, era en realidad establecerla en la migración, y simplemente buscar en db/schema.rb
para ver qué valores predeterminados está configurando la DB para cualquier modelo.
Contrariamente a lo que dice la respuesta de @Jeff Perrin (que es un poco antiguo), el enfoque de migración incluso aplicará el valor predeterminado cuando se usa Model.new
, debido a la magia de Rails. Trabajo verificado en rieles 4.1.16.
Lo más simple es a menudo lo mejor. Menos deudas 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 base de datos preexistentes y se establece con el valor predeterminado para este campo también. Puede excluir este parámetro en la migración si lo desea, ¡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
¿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 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.
¿Hay una forma canónica de establecer el valor predeterminado para los campos en el modelo ActiveRecord?
Algunos casos simples pueden manejarse definiendo un valor predeterminado en el esquema de la base de datos, pero eso no controla un número 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 sorprendente que no se proporcione una devolución de llamada after_new para este caso de uso obvio, pero he confirmado si el objeto ya ha persistido para indicar que no es nuevo.
Habiendo visto 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
Aunque hacer eso para establecer valores predeterminados es confuso e incómodo en la mayoría de los casos, también puede usar :default_scope
. Echa un vistazo a los comentarios de squil aquí .
Colocamos los valores predeterminados en la base de datos a través de las 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 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 la (s) migración (es).
Descubrí que el uso de un método de validación proporciona mucho control sobre la configuración de los valores predeterminados. Incluso puede establecer valores predeterminados (o fallar la validación) para las actualizaciones. Incluso establece un valor predeterminado diferente para las inserciones frente a las actualizaciones si realmente lo desea. Tenga en cuenta que el valor predeterminado no se establecerá hasta que #válido? 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: guides.rubyonrails.org/…
Desde el api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html Use el método before_validation
en su modelo, le brinda las opciones para crear una inicialización específica para crear y actualizar llamadas, por ejemplo, en este ejemplo (nuevamente código tomado del ejemplo de api docs) el campo de número se inicializa para una tarjeta de crédito. Puede adaptarlo 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
Sorprendió que su no haya sido sugerido 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.
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 n + 1 sutil 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
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 perezoso.
Los métodos de atributo (captadores) son, por supuesto, métodos en sí mismos, por lo que puede anularlos y proporcionar un valor predeterminado. Algo 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ñalado, tengas que 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 la base de datos lo admite.
En Rails 5+, puede usar el método de attribute en sus modelos, por ejemplo:
class Account < ApplicationRecord
attribute :locale, :string, default: ''en''
end
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
Hay varios problemas con cada uno de los métodos disponibles, pero creo que la definición de una after_initialize
llamada after_initialize
es el camino a seguir por las siguientes razones:
-
default_scope
inicializará los valores para los nuevos modelos, pero luego se convertirá en el ámbito en el que encontrará el modelo. Si solo quieres inicializar algunos números a 0, esto no es lo que quieres. - La definición de valores predeterminados en su migración también funciona parte del tiempo ... Como ya se ha mencionado, esto no funcionará cuando solo llame a Model.new.
- Anular la
initialize
puede funcionar, pero no olvides llamarsuper
! - Usar un plugin como phusion''s se está volviendo un poco ridículo. Esto es Ruby, ¿realmente necesitamos un complemento solo para inicializar algunos valores predeterminados?
- La
after_initialize
está en desuso a partir de Rails 3. Cuandoafter_initialize
en rails 3.0.3,after_initialize
la siguiente advertencia en la consola:
ADVERTENCIA DE DEPRECATION: Base # after_initialize ha sido desaprobado, por favor use el método Base.after_initialize: en su lugar. (Llamado desde / Users / me / myapp / app / models / my_model: 15)
Por lo tanto, diría que escriba una after_initialize
llamada after_initialize
, que le permite atributos predeterminados además de permitirle establecer valores predeterminados en asociaciones como:
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 solo tiene un lugar donde buscar la inicialización de sus modelos. Estoy usando este método hasta que a alguien se le ocurra uno mejor.
Advertencias:
Para campos booleanos hacer:
self.bool_field = true if self.bool_field.nil?
Vea el comentario de Paul Russell sobre esta respuesta para más detalles.
Si solo está seleccionando un subconjunto de columnas para un modelo (es decir, al
select
en una consulta comoPerson.select(:firstname, :lastname).all
) obtendrá unMissingAttributeError
si su métodoinit
accede a una columna que no tiene '' Se ha incluido en la cláusula deselect
. Puede protegerse contra este caso así:self.number ||= 0.0 if self.has_attribute? :number
y para una columna booleana ...
self.bool_field = true if (self.has_attribute? :bool_value) && 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)
Lo primero es lo primero: no estoy en desacuerdo con la respuesta de Jeff. Tiene sentido cuando tu aplicación es pequeña y tu lógica simple. Estoy aquí 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 cuando se construye algo pequeño, sino tenerlo en cuenta como un enfoque alternativo:
Una pregunta aquí es si este valor predeterminado en los registros es lógica de negocios. Si lo es, sería prudente ponerlo en el modelo ORM. Dado que el campo que menciona ryw está activo , esto suena como una lógica de negocios. Por ejemplo, el usuario está activo.
¿Por qué me preocuparía poner las preocupaciones de negocios en un modelo de ORM?
Se rompe el SRP . Cualquier clase heredada de ActiveRecord :: Base ya está haciendo muchas cosas diferentes, entre ellas la consistencia de los datos (validaciones) y la persistencia (guardar). Poner la lógica empresarial, por pequeña que sea, con AR :: Base breaks SRP.
Es más lento para probar. Si quiero probar cualquier forma de lógica que suceda en mi modelo ORM, mis pruebas deben inicializar Rails para poder ejecutarse. Esto no será un gran problema al principio de su aplicación, pero se acumulará hasta que las pruebas de su unidad demoren mucho tiempo en ejecutarse.
Romperá el SRP aún más en la línea y de manera concreta. ¿Dice que nuestro negocio ahora requiere que enviemos un correo electrónico a los usuarios cuando el artículo se active? Ahora estamos agregando la lógica del 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 de negocios . Estos no pertenecen al modelo ORM.
Es difícil diversificarse. He visto aplicaciones de Rails maduras con cosas como una base de datos 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 maneras, creo.
La forma PORO: Si bien este es un código un poco más, le permite mantener sus modelos de ORM y su lógica de negocios por separado. 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
Luego, con esto en su lugar, la forma de crear un nuevo artículo sería
SellableItemFactory.new
Y mis pruebas ahora simplemente podrían verificar que ItemFactory establece activos en Artículo si no tiene un valor. No es necesaria la inicialización de Rails, no se rompe el SRP. Cuando la inicialización del elemento se vuelve más avanzada (por ejemplo, establecer un campo de estado, un tipo predeterminado, etc.), se puede agregar esto a ItemFactory. Si terminamos con dos tipos de valores predeterminados, podemos crear un nuevo BusinesCaseItemFactory para hacer esto.
NOTA: También podría ser beneficioso utilizar la inyección de dependencia aquí para permitir que la fábrica genere muchos elementos activos, pero lo omití por simplicidad. Aquí está: self.new (klass = Elemento, atributos = {})
Los chicos de Phusion tienen un buen plugin para esto.
Me encontré con problemas con after_initialize
dando errores ActiveModel::MissingAttributeError
al hacer ActiveModel::MissingAttributeError
complejas:
p.ej:
@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)
"buscar" en el .where
es hash de condiciones
Así que terminé haciéndolo anulando inicializar de esta manera:
def initialize
super
default_values
end
private
def default_values
self.date_received ||= Date.current
end
La super
llamada es necesaria para asegurarse de que el objeto se inicialice correctamente desde ActiveRecord::Base
antes de personalizar mi código, es decir: valores_predeterminados
Preguntas similares, pero todas tienen un contexto ligeramente diferente: - ¿Cómo creo un valor predeterminado para los 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? utilizar 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 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''
? utilizar:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= ''P''
end
end
Recomiendo encarecidamente utilizar la gema "default_value_for": 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 db predeterminado es NULL, su modelo / ruby definido por defecto es "alguna cadena", pero realmente desea establecer el valor en nil por cualquier motivo: 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, entonces en lugar de tomar el enfoque ||=
, ¿ my_attr_changed?
a my_attr_changed?
...
PERO ahora imagine que su valor predeterminado de db es "alguna cadena", su valor predeterminado por el modelo / ruby es "alguna otra cadena", pero en un escenario determinado, desea establecer el valor en "alguna cadena" (el valor predeterminado de db): MyModel.new(my_attr: ''some_string'')
Esto resultará en my_attr_changed?
siendo 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 cadena", nuevamente, no en lo que deseaba.
Por esas razones, no creo que esto pueda lograrse correctamente con solo un gancho after_initialize.
Una vez más, creo que la gema "default_value_for" está adoptando el enfoque correcto: https://github.com/FooBarWidget/default_value_for
Si la columna es una columna de tipo ''estado'', y su modelo se presta para el uso de máquinas de estado, considere usar la gema aasm , después de lo cual simplemente puede hacer
aasm column: "status" do
state :available, initial: true
state :used
# transitions
end
Aún no inicializa el valor de los registros no guardados, pero es un poco más limpio que rodar los suyos con init
o lo que sea, y usted obtiene los otros beneficios de aasm, como los ámbitos para todos sus estados.
Sup chicos, terminé haciendo lo siguiente:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
end
¡Funciona de maravilla!
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
Consulte "Sobrescribir accesores predeterminados" en la documentación de ActiveRecord :: Base y más de sobre el uso de self .
Yo uso la gema de attribute-defaults
Desde 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"
usar default_scope en los rieles 3
ActiveRecord oculta la diferencia entre la configuración predeterminada definida en la base de datos (esquema) y la configuración predeterminada realizada en la aplicación (modelo). Durante la inicialización, analiza el esquema de la base de datos y anota los valores predeterminados especificados allí. Más adelante, al crear objetos, asigna los valores predeterminados especificados en el esquema sin tocar la base de datos.
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => ''active''
end
¡Para esto son los constructores! Anular el método de initialize
del modelo.
Utilice el método after_initialize
.
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end