Rails Active Record - Как смоделировать сценарий пользователя / профиля - PullRequest
9 голосов
/ 23 марта 2012

У меня есть приложение rails, у которого есть три разных типа пользователей, и мне нужно, чтобы они все использовали одну и ту же информацию общего профиля. Однако каждый отдельный пользователь также имеет уникальные атрибуты. Я не уверен, как выделить разные поля.

  • Администратор (администратор всего сайта)
  • Владелец (магазина / и т. Д.)
  • Член (например, член кооператива)

Я использую devise для аутентификации и cancan для авторизации. Поэтому у меня есть модель User с набором ролей, которые могут быть применены к пользователю. Этот класс выглядит так:

class User < ActiveRecord::Base
  # ... devise stuff omitted for brevity ...

  # Roles association
  has_many :assignments
  has_many :roles, :through => :assignments

  # For cancan: https://github.com/ryanb/cancan/wiki/Separate-Role-Model
  def has_role?(role_sym)
    roles.any? { |r| r.name.underscore.to_sym == role_sym }
  end
end

У каждого пользователя есть профиль, который включает:

  • Имя и Фамилия
  • Информация об адресе (город / улица / почтовый индекс / и т. Д.)
  • Телефон

Я не хочу загрязнять модель пользователя этой информацией, поэтому я добавляю ее в модель профиля. Эта часть довольно проста. Это превращает модель User в нечто подобное:

class User < ActiveRecord::Base
  # ... devise stuff omitted for brevity ...
  # ... cancan stuff omitted for brevity ... 
  has_one :profile
end

В дополнительных полях я испытываю беспокойство по поводу того, как моделировать ...

Если пользователь является администратором, у него будут уникальные поля, такие как:

  • admin_field_a: строка
  • admin_field_b: строка
  • и т.д.

Если пользователь является владельцем, у него будут уникальные поля ...

  • stripe_api_key: строка
  • stripe_test_api_key: строка
  • stripe_account_number: строка
  • has_one: store # AR Ссылка на другую модель, которой нет у Admin и Member.

Если пользователь является участником, у него будет несколько дополнительных полей:

  • stripe_account_number: строка
  • own_to: store # магазин, членом которого они являются
  • has_many: note

...

и модель магазина будет содержать has_many для участников, поэтому мы работаем с членами магазина.

Проблема связана с дополнительными полями. Я устанавливаю это как различные классы? Поместите их в другой В настоящее время я пробовал несколько разных способов установить это:

Один из способов - настроить модель пользователя в качестве совокупного корня

class User < ActiveRecord::Base
  # ...

  # Roles association
  has_many :assignments
  has_many :roles, :through => :assignments

  # Profile and other object types
  has_one :profile
  has_one :admin
  has_one :owner
  has_one :member

  # ...
end

Преимущество этого подхода в том, что модель User является корневой и может получить доступ ко всему. Недостатком является то, что если пользователь является «владельцем», то ссылки «admin» и «member» будут равны нулю (и декартовой системе других возможностей - admin, но не владельца или участника и т. Д.).

Другой вариант, о котором я думал, состоял в том, чтобы каждый тип пользователя наследовал от модели User как таковой:

class User < ActiveRecord::Base
  # ... other code removed for brevity
  has_one :profile
end

class Admin < User
  # admin fields
end

class Owner < User
  # owner fields
end

class Member < User
  # member fields
end

Проблема в том, что я загрязняю объект User всеми видами nil в таблице, где одному типу не нужны значения из другого типа / etc. Просто выглядит немного грязно, но я не уверен.

Другой вариант состоял в том, чтобы создать каждый тип учетной записи в качестве корневого, но иметь пользователя в качестве дочернего объекта, как показано ниже.

class Admin
  has_one :user
  # admin fields go here. 
end

class Owner
  has_one :user
  # owner fields go here. 
end

class Member
  has_one :user
  # member fields go here. 
end

Проблема с вышесказанным заключается в том, что я не уверен, как загрузить соответствующий класс после входа пользователя. У меня будет их user_id, и я смогу определить, какая у него роль (из-за роли ассоциация на пользовательской модели), но я не уверен, как перейти от пользователя UP к корневому объекту. Методы? Другой?

Заключение У меня есть несколько разных способов сделать это, но я не уверен, что правильный подход "рельсы". Как правильно моделировать это в рельсах AR? (Бэкэнд MySQL). Если не существует «правильного» подхода, что является лучшим из вышеперечисленного (я также открыт для других идей).

Спасибо!

Ответы [ 3 ]

6 голосов
/ 23 марта 2012

Мой ответ предполагает, что данный пользователь может быть только одним типом пользователя - например, ТОЛЬКО Администратор или ТОЛЬКО Участник.Если это так, то это похоже на идеальную работу для Полиморфная ассоциация ActiveRecord .

class User < ActiveRecord::Base
  # ...

  belongs_to :privilege, :polymorphic => true
end

