¿Cómo establecer valores predeterminados en Rails?

108

Estoy tratando de encontrar la mejor manera de establecer valores predeterminados para objetos en Rails.

Lo mejor que se me ocurre es establecer el valor predeterminado en el newmétodo en el controlador.

¿Alguien tiene alguna sugerencia si esto es aceptable o si hay una mejor manera de hacerlo?

biagidp
fuente
1
¿Cuáles son estos objetos? ¿Cómo se consumen / utilizan? ¿Se utilizan al renderizar las vistas o para la lógica del controlador?
Gishu
3
Si está hablando de un objeto ActiveRecord, debo decirle que no hay una solución sana para el problema de los 'valores predeterminados'. Solo hacks locos, y los autores de rieles no parecen pensar que la función valga la pena (increíble, ya que solo la comunidad de rieles es ...)
Mauricio
Dado que las respuestas aceptadas y la mayoría se centran en ActiveRecords, asumimos que la pregunta original era sobre AcitveRecords. Por lo tanto, posible duplicado de stackoverflow.com/questions/328525/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

98

"Correcto" es una palabra peligrosa en Ruby. Por lo general, hay más de una forma de hacer cualquier cosa. Si sabe que siempre querrá ese valor predeterminado para esa columna en esa tabla, configurarlos en un archivo de migración de base de datos es la forma más fácil:

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

Debido a que ActiveRecord detecta automáticamente las propiedades de su tabla y columna, esto hará que se establezca el mismo valor predeterminado en cualquier modelo que lo use en cualquier aplicación estándar de Rails.

Sin embargo, si solo desea establecer valores predeterminados en casos específicos, por ejemplo, es un modelo heredado que comparte una tabla con otros, entonces otra forma elegante es hacerlo directamente en su código Rails cuando se crea el objeto modelo:

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

Luego, cuando hagas una GenericPerson.new(), siempre se filtrará el atributo "Doe" a Person.new()menos que lo anules con otra cosa.

SFEley
fuente
2
Intentalo. Funcionará en nuevos objetos de modelo llamados con el .newmétodo de clase. La discusión de esa publicación de blog sobre la llamada directa de ActiveRecord .allocatefue sobre objetos modelo cargados con datos existentes de la base de datos. ( Y es una idea terrible que ActiveRecord funcione de esa manera, en mi opinión. Pero eso no
viene al caso
2
Si retrocedes para leer la pregunta del póster original nuevamente, Nikita, y luego mis comentarios en orden, puede que tenga más sentido para ti. Si no ... Bueno, la pregunta ha sido respondida. Que tengas un buen día.
SFEley
8
Mejor para la migración descendente: change_column_default :people, :last_name, nil stackoverflow.com/a/1746246/483520
Nolan Amy
9
Tenga en cuenta que para las versiones de rieles más recientes, necesita un parámetro de tipo adicional antes del parámetro predeterminado. Buscar: escriba en esta página.
Ben Wheeler
3
@JoelBrewer Me encontré con esto hoy: para Rails 4, también debe especificar el tipo de columna:change_column :people, :last_name, :string, default: "Doe"
GoBusto
56

Basado en la respuesta de SFEley, aquí hay uno actualizado / fijo para las versiones más nuevas de Rails:

class SetDefault < ActiveRecord::Migration
  def change
    change_column :table_name, :column_name, :type, default: "Your value"
  end
end
J -_- L
fuente
2
Tenga en cuenta que esto NO funciona en Rails 4.2.x (no probado en versiones posteriores). como change_columnpuede ser bastante "estructurante", no se puede deducir la operación inversa. Si no está seguro, simplemente pruébelo ejecutándolo db:migratee db:rollbackinmediatamente después. Respuesta aceptada como el mismo resultado, pero al menos se asume.
gfd
1
Esta es la respuesta correcta para mí. Funciona con rieles 5.1 pero tendrá que hacer upy downcomo se describe arriba de @GoBusto
Fabrizio Bertoglio
22

En primer lugar, no puede sobrecargar, initialize(*args)ya que no se llama en todos los casos.

Su mejor opción es poner sus valores predeterminados en su migración:

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

La segunda mejor opción es colocar valores predeterminados en su modelo, pero esto solo funcionará con atributos que inicialmente son nulos. Puede tener problemas como yo con las booleancolumnas:

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

Necesita el new_record?para que los valores predeterminados no anulen los valores cargados desde la base de datos.

Es necesario ||=dejar de rieles anule los parámetros pasados al método initialize.

Ian Purton
fuente
12
nota menor: desea hacer dos cosas: .... 1) no llame a su método after_initialize. Quieres after_initiation :your_method_name... 2 usoself.max_users ||= 10
Jesse Wolgamott
5
Para los valores booleanos, simplemente haga esto: prop = true if prop.nil?
Francis Potter
2
o en after_initialize dolugar dedef after_initialize
fotanus
self.max_users para estar seguro.
Ken Ratanachai S.
15

