Bagaimana cara mengatur nilai default di Rails?

108

Saya mencoba menemukan cara terbaik untuk mengatur nilai default untuk objek di Rails.

Yang terbaik yang dapat saya pikirkan adalah mengatur nilai default dalam newmetode di pengontrol.

Apakah ada yang punya masukan jika ini dapat diterima atau jika ada cara yang lebih baik untuk melakukannya?

biagidp
sumber
1
Benda apa ini; bagaimana mereka dikonsumsi / digunakan? Apakah mereka digunakan saat merender tampilan atau untuk logika pengontrol?
Gishu
3
Jika Anda berbicara tentang objek ActiveRecord, saya harus memberi tahu Anda bahwa tidak ada solusi yang masuk akal untuk masalah 'nilai default'. Hanya peretasan gila, dan penulis rel tampaknya tidak berpikir bahwa fitur itu sepadan (luar biasa karena hanya komunitas rel yang ..)
Mauricio
Karena jawaban yang diterima dan sebagian besar berfokus pada ActiveRecords, kami menganggap pertanyaan awal adalah tentang AcitveRecords. Oleh karena itu kemungkinan duplikat stackoverflow.com/questions/328525/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Kemungkinan duplikat dari Bagaimana cara menetapkan nilai default di ActiveRecord?
Lucas Caton

Jawaban:

98

"Benar" adalah kata yang berbahaya di Ruby. Biasanya ada lebih dari satu cara untuk melakukan sesuatu. Jika Anda tahu Anda akan selalu menginginkan nilai default untuk kolom itu di tabel itu, mengaturnya dalam file migrasi DB adalah cara termudah:

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

Karena ActiveRecord menemukan properti tabel dan kolom Anda secara otomatis, ini akan menyebabkan default yang sama disetel di model mana pun yang menggunakannya di aplikasi Rails standar.

Namun, jika Anda hanya ingin nilai default ditetapkan dalam kasus tertentu - katakanlah, itu adalah model yang diwariskan yang berbagi tabel dengan beberapa orang lain - maka cara elegan lainnya adalah melakukannya secara langsung di kode Rails Anda ketika objek model dibuat:

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

Kemudian, saat Anda melakukan a GenericPerson.new(), atribut "Doe" akan selalu menetes ke atas Person.new()kecuali Anda menimpanya dengan sesuatu yang lain.

SFEley
sumber
2
Cobalah. Ini akan bekerja pada objek model baru yang dipanggil dengan .newmetode kelas. Diskusi postingan blog tentang pemanggilan langsung ActiveRecord .allocateadalah tentang objek model yang dimuat dengan data yang ada dari database. ( Dan itu ide yang buruk bagi ActiveRecord untuk bekerja seperti itu, IMO. Tapi bukan itu intinya.)
SFEley
2
Jika Anda mundur untuk membaca pertanyaan poster asli lagi, Nikita, dan kemudian komentar saya secara berurutan, itu mungkin lebih masuk akal bagi Anda. Jika tidak ... Nah, pertanyaannya sudah terjawab. Semoga harimu menyenangkan.
SFEley
8
Lebih baik untuk migrasi bawah: change_column_default :people, :last_name, nil stackoverflow.com/a/1746246/483520
Nolan Amy
9
Harap dicatat bahwa untuk versi rel yang lebih baru, Anda memerlukan parameter tipe tambahan sebelum parameter default. Cari: ketik di halaman ini.
Ben Wheeler
3
@JoelBrewer Saya bertemu hari ini - untuk Rails 4, Anda juga perlu menentukan jenis kolom:change_column :people, :last_name, :string, default: "Doe"
GoBusto
56

Berdasarkan jawaban SFEley, berikut ini adalah yang diperbarui / diperbaiki untuk versi Rails yang lebih baru:

class SetDefault < ActiveRecord::Migration
  def change
    change_column :table_name, :column_name, :type, default: "Your value"
  end
end
J -_- L
sumber
2
Berhati-hatilah, ini TIDAK berfungsi pada Rails 4.2.x (tidak diuji pada versi yang lebih baru). karena change_columnbisa jadi sangat "penataan", operasi kebalikannya tidak dapat disimpulkan. Jika Anda tidak yakin, cukup uji dengan menjalankan db:migratedan db:rollbacksetelahnya. Jawaban yang diterima sebagai hasil yang sama tetapi setidaknya itu diasumsikan!
gfd
1
Ini adalah jawaban yang benar untuk saya .. Ini berfungsi dengan rel 5.1 tetapi Anda perlu melakukan updan downseperti yang dijelaskan di atas dari @GoBusto
Fabrizio Bertoglio
22

