Wie kann ich einen Standardwert in ActiveRecord festlegen?
Ich sehe einen Beitrag von Pratik, der ein hässliches, kompliziertes Stück Code beschreibt: 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
Ich habe die folgenden Beispiele gesehen, die herum goggelten:
def initialize
super
self.status = ACTIVE unless self.status
end
und
def after_initialize
return unless new_record?
self.status = ACTIVE
end
Ich habe auch gesehen, dass die Leute es in ihre Migration aufgenommen haben, aber ich würde es lieber im Modellcode definieren sehen.
Gibt es eine kanonische Methode zum Festlegen eines Standardwerts für Felder im ActiveRecord-Modell?
Es gibt verschiedene Probleme mit jeder der verfügbaren Methoden, aber ich glaube, dass das Definieren eines after_initialize
-Rückrufs aus den folgenden Gründen der richtige Weg ist:
default_scope
initialisiert Werte für neue Modelle, aber dann wird dies der Bereich, in dem Sie das Modell finden. Wenn Sie nur einige Zahlen auf 0 initialisieren möchten, ist dies nicht was Sie wollen.initialize
kann funktionieren, aber vergessen Sie nicht, super
anzurufen!after_initialize
wird nicht mehr empfohlen ab Rails 3. Wenn ich after_initialize
in Rails 3.0.3 überschreibe, erhalte ich die folgende Warnung in der Konsole:ABWEICHUNGSWARNUNG: Base # after_initialize ist veraltet. Verwenden Sie stattdessen die Base.after_initialize: -Methode. (Aufruf von/Users/me/myapp/app/models/my_model: 15)
Daher würde ich sagen, schreibe einen after_initialize
-Rückruf, mit dem Sie Standardattribute zusätzlich z festlegen können, wie folgt:
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
Jetzt haben Sie nur einen Platz, um nach der Initialisierung Ihrer Modelle zu suchen. Ich benutze diese Methode, bis jemand eine bessere findet.
Vorsichtsmaßnahmen:
Für boolesche Felder machen Sie:
self.bool_field = true if self.bool_field.nil?
Weitere Informationen finden Sie in Paul Russells Kommentar zu dieser Antwort
Wenn Sie nur eine Teilmenge von Spalten für ein Modell auswählen (dh select
in einer Abfrage wie Person.select(:firstname, :lastname).all
verwenden), erhalten Sie MissingAttributeError
, wenn Ihre init
-Methode auf eine Spalte zugreift, die keine hat wurde nicht in die select
-Klausel aufgenommen. Sie können sich gegen diesen Fall wie folgt schützen:
self.number ||= 0.0 if self.has_attribute? :number
und für eine Boolesche Spalte ...
self.bool_field = true if (self.has_attribute? :bool_value) && self.bool_field.nil?
Beachten Sie auch, dass die Syntax vor Rails 3.2 abweicht (siehe Kommentar von Cliff Darling unten).
Wir übergeben die Standardwerte in der Datenbank durch Migrationen (durch Angabe der Option :default
für jede Spaltendefinition), und Active Record kann diese Werte verwenden, um den Standardwert für jedes Attribut festzulegen.
IMHO ist dieser Ansatz an den Prinzipien von AR ausgerichtet: Konvention über Konfiguration, DRY, die Tabellendefinition treibt das Modell an, nicht umgekehrt.
Beachten Sie, dass sich die Standardwerte immer noch im Anwendungscode (Ruby-Code) befinden, jedoch nicht im Modell, sondern in den Migrationen.
Einige einfache Fälle können durch Definieren eines Standardwerts im Datenbankschema gehandhabt werden. Dies gilt jedoch nicht für eine Reihe schwierigerer Fälle, einschließlich berechneter Werte und Schlüssel anderer Modelle. Für diese Fälle mache ich das:
after_initialize :defaults
def defaults
unless persisted?
self.extras||={}
self.other_stuff||="This stuff"
self.assoc = [OtherModel.find_by_name('special')]
end
end
Ich habe mich für die Verwendung von after_initialize entschieden, ich möchte jedoch nicht, dass es auf Objekte angewendet wird, die nur die neuen gefunden oder erstellt wurden. Ich denke, es ist fast schockierend, dass für diesen offensichtlichen Anwendungsfall kein after_new-Rückruf bereitgestellt wird. Ich habe jedoch getan, indem ich bestätigte, ob das Objekt bereits persistent ist, um anzuzeigen, dass es nicht neu ist.
Nachdem Brad Murrays Antwort gesehen wurde, ist dies noch sauberer, wenn die Bedingung in die Rückrufanforderung verschoben wird:
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
In Rails 5+ können Sie die Methode attribute in Ihren Modellen verwenden, z. B .:
class Account < ApplicationRecord
attribute :locale, :string, default: 'en'
end
Das Callback-Muster after_initialize kann durch folgende Schritte verbessert werden
after_initialize :some_method_goes_here, :if => :new_record?
Dies hat einen nicht unerheblichen Vorteil, wenn Ihr Init-Code mit Verknüpfungen umgehen muss, da der folgende Code ein subtiles n + 1 auslöst, wenn Sie den ursprünglichen Datensatz lesen, ohne den zugehörigen Eintrag einzuschließen.
class Account
has_one :config
after_initialize :init_config
def init_config
self.config ||= build_config
end
end
Die Phusion-Jungs haben dazu ein Nice plugin .
Ein noch besserer/saubererer Weg als die vorgeschlagenen Antworten besteht darin, den Accessor wie folgt zu überschreiben:
def status
self['status'] || ACTIVE
end
Weitere Informationen finden Sie unter "Überschreiben von Standardzugriffsmitteln" in der ActiveRecord :: Base-Dokumentation und more von StackOverflow zur Verwendung von self .
Ich benutze den attribute-defaults
gem
Führen Sie in der Dokumentation: Sudo gem install attribute-defaults
aus und fügen Sie require 'attribute_defaults'
zu Ihrer App hinzu.
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"
Ähnliche Fragen, aber alle haben einen etwas anderen Kontext: - Wie erstelle ich einen Standardwert für Attribute im Modell von Rails activerecord?
Beste Antwort: hängt davon ab, was Sie wollen!
Wenn jedes Objekt mit einem Wert beginnen soll: verwenden Sie after_initialize :init
Sie möchten, dass das Formular new.html
beim Öffnen der Seite einen Standardwert hat? Verwenden Sie 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
Wenn für jedes Objekt ein Wert aus der Benutzereingabe berechnet werden soll: verwenden Sie before_save :default_values
Sie möchten, dass der Benutzer X
und dann Y = X+'foo'
eingibt benutzen:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= 'P'
end
end
Das Wichtigste zuerst: Ich stimme Jeffs Antwort nicht zu. Es ist sinnvoll, wenn Ihre App klein ist und Ihre Logik einfach ist. Ich versuche hier einen Einblick zu geben, wie es beim Erstellen und Warten einer größeren Anwendung zu Problemen kommen kann. Ich empfehle nicht, diesen Ansatz zuerst zu verwenden, wenn Sie etwas Kleines bauen, sondern als alternativen Ansatz im Auge behalten:
Eine Frage ist hier, ob diese Standardeinstellung für Datensätze Geschäftslogik ist. Wenn dies der Fall ist, würde ich vorsichtig sein, es in das ORM-Modell aufzunehmen. Da das Feld, das Ryw erwähnt, aktiv ist, klingt dies wie Geschäftslogik. Z.B. Der Benutzer ist aktiv.
Warum sollte ich vorsichtig sein, um geschäftliche Bedenken in ein ORM-Modell zu integrieren?
Es bricht SRP . Jede Klasse, die von ActiveRecord :: Base erbt, führt bereits ein lot aus verschiedenen Dingen aus, wobei hauptsächlich Datenkonsistenz (Validierungen) und Persistenz (Speichern) von Bedeutung sind. Wenn man die Geschäftslogik, so klein sie auch ist, einsetzt, bricht mit AR :: Base SRP.
Es ist langsamer zu testen. Wenn ich irgendeine Form von Logik in meinem ORM-Modell testen möchte, müssen meine Tests Rails initialisieren, damit sie ausgeführt werden können. Am Anfang Ihrer Anwendung ist dies kein allzu großes Problem, aber es sammelt sich an, bis die Tests Ihrer Einheiten lange dauern.
Es wird die SRP noch weiter und auf konkrete Weise brechen. Nehmen wir an, unser Unternehmen verlangt von uns nun eine E-Mail an Benutzer, wenn Artikel aktiv werden. Jetzt fügen wir dem Artikel-ORM-Modell eine E-Mail-Logik hinzu, deren Hauptaufgabe das Modellieren eines Artikels ist. Es sollte sich nicht um E-Mail-Logik kümmern. Dies ist ein Fall von geschäftlichen Nebenwirkungen. Diese gehören nicht in das ORM-Modell.
Es ist schwer zu diversifizieren. Ich habe reife Rails-Apps mit Dingen wie einem datenbankgestützten Feld init_type: string gesehen, dessen einziger Zweck darin besteht, die Initialisierungslogik zu steuern. Dies verschmutzt die Datenbank, um ein strukturelles Problem zu beheben. Ich glaube, es gibt bessere Wege.
The PORO way: Obwohl dies ein bisschen mehr Code ist, können Sie Ihre ORM-Modelle und Ihre Business Logic voneinander trennen. Der Code hier ist vereinfacht, sollte aber die Idee zeigen:
class SellableItemFactory
def self.new(attributes = {})
record = Item.new(attributes)
record.active = true if record.active.nil?
record
end
end
Dann wäre dies der richtige Weg, um ein neues Element zu erstellen
SellableItemFactory.new
Meine Tests konnten jetzt einfach überprüfen, ob ItemFactory für Item aktiviert ist, wenn es keinen Wert hat. Es ist keine Rails-Initialisierung erforderlich, kein SRP-Bruch. Wenn die Elementinitialisierung weiter fortgeschritten ist (z. B. ein Statusfeld, einen Standardtyp usw.), kann dies der ItemFactory hinzugefügt werden. Wenn wir zwei Arten von Standardeinstellungen erhalten, können wir eine neue BusinesCaseItemFactory erstellen.
HINWEIS: Es kann auch von Vorteil sein, hier Abhängigkeitsinjektion zu verwenden, damit die Fabrik viele aktive Dinge bauen kann. Ich habe es jedoch der Einfachheit halber ausgelassen. Hier ist es: self.new (klass = Artikel, Attribute = {})
Sup Jungs, ich habe am Ende folgendes getan:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
end
Klappt wunderbar!
Dafür sind Konstrukteure da! Überschreiben Sie die initialize
-Methode des Modells.
Verwenden Sie die after_initialize
-Methode.
Dies wurde lange Zeit beantwortet, aber ich brauche häufig Standardwerte und ziehe es vor, sie nicht in die Datenbank aufzunehmen. Ich erstelle ein DefaultValues
-Anliegen:
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
Und dann in meinen Modellen so verwenden:
class Widget < ApplicationRecord
include DefaultValues
defaults :category, to: 'uncategorized'
defaults :token, to: -> { SecureRandom.uuid }
end
Ich habe auch gesehen, dass die Leute es in ihre Migration gesteckt haben, aber ich möchte es lieber sehen. im Modellcode definiert.
Gibt es eine kanonische Methode zum Festlegen eines Standardwerts für Felder in ActiveRecord-Modell?
Der kanonische Rails-Weg vor Rails 5 sollte eigentlich in der Migration festgelegt werden. Schauen Sie im db/schema.rb
nach, wann immer Sie sehen möchten, welche Standardwerte von der DB für ein Modell festgelegt werden.
Im Gegensatz zu den Antworten von @Jeff Perrin (die etwas älter sind), wendet der Migrationsansatz sogar den Standardwert an, wenn Model.new
verwendet wird, aufgrund von Rails-Magie. Verifiziertes Arbeiten in Rails 4.1.16.
Das einfachste ist oft das Beste. Weniger Wissensverschuldung und mögliche Verwirrungspunkte in der Codebasis. Und es funktioniert einfach.
class AddStatusToItem < ActiveRecord::Migration
def change
add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" }
end
end
Der null: false
lässt keine NULL-Werte in der Datenbank zu. Außerdem werden alle vorhandenen DB-Datensätze mit dem Standardwert für dieses Feld aktualisiert. Sie können diesen Parameter bei der Migration ausschließen, wenn Sie möchten, aber ich fand ihn sehr praktisch!
Der kanonische Weg in Rails 5+ ist, wie @Lucas Caton sagte:
class Item < ActiveRecord::Base
attribute :scheduler_type, :string, default: 'hotseat'
end
Ich habe Probleme mit after_initialize
und gab ActiveModel::MissingAttributeError
Fehler, wenn ich komplexe Funde mache:
z.B:
@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)
"Suche" im .where
ist ein Hash von Bedingungen
Am Ende habe ich es so gemacht, dass initialize folgendermaßen überschrieben wurde:
def initialize
super
default_values
end
private
def default_values
self.date_received ||= Date.current
end
Der Aufruf super
ist erforderlich, um sicherzustellen, dass das Objekt korrekt von ActiveRecord::Base
initialisiert wird, bevor Sie meinen benutzerdefinierten Code ausführen, z
Ich empfehle dringend die Verwendung des Gems "default_value_for": https://github.com/FooBarWidget/default_value_for
Es gibt einige knifflige Szenarien, in denen die Initialisierungsmethode, die dieser Edelstein ausführt, überschrieben werden muss.
Beispiele:
Ihr DB-Standardwert ist NULL, Ihr Modell/Ruby-definierter Standardwert ist "some string", aber Sie möchten wollen den Wert aus irgendeinem Grund auf nil setzen: MyModel.new(my_attr: nil)
Die meisten Lösungen hier setzen den Wert nicht auf null, sondern auf den Standardwert.
OK, anstatt den ||=
- Ansatz zu wählen, wechseln Sie zu my_attr_changed?
...
[~ # ~] aber [~ # ~] Stellen Sie sich jetzt vor, Ihre DB-Standardeinstellung ist "some string", Ihre von Modell/Ruby definierte Standardeinstellung "some" andere Zeichenkette ", aber in einem bestimmten Szenario wollen setzen Sie den Wert auf" irgendeine Zeichenkette "(der DB-Standard): MyModel.new(my_attr: 'some_string')
Dies führt dazu, dass my_attr_changed?
false ist, da der Wert mit dem db-Standardwert übereinstimmt, wodurch wiederum der von Ruby definierte Standardcode und ausgelöst werden Setzen Sie den Wert auf "eine andere Zeichenfolge" - wieder nicht das, was Sie gewünscht haben.
Aus diesen Gründen glaube ich nicht, dass dies mit nur einem after_initialize-Hook erreicht werden kann.
Auch hier denke ich, dass der Edelstein "default_value_for" den richtigen Ansatz verfolgt: https://github.com/FooBarWidget/default_value_for
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
Das Problem bei den after_initialize-Lösungen besteht darin, dass Sie jedem einzelnen Objekt, das Sie aus der Datenbank herausfinden, ein after_initialize hinzufügen müssen, unabhängig davon, ob Sie auf dieses Attribut zugreifen oder nicht. Ich empfehle einen faulen Ansatz.
Die Attributmethoden (Getter) sind natürlich selbst Methoden, Sie können sie also überschreiben und einen Standard angeben. So etwas wie:
Class Foo < ActiveRecord::Base
# has a DB column/field atttribute called 'status'
def status
(val = read_attribute(:status)).nil? ? 'ACTIVE' : val
end
end
Wenn nicht, wie jemand darauf hingewiesen hat, Sie Foo.find_by_status ('ACTIVE') ausführen müssen. In diesem Fall denke ich, dass Sie wirklich den Standard in Ihren Datenbankeinschränkungen festlegen müssen, wenn die Datenbank dies unterstützt.
die after_initialize-Methode ist veraltet, verwenden Sie stattdessen den Rückruf.
after_initialize :defaults
def defaults
self.extras||={}
self.other_stuff||="This stuff"
end
die Verwendung von: default in Ihren Migrationen ist jedoch immer noch der sauberste Weg.
Hier ist eine Lösung, die ich verwendet habe und ich war ein wenig überrascht, die noch nicht hinzugefügt wurde.
Es gibt zwei Teile. Im ersten Teil wird der Standard für die tatsächliche Migration festgelegt. Im zweiten Teil wird eine Validierung in das Modell eingefügt, um sicherzustellen, dass die Präsenz wahr ist.
add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'
Sie sehen hier also, dass der Standard bereits eingestellt ist. Jetzt möchten Sie in der Validierung sicherstellen, dass immer ein Wert für die Zeichenfolge vorhanden ist
validates :new_team_signature, presence: true
Was Sie tun werden, ist den Standardwert für Sie festzulegen. (für mich habe ich "Willkommen im Team"), und dann geht es einen Schritt weiter, um sicherzustellen, dass für dieses Objekt immer ein Wert vorhanden ist.
Hoffentlich hilft das!
Obwohl das Einstellen der Standardwerte in den meisten Fällen verwirrend und umständlich ist, können Sie auch :default_scope
verwenden. Check out Squil's Kommentar hier .
Ich habe festgestellt, dass die Verwendung einer Validierungsmethode eine Menge Kontrolle über die Standardeinstellungen bietet. Sie können sogar Standardeinstellungen für Updates festlegen (oder die Überprüfung fehlschlagen). Sie haben sogar einen anderen Standardwert für Einfügungen vs. Aktualisierungen festgelegt, wenn Sie wirklich wollten .. _. Beachten Sie, dass der Standardwert erst nach #valid gesetzt wird. wird genannt.
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
Bei der Definition einer after_initialize-Methode kann es zu Leistungsproblemen kommen, da after_initialize auch von jedem Objekt aufgerufen wird, das zurückgegeben wird von: find: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find
Wenn es sich bei der Spalte um eine Statusspalte handelt und Ihr Modell sich für die Verwendung von Zustandsmaschinen eignet, sollten Sie die Option aasm gem verwenden. Danach können Sie dies einfach tun
aasm column: "status" do
state :available, initial: true
state :used
# transitions
end
Der Wert für nicht gespeicherte Datensätze wird immer noch nicht initialisiert, aber es ist ein bisschen sauberer als das Rollen mit init
oder was auch immer, und Sie profitieren von den anderen Vorteilen von aasm, z.
https://github.com/keithrowell/Rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end