También puede probar change_column_defaulten sus migraciones (probado en 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

change_column_default Documentos de la API de Rails

Sebastiaan Pouyet
fuente
1
La respuesta aceptada es más completa pero me gusta esta limpia y documentada ... ¡Gracias!
gfd
7

Si se refiere a objetos ActiveRecord, tiene (más de) dos formas de hacerlo:

1. Utilice un: parámetro predeterminado en la base de datos

P.EJ

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

Más información aquí: http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

2. Utilice una devolución de llamada

P.EJ before_validation_on_create

Más información aquí: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147

Vlad Zloteanu
fuente
2
¿No es el problema de usar valores predeterminados en la base de datos que no se configuran hasta que se guarda el objeto? Si quiero construir un nuevo objeto con los valores predeterminados, necesito configurarlos en el inicializador.
Rafe
Los booleanos no pueden predeterminarse en 1 o 0; deben establecerse en verdadero o falso (consulte la respuesta de Silasj).
Jamon Holmgren
@JamonHolmgren Gracias por el comentario, corregí la respuesta :)
Vlad Zloteanu
No hay problema @VladZloteanu
Jamon Holmgren
7

En Ruby on Rails v3.2.8, usando la after_initializedevolución de llamada ActiveRecord, puede llamar a un método en su modelo que asignará los valores predeterminados para un nuevo objeto.

La devolución de llamada after_initialize se activa para cada objeto que un buscador encuentra y crea una instancia, y after_initialize también se activa después de crear una instancia de los nuevos objetos ( consulte las devoluciones de llamada de ActiveRecord ).

Entonces, en mi opinión, debería verse así:

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_valuepara esta instancia, a menos que la instancia contenga attribute_whose_presence_has_been_validatedpreviamente un guardado / actualización. El default_valuese utilizará entonces en conjunción con su visión para hacer que el formulario utilizando el default_valuepara el baratributo.

En el mejor de los casos, esto es hacky ...

EDITAR - use 'new_record?' para comprobar si se crea una instancia desde una nueva llamada

En lugar de comprobar el valor de un atributo, utilice el new_record?método integrado con rieles. Entonces, el ejemplo anterior debería verse así:

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

Esto es mucho más limpio. Ah, la magia de Rails, es más inteligente que yo.

erróneo
fuente
Esto no funciona como esperaba: se suponía que solo establecería el valor predeterminado en nuevo, pero también establece el atributo predeterminado para cada objeto que se encuentra y se crea una instancia (es decir, registros cargados desde la base de datos). Puede hacer una verificación de un atributo de objeto buscando valores antes de asignar el predeterminado, pero esa no es una solución muy elegante o robusta.
erroric
1
¿Por qué recomienda after_initializedevolver la llamada aquí? La documentación de Rails sobre devoluciones de llamada tiene un ejemplo de configuración del valor predeterminado before_createsin verificaciones de condición adicionales
Dfr
@Dfr - Debo haberme perdido eso, ¿me pueden lanzar un enlace para revisar y actualizaré la respuesta ...
erroric
Sí, aquí api.rubyonrails.org/classes/ActiveRecord/Callbacks.html primer ejemplo, claseSubscription
Dfr
5
@Dfr: la razón por la que estamos usando after_initialize, en lugar de la before_createdevolución de llamada, es que queremos establecer un valor predeterminado para el usuario (para usar en una vista) cuando están creando nuevos objetos. La before_createdevolución de llamada se llama después de que el usuario haya recibido un nuevo objeto, haya proporcionado su entrada y haya enviado el objeto para su creación al controlador. Luego, el controlador verifica si hay before_createdevoluciones de llamada. Parece contrario a la intuición, pero es una cuestión de nomenclatura: se before_createrefiere a la createacción. Crear una instancia de un nuevo objeto no es createel objeto.
erroric
5

