Jak ustawić domyślne wartości w Railsach?

108

Próbuję znaleźć najlepszy sposób na ustawienie wartości domyślnych dla obiektów w Railsach.

Najlepsze, co przychodzi mi do głowy, to ustawienie wartości domyślnej w newmetodzie w kontrolerze.

Czy ktoś ma jakiś wkład, jeśli jest to dopuszczalne lub czy istnieje lepszy sposób na zrobienie tego?

biagidp
źródło
1
Co to za przedmioty; jak są one konsumowane / używane? Czy są używane podczas renderowania widoków, czy do logiki kontrolera?
Gishu
3
Jeśli mówisz o obiekcie ActiveRecord, muszę powiedzieć, że nie ma rozsądnego rozwiązania problemu „wartości domyślnych”. Tylko szalone hacki, a autorzy railsów nie wydają się uważać, że ta funkcja jest tego warta (niesamowite, ponieważ tylko społeczność railsów jest ...)
Mauricio
Ponieważ zaakceptowane i większość odpowiedzi skupia się na ActiveRecords, zakładamy, że pierwotne pytanie dotyczyło AcitveRecords. Dlatego możliwy duplikat stackoverflow.com/questions/328525/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

98

„Prawidłowo” to niebezpieczne słowo w języku Ruby. Zwykle jest więcej niż jeden sposób na zrobienie czegokolwiek. Jeśli wiesz, że zawsze będziesz potrzebować tej domyślnej wartości dla tej kolumny w tej tabeli, ustawienie ich w pliku migracji bazy danych jest najłatwiejszym sposobem:

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

Ponieważ ActiveRecord automatycznie wykrywa właściwości tabeli i kolumny, spowoduje to ustawienie tego samego domyślnego w każdym modelu używającym go w dowolnej standardowej aplikacji Railsowej.

Jeśli jednak chcesz ustawić wartości domyślne tylko w określonych przypadkach - powiedzmy, że jest to model dziedziczony, który dzieli tabelę z innymi - to innym eleganckim sposobem jest zrobienie tego bezpośrednio w kodzie Railsów, gdy tworzony jest obiekt modelu:

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

Następnie, gdy zrobisz a GenericPerson.new(), zawsze przepłynie atrybut „Doe” do, Person.new()chyba że zastąpisz go czymś innym.

SFEley
źródło
2
Spróbuj. Będzie działać na nowych obiektach modelu wywoływanych .newmetodą klasy. Omówienie tego posta na blogu na temat bezpośredniego wywoływania ActiveRecord .allocatedotyczyło obiektów modelu ładowanych istniejącymi danymi z bazy danych. ( I to okropny pomysł, aby ActiveRecord działał w ten sposób, IMO. Ale to nie ma znaczenia).
SFEley,
2
Jeśli cofniesz się i ponownie przeczytasz pytanie pierwotnego autora, Nikita, a potem moje komentarze w kolejności, może to mieć dla ciebie więcej sensu. Jeśli nie ... Cóż, odpowiedź na pytanie. Miłego dnia.
SFEley,
8
Lepsze w przypadku migracji w dół: change_column_default :people, :last_name, nil stackoverflow.com/a/1746246/483520
Nolan Amy,
9
Zwróć uwagę, że dla nowszych wersji szyn potrzebujesz dodatkowego parametru typu przed parametrem domyślnym. Szukaj: wpisz na tej stronie.
Ben Wheeler,
3
@JoelBrewer Natknąłem się na to dzisiaj - w przypadku Rails 4 musisz również określić typ kolumny:change_column :people, :last_name, :string, default: "Doe"
GoBusto
56

W oparciu o odpowiedź SFEley, tutaj jest zaktualizowany / poprawiony dla nowszych wersji Railsów:

class SetDefault < ActiveRecord::Migration
  def change
    change_column :table_name, :column_name, :type, default: "Your value"
  end
end
J L
źródło
2
Uważaj, to NIE działa na Railsach 4.2.x (nie testowane na późniejszych wersjach). co change_columnmoże być dość „strukturalne”, nie można wywnioskować operacji odwrotnej. Jeśli nie jesteś pewien, po prostu przetestuj go, uruchamiając db:migratei db:rollbackzaraz potem. Zaakceptowano odpowiedź jako ten sam wynik, ale przynajmniej przyjęto!
gfd
1
To jest dla mnie poprawna odpowiedź. Działa z szynami 5.1, ale musisz to zrobić upi downjak opisano powyżej z @GoBusto
Fabrizio Bertoglio
22

Przede wszystkim nie możesz przeciążać, initialize(*args)ponieważ nie jest to wywoływane we wszystkich przypadkach.

Najlepszą opcją jest dodanie ustawień domyślnych do migracji:

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

