Como definir valores padrão no Rails?

108

Estou tentando encontrar a melhor maneira de definir valores padrão para objetos no Rails.

O melhor que posso pensar é definir o valor padrão no newmétodo do controlador.

Alguém tem alguma opinião se isso é aceitável ou se existe uma maneira melhor de fazer isso?

biagidp
fonte
1
Quais são esses objetos; como são consumidos / usados? Eles são usados ​​para renderizar as visualizações ou para a lógica do controlador?
Gishu
3
Se você está falando sobre um objeto ActiveRecord, devo dizer que não há uma solução sensata para o problema dos 'valores padrão'. Apenas hacks insanos, e os autores de rails não parecem pensar que o recurso vale a pena (incrível como só a comunidade de rails é ...)
Mauricio
Como as respostas aceitas e a maioria se concentram em ActiveRecords, presumimos que a pergunta original era sobre AcitveRecords. Portanto, possível duplicata de stackoverflow.com/questions/328525/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Possível duplicata de Como posso definir valores padrão no ActiveRecord?
Lucas Caton

Respostas:

98

"Correto" é uma palavra perigosa em Ruby. Geralmente, há mais de uma maneira de fazer qualquer coisa. Se você sabe que sempre desejará esse valor padrão para aquela coluna na tabela, configurá-los em um arquivo de migração de banco de dados é a maneira mais 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

Como o ActiveRecord descobre automaticamente suas propriedades de tabela e coluna, isso fará com que o mesmo padrão seja definido em qualquer modelo que o use em qualquer aplicativo Rails padrão.

No entanto, se você deseja apenas valores padrão definidos em casos específicos - digamos, é um modelo herdado que compartilha uma tabela com alguns outros - então outra maneira elegante é fazer isso diretamente em seu código Rails quando o objeto de modelo é criado:

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

Então, quando você fizer um GenericPerson.new(), o atributo "Doe" será sempre adicionado a, a Person.new()menos que você o substitua por outra coisa.

SFEley
fonte
2
Tente. Ele funcionará em novos objetos de modelo chamados com o .newmétodo de classe. A discussão daquela postagem do blog sobre a chamada direta do ActiveRecord .allocatefoi sobre objetos de modelo carregados com dados existentes do banco de dados. ( E é uma péssima ideia que o ActiveRecord funcione dessa forma, IMO. Mas isso não
vem
2
Se você voltar para ler a pergunta do autor da postagem original novamente, Nikita, e depois meus comentários em ordem, pode fazer mais sentido para você. Se não ... Bem, a pergunta foi respondida. Tenha um bom dia.
SFEley
8
Melhor para a migração descendente: change_column_default :people, :last_name, nil stackoverflow.com/a/1746246/483520
Nolan Amy
9
Observe que, para versões mais recentes do rails, você precisa de um parâmetro de tipo adicional antes do parâmetro padrão. Pesquisar por: digite nesta página.
Ben Wheeler de
3
@JoelBrewer Eu encontrei isso hoje - para Rails 4, você também precisa especificar o tipo de coluna:change_column :people, :last_name, :string, default: "Doe"
GoBusto
56

Com base na resposta do SFEley, aqui está um atualizado / corrigido para as versões mais recentes do Rails:

class SetDefault < ActiveRecord::Migration
  def change
    change_column :table_name, :column_name, :type, default: "Your value"
  end
end
J -_- L
fonte
2
Cuidado, isso NÃO funciona no Rails 4.2.x (não testado em versões posteriores). como change_columnpode ser bastante "estruturante", a operação reversa não pode ser deduzida. Se você não tiver certeza, teste executando db:migratee db:rollbacklogo após. Resposta aceita como o mesmo resultado, mas pelo menos é presumida!
gfd
1
Esta é a resposta correta para mim .. Funciona com rails 5.1 mas você precisará fazer upe downcomo descrito acima de @GoBusto
Fabrizio Bertoglio
22

Em primeiro lugar, você não pode sobrecarregar, initialize(*args)pois não é chamado em todos os casos.

Sua melhor opção é colocar seus padrões em sua migração:

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

A segunda melhor opção é colocar padrões em seu modelo, mas isso só funcionará com atributos que inicialmente são nulos. Você pode ter problemas como eu tive com as booleancolunas:

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

Você precisa do new_record?para que os padrões não substituam os valores carregados do banco de dados.

Você precisa ||=impedir o Rails de sobrescrever os parâmetros passados ​​no método de inicialização.

Ian Purton
fonte
12
nota secundária - você quer fazer duas coisas: .... 1) não chame seu método after_initialize. Você quer after_initiation :your_method_name.... 2 usoself.max_users ||= 10
Jesse Wolgamott
5
Para booleanos, faça o seguinte: prop = true if prop.nil?
Francis Potter,
2
ou em after_initialize dovez dedef after_initialize
fotanus
self.max_users para ser seguro.
Ken Ratanachai S.
15

