¿Cómo puedo establecer valores predeterminados en ActiveRecord?
-
11-07-2019 - |
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?
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:
-
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. - 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.
- Anular
initialize
puede funcionar, ¡pero no olvides llamar asuper
! - 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?
- Reemplazar
after_initialize
está en desuso a partir de Rails 3. Cuando anuloafter_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:
-
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
-
Si solo está seleccionando un subconjunto de columnas para un modelo (es decir, usando
select
en una consulta comoPerson.select (: firstname,: lastname) .all
) obtendrá unMissingAttributeError
si su métodoinit
accede a una columna que no se ha incluido en la cláusulaselect
. 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?
-
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.
-
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.
-
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.
-
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
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.
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í