Тихо и незаметно вышла ORMv2.1 (90kb) changelog: + поддержка MySQL + привязка к произвольной структуре db + метод ORM::Get() возращающий коллекцию классов + подстановка в IDE свойств объкта и ORM_List - many-to-may
PHP: <? require_once 'bootstrap.php'; header('Content-Type: text/html; charset=utf-8'); $scheme = ORM_Scheme::GetInstance(); // setup.php if (!$scheme->exists('User')) { // импортируем схему из базы $class = $scheme->bind('User', 'my_table_user'); $class->bindAllFromDb(); $class = $scheme->bind('Phone', 'my_table_phone'); $class->bindAllFromDb(); // отвязываем свойство user_id // привязываем свойство user (my_table_phone.user_id) к классу User $class->unbind('user_id')->bind('user', 'user_id', 'User'); $scheme->save(); // фалшивые-классы (для автоподстановки в IDE). Не подключайте этот файл! file_put_contents(dirname(__FILE__).'/fake.php', '<'."?\n".ORM_Scheme_Export_Php::Export()); } $user = ORM()->User(); $user->name = 'myName'; $user->save(); $phone = ORM()->Phone(); $phone->number = '512311'; $phone->user = $user; $phone->save(); // небольшое how-to: // объект ORM_Object представляют из себя записи: // Добавление записи $category = ORM()->Shop_Category(); $category->title = 'waka'; $category->save(); // Удаление $category->delete(); // Изменение $category->title = 'new title'; $category->save(); // Для получения записи используется ORM_List // коллекция ORM_List объектов $collect = ORM(); // получение объекта ORM_List $categoryList = $collect->Shop_Category; // ORM_List (спиок Shop_Category) // объекты ORM_List управляют выборкой из базы: // WHERE $categoryList->title = 'test'; // WHERE title = 'test' $categoryList->equals('title', 'test'); // ... AND title = 'test' $categoryList->notEquals('title', 'wtf?'); // ... AND title != 'wtf?' $categoryList->isNull('title'); // ... AND ISNULL(title) $categoryList->isNotNull('title'); // ... AND NOT ISNULL(title) $categoryList->addSymbol('id', '<', 10); // ... AND id < 10 $categoryList->inArray('id', array(1,3,5)); // ... AND id IN (1,3,5) $categoryList->like('title', '%waka'); // ... AND title LIKE '%waka' // ORDER BY $categoryList->addOrder('title'); // ORDER BY title $categoryList->addOrder('id', false); // ..., id DESC $categoryList->addOrderRandom(); // ..., RAND(); // LIMIT $categoryList->setLimit(5); // LIMIT 5 $categoryList->setOffset(10); // OFFSET 10 $count = $categoryList->count(); // колличество записей без учета LIMIT и OFFSET (SELECT COUNT(*) FROM ... WHERE ...) $count = count($categoryList); // или так $count = $categoryList->countInResult(); // колличество записей в результате (mysql_num_rows) echo $categoryList->getQueryBuilder()->getSelect(); // посмотреть какой запрос будет выполнен foreach ($categoryList as $cagegory) { // $category instanceof ORM_Object === true // ... } // получение первой записи $category = $categoryList->getOne(); // // получение записи по ID $category = $collect->Shop_Category(5); echo $category->id; // 5 // некоторые свойства объектов - ORM_Object $parentCategory = $category->parent; $parentParentCategory = $parentCategory->parent; // $title = ORM::Get()->Shop_Category ->equals('title', 'waka') ->inArray('id', array(123, 2, 3)) ->setLimit(5) ->addOrder('title', false) ->getOne() ->title;
А что не устраивает в готовых PHP framework с ORM? Есть же CodeIgniter, Kohana, ActiveRecord и т.д. Я тоже пытаюсь сделать нечто подобное ORM на PHP. Но меня не устраивает в существующих фреймворках отсутствие необходимого для меня комплекса: ORM+"кроссбазность"+MVC+набор Helpers. Кроме того, мне хотелось бы видеть описание модели данных аналогично EJB3 для J2EE. Из всех фреймворков более-менее моим запросам удовлетворяют CodeIgniter, Kohana и EzPDo, но в каждом чего-то да не хватает. CodeIgniter сделан в стиле PHP4. Kohana - в последних версиях не совсем кроссбазная. В EzPDO есть только ORM без MVC, зато там модель задается так как мне надо. Вот пытаюсь наваять что-то сам, только кривовато выходит. Могу обменяться опытом, если надо.
dvloper Когда ваш запрос превратится в нечто монструозное или логически сложное - о кроссбазности говорить не придётся, потому что в разных базах придётся писать разные запросы. Давно пора это понять.
Не разделяю этой точки зрения, именно потому как пришлось немало поделать таких монструозных запросов на разных базах. В итоге пришел к выводу, что подобные запросы возникают когда плохо продумана бизнес-логика проекта. В классической трехзвенке бизнес-логику можно сделать либо в базе, либо в сервере приложений. Первый вариант не требует от сервера приложений "монструозных" запросов, но накрывается медным тазом, когда к вам приходит заказчик и говорит: "Хочу тоже самое но на сервере XX (Oracle, MSSQL и т.д)" Второй вариант при нормально реализованной ORM вообще не должен по идее требовать SQL: пример - EJB в Java. Взаимодействие с базой (внутри ORM) строится простейшими запросами самыми сложными из которых - это джойны. Т.е. логика делается не с помощью SQL, а с помощью другого языка. Монструозность запросов - это прямое следствие того, что SQL предназначен только для работы с РЕЛЯЦИОННЫМ представлением данных. В реальности же приходится работать с бизнес-объектами, которые суть ОБЪЕКТНОЕ представление данных. Давно пора это понять.
dvloper Видел я такие списки сложных данных - на страницу 500-600 запросов, потому что по другому в бизнес приложении вобщем-то сделать трудно, хотя были варианты оптимизаций, но слишком трудоёмкие и всёравно запросов оставалось штук 100 при 40 записях на страницу. С системой работало 5-6 операторов и был куплен мощный сервер. Для сайта был сделан лёгкий сайт с синхронизацией баз из основной системы для облегчения самой базы и позволить серверу быстро работать с ней. Теперь возмите соц. сеть. Запрос, который длится больше 0.2-0.3 впринципе тормоз и его нужно оптимизировать. Не получается? Кешировать, писать демоны - неважно как - это надо сделать. На голом ANSI SQL этого не сделать, а ваш пресловутый ORM скорее всего наделает кучу запросов вместо одного. В WEB сайте важна латентность и скорость загрузки. ORM может сделать такой запрос? [SQL]SELECT SQL_CALC_FOUND_ROWS tg.id, tg.category_id, tg.numratings, tg.ratingsum, DATE_FORMAT(tg.last_changes, "%Y-%m-%d %H:%i:%s") AS tg_last_changes, IF(tg.last_changes > NOW() - INTERVAL 1 DAY, 1, 0) AS is_new, tgt.name AS tgt_name, c.image, c.activity, ct.name AS ct_name, tic.cnt AS torr_cnt, tic.comm_sum AS comm_sum, tic.leechers AS leechers, tic.seeders AS seeders, tic.downloaded AS downloaded, tg.original_name, tic.year, CONCAT(IFNULL(tg.genre, ""), IFNULL(tg.genre2, ""), IFNULL(tg.genre3, "")) AS genre, tic.season_part, tic.producer FROM torr_groups tg JOIN torr_groups_trans tgt ON tgt.id=tg.id AND tgt.lang_code="rus" JOIN categories c ON c.id=tg.category_id JOIN categories_trans ct ON ct.id=c.id AND ct.lang_code="rus" LEFT JOIN ( SELECT ti.torr_group_id, count(*) as cnt, sum(ti.comments) as comm_sum, SUM(ti.seeders) AS seeders, SUM(ti.leechers) AS leechers, SUM(ti.downloaded) AS downloaded, ti.year, ti.season_part, tt.producer FROM torr_info AS ti LEFT JOIN torr_trans AS tt ON tt.id = ti.id AND tt.lang_code="rus" WHERE ti.visible="yes" GROUP BY ti.torr_group_id ) tic ON tic.torr_group_id=tg.id WHERE tic.cnt > 0 ORDER BY tg.last_changes desc LIMIT 0, 30;[/SQL]
Я ему про фому, а он про ерему 1. Не надо меряться "крутизной". Повторюсь - мне приходилось писать запросы для сложных приложений. Как вам хранимая процедура килов на 30 голого SQL? Или запросы на выборку отчетов тоже килов на 10 со всеми прелестями: join'ами, union' ами, группировками и сортировками? Естественно все писалось и оптимизировалось под конкретную БД руками, без всяких билдеров. Так что если хотите показать свою крутость - лучше на ком-нибудь другом. 2. Я пытаюсь донести простую вещь: во всех нормальных языках программирования уже давно есть средства по отвязыванию бизнес-логики от SQL, а значит и от вида сервера БД. Мне нужна такая же вещь в PHP. Почему кросс-базная? Потому что возможна перспектива дальнейшего переноса приложений с хостинга на сервера разных клиентов у которых и запросы к БД могут быть разные. Есть ли такая возможность в существующих фреймворках - другой вопрос. Потому и говорю, что приходится кропать что-то свое. 2. По поводу кучи запросов от ORM - если нормально сделать ORM, может кучи запросов и не потребуется. Кроме того, суть не в запросах, а в том, чтобы бизнес-логику можно было нормально делать в PHP а не SQL. 3. Быстродействие выборки данных - это всего лишь вопрос цены за сервер. Если вы делаете Web-приложение социальной сети на виртуальном а не выделенном сервере - флаг вам в руки, экономьте, но как бы не оптимизировались запросы, сервак рано или поздно у вас не выдержит, т.к. не одни вы на нем висите. А на выделенном сервере при нынешнем железе я не вижу проблем с быстродействием. 4. По поводу примера: а нужен вообще такой запрос ?(кстати не очень он уж и "монстерский"). Может вы неправильно сделали отношения между сущностями в БД, что вам такой запрос требуется? Или может надо добавить пару лишних полей, вовремя их апдейтить и тогда и суммирование в подзапросе не потребуется? Это я к тому, что БД обычно разрабатываются "в лоб". Взяли накидали табличек, а потом думают, как из оного что-то SQL-ем вытащить. Про то надо оно с точки зрения бизнес-модели или нет обычно не думают, вот вам и "монстерские" запросы. Был у меня случай: в системе страшно тормозил модуль работы с курсами валют. Оказывается от большого ума какой-то умник вставил подзапрос на выборку текущего курса в главном запросе: [sql]select Rate from Rates order by CurDate desc[/sql]. А стоило добавить поле текущего курса в таблицу типов валют с обновлением его 1 раз в день и вуаля! Вместо подзапроса обычный join и модуль стал просто летать! Вобщем, принцип KISS рулит всегда.
Это и не было связано никогда. мало смотрел наверно может ты не понял, что этот запрос делает? ps: прошу прощения за такой пафос
Вообще я отчасти согласен с dvloper, сам недавно такой запрос рефакторил. имхо, все же "comments", "seeders", "leechers", "downloaded" стоило денормализовать. Как говорил один мой знакомый Джава-прогер, опытный хайлоадщик: "Джоины работают быстро, пока ты их не заставляешь считать". Что-то типа такого.
dvloper Теоретически - да, можно было сделать поля, в которых тот JOIN (SELECT ...) кешировался. Однако, т.к. вы не знакомы с работой трекера, вы такой оптимизацией скорее просто убъёте его нагрузкой. Фишка в том, что seeders, leechers, downloads это статистика по каждому конкретному торренту. А торренты собраны в группы, список которых этот запрос и показывает. По торрентам вытаскивается общая для них инфа как название, год и.т.д. А теперь представте кол-во update запросов, которые будут вызывать такие простейшие вещи как добавление пира, переход из личера в сидеры? При скачке торрент файла. Если сейчас у меня ~450 queries/sec, то оно резко подскочит где-то до 650-700 штук в секунду. Я и так из-за слишком большого кол-ва UPDATE/INSERT запросов использую DELAYED режим для UPDATE/INSERT, иначе сервер просто не успевает нормально делать селекты и выкрученные буфера вставки данных. Денормализация более-менее сделана, иначе здесь бы было ещё как минимум 3-4 суб селекта. Вообще всё это хорошо работает по той простой причине, что таблица пиров сделана Memory, потому что InnoDB просто плохо справлялось c 50% на 50% read/write при 90% запросов идущих к ней. Да, сама система может быть немного переделана, это и планируется в ближайший месяц-два сделать - будет легче выбирать данные. Но таблицы с переводами в любом случае останутся, т.к. на сайте 3 языка, да и статистика по кол-ву сидеров и личеров, скачек всёравно будет выбираться live - она слишком быстро меняется, а делать прекалькулированные поля на таблицу, которая постоянно читается только добавлять дополнительный bottleneck. Можно сделать обновление раз в Х минут, что вариант. Но это нужно ещё исследовать и проверять. Потом доделывать кеширование и.т.д. Так что в реальности не всё так просто как кажется - пихнул ORM и всё гут. ORM при правильном использовании это не плохо, но смысл оно имеет только на большом проекте. В маленьком это пушка, паляшая по воробъям и на каком-нить VDS добавляющая жуткий Overhead. Добавте ещё Smarty в комплект, и вообще будет весело - ему даже bytecode cacher's сильно не помогают. Точнее помогают - дают прирост в 2.5 раза где-то. Только native шаблонизатор простейший тупо работает раз в 30-40 быстрее и без кешеров (на хабре пару недель назад было об этом). А ещё ORM как правило делают Active Record, что сказывается на memory usage, иногда, знаете ли, весьма существенный пункт. Добавить лишний гиг рама одному серверу - фигня. А когда у тебя 5-6 серверов, требующих буфферизированную DDR2 ECC память, которая стоит так, что 10 раз подумаешь что дешевле - заставить программиста месяц оптимизировать или докупить памяти на 2 его зарплаты? Что до процедур - каюсь, я их особо не делал. Но видел процедуры по 2.5-4к строчек SQL кода. Биллинговая система учёта трафика у магистрального Internet провайдера - я представляю что это такое. Все мы так или иначе пуганы. Что до примера с курсом валют - надо было не subselect ставить, а derived query сделать, если надо было live - курс теоретически может и чаще менятся чем раз в сутки Что до бизнес-логики - вы, батенька, спуститесь до уровня PHP сперва Какая бизнес логика, какие ORM? Мы тут пишем кашу из PHP, HTML и CSS, а вы нас бизнес-логикой пугаете. Ирония? Да, но с долей истины. PHP из-за своей направленности и особенностей не приспособлен для разработки сверх-тяжелых приложений с использованием множества тяжелых библиотек - это медленно работает. Это не Java, не C++ - это надо понять раз и на всегда. К тому-же, как я считаю, конструкции вида: PHP: <?php $orm->select('table')->addFields(array('field1', 'field2', 'SUM(field3)')->where(array('field4' => 4)->order_by('field5 DESC')->limit(0, 20); Куда более не понятны при любом, более-менее многострочном запросе, чем сам запрос. Не говоря уже об издержках на создание запроса таким манером. Возьму Doctrine и потестирую как-нить, посмотрим что из этого выйдет. На крайняк есть PDO - используйте. TheShock Джойны плохо работают, когда таблицы за большие становятся, а cardinality индексов мала - тогда иногда проще сделать не через JOIN, а через SELECT FROM (SELECT FROM ) - так кстати работают с MySQL NDB Cluster, т.к. из-за его особенностей JOIN'ы по большей части там обсалютно не юзабельны из-за необходимости слияния таблиц. А лучше всего простые запросы на одну таблицу, особенно по primary key (IN() в этом смысле крайне крут).
В настоящий момент ORM v3 не разрабатываю т.к. много работы и других не менее интересных проектов Желание на пропало, не остыло и обязательно будет оформлено в виде кода. между тем, подросла ветка v2: - небольшие багофиксы - добавлена функция ORM() использование функции ORM() PHP: <? // 1. Создание объекта (эквивалент new Phone) $phone = ORM()->Phone(); $phone->number = '+7 (222) 222-22-22'; $phone->save(); // удобно для разименования: ORM()->Phone()->set('numer', '+7 (333) 333-33-33')->save(); // 2. Получение по ID $phoneId5 = ORM()->Phone(5); // или так: $phoneId6 = ORM('Phone', 6) // 3. Спискок $phoneList = ORM()->Phone; foreach($phoneList as $phone) echo $phone->number; // или так: $phoneList = ORM('Phone'); foreach($phoneList as $phone) echo $phone->number; скачать ORM v2.2 Enjoy! P.S. Для автоподстановки в IDE генерируется специальный фейк-файл. Благодаря этому тот же Zend Studio 7 знает какого типа находится объект внутри foreach без дополнительного phpdoc комментария @var: PHP: <?php foreach(ORM()->Phone as $phone) { echo $phone->user->name; } user и name в данном случае будет ctrl-кликабельный. Но ZDE не предоставляет автоподстановку в этом случае что добавляет неудобства в разработке и решается лишь phpdoc комментарием.
А чем вам не нравятся SQL запросы в чистом виде, ну ещё канешь написать небольшой класс который от инъекций обезопасит ну и данные вернёт в объекте, а так к чему эти санки когда лето на дворе ?
r00les Реализуя крупные проекты без ООП — как плавать в океане на надувном матрасе. ORM — это «небольшой класс который от инъекций обезопасит ну и данные вернёт в объекте», над которым провели декомпозицию и немного расширили функционал.
в хотелки: PHP: <?php // SQL: INSERT INTO user(nick, icq) VALUES('Ti', '177717007') ORM()->User[] = array('nick'=>'Ti', 'icq'=>'177717007'); // SQL: UPDATE user SET phone = '1111' WHERE id = 1 ORM()->User[1] = array('phone'=>111); // или: ORM()->User[1]['phone'] = 111; // SQL: DELETE FROM user WHERE id = 1 unset(ORM()->User[1]);
такое если я правильно помню работать не будет. А будет ругаться нотисами про непрямое изменение перегруженного свойства. И остальное тоже, если предварительно элементы не были установлены. Для того чтобы это работало тебе придется реализовывать User с интерфейсом ArrayAccess а для такого И каждое свойство делать объектом с интерфейсом ArrayAccess
PHP: <?php mysql_query("INSERT INTO log (id,text) VALUES(NULL,'В жопу ОРМ=)')"); Ti я понял почему мне так не нравятся обёртки - неочевидностью выполняемого дейтсвия
User уже имеет реализацию ArrayAccess и User['phone'] = '123' уже работает. Пока не работает ORM()->User[$id] - ибо ORM()->User - возращает не класс User а итератор этого класса, у которого пока не реализован ArrayAccess. Каждое свойство не нужно делать ArrayAccess. Конечно, если свойство является объектом ORM он имеет ту же самую реализацию. Mr.M.I.T. ты неправильный эквивалент подобрал. Твой "очевидный" вариант должен выглядеть по меньшей мере так: PHP: <?php $text = mysql_escape_string("В жопу ОРМ=)"); $result = mysql_query("INSERT INTO log (id,text) VALUES(NULL,'$text')"); if ($result) trigger_error("Fail: ".mysql_error()); И чем это удобней, понятней, безопасней и очевидней ORM? PHP: <?php ORM()->log[] = array('text'=>'В жопу голый SQL');
всем. мне не нужно учить чужой голимый API. я работаю напрямую с php'ным апи ядра. те кого пугают всякие "обезопаснивания" юзают плейсхолдеры и простенькие обёртки PHP: <? DB::Query("INSERT INTO log (id,text) VALUES(NULL,'?')","В жопу ОРМ =)"); DB::Insert("log",array("text"=>"В жопу его")); ?>