Диагноз - ORM :-)

atv

Новичок
Диагноз - ORM :)

Как сказал fisher, ORM - это инфекция. Встречайте ещё одного больного :D , тобиш меня. После того разговора я ещё долго думал на тему ORM, и решил что рано ещё списывать её со счетов. И решил я так потому, что, как мне кажеться, смог привнести нечто новое в её реализацию.

У меня появилась пара идей по реализации, и я решил проверить их на практике. Что из этого получилось, смотрите ниже.

Итак, возможности. Прежде всего стандартные возможности ORM:
- Возможность указывать маппинг или использовать маппинг по умолчанию.
- Отложенная загрузка атрибутов.

Теперь возможности, ради которых затевалась эта работа.
- Как можно более простое и прозрачное использование. Точнее сформулировать пока не могу :) Подробности будут в примере.
- Отсутствие требования наследовать отображаемый класс от базового класса библиотеки.
- Навигация по свзязанным объектам и коллекциям через свойства оображаемого объекта. Вот такая пространная формулировка :) Пример всё прояснит.
- Повторное использование существующих отображаемых объектов. Опять же подробности в примере :)

Саму работу можно взять здесь. А теперь пример использования.
Код:
Структура БД.
                       _____________ 
 _____________        | orders      |
| sellers     |       |-------------|         _______________
|-------------|       | order_id    |        |  customers    |
| seller_id   |<------| seller_id   |        |---------------|
| seller_name |       | customer_id |------->| customer_id   |
|_____________|       | order_sum   |        | customer_name |
                      |_____________|        |_______________|
PHP:
<?php
require_once('Collection.php');
// определяем классы для отображения.
class Seller
{
    // указываем отображение свойств класса на поля таблицы.
    protected $map = array(
        'seller_id'     => 'id',
        'seller_name'   => 'name'
    );
}

class Customer
{
    protected $map = array(
        'customer_id'   => 'id',
        'customer_name' => 'name'
    );
}

class Order
{
    protected $map = array(
        'order_id'      => 'id',
        'seller_id'     => 'seller_id',
        'customer_id'   => 'customer_id',
        'order_sum'     => 'sum'
    );

    // указываем отображение свойств класса на связи таблиц.
    protected $relations = array(
        'sellers'       => 'seller',
        'customers'     => 'customer'
    );
}

$db = new PDODB();
$db->dsn = 'mysql:host=localhost;dbname=orm';
$db->username = 'root';
$db->password = 'db_pass';

// вся работа ведётся через объекты коллекции.
$sellers = new Collection($db, 'sellers', 'id', 'Seller');
$customers = new Collection($db, 'customers', 'id', 'Customer');
$orders = new Collection($db, 'orders', 'id', 'Order');

// устанавливаем связи между коллекциями
$sellers->setOne2ManyRelation($orders, 'seller_id');
$customers->setOne2ManyRelation($orders, 'customer_id');

// всё, ORM готова к работе.

// чтобы легче было разобраться в коде, я буду приводить для сравнения
// SQL запрос, которому соответсвует указанный код.
// Это не тот SQL запрос, который генерируется ORM.
/*
SELECT * FROM sellers;
*/
// чтобы получить весь список служащих (и тех у которых нет заказов),
// независимо от связи с orders, нужно указать объединение
$sellers->join = Collection::LEFT_JOIN;
foreach ($sellers as $seller) {
    print $seller->name;
}

/*
SELECT * FROM sellers JOIN orders USING(seller_id) WHERE order_sum > 50;
*/
// вот тут заключается особенность этой ORM, order_sum относиться к таблице
// orders, значит для неё и используем фильтр.
$orders->setFilter("sum > 50");

// так как выше по коду для $sellers было установлено объединение,
// то теперь его нужно убрать, так как оно уже не нужно.
$sellers->join = null;
// и снова перебираем список
foreach ($sellers as $seller) {
    print $seller->name;
}