Drugą najlepszą metodą jest umieszczenie w modelu wartości domyślnych, ale zadziała to tylko z atrybutami, które początkowo są zerowe. Możesz mieć problemy, tak jak ja z booleankolumnami:

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

Potrzebujesz, new_record?aby wartości domyślne nie zastępowały wartości załadowanych z bazy danych.

Musisz ||=powstrzymać Railsy przed nadpisywaniem parametrów przekazanych do metody initialize.

Ian Purton
źródło
12
drobna uwaga - chcesz zrobić dwie rzeczy: .... 1) nie wywołuj swojej metody after_initialize. Chcesz after_initiation :your_method_name... 2 użyćself.max_users ||= 10
Jesse Wolgamott
5
Dla logicznych, po prostu zrób to: prop = true if prop.nil?
Francis Potter,
2
lub after_initialize dozamiastdef after_initialize
fotanus
self.max_users, aby być bezpiecznym.
Ken Ratanachai S.,
15

Możesz także spróbować change_column_defaultswoich migracji (testowane w Railsach 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 Dokumentacja Rails API

Sebastiaan Pouyet
źródło
1
Zaakceptowana odpowiedź jest bardziej kompletna, ale podoba mi się ta przejrzysta i udokumentowana… Dzięki!
gfd
7

Jeśli odnosisz się do obiektów ActiveRecord, masz na to (więcej niż) dwa sposoby:

1. Użyj: default parametru w DB

NA PRZYKŁAD

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

Więcej informacji tutaj: http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

2. Użyj oddzwaniania

NA PRZYKŁAD before_validation_on_create

Więcej informacji tutaj: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147

Vlad Zloteanu
źródło
2
Czy nie jest problem z używaniem wartości domyślnych w bazie danych, które nie są ustawiane, dopóki obiekt nie zostanie zapisany? Jeśli chcę skonstruować nowy obiekt z wypełnionymi wartościami domyślnymi, muszę ustawić je w inicjatorze.
Rafe
Wartości logiczne nie mogą mieć wartości domyślnej 1 ani 0 - muszą być ustawione na wartość true lub false (patrz odpowiedź Silasj).
Jamon Holmgren,
@JamonHolmgren Dzięki za uwagę, poprawiłem odpowiedź :)
Vlad Zloteanu,
Żaden problem @VladZloteanu
Jamon Holmgren
7

W Ruby on Rails v3.2.8, używając after_initializewywołania zwrotnego ActiveRecord, możesz wywołać metodę w swoim modelu, która przypisze domyślne wartości dla nowego obiektu.

Funkcja zwrotna after_initialize jest wyzwalana dla każdego obiektu, który został znaleziony i utworzony przez wyszukiwarkę, przy czym funkcja after_initialize jest wyzwalana również po utworzeniu instancji nowych obiektów ( zobacz Wywołania zwrotne ActiveRecord ).

Więc IMO powinno wyglądać mniej więcej tak:

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_valuew tym przypadku, chyba że instancja zawiera attribute_whose_presence_has_been_validatedpoprzednio zapisaną / aktualizację. default_valueZostaną następnie wykorzystane w połączeniu z widoku do renderowania formularz używając default_valuedo baratrybutu.

W najlepszym razie jest to hacky ...

EDYCJA - użyj „new_record?” aby sprawdzić, czy tworzona jest instancja z nowego połączenia

Zamiast sprawdzać wartość atrybutu, użyj new_record?wbudowanej metody z railsami. Tak więc powyższy przykład powinien wyglądać następująco:

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

To jest dużo czystsze. Ach, magia Railsów - jest mądrzejsza ode mnie.

błędny
źródło
To nie działa tak, jak się spodziewałem - miało ustawić tylko wartość domyślną na nowy, ale ustawia również wartość domyślną dla każdego obiektu, który został znaleziony i utworzony (tj. Rekordy ładowane z bazy danych). Możesz sprawdzić atrybut obiektu, szukając wartości przed przypisaniem wartości domyślnej, ale nie jest to bardzo eleganckie ani niezawodne rozwiązanie.
błędny
1
Dlaczego after_initializetutaj polecasz callback? Dokumentacja Railsów dotycząca wywołań zwrotnych zawiera przykład ustawienia wartości domyślnej before_createbez dodatkowego sprawdzania warunków
Dfr
@Dfr - Musiałem to przeoczyć, czy możesz wrzucić link do recenzji, a zaktualizuję odpowiedź ...
błąd
Tak, tutaj api.rubyonrails.org/classes/ActiveRecord/Callbacks.html pierwszy przykład, klasaSubscription
Dfr
5
@Dfr - Powodem, dla którego używamy after_initializezamiast before_createwywołania zwrotnego, jest to, że chcemy ustawić domyślną wartość dla użytkownika (do użycia w widoku) podczas tworzenia nowych obiektów. before_createZwrotna jest wywoływana po użytkownik został doręczony nowego obiektu, pod warunkiem ich wejścia i złożony obiekt, dla stworzenia do sterownika. Następnie kontroler sprawdza wszelkie before_createwywołania zwrotne. Wydaje się to sprzeczne z intuicją, ale to rzecz nomenklaturowa - before_createodnosi się do createakcji. Utworzenie wystąpienia nowego obiektu nie powoduje jego createpowstania.
błąd
5