Pertama-tama Anda tidak dapat membebani initialize(*args)karena tidak dipanggil dalam semua kasus.

Pilihan terbaik Anda adalah menempatkan default Anda ke dalam migrasi Anda:

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

Terbaik kedua adalah menempatkan default ke dalam model Anda, tetapi ini hanya akan berfungsi dengan atribut yang awalnya nihil. Anda mungkin mengalami masalah seperti yang saya lakukan dengan booleankolom:

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

Anda memerlukan new_record?agar default tidak menimpa nilai yang dimuat dari database.

Anda harus ||=menghentikan Rails dari mengganti parameter yang diteruskan ke metode inisialisasi.

Ian Purton
sumber
12
catatan kecil - Anda ingin melakukan dua hal: .... 1) jangan panggil metode Anda after_initialize. Anda ingin after_initiation :your_method_name.... 2 penggunaanself.max_users ||= 10
Jesse Wolgamott
5
Untuk boolean, lakukan ini: prop = true if prop.nil?
Francis Potter
2
atau after_initialize doalih-alihdef after_initialize
fotanus
self.max_users agar aman.
Ken Ratanachai S.
15

Anda juga dapat mencoba change_column_defaultmigrasi Anda (diuji di 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 dokumen API Rails

Sebastiaan Pouyet
sumber
1
Jawaban yang diterima lebih lengkap tapi saya suka yang bersih dan terdokumentasi ini ... Terima kasih!
gfd
7

Jika Anda merujuk ke objek ActiveRecord, Anda memiliki (lebih dari) dua cara untuk melakukan ini:

1. Gunakan: parameter default di DB

MISALNYA

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

Info lebih lanjut di sini: http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

2. Gunakan panggilan balik

MISALNYA before_validation_on_create

Info lebih lanjut di sini: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147

Vlad Zloteanu
sumber
2
Bukankah masalah dengan menggunakan default dalam database yang tidak mereka setel hingga objek disimpan? Jika saya ingin membuat objek baru dengan default terisi, saya perlu mengaturnya di penginisialisasi.
Rafe
Boolean tidak bisa default ke 1 atau 0 - mereka harus disetel ke benar atau salah (lihat jawaban Silasj).
Jamon Holmgren
@JamonHolmgren Terima kasih atas komentarnya, saya mengoreksi jawabannya :)
Vlad Zloteanu
Tidak masalah @VladZloteanu
Jamon Holmgren
7

Di Ruby on Rails v3.2.8, dengan menggunakan after_initializecallback ActiveRecord, Anda bisa memanggil metode dalam model Anda yang akan menetapkan nilai default untuk objek baru.

callback after_initialize dipicu untuk setiap objek yang ditemukan dan dibuat oleh finder, dengan after_initialize dipicu setelah objek baru juga dibuat ( lihat Callback ActiveRecord ).

Jadi, IMO itu akan terlihat seperti:

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_valueuntuk contoh ini kecuali jika contoh tersebut berisi attribute_whose_presence_has_been_validatedpenyimpanan / pembaruan sebelumnya. The default_valuekemudian akan digunakan bersama dengan pandangan Anda untuk membuat bentuk menggunakan default_valueuntuk baratribut.

Paling banter ini hacky ...

EDIT - gunakan 'catatan_baru?' untuk memeriksa apakah membuat instance dari panggilan baru

Alih-alih memeriksa nilai atribut, gunakan new_record?metode bawaan dengan rel. Jadi, contoh di atas akan terlihat seperti:

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

Ini jauh lebih bersih. Ah, keajaiban Rails - lebih pintar dariku.

salah
sumber
Ini tidak berfungsi seperti yang saya harapkan - ini seharusnya hanya menyetel default pada new, tetapi juga menyetel default ke atribut untuk setiap objek yang ditemukan dan dibuat instance-nya (yaitu catatan yang dimuat dari db). Anda dapat melakukan pemeriksaan atribut objek untuk mencari nilai sebelum menetapkan default, tetapi itu bukan solusi yang sangat elegan atau kuat.
kesalahan
1
Mengapa Anda merekomendasikan after_initializepanggilan balik di sini? Dokumentasi Rails tentang callback memiliki contoh pengaturan nilai default before_createtanpa pemeriksaan kondisi tambahan
Dfr
@Dfr - Saya pasti melewatkannya, dapatkah Anda memberi saya tautan untuk meninjau dan saya akan memperbarui jawabannya ...
kesalahan
Ya, di sini api.rubyonrails.org/classes/ActiveRecord/Callbacks.html contoh pertama, kelasSubscription
Dfr
5
@Dfr - Alasan kita menggunakan after_initialize, alih-alih before_createcallback, adalah kita ingin menyetel nilai default untuk pengguna (untuk digunakan dalam tampilan) saat mereka membuat objek baru. The before_createcallback disebut setelah para pengguna telah menjabat objek baru, memberikan masukan mereka, dan diserahkan objek untuk penciptaan ke controller. Pengontrol kemudian memeriksa setiap before_createpanggilan balik. Tampaknya kontra-intuitif, tetapi ini adalah nomenklatur - before_createmengacu pada createtindakan. Membuat instance objek baru bukanlah createobjek.
kesalahan
5