/*
SELECT * FROM sellers JOIN orders USING(seller_id) JOIN customers USING(customer_id)
WHERE order_sum = 100 AND customer_name = 'Customer 1';
*/
$orders->setFilter("sum = 100");
$customers->setFilter("name = 'Customer 1'");
foreach ($sellers as $seller) {
    print $seller->name;
}

/*
SELECT * FROM orders JOIN sellers USING(seller_id) WHERE seller_name = 'Seller 1';
*/
$sellers->setFilter("name = 'Seller 1'");
// вариант первый
foreach ($orders as $order) {
    print $order->sum;
}

// вариант второй
$seller = $sellers->getIterator()->current();
foreach ($seller->orders as $order) {
    print $order->sum;
}
// это и есть навигация по связанным объектам.
// ещё пример навигации
$order = $orders->getIterator()->current();
print $order->seller->name;
print $order->customer->name;

// примеры создания изменения и удаления.
// создание... начинаем транзакцию
$db->beginTransaction();

$seller = $sellers->createItem();
$seller->id = mt_rand();
$seller->name = 'Seller 1';

$customer = $customers->createItem();
$customer->id = mt_rand();
$customer->name = 'Customer 1';

$order = $orders->createItem();
$order->id = mt_rand();
$order->sum = 75;
$order->seller_id = $seller->id;
$order->customer_id = $customer->id;

$db->commit();
// всё, объекты сохранены и готовы к дальнейшей работе
print $seller->orders->current()->sum;
print $order->seller->name;
print $order->customer->id;

// обратите внимание
$sellers->setFilter("name = 'Seller 1'");
$seller1 = $sellers->getIterator()->current();
// в приложении уже существует объект ссылающийся на данную запись в БД - это
// объект $seller, поэтому $seller1 будет содержать ссылку на объект $seller

// обновление...
$db->beginTransaction();
$seller1->name = 'New Seller Name';
$db->commit();

// обратите внимание что
print $seller->name // New Seller Name

// удаление...
$db->beginTransaction();
$sellers->removeItem($seller1);
$db->commit();
?>
Вот вроде бы и всё, может что-то осталось за кадром, так что задавайте вопросы.

Пожелания, подсказки, идеи и доработки также приветсвуются :)


Да, забыл написать недостатки.
- Для отображения данных на объект используется декоратор, поэтому проверки вида
PHP:
<?php $object instanceof Seller ?>
не пройдут.
- Некоторые функциональные возможности реализованы в ущерб производительности, в частности связывание коллекций происходит на стороне PHP. Данных о производительности и её снижении пока нет. (я даже пока не представляю что и как замерять :? )
- Может ещё какие есть, пока не помню :)
 

zerkms

TDD infected
Команда форума
а если в разных таблицах будут одинаковые имена полей? например id.

также не очень понятны па с джоином
// теперь незабыть отключить объединение для sellers, чтобы заработала связь с orders
$sellers->join = null;
это вот например

ps: имхо орм должна скрывать как раз все отношения и тд, чтобы работа с ними была как можно более прозрачна, а тут приходится устанавливать объединения + запоминать что, когда и как мы объединили.
 

atv

Новичок
а если в разных таблицах будут одинаковые имена полей? например id.
Не пробовал, но проблем быть не должно.

ps: имхо орм должна скрывать как раз все отношения и тд, чтобы работа с ними была как можно более прозрачна, а тут приходится устанавливать объединения + запоминать что, когда и как мы объединили.
Я не совсем удачно выразился. Имеется ввиду, что если у seller ещё нет ни одного заказа, то он не будет получен в итерации. Поэтому, если надо получить список всех служащих, то нужно установить LEFT_JOIN, а далее, по примеру, его нужно убрать, так как он уже не нужен.
 

zerkms

TDD infected
Команда форума
Не пробовал, но проблем быть не должно.
если учесть что строятся запросы вида:

SELECT * FROM sellers JOIN orders USING(seller_id) WHERE order_sum > 50;

тогда очевидно, что если в нескольких таблицах есть поле id, тогда из-за * возможно будет получить лишь одно значение, придётся делать алиасы и прописывать их явно

то нужно установить LEFT_JOIN, а далее, по примеру, его нужно убрать, так как он уже не нужен.
если отношение 1:N и при этом N может быть равным 0, то почему бы сразу не использовать LEFT ?
 

atv

Новичок
если учесть что строятся запросы вида:

SELECT * FROM sellers JOIN orders USING(seller_id) WHERE order_sum > 50;
Нет, запросы не такого вида. Запрос показан просто для сравнения, чтобы легче было понять что именно происходит в коде. Все запросы однотабличные. Так что проблем с одинаковым названием полей быть не должно.

если отношение 1:N и при этом N может быть равным 0, то почему бы сразу не использовать LEFT ?
Обрати внимание как многотабличные запросы из примера реализуются в коде, как там устанавливаются фильтры. Связь между коллекциями поддерживается на строне PHP, поэтому и нужно включать или выключать LEFT_JOIN.
 

zerkms

TDD infected
Команда форума
atv
хм.... т.е. другими словами для выборки объекта (ов) и коллекции связанных с ним(и) - делается 2 запроса
1. сам объект(ы)
2. выборка связанных с IN() ?

-~{}~ 29.03.07 20:30:

хотя нет... если объектов N, то для выборки этих объектов + связанных с ним - потребуется 1 + N запросов?
 

atv

Новичок
если объектов N, то для выборки этих объектов + связанных с ним - потребуется 1 + N запросов?
Не совсем. Для выборки объекта потребуется столько запросов, сколько коллекций учавствует в связях. Например, в предыдущем примере есть три взаимосвязанных коллекции, значит для выборки объекта из любой коллекции потребуется три запроса.

Однако, коллекции имеют, также, буфер. По умолчанию он равен 100. Если количество записей БД больше 100 (с учётом фильтра), то добавиться ещё один запрос на каждую сотню записей. Таким образом, с помощюь буфера можно варьировать производительностью и расходом памяти.
 

bkonst

.. хочется странного?...
Я немножко покритикую/поспрашиваю, можно? (Сам долгое время пользуюсь ORM и для меня интересны альтернативные варианты).

Сразу возникает вопрос: почему дублируется информация о связях?
PHP:
class Order
{
....
    // указываем отображение свойств класса на связи таблиц.
    protected $relations = array(
        'sellers'       => 'seller',
        'customers'     => 'customer'
    );
} 
...
// устанавливаем связи между коллекциями
$sellers->setOne2ManyRelation($orders, 'seller_id');
$customers->setOne2ManyRelation($orders, 'customer_id');
По-моему, при таком подходе при изменении структуры базы легко можно забыть внести правки в одном из двух мест. Почему бы не добавлять элементы в $relations внутри setOne2ManyRelation?

Далее, что будет, если параллельно надо работать с двумя различными подмножествами продавцов? Заводить для каждого из них свой набор объектов Collection и устанавливать связи?

Возможно ли будет фильтровать/отбирать не по непосредственно связанным таблицам? (например, как будет выглядеть выбор всех продавцов, работавших с данным покупателем?)

Как (можно ли) добавить в базу уже существующий объект (скажем, созданный на основании взятого из базы)?

Откуда берется первичный ключ? mt_rand() не обойтись - эта фанкция может (даже должна) выдавать неуникальные значения.
 

atv

Новичок
Я немножко покритикую/поспрашиваю, можно? (Сам долгое время пользуюсь ORM и для меня интересны альтернативные варианты).
Неужели я похож на невростеника, питающего иллюзии по поводу своего кода :D Конечно можно, для того я его и представил. :)

