Этот пункт для меня оказался самым сложным и времязатратным, так как проблемы были как и с придумыванием достаточно оптимального решения, так и с его дальнейшей реализацией. Причем с реализацией проблем было намного больше. Ну, по порядку.
Если бы поля у всех пользователей были одинаковые, то хватило бы только добавить еще одно поле Тип. Но, так как поля у разных типов пользователей разные, то сложности начинаются с того, что нужно решить, как избавиться от лишних полей для пользователей, у которых этих полей быть не должно. Я обдумывал три разных решений:
- Тупо делать отдельную таблицу под каждый тип с полным набором полей.
Такое решение, например, предлагается в ридми Devise. В каждой таблице типа пользователя в этом варианте будут повторяющиеся атрибуты типа e-mail, пароля, итд, что уже является очевидным излишком. Повторяющиеся атрибуты надо отделить в свой отдельный логический кусок. Плюс, при таком варианте, при логине нужно было бы каждый раз делать юнион email/паролей всех этих таблиц. Хорошего, в итоге, в этом решение нет ничего, кроме, пожалуй, его простоты.
- Оставить все в одной таблице, и делить на типы с помощью Single Table Inheritance.
Тогда в изначальную таблицу Users нужно было бы добавить все возможные варианты атрибутов и поле Тип, по которому бы ActiveRecord определял, какие атрибуты подгружать в одноименную модель. Иметь множество null атрибутов для каждой записи в принципе кажется мне признаком плохой схемы, и, если еще для всего трех типов пользователей может быть адекватным решением, то при еще дальнейшем расширении таблица все больше будет превращаться в кашу из nullов. Поэтому, это решение кажется мне не оптимальным.
- Сделать одну таблицу с общими атрибутами, а для каждого уникального типа пользователя сделать отдельную таблицу, в которой хранить его уникальные атрибуты.
Общие атрибуты это логин/пароль, плюс у большинства типов пользователей должна быть возможность загрузки аватарки, поэтому я решил сделать пользовательский аватар тоже общим атрибутом. Плюс, у общей таблицы должно опять же быть поле Тип, по которому определяется в какой дополнительной таблице следует искать остаток пользовательских атрибутов. Все дополнительные таблицы должны иметь внешним ключом ID общей таблицы, и, в идеале, он же должен являться еще и первичным ключом в дополнительной таблице, чтобы не множить лишних идентификаторов.
Такое решение кажется мне оптимальным, так как не создает лишних атрибутов, является легко расширяемым и остается простым и логичным.
В итоге получается 4 таблицы: user_logins - здесь хранятся общие атрибуты admins - здесь хранятся уникальные атрибуты для админов shop_onwers - уникальные атрибуты для владельцев магазинов guests - уникальные атрибуты гостей
Теперь о возникших проблемах с реализацией.
Как это реализовать, используя ActiveRecord? Тут обычная связь один к одному, но по умолчанию AR связывает одну таблицу только с одной таблицей, поэтому либо писать кастомные методы для унификации работы с несколькими связями один к одному, либо использовать полиморфическую связь AR.
При полиморфической связи в таблице, user_logins необходимо, вкупе с полем user_type, иметь еще и поле user_id, которое ссылается на уникальный ID записи в одной из вспомогательных таблиц. AR по умолчанию создает первичные ключи для каждой таблицы, тем самым такая реализация уже отличается от моей изначальной задумки. Плюс, с полиморфической связью не используется внешний ключ, т.е. для этих таблиц не осуществляется целостность данных на уровне БД. В итоге, чтобы воспользоваться полиморфической связью, с этими двумя моментами я смирился, но вот не знаю насколько такое решение оптимально. Поэтому это получается первым кандидатом для рефакторинга. Итоговая схема приложена.
Далее, я создал 4 модели: UserLogins - логика для логинов/паролей User - абстрактный класс, инкапсулирует общее поведение разных типов пользователей Admins - модель админов, наследуется от User ShopOwners - модель владельцев магазинов Guests - модель гостей Модели все приложены.
Далее, начались проблемы с реализацией взаимодействия. С вложенными формами до этого не работал, поэтому разбирался с ними. Разбирался как работают нэймспейсы с маршрутами. Далеко не сразу понял, что при полиморфической связи AR по-умолчанию добавляет связанные записи одной транзакцией, подставляя куда нужно правильные ID. Также, чтобы сдвинуться с мертвой точки мне пришлось отказаться от Devise, потому что не полностью понимал, как он работает, и в совокупностью со всеми остальными моментами, которые я не до конца понимал, я надолго застрял. Переписав же авторизацию пользователей с нуля, удалось разобраться и со всем остальным.
Маршруты получились такие: я создал нэймспейс users, и в этом нэймспейсе под каждый тип пользователя свой ресурс. (см. файл routes.rb)
Для каждого типа юзера я с создал свой контроллер. Приведу пример только одного, так как они по сути почти одинаковые, отличаются только приватными методами с strong parameters. Тут очевидно получается не DRY и нужно рефакторить. Следует ли это вынести в один контроллер, в котором тип юзера будет определяться по URL? Или есть какой-нибудь еще более верный способ? Не приведет ли совмещение всех пользовательских контроллеров в один к излишней запутанности при расширении?
Views. Я сделал общий партиал для вложенной формы с user_login атрибутами, и отдельный форму для каждого юзера. Форма для каждого юзера тоже партиал, чтобы использовать как для new, так и для edit.
Что в итоге мне кажется здесь нужно переделать:
- Убрать лишние ID в схеме
- Объеденить пользовательские контроллеры
- Возможно следует вернуть Devise вместо кастомной регистрации.
@slavone
Если при отсутствии аватаров, это имя еще подходит, то при их наличии - нет.
Logins и avatars имеют мало чего общего между собой.
Возможно, имя могло быть profile_attribures или даже profile.
Также есть "знак", который заставляет задуматься: UserLogin знает о том, что в системе
есть Admin, ShopOwner и Guest, что "приклеивает" UserLogin к ним.
Структура классов могла быть, например, следущей:
В этом случае, Profile остается сам по себе, даже при добавлении новых сущностей,
которые могут играть роль Profilable, код Profile остается неизменным.
На самом деле есть еще несколько способов как реализовать валидации:
например, с validates_associated (см. пункт 3), с validates вместо validate и
делегированием.
http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
http://guides.rubyonrails.org/active_record_validations.html#custom-methods
http://guides.rubyonrails.org/active_record_validations.html#validates-associated
http://apidock.com/rails/Module/delegate
3. accepts_nested_attributes_for - лучше не использовать.
Так как она привносит больше проблем, чем решает.
Может на первых этапах, когда все очень просто, accepts_nested_attributes_for
и кажется подходящим решением, то в будущем еще аукнется.
Причина та же - связывание/"склеивание" (англ. coupling) сущностей.