Борьба с концепцией ООП
Я действительно борюсь с повторяющейся концепцией ООП/базы данных.
Пожалуйста, позвольте мне объяснить проблему с псевдо-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;
}
}
}
... но тогда, конечно, если я создаю класс "community", в его конструкторе мне нужно будет присоединиться к таблице communities
с таблицей communities_groups
с таблицей groups
с помощью groups_users
с таблицей users
.
... и если я создаю класс "city", в его конструкторе мне нужно будет присоединиться к таблице 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
s, но это было глупо, и этот пример намного яснее.)
РЕШЕНИЕ:
В итоге я нашел PHP ORM, который делает именно это. Это Laravel Eloquent. Вы можете указать отношения между вашими моделями и интеллектуально строить оптимизированные запросы для активной загрузки с использованием синтаксиса следующим образом:
Group::with('users')->get();
Это абсолютная спасательная жизнь. Мне не пришлось писать ни одного запроса. Он также не работает с использованием соединений, он интеллектуально компилирует и выбирает на основе внешних ключей.
Ответы
Ответ 1
ActiveRecord в Rails реализует концепцию ленивой загрузки, то есть откладывает запросы к базе данных, пока вам не понадобятся данные. Поэтому, если вы создаете экземпляр объекта my_car = Car.find(12)
, он запрашивает только таблицу автомобилей для этой одной строки. Если позже вы хотите my_car.wheels
, тогда он запрашивает таблицу колес.
Мое предложение для вашего псевдо-кода выше - не загружать каждый связанный объект в конструкторе. Конструктор автомобиля должен запрашивать только автомобиль и должен иметь способ запросить все его колеса, а другой - запросить у него дилерскую сеть, которая запрашивает только дилерский центр и отбирает все другие автосалоны, пока вы конкретно не скажете что-нибудь например my_car.dealership.cars
Postscript
ORM - это уровни абстракции базы данных, и поэтому они должны быть настроены для удобства запросов, а не для точной настройки. Они позволяют быстро создавать запросы. Если позже вы решите, что вам нужно точно настроить свои запросы, вы можете переключиться на выпуск сырых SQL-команд или попытаться иначе оптимизировать, сколько объектов вы извлекаете. Это стандартная практика в Rails, когда вы начинаете выполнять настройку производительности - ищите запросы, которые были бы более эффективными при выпуске с сырым sql, а также искать способы избежать нетерпеливой загрузки (напротив ленивой загрузки) объектов, прежде чем они вам понадобятся.
Ответ 2
Скажем, у вас есть класс "колесо", который загружает свои данные из таблицы колес в свой конструктор
Конструкторы не должны выполнять какую-либо работу. Вместо этого они должны содержать только задания. В противном случае вам очень сложно проверить поведение экземпляра.
Теперь у нас есть класс "автомобиль", который загружает свои данные из таблицы автомобилей, соединенных с таблицей cars_wheels, и создает объекты колес из возвращенных wheel_ids:
Нет. Есть две проблемы с этим.
Ваш класс Car
не должен содержать оба кода для реализации "логики автомобиля" и "логики устойчивости". В противном случае вы нарушаете SRP. И колеса зависят от класса, что означает, что колеса должны быть введены в качестве параметра для конструктора (скорее всего, в виде набора колес или, может быть, массива).
Вместо этого вы должны иметь класс mapper, который может извлекать данные из базы данных и хранить их в экземпляре WheelCollection
. И картограф для автомобиля, который будет хранить данные в экземпляре Car
.
$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?
Нет, уродство происходит из-за факта, что активная запись используется только для простейшего из случаев использования (где почти нет логики, прославленные объекты ценности с сохранением). Для любой нетривиальной ситуации предпочтительнее применить шаблон dataartper.
.. и если я создам класс "город", в его конструкторе мне нужно будет присоединиться к таблице городов с таблицей 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сильная > книга. Или, по крайней мере, начните просмотр лекций, перечисленных здесь.
my 2 cents
Ответ 3
В общем, я бы рекомендовал иметь конструктор, который эффективно берет строку запроса или часть более крупного запроса. Как это будет зависеть от вашего ORM. Таким образом, вы можете получить эффективные запросы, но после этого вы сможете построить другие объекты модели.
Некоторые ORM (модели django, и я считаю, что некоторые из Ruby ORM) стараются понять, как они строят запросы и могут автоматизировать это для вас. Хитрость заключается в том, чтобы выяснить, когда потребуется автоматизация. У меня нет личного знакомства с PHP ORM.