Schienen - Best Practice: So erstellen Sie abhängige has_one-Beziehungen

75

Können Sie mir sagen, was die beste Vorgehensweise zum Erstellen von has_one-Beziehungen ist?

zB wenn ich ein Benutzermodell habe und es ein Profil haben muss ...

Wie könnte ich das erreichen?

Eine Lösung wäre:

# user.rb
class User << ActiveRecord::Base
  after_create :set_default_association

  def set_default_association
    self.create_profile
  end
end

Aber das scheint nicht sehr sauber zu sein ... Irgendwelche Vorschläge?

BvuRVKyUVlViVIc7
quelle

Antworten:

124

Die beste Vorgehensweise zum Erstellen einer has_one-Beziehung besteht darin, den ActiveRecord-Rückruf before_createanstelle von zu verwenden after_create. Oder verwenden Sie einen noch früheren Rückruf und behandeln Sie die Probleme (falls vorhanden) des Kindes, das seinen eigenen Validierungsschritt nicht besteht.

Weil:

  • Mit einer guten Codierung haben Sie die Möglichkeit, dass die Validierungen des untergeordneten Datensatzes dem Benutzer angezeigt werden, wenn die Validierungen fehlschlagen
  • Es ist sauberer und wird von ActiveRecord explizit unterstützt. AR füllt den Fremdschlüssel im untergeordneten Datensatz automatisch aus, nachdem der übergeordnete Datensatz gespeichert wurde (beim Erstellen). AR speichert dann den untergeordneten Datensatz als Teil der Erstellung des übergeordneten Datensatzes.

Wie es geht:

# in your User model...
has_one :profile
before_create :build_default_profile

private
def build_default_profile
  # build default profile instance. Will use default params.
  # The foreign key to the owning User model is set automatically
  build_profile
  true # Always return true in callbacks as the normal 'continue' state
       # Assumes that the default_profile can **always** be created.
       # or
       # Check the validation of the profile. If it is not valid, then
       # return false from the callback. Best to use a before_validation 
       # if doing this. View code should check the errors of the child.
       # Or add the child's errors to the User model's error array of the :base
       # error item
end
Larry K.
quelle
Könnte das auch mit einer einzigen Zeile erledigt werden? -> before_filter: build_profile?
BvuRVKyUVlViVIc7
2
@Lichtamberg: Ja, aber ich würde einen Kommentar hinzufügen: "Erstellt ein Standardprofil. MUSS immer validieren." HINWEIS: Es wäre "before_create: build_profile" und nicht "before_filter". Wenn es nicht validiert wird, erhalten Sie eine sehr verwirrende Fehlermeldung an den Benutzer. Oder es würde tatsächlich NICHT erstellt, was bedeuten würde, dass Sie einen Benutzer ohne Profil haben würden. Sie sollten auch die Eckfälle in Ihren Tests testen.
Larry K
Ich habe diesen Code ausprobiert, aber er ist fehlgeschlagen. Ich musste profile = build_profile
jakeonrails
1
Wie fügt man dieses Objekt dann dem neuen hinzu user, da dies der before_createFall ist ?
Meekohi
9
Beachten Sie jedoch, dass bei Rails 5 die Verwendung von before_create zum Erstellen des abhängigen Datensatzes nicht möglich ist, ohne die Standardeinstellungen für den zugehörigen Datensatz zu überschreiben. Die Standardeinstellung erwartet nun, dass der Datensatz "eins_zu "vorhanden ist, andernfalls wird ein Fehler ausgegeben.
Viet
28

Ihre Lösung ist definitiv ein anständiger Weg, dies zu tun (zumindest bis Sie herauswachsen), aber Sie können es vereinfachen:

# user.rb
class User < ActiveRecord::Base
  has_one      :profile
  after_create :create_profile
end
Bo Jeanes
quelle
24

Wenn dies eine neue Zuordnung in einer vorhandenen großen Datenbank ist, verwalte ich den Übergang wie folgt:

class User < ActiveRecord::Base
  has_one :profile
  before_create :build_associations

  def profile
    super || build_profile(avatar: "anon.jpg")
  end

private
  def build_associations
    profile || true
  end
end

Damit erhalten vorhandene Benutzerdatensätze ein Profil, wenn sie danach gefragt werden, und neue werden damit erstellt. Dies platziert auch die Standardattribute an einer Stelle und funktioniert ordnungsgemäß mit accept_nested_attributes_for ab Rails 4.

inopinatus
quelle
Dies ist eine brillante Lösung, aber wir verwenden super || create_profile (Avatar: "anon.jpg"), um sicherzustellen, dass das neue Profil sofort beibehalten wird.
Sandre89
1
@ sandre89 Sie riskieren einen Callback-Albtraum, Validierungsausnahmen und falsch persistierte Datensätze und vieles mehr, indem Sie eine Persistenzmethode in einem before_create-Callback verwenden. Avdi Grimm hat eine / vierteilige Serie / über RubyTapas geschrieben, in der die vielen Katastrophen behandelt werden, die darauf warten, dass die Modellebene sich selbst rettet. Ich wünsche dir das Beste, aber ich fürchte das Schlimmste.
Inopinatus
9

Wahrscheinlich nicht die sauberste Lösung, aber wir hatten bereits eine Datenbank mit einer halben Million Datensätzen, von denen einige bereits das 'Profil'-Modell erstellt hatten und einige nicht. Wir haben diesen Ansatz gewählt, der garantiert, dass zu jedem Zeitpunkt ein Profilmodell vorhanden ist, ohne dass alle Profilmodelle durchlaufen und rückwirkend generiert werden müssen.

alias_method :db_profile, :profile
def profile
  self.profile = Profile.create(:user => self) if self.db_profile.nil?
  self.db_profile
end
Andrew Vilcsak
quelle
5

So mache ich es. Ich bin mir nicht sicher, wie Standard dies ist, aber es funktioniert sehr gut und ist faul, da es keinen zusätzlichen Overhead verursacht, es sei denn, es ist notwendig, die neue Zuordnung aufzubauen (ich bin froh, dass dies korrigiert wird):

def profile_with_auto_build
  build_profile unless profile_without_auto_build
  profile_without_auto_build
end

alias_method_chain :profile, :auto_build

Dies bedeutet auch, dass der Verein da ist, sobald Sie ihn brauchen. Ich denke, die Alternative besteht darin, sich in after_initialize einzuhängen, aber dies scheint einen erheblichen Overhead zu verursachen, da es jedes Mal ausgeführt wird, wenn ein Objekt initialisiert wird, und es kann Zeiten geben, in denen Sie nicht auf die Zuordnung zugreifen möchten. Es scheint eine Verschwendung zu sein, seine Existenz zu überprüfen.

Brendon Muir
quelle
Ich denke, diese Antwort ist eine bessere Lösung als andere, da Probleme mit der Validierung im Profilmodell vermieden werden. Danke
ole
Sie können auch automatisch speichern:has_one :profile, :autosave => true
montrealmike
@montrealmike, geht es zunächst um ein fehlendes Profil? Dh wenn man build_profile noch nicht ausgeführt hat, würde dies beim Speichern ein erstellen? Ich bin auch auf Folgendes gestoßen : github.com/phildionne/associates, das möglicherweise einen anderen Weg um Formulare mit mehreren Modellen bietet.
Brendon Muir
@BrendonMuir nein, es wird kein fehlendes Profil erstellt. Es ist nur etwas, das Sie zu Ihrer obigen Lösung hinzufügen können, damit das Profil automatisch
gespeichert wird,
0

Ich hatte ein Problem damit und akzeptiere_nested_attributes_for, da das zugehörige Modell dort erstellt wurde, wenn verschachtelte Attribute übergeben wurden. Am Ende habe ich es getan

after_create :ensure_profile_exists
has_one :profile
accepts_nested_attributes_for :profile


def ensure_profile_exists
  profile || create_profile
end
kkelleey
quelle