Para los campos booleanos en Rails 3.2.6 al menos, esto funcionará en su migración.

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

Poner 1o 0por defecto no funcionará aquí, ya que es un campo booleano. Debe ser un valor trueo false.

Silasj
fuente
2

Genera una migración y uso change_column_default, es sucinto y reversible:

class SetDefaultAgeInPeople < ActiveRecord::Migration[5.2]
  def change
    change_column_default :people, :age, { from: nil, to: 0 }
  end
end
Pere Joan Martorell
fuente
1

Si solo está configurando valores predeterminados para ciertos atributos de un modelo respaldado por una base de datos, consideraría usar valores de columna predeterminados de SQL: ¿puede aclarar qué tipos de valores predeterminados está utilizando?

Hay varios enfoques para manejarlo, este complemento parece una opción interesante.

paulthenerd
fuente
1
Creo que no deberías depender de la base de datos para lidiar con los valores predeterminados y las restricciones, todo eso debe resolverse en la capa del modelo. Ese complemento solo funciona para los modelos de ActiveRecord, no es una forma genérica de establecer valores predeterminados para los objetos.
Lukas Stejskal
Yo diría que depende de los tipos de valores predeterminados que está tratando de usar, no es algo que tenga que hacer muy a menudo, pero diría que las restricciones están de hecho mucho mejor si se establecen tanto en la base de datos como en el modelo: para evitar que sus datos se vuelvan inválidos en la base de datos.
paulthenerd
1

La sugerencia de anular new / initialize probablemente esté incompleta. Rails (con frecuencia) llamará a allocate para los objetos ActiveRecord, y las llamadas a allocate no resultarán en llamadas a inicializar.

Si está hablando de objetos ActiveRecord, eche un vistazo a anular after_initialize.

Estas publicaciones de blog (no las mías) son útiles:

Valores predeterminados No se llaman los constructores predeterminados

[Editar: SFEley señala que Rails en realidad mira el valor predeterminado en la base de datos cuando crea una instancia de un nuevo objeto en la memoria; no me había dado cuenta de eso].

James Moore
fuente
1

Necesitaba establecer un valor predeterminado como si estuviera especificado como valor de columna predeterminado en la base de datos. Entonces se comporta así

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

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

Debido a que se llama a la devolución de llamada after_initialize después de establecer atributos a partir de argumentos, no había forma de saber si el atributo es nil porque nunca se estableció o porque se estableció intencionalmente como nil. Así que tuve que hurgar un poco y vine con esta sencilla solución.

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

Funciona muy bien para mi. (Rieles 3.2.x)

Petr '' Bubák '' Šedivý
fuente
0

Respondí una pregunta similar aquí ... una forma limpia de hacer esto es usando Rails attr_accessor_with_default

class SOF
  attr_accessor_with_default :is_awesome,true
end

sof = SOF.new
sof.is_awesome

=> true

ACTUALIZAR

attr_accessor_with_default ha quedado obsoleto en Rails 3.2 ... puedes hacer esto en su lugar con Ruby puro

class SOF
  attr_writer :is_awesome

  def is_awesome
    @is_awesome ||= true
  end
end

sof = SOF.new
sof.is_awesome

#=> true
Orlando
fuente
attr_accessor_with_defaultestá obsoleto a partir de rieles> 3.1.0
flynfish
En su ejemplo, is_awesome siempre será verdadero incluso cuando @is_awesome == falso.
Ritchie
-3

Puede anular el constructor del modelo ActiveRecord.

Me gusta esto:

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
fuente
2
Este es un lugar terrible para cambiar la funcionalidad de los rieles centrales. Hay otras formas mucho más estables de romper otras cosas que se mencionan en otras respuestas. El parche de monos solo debe ser un último recurso para cosas que no se pueden hacer de otra manera.
Will el