Почему бы не добавлять элементы в $relations внутри setOne2ManyRelation?
Ну это, в принципе, немного разные вещи. В $relations указывается маппинг отношений, чтобы потом можно было писать $order->seller, а не $order->sellers или $order->sellers_table. Точно также можно было бы добавить маппинг отношения и для классов Seller и Customer:
PHP:
class Seller
{
    protected $relations = array(
        'orders' => 'sellerOrders'
    )
}
и обращатся как $seller->sellerOrders. В примере для этих классов используется маппинг по умолчанию.

Вставлять весь этот функционал в setOne2ManyRelation() будет слишком неудобно.

что будет, если параллельно надо работать с двумя различными подмножествами продавцов?
После того как забрали итератор ко всем связанным колекциям можно применять новые настройки: setFilter(), join и buffer
PHP:
$iterator1 = $sellers->getIterator();

$sellers->setFilter('id = 5');
$orders->setFilter('sum > 100');

$iterator2 = $sellers->getIterator();
$iterator1 и $iterator2 содержат два различных подмножества, причём если они пересекаются, то содержат ссылки на объекты пересечения. Т.е. если объект seller с идентификатором 5 содержиться в $iterator1, то в $iterator2 будет содержаться не новая копия объекта, а ссылка.

Возможно ли будет фильтровать/отбирать не по непосредственно связанным таблицам? (например, как будет выглядеть выбор всех продавцов, работавших с данным покупателем?)
Да, связь многие-ко-многим именно так и реализуется.
PHP:
$customers->setFilter('name = "John"');

foreach ($sellers as $seller) {
    print $seller->name;
}
Как (можно ли) добавить в базу уже существующий объект (скажем, созданный на основании взятого из базы)?
Нет, такой функционал я не предусматривал. Надо будет подумать.

Откуда берется первичный ключ?
Пока что вручную. Я ещё не определился с наиболее подходящим вариантом, толи lastInsertId, толи sequence. Даже и не знаю :(
 

kostya.sys

Новичок
а пре,пост-фильтры
валидация?
это все должно быть в ОРМ...

мне кажется реализация ОРМ на языке не поддерживающем перегрузок методов упирается в кучу косяков и гвоздей типа
$sellers->setFilter('id = 5');
$sellers->getIterator();

после рельсовского стиля
sellers->find:)all, ['id=%d', id])
както убого

После нескольких месяцев работы с рельсами и возврата к пхп у меня тоже было оргромное желание сделать нечто подобное на пхп, но в итоге получалось чтото похожее
 

atv

Новичок
а пре,пост-фильтры
валидация?
это все должно быть в ОРМ...
Я не поддерживаю мнения, что это должно быть в ORM. Мне кажеться, что туда это запихнули, так как сложно было сделать по другому.

В варианте, который предлагаю я, есть возможность отображать данные на объекты из вашей собственной иерархии. У вас есть полный контроль над свойствами объекта. Например:
PHP:
class Seller
{
    protected $properties;

    public function __get($prop)
    {
        // some code
        return $this->properties[$prop];
    }

    public function __set($prop, $value)
    {
        // some code
        $this->properties[$prop] = $value;
    }
}
Добавив в ваш собственный класс эти методы, вы можете выполнять нужный вам код при обращении к свойствам, т.е. можете использовать валидацию, удобную вам, а не навязанную ORM.

после рельсовского стиля... както убого
Это не значит что нужно прекратить разработки ORM для PHP.
 

kostya.sys

Новичок
Решать каждому, я отказался из-за отсутствия времени на такую отвлеченную разработку. Советую внимательно присмотреться к идеям в рельсах, там просто забываешь что где-то сзади есть база. Что тоже не всегда хорошо, архитектура ООП сильно отличается от архитектуры БД, то что лаконично смотриться в коде иногда вызывает ужас при посмотре логов SQL запросов.

