Я действительно борюсь с повторяющейся концепцией ООП / базы данных.
Пожалуйста, позвольте мне объяснить проблему с псевдо-PHP-кодом.
Скажем, у вас есть «пользовательский» класс, который загружает свои данные из таблицы users
в свой конструктор:
class User { public $name; public $height; public function __construct($user_id) { $result = Query the database where the `users` table has `user_id` of $user_id $this->name= $result['name']; $this->height = $result['height']; } }
Простой, удивительный.
Теперь у нас есть класс «group», который загружает свои данные из таблицы groups
соединенной с таблицей groups_users
и создает объекты user
из возвращаемого user_id
s:
class Group { public $type; public $schedule; public $users; public function __construct($group_id) { $result = Query the `groups` table, joining the `groups_users` table, where `group_id` = $group_id $this->type = $result['type']; $this->schedule = $result['schedule']; foreach ($result['user_ids'] as $user_id) { // Make the user objects $users[] = new User($user_id); } } }
Группа может иметь любое количество пользователей.
Красивая, элегантная, удивительная … на бумаге. В действительности, однако, создание нового объекта группы …
$group = new Group(21); // Get the 21st group, which happens to have 4 users
… выполняет 5 запросов вместо 1. (1 для группы и 1 для каждого пользователя.) И что еще хуже, если я создаю класс community
, в котором есть много групп, в каждом из которых есть много пользователей внутри них, нечестивое число запросы запущены!
Решение, которое не подходит мне
В течение многих лет способ, которым я обходился, состоит в том, чтобы не кодировать вышеописанным способом, но вместо этого, например, при создании group
, я бы также присоединился к таблице groups
таблице groups_users
таблице users
и создал массив массивов пользовательских объектов в объекте group
(никогда не применяя / не затрагивая класс user
):
class Group { public $type; public $schedule; public $users; public function __construct($group_id) { $result = Query the `groups` table, joining the `groups_users` table, **and also joining the `users` table,** where `group_id` = $group_id $this->type = $result['type']; $this->schedule = $result['schedule']; foreach ($result['users'] as $user) { // Make user arrays $users[] = array_of_user_data_crafted_from_the_query_result; } } }
… но тогда, конечно, если я groups_users
класс «community», в его конструкторе мне нужно будет присоединиться к таблице communities
с помощью таблицы communities_groups
таблицей groups
таблицей groups_users
с таблицей users
.
… и если я cities_communities
класс «город», в его конструкторе мне нужно будет присоединиться к таблице cities
таблицей cities_communities
таблицей communities
таблицей communities_groups
таблицей groups
таблицей groups_users
с таблицей users
.
Какая непревзойденная катастрофа!
Должен ли я выбирать между красивым кодом ООП с миллионом запросов VS. 1 запрос и запись этих объединений вручную для каждого отдельного супермножества? Разве нет системы, которая автоматизирует это?
Я использую CodeIgniter и смотрю на бесчисленные другие MVC и проекты, которые были построены в них, и не может найти ни одного хорошего примера для тех, кто использует модели, не прибегая к одному из двух ошибочных методов, которые я изложил.
Похоже, это никогда не делалось раньше.
Один из моих коллег пишет структуру, которая делает именно это – вы создаете класс, который включает в себя модель ваших данных. Другие, более высокие модели могут включать эту единственную модель, и она создает и автоматизирует объединения таблиц для создания более высокой модели, которая включает в себя объектные экземпляры нижней модели, все в одном запросе . Он утверждает, что никогда раньше не видел рамки или системы для этого.
Обратите внимание: я действительно всегда использую отдельные классы для логики и настойчивости. (VO и DAO – это весь пункт MVC). Для простоты я просто объединил эти два эксперимента в этой мысли-эксперименте вне архитектуры MVC. Будьте уверены, что эта проблема сохраняется независимо от разделения логики и настойчивости. Я считаю, что эта статья , представленная мне Джеймсом в комментариях к этому вопросу, кажется, указывает на то, что мое предлагаемое решение (которое я слежу в течение многих лет) фактически является тем, что сейчас делают разработчики для решения этой проблемы. Этот вопрос, однако, пытается найти способы автоматизации этого точного решения, поэтому его не всегда нужно кодировать вручную для каждого надмножества. Из того, что я вижу, это никогда не делалось в PHP раньше, и моя структура моего коллектива будет первой, если кто-то не сможет указать мне на то, что делает.
И, конечно же, я никогда не загружаю данные в конструкторы, и я вызываю только методы load (), которые создаю, когда мне действительно нужны данные. Тем не менее, это не связано с этой проблемой, как в этом мысленном эксперименте (и в реальных ситуациях, когда мне нужно автоматизировать это), мне всегда нужно с нетерпением загружать данные всех подмножеств детей так же далеко, как и он идет, а не лениво загружает их в какой-то будущий момент времени по мере необходимости. Мысленный эксперимент является кратким – что он не соответствует лучшим методам – спорный вопрос, и ответы, которые пытаются решить его макет, также не имеют смысла.
EDIT: для ясности схема базы данных.
CREATE TABLE `groups` ( `group_id` int(11) NOT NULL, <-- Auto increment `make` varchar(20) NOT NULL, `model` varchar(20) NOT NULL ) CREATE TABLE `groups_users` ( <-- Relational table (many users to one group) `group_id` int(11) NOT NULL, `user_id` int(11) NOT NULL ) CREATE TABLE `users` ( `user_id` int(11) NOT NULL, <-- Auto increment `name` varchar(20) NOT NULL, `height` int(11) NOT NULL, )
(Также обратите внимание, что я изначально использовал концепции wheel
дисков и car
, но это было глупо, и этот пример намного яснее).
РЕШЕНИЕ:
Я закончил поиск PHP ORM, который делает именно это. Это Ярость Ларавеля . Вы можете указать отношения между вашими моделями и интеллектуально строить оптимизированные запросы для активной загрузки с использованием синтаксиса следующим образом:
Group::with('users')->get();
Это абсолютная спасательная жизнь. Мне не пришлось писать ни одного запроса. Он также не работает с использованием соединений, он интеллектуально компилирует и выбирает на основе внешних ключей.
ActiveRecord в Rails реализует концепцию ленивой загрузки, то есть откладывает запросы к базе данных, пока вам не понадобятся данные. Поэтому, если вы создаете экземпляр my_car = Car.find(12)
, он запрашивает только таблицу автомобилей для этой одной строки. Если позже вы хотите my_car.wheels
тогда он запрашивает таблицу колес.
Мое предложение для вашего псевдо-кода выше – не загружать каждый связанный объект в конструкторе. Конструктор автомобиля должен запрашивать только автомобиль и должен иметь способ запросить все его колеса, а другой – запрашивать у него дилерский центр, который запрашивает только дилерский центр и отказывается от сбора всех других автомобилей дилера, пока вы специально не скажете что-нибудь как my_car.dealership.cars
постскриптум
ORM – это уровни абстракции базы данных, и поэтому их необходимо настроить для удобства запросов, а не для точной настройки. Они позволяют быстро создавать запросы. Если позже вы решите, что вам нужно точно настроить свои запросы, вы можете переключиться на выпуск сырых SQL-команд или попытаться иначе оптимизировать, сколько объектов вы извлекаете. Это стандартная практика в Rails, когда вы начинаете выполнять настройку производительности – ищите запросы, которые были бы более эффективными при выпуске с сырым sql, а также искать способы избежать нетерпеливой загрузки (напротив ленивой загрузки) объектов, прежде чем они вам понадобятся.
Скажем, у вас есть класс «колесо», который загружает свои данные из таблицы колес в свой конструктор
Конструкторы не должны выполнять какую-либо работу. Вместо этого они должны содержать только задания. В противном случае вам очень сложно проверить поведение экземпляра.
Теперь у нас есть класс «автомобиль», который загружает свои данные из таблицы автомобилей, соединенных с таблицей cars_wheels, и создает объекты колес из возвращенных wheel_ids:
Нет. Есть две проблемы с этим.
Ваш класс Car
не должен содержать оба кода для реализации «логики автомобиля» и «логики устойчивости». В противном случае вы нарушаете SRP . И колеса зависят от класса, а это означает, что колеса должны быть введены в качестве параметра для конструктора (скорее всего, в виде набора колес или, может быть, массива).
Вместо этого у вас должен быть класс mapper, который может извлекать данные из базы данных и хранить их в экземпляре WheelCollection
. И картограф для автомобиля, который будет хранить данные в Автоприцепе.
$car = new Car; $car->setId( 42 ); $mapper = new CarMapper( $pdo ); if ( $mapper->fetch($car) ) //if there was a car in DB { $wheels = new WheelCollection; $otherMapper = new WheelMapper( $pdo ); $car->addWheels( $wheels ); $wheels->setType($car->getWheelType()); // I am not a mechanic. There is probably some name for describing // wheels that a car can use $otherMapper->fetch( $wheels ); }
Что-то вроде этого. В этом случае сборщик отвечает за выполнение запросов. И у вас может быть несколько источников для них, например: есть один картограф, который проверяет кеш, и только, если это не удается, вытащите данные из SQL.
Мне действительно нужно выбирать между красивым кодом ООП с миллионом запросов VS. 1 запрос и отвратительный код un-OOP?
Нет, уродство происходит из-за того, что активный шаблон записи предназначен только для простейшего из случаев использования (где почти нет связанных с логикой, прославленных ценностей-объектов с настойчивостью). Для любой нетривиальной ситуации предпочтительнее применять шаблон отображения данных .
.. и если я создам класс «город», в его конструкторе мне нужно будет присоединиться к таблице городов с таблицей city_dealerships с таблицей дилеров с таблицей dealerships_cars с таблицей автомобилей с таблицей cars_wheels с таблицей дисков.
Jut, потому что вам нужны данные о «доступных заботах о дилерстве в Москве», это не значит, что вам нужно создавать экземпляры Car
, и вам определенно не понравится колес. Различные части сайта будут иметь разную шкалу, на которой они работают.
Другое дело, что вы должны перестать думать о классах как абстракции таблиц. Не существует правила, в котором говорится: «У вас должно быть соотношение 1: 1 между классами и таблицами» .
Возьмите пример Car
снова. Если вы посмотрите на него, то отдельный класс Wheel
(или даже WheelSet
) просто глуп. Вместо этого вы должны просто иметь класс Car
который уже содержит все его части.
$car = new Car; $car->setId( 616 ); $mapper = new CarMapper( $cache ); $mapper->fetch( $car );
Картограф может легко извлекать данные не только из таблицы «Автомобили», но также из «Колеса» и «Двигателей» и других таблиц и заполнять объект $car
.
PS: также, если вы заботитесь о качестве кода, вы должны начать читать книгу PoEAA . Или, по крайней мере, начать просмотр лекций, перечисленных здесь .
мои 2 цента
В общем, я бы рекомендовал иметь конструктор, который эффективно берет строку запроса или часть более крупного запроса. Как это будет зависеть от вашего ORM. Таким образом, вы можете получать эффективные запросы, но после этого вы можете построить другие объекты модели.
Некоторые ORM (модели django, и я считаю, что некоторые из Ruby ORM) стараются понять, как они строят запросы и могут автоматизировать это для вас. Хитрость заключается в том, чтобы выяснить, когда потребуется автоматизация. У меня нет личного знакомства с PHP ORM.