Эта ассоциация дает Пользователю средство доступа, называемое «привилегией» (из-за отсутствия лучшего термина и во избежание наименованияпутаница, которая станет очевидной позже).Поскольку он полиморфен, он может возвращать различные классы.Полиморфное отношение требует двух столбцов в соответствующей таблице - один (accessor)_type и (accessor)_id.В моем примере таблица User получит два поля: privilege_type и privilege_id, которые ActiveRecord объединяет для поиска связанной записи во время поиска.

Ваши классы Admin, Owner и Member выглядят так:

class Admin
  has_one :user, :as => :privilege
  # admin fields go here. 
end

class Owner
  has_one :user, :as => :privilege
  # owner fields go here. 
end

class Member
  has_one :user, :as => :privilege
  # member fields go here. 
end

Теперь вы можете делать такие вещи:

u = User.new(:attribute1 => user_val1, ...)
u.privilege = Admin.new(:admin_att1 => :admin_val1, ...)
u.save!
# Saves a new user (#3, for example) and a new 
# admin entry (#2 in my pretend world).

u.privilege_type  # Returns 'Admin'
u.privilege_id    # Returns 2

u.privilege       # returns the Admin#2 instance.
# ActiveRecord's SQL behind the scenes: 
#    SELECT * FROM admin WHERE id=2 

u.privilege.is_a? Admin   # returns true
u.privilege.is_a? Member  # returns false

Admin.find(2).user        # returns User#3
# ActiveRecord's SQL behind the scenes: 
#    SELECT * FROM user WHERE privilege_type='Admin' 
#    AND privilege_id=2

Я бы порекомендовал вам сделать поле (accessor)_type в базе данных ENUM, если вы ожидаете, что оно будет известным набором значений.ENUM, IMHO, является лучшим выбором, чем VARCHAR255, который, как правило, по умолчанию использует Rails, проще / быстрее / меньше для индексации, но делает изменения в будущем более трудными / трудоемкими, когда у вас миллионы пользователей.Кроме того, правильно индексируйте связь:

add_column :privilege_type, "ENUM('Admin','Owner','Member')", :null => false
add_column :privilege_id, :integer, :null => false

add_index :user, [:privilege_type, :privilege_id], :unique => true
add_index :user, :privilege_type

Первый индекс позволяет ActiveRecord быстро находить обратную связь (например, найти пользователя с привилегией Admin # 2), а второй индекс позволяет найти всеАдминистраторы или все члены.

Этот RailsCast немного устарел, но, тем не менее, является хорошим руководством по полиморфным отношениям.

Последнее замечание: в своем вопросе вы указали Admin,Владелец или член - это тип пользователя, который является достаточно подходящим, но, как вы, вероятно, видите, я должен объяснить, что в вашей пользовательской таблице будет поле user_type_type.

1 голос
/ 11 апреля 2012

Вы уже приняли ответ, но, несмотря на это, я столкнулся с подобной ситуацией и решил использовать ваш подход «Модель пользователя как совокупный корень».Модель «Мой пользователь» содержит всю информацию «профиля», а пользователь, если использовать вымышленный пример, has_one: покупатель и has_one: продавец.Я использую простое поле tinyint в качестве битовых флагов, для которых роли выполняет пользователь, поскольку пользователи могут быть как покупателями, так и продавцами (или другими ролями, которые мне понадобятся в будущем).Если бит установлен, вы можете предположить, что соответствующая ассоциация не равна nil (для меня это не проблема, так как я всегда проверяю битовые флаги перед использованием ссылки на ассоциацию).На самом деле у меня не так много уникальных полей в моих настоящих подчиненных моделях, но очень полезно сохранять деклютеринг, когда каждая подчиненная модель имеет дополнительные ассоциации, например, если продавец has_one: merchant_account "и покупатель has_one: purchase_history и т.д.еще не вышел, но когда я это сделаю, я буду следить за этим сообщением с любыми проблемами, с которыми я сталкиваюсь.

1 голос
/ 23 марта 2012

Я, вероятно, не собираюсь давать вам одобренное предложение, но ..

Разделение вашего профиля - хороший вызов.Попробуйте использовать шаблон Decorator для роли.Вы можете иметь AdminUserDecorator, OwnerUserDecorator или MemberOwnerDecorator.Вы также можете динамически добавлять дополнительные поля непосредственно в экземпляр (в конце концов, это Ruby), но я думаю, что это будет уродливо и сложно.(Если вы действительно хотите делать плохие вещи, используйте объект посетителя, чтобы дать вам экземпляр вашего декоратора из метода класса пользователя.)

Кроме того, зачем вместо этого ставить конфигурацию полосы или оплаты на владельцабыть частью магазина информации?Разве возможно, что владелец может иметь несколько магазинов и использовать одну и ту же платежную информацию для каждого магазина?

ОБНОВЛЕНИЕ : Я должен также предложить использовать TDD для очистки того, что работает.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...