>Отсутствие требования наследовать отображаемый класс от
>базового класса библиотеки.
тут тоже нужно подумать, часто бывает нужно конкретезировать поведение, без наследования будет тяжко

ЗЫ: а реализация скафолдинга тоже в планах?
 

atv

Новичок
то что лаконично смотриться в коде иногда вызывает ужас при посмотре логов SQL запросов.
Для данной ORM это не грозит, так как все запросы однотабличные.

ЗЫ: а реализация скафолдинга тоже в планах?
Нет, даже не в планах.

В планах тест производительности, так как он всё расставит по своим местам. Я надеялся услышать советы по этому поводу.
 

bkonst

.. хочется странного?...
kostya.sys
Скаффолдинг - это уже далеко за пределами ORM

atv
По поводу производительности: наверное, лучший тест - реализация не очень сложной реальной задачи и анализ результатов профайлинга; универсальный синтетический тест придумать трудно.

Из своего опыта скажу, что имеет смысл протестировать скорость получения объектов из связанных коллекций - именно здесь может генерироваться большое количество SQL-запросов.

Еще один вопрос: как отобразятся на такую ORM запросы типа "выбрать сумму заказов по данному продавцу" или "выбрать продавца с максимальным объемом продаж за период"? Это важно, так как такие запросы встречаются буквально на каждом шагу.
 

kostya.sys

Новичок
производительность как раз зависит от сферы применения, если задачи простые (SIUD) по одной табдице и итераторы работаю грамотно, тоесть класс без необходимости не будет пробегать по результату запроса лишний раз, то все будет нормально.
 

zerkms

TDD infected
Команда форума
то что лаконично смотриться в коде иногда вызывает ужас при посмотре логов SQL запросов.
вы пользуетесь БД и смотрите логи лишь для получения эстетического удовлетворения? как по мне - так красоте запроса я предпочту его скорость
 

atv

Новичок
как отобразятся на такую ORM запросы типа "выбрать сумму заказов по данному продавцу" или "выбрать продавца с максимальным объемом продаж за период"? Это важно, так как такие запросы встречаются буквально на каждом шагу.
Условия выборки setFilter(), сейчас без изменений (а в планах с небольшими изменениями) подставляются в условие запроса WHERE, т.е. то, что возможно в этом условии определяет возможности выборки. Соответсвенно, "выбрать сумму заказов по данному продавцу" не представляется возможным, тем более, что сумма заказов не является сущьностью и для неё нет соответсвующего объекта.

По поводу получения всевозможных данных я предлагаю следующий критерий оценки: если данные предназначены для представления их пользователю, то для них лучше использовать обычный DataSet. ORM предназначена для предоставления данных для приложения.

Если приложению потребовалось "выбрать сумму заказов по данному продавцу", то лучше сделать это обычным способом, так, как будто перед вами обычная коллекция объектов, и у неё за спиной нет БД, т.е. проссумировать сумму в цикле.

Такой подход будет действительно прозрачным и освободит ORM от излишней функциональной нагрузки, и, соответсвенно, ORM будет проще, а значит и надёжней.
 

bkonst

.. хочется странного?...
kostya.sys
если задачи простые (SIUD) по одной табдице
Это уже совсем простые... даже с ходу не могу придумать задачу, в которой бы не использовались данные из связанных таблиц.

zerkms
Быстрый запрос красив по определению :)
 

bkonst

.. хочется странного?...
atv
Подход понятен, спасибо.

Однако с ним я не согласен - уже на небольших количествах записей (по моим экпериментам - от сотни и выше) затраты на получение всех данных от сервера, инициализацию и обработку объектов становятся очень даже заметны; при тысячах / десятках тысяч записей - будут жестокие тормоза. Приходим к тому, что для таких задач в конечном итоге будут использоваться SQL-запросы и получаем мешанину из SQL запросов и обращений к ORM в коде -> усложнение поддержки скрипта.
 
Сверху