webentwicklung-frage-antwort-db.com.de

Wie kann ich in ActiveRecord Standardwerte festlegen?

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?

397
ryw

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:

  1. 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.
  2. Das Definieren von Standardeinstellungen in Ihrer Migration funktioniert in manchen Fällen auch ... Wie bereits erwähnt, funktioniert dies nicht, wenn Sie nur Model.new aufrufen.
  3. Das Überschreiben von initialize kann funktionieren, aber vergessen Sie nicht, super anzurufen!
  4. Ein Plugin wie das von Phusion zu verwenden, wird ein bisschen lächerlich. Dies ist Ruby. Brauchen wir wirklich ein Plugin, um einige Standardwerte zu initialisieren?
  5. Überschreiben von after_initializewird 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:

  1. 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

  2. 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).

545
Jeff Perrin

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.

45
Laurent Farcy

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
39
Joseph Lord

In Rails 5+ können Sie die Methode attribute in Ihren Modellen verwenden, z. B .:

class Account < ApplicationRecord
  attribute :locale, :string, default: 'en'
end
36
Lucas Caton

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
17
Brad Murray

Die Phusion-Jungs haben dazu ein Nice plugin .

16
Milan Novota

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 .

8
peterhurford

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"
8
aidan

Ä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
7
Blair Anderson

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?

  1. 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.

  2. 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.

  3. 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.

  4. 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 = {})

4
Houen

Sup Jungs, ich habe am Ende folgendes getan:

def after_initialize 
 self.extras||={}
 self.other_stuff||="This stuff"
end

Klappt wunderbar!

4
Tony

Dafür sind Konstrukteure da! Überschreiben Sie die initialize-Methode des Modells.

Verwenden Sie die after_initialize-Methode.

4
John Topley

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
3
clem

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
3
Magne

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

1
Sean

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

1
etipton
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end
1
Mike Breen

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.

1
Jeff Gran

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.

0
Greg

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!

0
kdweber89

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 .

0
skalee

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

0
Kelvin

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.

0
Bad Request

https://github.com/keithrowell/Rails_default_value

class Task < ActiveRecord::Base
  default :status => 'active'
end
0
Keith Rowell