Przynajmniej dla pól boolowskich w Railsach 3.2.6 będzie to działać podczas migracji.

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

Wstawienie 1lub 0jako wartości domyślnej nie zadziała tutaj, ponieważ jest to pole logiczne. Musi to być wartość a truelub false.

Silasj
źródło
2

Wygeneruj migrację i użyj change_column_default, jest zwięzły i odwracalny:

class SetDefaultAgeInPeople < ActiveRecord::Migration[5.2]
  def change
    change_column_default :people, :age, { from: nil, to: 0 }
  end
end
Pere Joan Martorell
źródło
1

Jeśli ustawiasz tylko wartości domyślne dla niektórych atrybutów modelu opartego na bazie danych, rozważyłbym użycie domyślnych wartości kolumn sql - czy możesz wyjaśnić, jakich typów domyślnych używasz?

Istnieje wiele podejść do tego, ta wtyczka wygląda na interesującą opcję.

paulthenerd
źródło
1
Myślę, że nie powinieneś polegać na bazie danych, aby radzić sobie z ustawieniami domyślnymi i ograniczeniami, wszystko to powinno być uporządkowane w warstwie modelu. Ta wtyczka działa tylko dla modeli ActiveRecord, nie jest to ogólny sposób ustawiania wartości domyślnych dla obiektów.
Lukas Stejskal
Twierdzę, że zależy to od typów domyślnych, których próbujesz użyć, nie jest to coś, co musiałem robić bardzo często, ale powiedziałbym, że w rzeczywistości ograniczenia są znacznie lepsze, gdy są ustawione zarówno w bazie danych, jak i model - aby zapobiec utracie danych w bazie.
paulthenerd
1

Propozycja zastąpienia nowego / inicjalizacji jest prawdopodobnie niekompletna. Railsy będą (często) wywoływać alokację dla obiektów ActiveRecord, a wywołania alokacji nie spowodują wywołań inicjalizacji.

Jeśli mówisz o obiektach ActiveRecord, przyjrzyj się zastępowaniu after_initialize.

Te posty na blogu (nie moje) są przydatne:

Wartości domyślne Nie wywołano konstruktorów domyślnych

[Edycja: SFEley wskazuje, że Railsy faktycznie patrzą na domyślne w bazie danych, gdy tworzy instancję nowego obiektu w pamięci - nie zdawałem sobie z tego sprawy.]

James Moore
źródło
1

Musiałem ustawić wartość domyślną, tak jakby została określona jako domyślna wartość kolumny w DB. Więc zachowuje się tak

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

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

Ponieważ wywołanie zwrotne after_initialize jest wywoływane po ustawieniu atrybutów z argumentów, nie było możliwości sprawdzenia, czy atrybut jest zerowy, ponieważ nigdy nie został ustawiony lub został celowo ustawiony na zero. Musiałem więc trochę zagrzebać się w środku i przyszedłem z tym prostym rozwiązaniem.

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

U mnie działa świetnie. (Rails 3.2.x)

Petr '' Bubák '' Šedivý
źródło
0

I odpowiedział na podobne pytanie tutaj .. czysty sposób, aby to zrobić przy użyciu Rails attr_accessor_with_default

class SOF
  attr_accessor_with_default :is_awesome,true
end

sof = SOF.new
sof.is_awesome

=> true

AKTUALIZACJA

attr_accessor_with_default został uznany za przestarzały w Railsach 3.2 .. zamiast tego możesz to zrobić za pomocą czystego Rubiego

class SOF
  attr_writer :is_awesome

  def is_awesome
    @is_awesome ||= true
  end
end

sof = SOF.new
sof.is_awesome

#=> true
Orlando
źródło
attr_accessor_with_defaultjest przestarzałe od rails> 3.1.0
flynfish
W twoim przykładzie is_awesome zawsze będzie true, nawet jeśli @is_awesome == false.
Ritchie,
-3

Możesz nadpisać konstruktora dla modelu ActiveRecord.

Lubię to:

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
źródło
2
To okropne miejsce, w którym można zmieniać podstawowe funkcje szyn. Istnieją inne, znacznie stabilniejsze, mniej skłonne do zepsucia innych rzeczy, sposoby na to wymienione w innych odpowiedziach. Łaty małpy powinny być tylko ostatecznością w przypadku rzeczy, których nie można zrobić w inny sposób.
Będzie