Comments 13
С одной стороны выглядит как "сахар" вокруг одной таблицы, с другой возникает мысль как уследить за навозможностью сделать insert одной сущности, когда в таблице есть not null поле и оно существует в другой сущности.
Ведь Виктор Протагонистов мог рассмотреть вариант группировки данных о пользователе по другим таблицам и связывание их через идентификатор с основной таблицей. Тогда бы сущность User, не росла бы так быстро, дополнительные данные можно было добавлять в связанные сущности(таблицы) и получение нужных данных возложить на необходимый QueryBuilder.
ИМХО, решение простое.
У нас есть реальные таблицы, и есть их вьюшки.
Если нам нужен сабсет данных - создаем вьюшку. Если нам нужен join для отображения данных - создаем вьюшку.
Таким же образом можно и права нарезать, если в одну базу ходят несколько сервисов.
Да, это хорошее замечание.
Если данные действительно имеют разный жизненный цикл, разные правила владения и независимо развиваются, то вынесение их в отдельные таблицы и связи по user_id – лучшее решение.
Просто бывают случаи, когда на уровне БД одна таблица уже выбрана осознанно и это нормально, но на уровне кода одна большая User-entity начинает становиться слишком жирной и неудобной для разных модулей.
С insert и NOT NULL ещё интереснее: предусмотрен механизм "валидирования" такой ситуации, чтобы заранее отловить проблему. Но тут как бы от базы не уйдешь – либо нужно эту колонку писать во всех сценариях, либо она всё-таки необязательная :) Ну либо можно сделать read-only модель под сценарий, когда запись не предполагается.
Таки кто-то вспомнил, что значит I в SOLID?
CREATETABLEusers ( idINTUNSIGNEDAUTO_INCREMENT PRIMARY KEY, emailVARCHAR(255)NOTNULL, passwordVARCHAR(255) DEFAULT NULL, stripe_customer_idVARCHAR(255) DEFAULT NULL)
Момент быть для начала выкинем ОРМ и начнем проектировать нормальные - нормализированные БД?
У вас в таблице - stripe_cudtomer_id - уникальный аттрибут, который к user не относится. И должен быть вынесен в отдельную таблицу.
Далее вы предполагаете что у пользователя один email- который not null, и password, который null.
Т.е. вы можете создать пользователя без пароля, а как пароль задавать?
Далее при регистрации пользователь задал email: vasja@pupkin.com,
Вы добавили пользователя в таблицу, а как будем проверять email? Вдруг это спамер который пытается зарегистрироваться со всеми возможными email? Соответственно вам надо слать запрос на подтверждение email и отслеживать его статус.
Далее Вася Пупкин 100 лет назад зарегистривал домен pupkin.org и email: admin@pupkin.org, с которым зарегистрировался в вашей системе.
Потом он забыл пролить домен и домен приобрел Петя Пупкин - его однофамилиц.
И
Недолго думая Петя пытается зарегистрироваться у вас на сервере с email: admin@pupkin.org.
Ваш сервис…
Высылает ему подтверждение на изменение забытого пароля, и Петя получает доступ к данным Васи.
Вася в восторге …
Все бы это не случилось если бы БД была нормально спроектирована а не код-ферст и т.д. И т.п.
А какие варианты проверки, что пароль сбрасывает Вася, а не Петя?
Спасибо за подробный комментарий.
Конечно, правильное проектирование это... правильно :) Впрочем, иногда денормализация БД – тоже осознанно выбранный подход. В примерах представлено что-то среднее.
Попробую подсветить пару моментов:
Если вынести
stripe_customer_idв отдельную таблицу, основнойUserвсё равно как бы будет знать о биллинге:$user->getStripeCustomer();Хотя в большинстве сценариев работы с пользователем этот релейшен вообще не нужен.
nullable
passwordв этих историях (надеюсь) не означает "любой пользователь может существовать без пароля". Это лишь ещё один пример, когда общепринятый подход может быть изменён. Например, поддерживается несколько типов пользователей с разными конфигурациями (аля системный пользователь или пользователь, аутентифицирующийся через внешний провайдер). В пользователя без пароля я бы вообще логиниться не давал. Но опять же, это детали логики где-то в коде.
SELECT id, email, password FROM users WHERE id = 1 LIMIT 1SELECT id, email, stripe_customer_id FROM users WHERE id = 1 LIMIT 1UPDATE users SET email = 'new-email@example.test', stripe_customer_id = 4 WHERE id = 1
Не стоит так делать, это мина замедленного действия. Нет гарантии, что данные не изменились между выполнением SELECT и UPDATE.
На этом примере это сложно продемонстрировать, но представьте, что в коде есть правило, которое запрещает устанавливать stripe_customer_id > 100 для пользователей с коротким паролем.
Согласен.
По-прежнему остаётся важным момент, где проходит граница инварианта. Если есть правило, которое одновременно зависит и от password, и от stripe_customer_id, то это уже не два независимых среза данных. Значит, для такого сценария должно быть представление, где видны оба поля, и изменение должно идти через него.
Если поверх этого есть общий optimistic lock на строку, то проблема как раз решается штатно: проверяем инвариант на актуальном состоянии и не даём молча записать устаревшее изменение.
У меня пока в планах добавить optimistic lock к функционалу либы, а также может быть пересмотреть реализованный pessimistic вариант.
И получаем TableModule. Может стоит книги почитать старые? Прежде чем в обнимку с ИИ приносить "новые" подходы?
Если вы имеете в виду Table Module из Фаулера, то это всё-таки другой паттерн.
Там через один объект работаем со всей таблицей целиком, у меня же идея почти обратная: разрешить несколько независимых представлений одной и той же строки таблицы.
Если вы видите более близкий паттерн или пример реализации именно такого отображения — будет интересно посмотреть.
Что будет, если убрать из ORM предположение «одна таблица — одна entity»?