Untuk bidang boolean di Rails 3.2.6 setidaknya, ini akan berfungsi dalam migrasi Anda.

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

Menempatkan 1atau 0untuk default tidak akan berfungsi di sini, karena ini adalah bidang boolean. Ini harus berupa trueatau falsenilai.

Silasj
sumber
2

Hasilkan migrasi dan gunakan change_column_default, ringkas dan dapat dibalik:

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

Jika Anda hanya menetapkan default untuk atribut tertentu dari model yang didukung database, saya akan mempertimbangkan untuk menggunakan nilai kolom default sql - dapatkah Anda menjelaskan jenis default yang Anda gunakan?

Ada sejumlah pendekatan untuk menanganinya, plugin ini sepertinya merupakan opsi yang menarik.

paulthenerd
sumber
1
Saya pikir Anda seharusnya tidak bergantung pada database untuk menangani default dan kendala, semua hal itu harus diselesaikan dalam lapisan model. Plugin itu hanya berfungsi untuk model ActiveRecord, ini bukan cara umum untuk menyetel default untuk objek.
Lukas Stejskal
Saya berpendapat bahwa itu tergantung pada jenis default yang Anda coba gunakan, itu bukan sesuatu yang perlu saya lakukan sangat sering tetapi saya akan mengatakan bahwa kendala sebenarnya jauh lebih baik ditetapkan di database dan model - untuk mencegah data Anda menjadi tidak valid di database.
paulthenerd
1

Saran untuk menimpa baru / menginisialisasi mungkin tidak lengkap. Rails akan (sering) memanggil alokasi untuk objek ActiveRecord, dan panggilan untuk mengalokasikan tidak akan menghasilkan panggilan untuk diinisialisasi.

Jika Anda berbicara tentang objek ActiveRecord, lihat overriding after_initialize.

Posting blog ini (bukan milik saya) berguna:

Nilai default Konstruktor default tidak dipanggil

[Sunting: SFEley menunjukkan bahwa Rails benar-benar melihat default dalam database ketika membuat instance objek baru dalam memori - saya tidak menyadarinya.]

James Moore
sumber
1

Saya perlu mengatur default seperti jika itu ditentukan sebagai nilai kolom default di DB. Jadi berperilaku seperti ini

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

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

Karena callback after_initialize dipanggil setelah menyetel atribut dari argumen, tidak ada cara untuk mengetahui apakah atribut tersebut nihil karena tidak pernah disetel atau karena sengaja disetel sebagai nil. Jadi saya harus sedikit mencari tahu dan datang dengan solusi sederhana ini.

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

Bekerja dengan baik untuk saya. (Rel 3.2.x)

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

saya menjawab pertanyaan serupa di sini .. cara bersih untuk melakukan ini adalah menggunakan Rails attr_accessor_with_default

class SOF
  attr_accessor_with_default :is_awesome,true
end

sof = SOF.new
sof.is_awesome

=> true

MEMPERBARUI

attr_accessor_with_default sudah tidak digunakan lagi di Rails 3.2 .. Anda bisa melakukan ini dengan Ruby murni

class SOF
  attr_writer :is_awesome

  def is_awesome
    @is_awesome ||= true
  end
end

sof = SOF.new
sof.is_awesome

#=> true
Orlando
sumber
attr_accessor_with_defaultdihentikan pada rel> 3.1.0
flynfish
Dalam contoh Anda, is_awesome akan selalu benar meskipun @is_awesome == false.
Ritchie
-3

Anda dapat mengganti konstruktor untuk model ActiveRecord.

Seperti ini:

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
sumber
2
Ini adalah tempat yang buruk untuk mengubah fungsi rel inti. Ada banyak hal lain yang jauh lebih kecil kemungkinannya untuk merusak hal-hal lain, cara untuk melakukan ini disebutkan dalam jawaban lain. Penambalan monyet seharusnya hanya menjadi pilihan terakhir untuk hal-hal yang tidak mungkin dilakukan dengan cara lain.
Apakah