Você também pode tentar change_column_defaultem suas migrações (testado no 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 API do Rails docs

Sebastiaan Pouyet
fonte
1
A resposta aceita é mais completa, mas eu gosto desta limpa e documentada ... Obrigado!
gfd
7

Se estiver se referindo a objetos ActiveRecord, você tem (mais de) duas maneiras de fazer isso:

1. Use um: parâmetro padrão no banco de dados

POR EXEMPLO

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

Mais informações aqui: http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

2. Use um retorno de chamada

POR EXEMPLO before_validation_on_create

Mais informações aqui: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147

Vlad Zloteanu
fonte
2
O problema de usar padrões no banco de dados não é que eles não são definidos até que o objeto seja salvo? Se eu quiser construir um novo objeto com os padrões preenchidos, preciso defini-los no inicializador.
Rafe
Os booleanos não podem ser padronizados como 1 ou 0 - eles devem ser definidos como verdadeiro ou falso (veja a resposta de Silasj).
Jamon Holmgren
@JamonHolmgren Obrigado pela observação, corrigi a resposta :)
Vlad Zloteanu
Sem problemas @VladZloteanu
Jamon Holmgren
7

No Ruby on Rails v3.2.8, usando o after_initializeretorno de chamada ActiveRecord, você pode chamar um método em seu modelo que atribuirá os valores padrão para um novo objeto.

after_initialize callback é disparado para cada objeto que é encontrado e instanciado por um localizador, com after_initialize sendo disparado depois que novos objetos são instanciados também ( veja ActiveRecord Callbacks ).

Então, IMO, deve ser algo como:

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 instância, a menos que a instância contenha attribute_whose_presence_has_been_validatedanteriormente em salvar / atualizar. O default_valueserá então usado em conjunto com sua visualização para renderizar o formulário usando o default_valuepara o baratributo.

Na melhor das hipóteses, isso é hacky ...

EDITAR - usar 'new_record?' para verificar se instanciando de uma nova chamada

Em vez de verificar um valor de atributo, use o new_record?método integrado com trilhos. Portanto, o exemplo acima deve ser semelhante a:

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

Isso é muito mais limpo. Ah, a magia do Rails - é mais inteligente do que eu.

errôneo
fonte
Isso não funciona como eu esperava - deveria apenas definir o padrão em novo, mas também define o padrão para o atributo de cada objeto que é encontrado e instanciado (ou seja, registros carregados do banco de dados). Você pode fazer uma verificação de um atributo de objeto procurando por valores antes de atribuir o padrão, mas essa não é uma solução muito elegante ou robusta.
erro
1
Por que você recomenda after_initializeretorno de chamada aqui? A documentação do Rails sobre callbacks tem exemplo de configuração do valor padrão before_createsem verificações de condição extra
Dfr
@Dfr - Devo ter perdido isso, você pode me jogar um link para revisar e eu atualizarei a resposta ...
erro
Sim, aqui api.rubyonrails.org/classes/ActiveRecord/Callbacks.html primeiro exemplo, classeSubscription
Dfr
5
@Dfr - O motivo pelo qual estamos usando after_initialize, em vez do before_createretorno de chamada, é que queremos definir um valor padrão para o usuário (para uso em uma visualização) quando ele estiver criando novos objetos. O before_createretorno de chamada é chamado após o usuário receber um novo objeto, fornecer sua entrada e enviar o objeto para criação ao controlador. O controlador então verifica se há before_createretornos de chamada. Parece contra-intuitivo, mas é uma coisa de nomenclatura - before_createrefere-se à createação. Instanciar um novo objeto não createo faz.
errôneo de
5

