Профили пользователей: плюсы, минусы, подводные камни

    Не секрет, что работу с профилями пользователей в Django иначе как несчастьем не назовёшь. Все мы сталкивались с монолитностью модели auth.User, неадкеватным набором полей у неё, а также всеми теми ухищрениями, к которым приходилось прибегать.

    Извращаться приходилось всем: не только пользователям джанги, но и самим её core-разработчикам. Помните, например, как в Django 1.2 внезапно стало возможно использовать в поле username символы собаки (@) и точки? Знаете зачем? Чтобы в качестве логинов можно было использовать адреса e-mail.

    Нам, простым пользователям, тоже жилось несладко. Для того, чтобы изменить профиль пользователя, добавив ему какие-нибудь интересных полей — обычная, казалось бы, вещь, да? — приходилось действовать разными способами.

    • Взять, к примеру, наследование. Требовалось создать собственную модель, унаследованную от auth.User

      # models.py
      from django.db import models
      from django.contrib.auth.models import User
      
      class MyUser(User):
          birthday = models.DateField()    
      

      … и написать миддльварь, которая бы подменяла класс для request.user. Или не миддльварь, а AUTHENTICATION_BACKEND. Неважно :)

      Плюсом схемы было то, что наш, «клиентский» код (то бишь код проектов) не усложнялся, мы просто работали с нашим пользователем, как с обычным джанговским.

      Основной минус такой схемы то, что при наследовании моделей заполняется не одна таблица в БД, а две: исходная джанговская django_auth и наша ourproj_user, в которой есть внешний ключ на django_auth. Да, наследование моделей в Django — это всего лишь OneToOneField с какими-то дополнительными атрибутами. Захотите использовать — имейте в виду.

    • Не менее известный костыль, предложенный самими создателями Django — так называемая модель профиля. Нам предлагалось создать модель Profile со связью один-к-одному на auth.User

      # models.py
      from django.db import models
      from django.contrib.auth.models import User
      
      class Profile(models.Model):
          user = models.OneToOneField(User)
          birthday = models.DateField()
      

      … а потом добавить в settings нечто такое:

      AUTH_PROFILE_MODULE = 'accounts.Profile'

      После этого в клиентском коде мы могли работать с профилем:

      
      profile = request.user.get_profile()
      profile.birthday = datetime.today()
      profile.save()
      

      Плюс у такой схемы — не знаю :) может, любой набор любых полей с любым именем? С минусами всё прозрачнее:
      • сложность поддержки: у нас теперь не один объект для редактирования, а два. Надо не забыть, что дату рождения мы меняем у profile, а, к примеру, пароль у user. Немудрено и запутаться.

      • нецелевая растрата ресурсов: каждое обращение к get_profile() вызывает запрос к БД. На страницах, где инстанс пользователя всего один (редактирование, например) это не страшно. Если же такая штука будет, например, в комментариях, результат будет плачевен. Разумеется, select_related(), как вы понимаете, не спасёт, поскольку не User зависит от Profile, а наоборот.
      • а ещё всё надо руками делать! Создание модели User не означает, что автоматом создастся связанная модель Profile. Зато при обращении к get_profile() для вновь созданного юзера вылетит исключение — вот тут сомневаться не приходится. И хоть эта беда лечится в несколько строк простейшим сигналом,

        # profile.models
        from django.db import models
        from django.contrib.auth.models import User
        
        class Profile(models.Model):
            'что-нибудь хорошее'
        
        def create_profile(sender, **kwargs):
            if kwargs['created']:
                Profile.objects.create(user=kwargs['instance'])
        models.signals.post_save.connect(create_profile, sender=User)
        

        всё же раздражает необходимость её «ручного» решения.
    • Манкипатчинг, то есть изменение поведения программы без переписывания кода. После инициализации приложения в каком-нибудь месте проекта (как правило, в корневом urls, settings или models специально заведённого приложении) писали код, модифицирующий нашего Юзера:

      # monkey_patching.models
      from django.db import models
      from django.contrib.auth.models import User
      User.add_to_class('birthday', models.DateField() )
      

      Плюс, как и при наследовании, лёгкость в клиентском коде. Минусы — неочевидность. С магией, как известно, нужно обращаться очень осторожно, ведь можно невзначай переопределить какую-нибудь штуку, а всплывёт совсем в другом месте. C другой стороны, если очень осторожно, то почему бы и нет?

    Кстати, об обезьянах...


    Ребята, подарившие миру легендарный sorl.thumbnail, очередной раз отличились и произвели на счет ещё одну убойную штуку. Встречайте: django-primate, приложение, которое используя методы манкипатчинга (приматы и обезьяны, чуете корреляцию?) позволяют очень просто превратить вашу собственную модель в auth.User. То есть, по-русски говоря, составлять профиль из нужных полей.

    Начать использовать достаточно легко. Сначала надо поставить django-primate. Можно с PyPI:

    pip install django-primate

    … а можно и последнюю версию из их репозитория:

    pip install  -e git+https://github.com/aino/django-primate.git#egg=django-primate

    Вызывать патч нужно при старте. Создатели рекомендуют применять его в manage.py

    #!/usr/bin/env python
    from django.core.management import setup_environ, ManagementUtility
    import imp
    try:
        imp.find_module('settings') # Assumed to be in the same directory.
    except ImportError:
        import sys
        sys.stderr.write(
            "Error: Can't find the file 'settings.py' in the directory "
            "containing %r. It appears you've customized things.\nYou'll have to "
            "run django-admin.py, passing it your settings module.\n" % __file__
            )
        sys.exit(1)
    
    import settings
    
    if __name__ == "__main__":
        setup_environ(settings)
        import primate
        primate.patch()
        ManagementUtility().execute()
    

    теперь остаётся только указать в settings класс модели, который мы хотим использовать

    AUTH_USER_MODEL = 'users.models.User'

    … и можно начинать придумывать модели:

    # users.models
    from django.db import models
    from primate.models import UserBase, UserMeta
    
    class User(UserBase):
        __metaclass__ = UserMeta
        birthday = models.DateField()
        # На что фантазии хватит?
    


    Теперь каждый раз, когда какой-либо компонент проекта будет обращаться к django.contrib.auth.models.User, он будет получать модель users.models.User. Обратное тоже верно. Админка пропатчится автоматом, никаких специальных действий для её подключения производить не надо.

    По умолчанию, модель пользователя django-primate имеет следующие отличия от auth.User:
    • Убраны поля first_name и last_name, зато добавлено name;
    • Максимальная длина поля username составляет 50 символов (в auth.User было 30)
    • Полю email добавлен уникальный индекс
    • Метод get_profile возвращает self, так что можно не опасаться за код, использующий user.get_profile()

    В остальном же модель пользователя primate гуляет как утка, плавает как утка и крякает как утка ведёт себя очень похоже на джанговский первоисточник.

    Разумеется, можно использовать абсолютно любой набор полей, но в этом случае есть риск, что возникнет несовместимость между сторонними приложениями, так что поля username, password и email лучше не переименовывать.

    Ещё один момент: для совместимости с джангой, primate пропачит модель так, что её app_label станет не users (как в примере), а auth. Это особо касается пользователей South, которые могут некоторое время не понимать, почему

    ./manage.py schemamigration users --auto

    не создаёт никаких миграций.

    Впрочем, все желающие могут ознакомиться с README, который я оочень вольно перевёл. Если я что-то упустил, ошибся или допустил неточность, пишите в комментариях или делитесь своими соображениями по поводу.

    Спасибо за внимание :)
    Share post

    Similar posts

    Comments 45

      0
      Создание модели User не означает, что автоматом создастся связанная модель Profile. Зато при обращении к get_profile() для вновь созданного юзера вылетит исключение — вот тут сомневаться не приходится.
      А я вот сомневался :-[

      Спасибо, годная штука судя по описанию.
      • UFO just landed and posted this here
          +2
          Верно для request.user, но не работает для десятка comment.user.get_profile() в списке комментов.
            0
            Да, профиль кешируется в инстансе User, но есть одно «но»: инстансыэти, хоть и одинаковые, являются лишь друг друга :)

            >>> posts = Post.objects.filter(user__pk=2).order_by('?')[:2]
            >>> assert posts[0] != posts[1]
            >>> assert posts[0].user == posts[1].user
            >>> assert posts[0].user is posts[1].user
            Traceback (most recent call last):
              File "<console>", line 1, in <module>
            AssertionError


            Как видите, инстансы разные, соотвественно, кеш не сработает
          • UFO just landed and posted this here
              0
              Можно, только осторожно.

              Если Вы знаете, как работают все приложения, которые используете у себя и уверены, что ни одно из них не обратится к User.username, называйте его, как хотите, хоть imya_polzovatelya. Или вообще убирайте, пусть будет аутентификация по имейлу. Единственное только, придётся собственные auth-быкенды писать.

              Но как по мне, дефолтный набор полей у Примата довольно универсален. Гораздо более интересна возможность создавать свои поля :)
              +19
              Профайлы через наследование и манки-патчинг — жуть, конечно. Но через OneToOne вот меня профайлы не парили никогда.

              а) у человека ведь часто много профайлов, от разных приложений; сложность и дополнительные зависимости как раз создадутся, если все приложения должны будут использовать 1 модель — какие-то миграции делать туда-сюда при подключении/отключении приложений, например;
              б) чтоб создавать все автоматом — есть AutoOneToOne;
              в) насчет select_related — неправда, он работает для OneToOne в обе стороны уже года полтора как.

              AUTH_PROFILE_MODULE = 'accounts.Profile' — штука необязательная и чуть ли не вредная, как и метод get_profile. Иногда удобно, но обычно просто путает. Проще явно user.profile написать уж, если нужно.

              А с django-primate все как-то непонятно становится сразу, больно уж на хак смахивает, особенно убирание полей first_name и last_name, на которые вообще-то сторонний код завязан может быть, и манки-патчинг админки. Это, по сути, метод с манки-патчингом модели, доведенный до логического завершения. Это должно быть удобнее OneToOne при написании формы редактирования профайла, но в остальном преимуществ не вижу, с осторожностью лучше к таким штукам относиться.

              Автор sorl-thumbnails вообще странный чувак бывает иногда — убрал как-то старый sorl-thumbnails из репозитория на гуглокоде и с pypi, а на pypi выложил переписанную версию (с ненужными в большинстве случаев наворотами), которая обратно со старой не совместима. Но это так, лирика)
                0
                Относительно проблематики monkey patching-а — мне тоже кажется, что это очень непрозрачно. Пришел в проект новый разработчик, смотрит на contrib.auth.models.User, а у него first_name нету. Мне бы не пришло в голову искать причину этому в manage.py.
                  0
                  Такие штуки принято документировать и писать тесты, которые враз скажут, что откуда и куда.
                  0
                  Я сделал допущение, что профиль только один. Если профилей по каким-то причинам должно быть несколько, то да, конечно, правильнее их сделать отдельно от самого пользователя. Но у меня таких случаев пока не бывало.

                  Поле AutoOneToOne штука хорошая. Но нестандартная :) Обратно, всякие get_profile и AUTH_PROFILE_MODEL предусмотрены создателями джанги и даже вроде как ими рекомендуются. Наверное, поэтому многие, в особенности новички, предпочитают идти именно вторым путём: он описан в мануале и ничего не надо ставить.

                  Примат действительно является манки-патчингом, если так можно выразиться, эволюционировавшим до человека :) Но всё-таки это магия, о чём я и написал. Использовать её надо с осторожностью. Но тем, кто пишет тесты, особо бояться нечего.
                    0
                    Кстати, насчёт тестов, совсем забыл: с приматом некоторые родные джанговские тесты на contrib.auth поломаются. Например, на неуникальность email. Надо об этом помнить, если кто решится исопльзовать.
                    +1
                    AUTH_PROFILE_MODULE получается в таком контексте вообще бессмысленна. Такое связывание, не избавит от одного лишнего запроса, от «ручного» кодирования не избавляет.
                      +1
                      Так и есть, штука бессмысленная. На нее в рассылке уже ругались, но до того, чтоб объявить deprecated или документацию по-другому написать, руки так и не дошли ни у кого.
                      0
                      > в) насчет select_related — неправда, он работает для OneToOne в обе стороны уже года полтора как.
                      А как он работает в обратную сторону? Действительно, интересно. Буквально на днях я поэкспериментировал: если передавать список объектов с FK-полем в шаблон и выводить их и объекты, на которые ссылается FK-поле, я получал 3 запроса к БД. Если же передавать список объектов, а на них ссылающиеся с FK-полем доставать через Instance.entry_set.all, то запросов к БД получается много.
                      • UFO just landed and posted this here
                          0
                          Если я правильно понимаю, в обратную сторону оно работает только с полями OneToOneFields
                          • UFO just landed and posted this here
                      0
                      Eже успел написать свою модельку профайла (наследование), но под обезьяну думаю переделаю, выглядит удобнее. Спасибо за примата, обязательно попробую.
                        0
                        Пожалуйста :)

                        Но имейте в виду, что этот пост — не джинса django-primate. С магией надо быть очень осторожным. Если не пишете тесты, будет хорошей идеей начать это делать.
                        +2
                        очевидно, что django.contrib.auth писали какие-то панки. но так делать тоже нельзя — ломающиеся джанговские тесты являются прямым тому доказательством.
                        модель пользователя django-primate имеет следующие отличия от auth.User… В остальном же модель пользователя primate гуляет как утка, плавает как утка и крякает как утка ведёт себя очень похоже на джанговский первоисточник.

                          0
                          С одной стороны, неплохо было бы в примате пропатчить и тесты джанги, чтобы они хотя бы не ломались. Но тут вопрос скользкий: примат не накладывает ограничений на схему новой модели пользователей, поэтому тесты, написанные для дефолтной приматовской модели всё равно оказались бы бесполезными, если в проекте модель пользователя будет изменена.

                          Думаю, лучше полагаться на разработчика проекта: пусть сам пишет тесты согласно своим требованиям. Или не пишет, но тогда он сам себе злобный Буратино.
                            +1
                            С одной стороны, неплохо было бы в примате пропатчить и тесты джанги, чтобы они хотя бы не ломались.
                            патчить тесты не нужно ни в коем случае, ведь тесты отражают принятые соглашения. если новая реализация ломает тесты, это означает лишь одно — реализация написана с ошибками (не важно: намеренными или нет).
                              0
                              Давайте назовём это чуточку политкорректнее: дефолтный функционал реализации отличается от исходного :) Сами же согласились, что django.contrib.auth наркоманский. Патч в общем случае делает его гибче. Никто ж, в конце концов, не мешает использовать модель User без уникального индекса на email и с полями first_name и last_name. Тогда и тесты не сломаются.
                          +2
                          Я раньше тоже извращался и с манкипатчингом и с наследованием. Сейчас мне всё это кажется созданием проблем на ровном месте. Я просто создаю дополнительную модель с нужными полями и связываю её через OneToOneField, даже без префикса Auto, просто создаю по сигналу эту модель. Считаю это самым простым и прозрачным способом.
                            0
                            А я вот как-то наоборот постарался уйти и от ненужных сигналов, и от того, что часть информации хранится в User, а часть — User.profile. Как по мне, это чертовски неудобно, по крайней мере, если у пользователя может быть только один профиль.

                            Видимо, каждому своё :)
                            +1
                            Чем вам не угодил подход с отдельной моделью профиля? Не понимаю, почему вдруг один из ключевых принципов теории реляционых баз данных стал костылем? УМВР, не было проблем с профилями пользователей.

                            По вашим пунктам:

                            > сложность поддержки

                            Запутаться??? Логин/пароль и допустим сексуальная ориентация пользователя — это же совершенно разные данные, умоляю, почему они все должны храниться в одном объекте? К тому же, что делать, если у меня вообще три разных типа профилей для пользователей, и в каждом свои данные?

                            > нецелевая растрата ресурсов

                            А не будет ли растратой ресурсов то, что я каждый раз подгружаю совершенно не нужные мне данные чтобы получить, допустим, список пользователей? (конечно даже если не указывать конкретные поля в запросе, django может быть оптимизирует всё что нужно, и лишние данные не будут передаваться, но всё равно семантика нарушается, и это всё уже зависит от того как напишешь)

                            > а ещё всё надо руками делать!

                            Ну вообще это нормально, не нужен мне профиль для какого-то пользователя — не буду создавать.

                            В общем не убедили вы меня в необходимости использования столь жесткого манкипатчинга в решении данной проблемы, но за статью про эту библиотеку всё равно спасибо — больше костылей, хороших и разных!
                              0
                              Чем мне не угодил подход с отдельной моделью я уже написал: две сущности там, где нужна одна (если нужен один тип профиля. Для разных типов же такой способ считаю наиболее верным).

                              Логин/пароль и допустим сексуальная ориентация пользователя — это же совершенно разные данные, умоляю, почему они все должны храниться в одном объекте?
                              Окей. А имя и фамилия где должны храниться — там же, где пароль, или рядом с сексуальной ориентацией? :-)

                              А не будет ли растратой ресурсов то, что я каждый раз подгружаю совершенно не нужные мне данные чтобы получить, допустим, список пользователей?
                              У меня для Вас плохие новости: джанговский ORM загружает модели полностью. Даже если Вы обращаетесь к username пользователя, сервер БД передаст и фамилию, и имя, и пароль (захешированный, конечно) и пресловутую сексуальную ориентацию.

                                0
                                А имя и фамилия где должны храниться — там же, где пароль, или рядом с сексуальной ориентацией? :-)

                                0
                                У меня для Вас плохие новости: джанговский ORM загружает модели полностью. Даже если Вы обращаетесь к username пользователя, сервер БД передаст и фамилию, и имя, и пароль (захешированный, конечно) и пресловутую сексуальную ориентацию.


                                Ну вот же! А ведь если разбить эти данные на несколько моделей, то мы получим возможность не передавать лишние. Лучше 20 мелких запросов, чем 10 но в 3 раза больших.
                                  0
                                  Я не очень понимаю, зачем плодить число запросов там, где можно этого не делать. Основное время занимает выполнение запроса, а не передача данных, разве нет? Или Вы хотите внутрисетевой (обычно) трафик экономить?
                                    0
                                    зачем плодить число запросов там, где можно этого не делать

                                    Это замечание справедливо тогда, когда вы пытаетесь выполнить необходимую работу скриптом вместо того чтобы предоставить её базе данных. Здесь же, нагрузка от обработки лишнего объема данных (в данной ситуации 1/3 объема — лишние, а на практике обычно в таких ситуациях всё гораздо хуже), превзойдет нагрузку от обработки большего числа запросов. Хотя это конечно зависит от базы данных, какой-нибудь MySQL с каким-нибудь движком с кривыми транзациями может вполне и загнуться от кучи мелких запросов… Но опять же, эти 20 запросов прекрасно превращаются в 10, с тем же объемом данных, если грамотно подойти к делу. А чтобы вытащить тот же объем из лишнего базе придется проделать бОльшую работу. Как уже говорили здесь, нормализация рулит.

                                    А трафик в большистве случаев вообще локальный. И действительно, в большинстве случаев может не учитываться… Но это в большинстве. А меньшинство составляют Highload-проекты, где снижение трафика на сервере баз данных на 1/3 это существенный профит, ради которого разработчики не то что модели перелопатить, но и новую СУБД могут написать.
                                      0
                                      Здесь же, нагрузка от обработки лишнего объема данных… превзойдет нагрузку от обработки большего числа запросов.
                                      Откуда такие выводы? Измерения проводились?

                                      Но опять же, эти 20 запросов прекрасно превращаются в 10, с тем же объемом данных, если грамотно подойти к делу. А чтобы вытащить тот же объем из лишнего базе придется проделать бОльшую работу. Как уже говорили здесь, нормализация рулит.
                                      Не поспеваю за Вашей мыслью. Что значит «грамотно подойти к делу»? И при чём тут нормализация?
                                  0
                                  две сущности там, где нужна одна


                                  Нет, здесь нужны две — auth.User начинается на auth, что как бы намекает. А myapp.Profile начинается на myapp. Не нужно смешивать независимые друг от друга данные без необходимости.
                                    0
                                    Не нужно смешивать независимые друг от друга данные без необходимости.
                                    Они первые придумали засунуть необязательные поля first_name и last_name в модель, которая по идее должна хранить только аутентификационную информацию.
                                      0
                                      first_name и last_name нужны непосредственно для аутентификации. По крайней мере, хотя бы для того чтобы вывести «Здравствуйте, {{ first_name }} {{ last_name }}, вы успешно аутентифицировались». Это данные, идентифицирующие пользователя. Ну и да, куда их засунуть, если не сюда?
                                        0
                                        «Здравствуйте, {{ first_name }} {{ last_name }}, вы успешно аутентифицировались. А ваша сексуальная ориентация — {{ orientation }}». Не?
                                          0
                                          Не, orientation будет в другом шаблоне, где-нибудь справа в отдельном блоке, не зависимо от аутентификации.
                                            +1
                                            Это было [irony][/irony]. На мой взгляд, имя и фамилия к аутентификации относятся ничуть не больше той же пресловутой ориентации. И неважно где её выводить
                                            0
                                            И почему бы не дать профиль пользователю, у которого ещё нет своего auth.User?
                                              0
                                              Например, потому что его, пользователя, просто не существует? =)
                                    0
                                    Чёрт, не дописал самое главное.

                                    Я ни разу не убеждаю использовать primate. Это магия и с ней надо быть осторожным. Я бы, пожалуй, рекомендовал прислушаться к совету   kmike и наследовался модели, хоть это тоже не лишено некоторых рутинных дейтсвий, от которых избавляет примат. Или не позволяет выкинуть нежелательные поля.

                                    С другой стороны, дополнительная с прикреплённым к и без того не аскетичному User у меня вызывает неприязнь. Ещё раз повторюсь, что речь идёт о случаях, когда нужен всего один тип профиля. У меня других не было, если что.
                                      0
                                      kmike тоже советует использовать отдельную модель для профиля…

                                      Не важно один тип профиля или несколько. Важна производительность и удобство работы. Если в какой-то ситуации действительно удобно обращаться ко всем данным через одну модель и это не вызывает существенного спада производительности — можно использовать primate, как гарантию того, что вы не наткнетесь через манкипатчинг на какие-то грабли самостоятельно.

                                      Я бы как раз позиционировал эту библиотеку как попытку избавить молодых джангистов от желания самостоятельно лезть в чужие структуры данных без особого на то повода.
                                        0
                                        Если я правильно понял kmike, он советует использовать отдельную модель для каждого профиля, в случае когда их, профилей, несколько. Я же говорю про случае с единственным профилем, бо у меня просто по-другому не было.

                                        Я считаю, что удобство — это когда есть одна форма и одна модель. И когда не нужно при редактировании Profile во вьюшке обновлять связанный с Profile объект User, получая из form.cleaned_data значения, которые нужно охранить у пользователя. Как-то так :)
                                    0
                                    А почему не освещен еще один вариант предлагаемый джангой — использование прокси класса:

                                    class ExtUser(User):
                                    '''
                                    Расширение стандартного класса пользователей
                                    '''

                                    class Meta:
                                    proxy = True


                                    Внутри можно переопределять любой из метода, но поля не добавить, это только через профайл.
                                    Пруфлинк: docs.djangoproject.com/en/1.3/topics/db/models/#proxy-models

                                    Only users with full accounts can post comments. Log in, please.