Rails - найкраща практика: Як створити залежні відносини has_one


75

Не могли б ви сказати мені, яка найкраща практика створення відносин has_one?

fe, якщо у мене є модель користувача, і вона повинна мати профіль ...

Як я міг це досягти?

Одним із рішень було б:

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

  def set_default_association
    self.create_profile
  end
end

Але це не здається дуже чистим ... Будь-яка пропозиція?

Відповіді:


125

Найкращою практикою створення відносини has_one є використання зворотного виклику ActiveRecord, before_createа не after_create. Або скористайтеся ще більш раннім зворотним викликом і вирішіть проблеми (якщо такі є) дитини, яка не пройшла власний етап перевірки.

Оскільки:

  • при хорошому кодуванні ви маєте можливість перевірки дочірнього запису відображатись користувачеві, якщо перевірки не вдаються
  • він чистіший і явно підтримується ActiveRecord - AR автоматично заповнює зовнішній ключ у дочірньому записі після збереження батьківського запису (при створенні). Потім AR зберігає дочірній запис як частину створення батьківського запису.

Як це зробити:

# 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

Чи можна цим керувати за допомогою одного рядка? -> before_filter: build_profile?
BvuRVKyUVlViVIc7

2
@Lichtamberg: Так, але я б додав коментар: "Створює профіль за замовчуванням. ПОВИНЕН завжди перевіряти." ПРИМІТКА: це буде "before_create: build_profile", а не "before_filter". Якби це не перевірило, ви отримаєте дуже заплутане повідомлення про помилку для користувача. Або насправді це НЕ буде створено, що означало б, що у вас опиниться Користувач без профілю. Ви також повинні перевірити кутові випадки у своїх тестах.
Larry K

Я спробував цей код, але він не вдався, мені довелося зробити profile = build_profile
jakeonrails

1
Тоді як додати цей об’єкт до нового user, оскільки це так before_create?
Meekohi

9
Тільки майте на увазі, що Rails 5 використовує функцію before_create, щоб створити залежний запис неможливо без перевизначення за замовчуванням запису принадлежи_to. За замовчуванням тепер очікується, що запис belong_to існує, інакше виникає помилка.
В'єт

28

Ваше рішення - це, безумовно, гідний спосіб це зробити (принаймні, поки ви це не переростете), але ви можете спростити його:

# user.rb
class User < ActiveRecord::Base
  has_one      :profile
  after_create :create_profile
end

24

Якщо це нова асоціація у існуючій великій базі даних, я керуватиму переходом так:

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

так що існуючі записи користувачів отримують профіль, коли про це запитують, і разом із ними створюються нові. Це також розміщує атрибути за замовчуванням в одному місці і працює коректно з accept_nested_attributes_for у Rails 4 і далі.


Це чудове рішення, але ми використовуємо супер || create_profile (аватар: "anon.jpg"), щоб переконатися, що новий профіль зберігається негайно.
sandre89

1
@ sandre89 ви ризикуєте кошмаром зворотного дзвінка, винятками для перевірки та неправильно збереженими записами, а також, крім того, використовуючи метод збереження у зворотному виклику before_create. Авді Грімм написав / серію з чотирьох частин / на RubyTapas, висвітлюючи численні катастрофи, які чекають вас від того, щоб модельний шар зробив будь-яке самозбереження. Я бажаю тобі найкращого, але боюся гіршого.
inopinatus

9

Можливо, це не найчистіше рішення, але ми вже мали базу даних із півмільйона записів, деякі з яких уже створили модель «Профіль», а деякі - ні. Ми застосували такий підхід, який гарантує, що модель профілю присутня в будь-який момент, без необхідності проходити і ретроактивно генерувати всі моделі профілю.

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

5

Ось як я це роблю. Не впевнений, наскільки це стандартно, але він працює дуже добре, і він ледачий, оскільки не створює зайвих накладних витрат, якщо це не потрібно для побудови нової асоціації (я радий бути виправленим щодо цього):

def profile_with_auto_build
  build_profile unless profile_without_auto_build
  profile_without_auto_build
end

alias_method_chain :profile, :auto_build

Це також означає, що асоціація з’являється, як тільки вона вам потрібна. Я думаю, альтернативою є підключення до after_initialize, але це, здається, додає трохи накладних витрат, оскільки він запускається кожного разу, коли об'єкт ініціалізується, і можуть бути випадки, коли вам не байдуже отримати доступ до асоціації. Здається, марно витрачати перевірку на його існування.


Я думаю, що ця відповідь є кращим рішенням, ніж інші, оскільки уникайте проблем із перевіренням у моделі профілю. Дякую
ole

Ви також можете автозберегти:has_one :profile, :autosave => true
montrealmike

@montrealmike, чи це стосується відсутнього профілю для початку? Тобто, якщо хтось ще не виконав build_profile, чи буде це створювати при збереженні? Я також стикався з цим: github.com/phildionne/associates, який може запропонувати інший спосіб обійти багатомодельні форми.
Brendon Muir

@BrendonMuir ні, це не створить відсутній профіль. Це просто те, що ви можете додати до свого рішення вище, щоб воно автоматично зберігало профіль при збереженні користувача (а також перевіряло профіль)
montrealmike,


0

У мене була проблема з цим і accept_nested_attributes_for, тому що якщо вкладені атрибути були передані, пов'язана модель була створена там. Я закінчив робити

after_create :ensure_profile_exists
has_one :profile
accepts_nested_attributes_for :profile


def ensure_profile_exists
  profile || create_profile
end
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.