Para campos booleanos no Rails 3.2.6 pelo menos, isso funcionará em sua migração.

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

Colocar um 1ou 0para um padrão não funcionará aqui, pois é um campo booleano. Deve ser um valor trueou false.

Silasj
fonte
2

Gerar uma migração e usar change_column_default, é sucinto e reversível:

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

Se você estiver apenas definindo padrões para certos atributos de um modelo de banco de dados, consideraria usar valores de coluna padrão sql - você pode esclarecer quais tipos de padrões você está usando?

Existem várias abordagens para lidar com isso, este plugin parece uma opção interessante.

Paulthenerd
fonte
1
Eu acho que você não deve depender do banco de dados para lidar com padrões e restrições, todas essas coisas devem ser classificadas na camada de modelo. Esse plugin funciona apenas para modelos ActiveRecord, não é uma forma genérica de definir padrões para objetos.
Lukas Stejskal
Eu diria que depende dos tipos de padrões que você está tentando usar, não é algo que eu precise fazer com frequência, mas eu diria que as restrições são, na verdade, muito melhores definidas no banco de dados e o modelo - para evitar que seus dados se tornem inválidos no banco de dados.
paulthenerd
1

A sugestão para substituir new / initialize está provavelmente incompleta. O Rails (freqüentemente) chamará alocação para objetos ActiveRecord, e chamadas para alocar não resultarão em chamadas para inicializar.

Se você estiver falando sobre objetos ActiveRecord, dê uma olhada em substituir after_initialize.

Estas postagens do blog (não as minhas) são úteis:

Valores padrão Construtores padrão não chamados

[Edit: SFEley aponta que Rails realmente olha para o padrão no banco de dados quando instancia um novo objeto na memória - eu não tinha percebido isso.]

James Moore
fonte
1

Eu precisava definir um padrão como se fosse especificado como valor de coluna padrão no banco de dados. Então se comporta assim

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

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

Como o retorno de chamada after_initialize é chamado depois de definir atributos de argumentos, não havia como saber se o atributo é nulo porque nunca foi definido ou porque foi intencionalmente definido como nulo. Então eu tive que fuçar um pouco e vim com esta solução simples.

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

Funciona muito bem para mim. (Rails 3.2.x)

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

Eu respondi a uma pergunta semelhante aqui .. uma maneira limpa de fazer isso é usando Rails attr_accessor_with_default

class SOF
  attr_accessor_with_default :is_awesome,true
end

sof = SOF.new
sof.is_awesome

=> true

ATUALIZAR

attr_accessor_with_default tornou-se obsoleto no Rails 3.2 .. você poderia fazer isso ao invés com Ruby puro

class SOF
  attr_writer :is_awesome

  def is_awesome
    @is_awesome ||= true
  end
end

sof = SOF.new
sof.is_awesome

#=> true
Orlando
fonte
attr_accessor_with_default está obsoleto a partir de rails> 3.1.0
flynfish
Em seu exemplo, is_awesome sempre será verdadeiro mesmo quando @is_awesome == false.
Ritchie
-3

Você pode substituir o construtor do modelo ActiveRecord.

Como isso:

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
fonte
2
Este é um lugar terrível para mudar a funcionalidade dos trilhos centrais. Existem outras formas muito estáveis ​​com menos probabilidade de quebrar outras coisas, maneiras de fazer isso mencionadas em outras respostas. O patching do macaco deve ser apenas o último recurso para coisas que não podem ser feitas de outra maneira.
Será