Конструктор SQL-запросов

whirlwind

TDD infected, paranoid
Автор оригинала: grigori
whirlwind
а ты мог бы описать ситуацию, когда написание всей этой ООП-лапши проще или удобней, чем написать другую функцию с полным SQL-запросом целиком?
Я же вроде привел несколько ситуаций? Где запрос изменяется незначительно, проще передать ООП-лапшу в виде объекта, чем строчить километровую SQL-лапшу повторно. Больше буков - больше ошибок. Зачем тестировать 2 раза один и тот же запрос, если можно протестировать разницу?

ЗЫ. предвидя камни в огород TDD - замените тестировать на отлаживать.

AmdY а с and/or есть варианты? Почему я внутрь execute не засовываю - что бы с любыми драйверами работало.
 

zerkms

TDD infected
Команда форума
а ты мог бы описать ситуацию, когда написание всей этой ООП-лапши проще или удобней, чем написать другую функцию с полным SQL-запросом целиком?
grigori
а можно мне за него?
такая ОО лапша очень удобно, когда части запроса приходят из разных источников. в этом случае пресловутый $sql передаётся в методы аргументом, где в него добавляют нужные данные.
 

fixxxer

К.О.
Партнер клуба
Хм. А можно примеры, где такое "наследование" (если я правильно понял) будет уместно? Просто не представляю себе случая, когда нужно что-то изменить глобально для всех наследников. Тест тут типовой получается, его в любом случае делать надо на модель.

А когда части запроса приходят из разных источников, это по-моему фигня какая то =)

Вообще, хочется увидеть подход целиком. Ибо уместность зависит по моему от архитектуры в целом, и я пока что ее себе таковой не представляю.
 

whirlwind

TDD infected, paranoid
fixxxer базовый класс вывод транзакций в админке, наследник - вывод транзакций для мерчанта.
 

zerkms

TDD infected
Команда форума
fixxxer
да легко:

$sql = .. // тут собираем основу запроса
$this->addPager($sql);

в методе baseController::addPager() получаем нужную инфу: текущую страницу, число объектов на страницу, считаем лимит/оффсет.
 

fixxxer

К.О.
Партнер клуба
У меня здесь уже скорее соображения пойдут как код ревьюера/перформанс аналитика. Я теряюсь и пугаюсь в таких случаях, когда генерация sql запроса размазана по классам. Просто потому что когда я вижу в mysql slow log (например) медленный запрос, я что делаю. У нас есть такое соглашение, что SQL запросы пробиваются в виде SQL_* констант в классах моделей и никак иначе. Соответственно, все SQL-запросы (а точнее их шаблоны) легко собираются на одну страничку во внутренней документации, с контекстом их использования.

В варианте с пейджером мне в принципе понятно, но в более навороченных случаях оптимизация может стать болью в заднице - ведь надо, во-первых вообще понять что откуда растет, а во вторых, еще и модицифировать запрос не задев соседние...

UPD
в целом, я сторонник радикального подхода :) либо мы в принципе пишем сложные запросы - и потому все пишем ручками, либо используя датамапперы с актив рекордами, принципиально ограничиваемся только простыми. Нет ничего хуже смеси подходов в крупном проекте с кучей разработчиков. Второй вариант с реляционками тупо может не сработать. Со всякими BigTable-ами - вероятно, сработает, но здесь я бы пока предпочел услышать тех, кто пробовал.
 

whirlwind

TDD infected, paranoid
Дык никто же не мешает взять и вместо $sql->getText() написать ручками? Вот по этому я внутрь конструктора предлагаю не пихать драйвер - ибо конструктор конструирует.
 

zerkms

TDD infected
Команда форума
Просто потому что когда я вижу в mysql slow log (например) медленный запрос, я что делаю. У нас есть такое соглашение, что SQL запросы пробиваются в виде SQL_* констант в классах моделей и никак иначе. Соответственно, все SQL-запросы (а точнее их шаблоны) легко собираются на одну страничку во внутренней документации, с контекстом их использования.
это уже другие проблемы. да, может быть искать и будет проблематично, но генератор так и так генерит абсолютно такой же запрос, как и если бы ты писал его просто строкой.
 

fixxxer

К.О.
Партнер клуба
В общем, надо рассматривать конкретные кейсы. Может, и не так страшен черт =)
 

AmdY

Пью пиво
Команда форума
может быть, только кроме Доктрины ни одной нормальной реализации ORM я не встречал. Тот же AR в ROR внутри страшен, там не соблюден нормальный баланс между ORM и SQL. С точки зрения функциональности ORM получаем улучшения в разы, производительность же в обратную сторону.
Сейчас у меня любая форма сохраняется одним методом $form->merge($record, $data);
внутри которого уже рассматривается все связи записи и можно сохранять даже сложные массивы.
PHP:
array(
'name' => 'test'
'Projects' => array(1,2,4) 
)
PHP:
array(
'visible' => 1,
'Translation' => array(
        'RU' => array('title' => 'Русский'),
        'EN' => array('title' => 'EN'),
    ) 
)
 

StUV

Rotaredom
А давайте поднимем старую тему. Прошло уже дофига лет, каждый наверное смотри сейчас по-другому на сабж.
не, все точно так же...
+1 к http://phpclub.ru/talk/showthread.php?postid=850998#post850998

++
у нас вообще нет "прямых" запросов к бд - весь "скл" в хранимых pl/sql-процедурах/функциях - в коде только command's - т.е. относительно устойчивые интерфейсы и "на каждый чих" своя процедура - т.е. в бд можно оптимайзить что угодно и как угодно - клиентский (по отношению к бд) код модели меняется крайне редко (в случаях "клиентских" оптимизаций - чаще добавляется новая процедура с постепенным вытеснением из кода старой в "deprecated").
 

atv

Новичок
Без реальных примеров перед глазами, говорить бесполезно.
Итак, запрос из реального проекта, коих сейчас много:
PHP:
$queryCondition = '';
    	if ($tagWord!="") {
	    	$listId = ClientTag::GetContentByTag($tagWord);
	    	if (count($listId)>0) {
	    		$queryCondition = " AND A.aId in (".implode(',', $listId).")";
	    	}
    	}

    	if(isset($params['genre']) && trim($params['genre']) != '') {
    	   $genres = htmlspecialchars(trim($params['genre']),ENT_QUOTES);
    	   $genres = explode(';',$genres);

    	   if(count($genres) > 0) {
    	      $queryCondition .= ' AND (';
    	      foreach($genres as $genre) {
    	         if(trim($genre) != '')
    	           $queryCondition .=  "AG.GenreId = (SELECT gId FROM Genre WHERE gName = '" . $genre . "' LIMIT 1) OR ";
    	      }
    	      $queryCondition = substr($queryCondition,0,-3);
    	      $queryCondition .= ') ';
    	    }
    	}

		$query = "SELECT COUNT(*) as cnt
			        FROM Artist A
			        LEFT JOIN ArtistGenre AG ON A.aId = AG.ArtistId
				   INNER JOIN Client C ON C.cId = A.ClientId AND C.cEnabled = 1 AND C.cActivated = 1
			       WHERE A.aEnabled=1 and A.aIsExpert=0
				     ".( $queryCondition != '' ? $queryCondition : '' );
Это ещё не самый сложный вариант, есть и посложнее. И сложность эта не в самом запросе, а в его построении. Есть запросы с десятками условий, и не только в предложении "where", но и с подключением разных таблиц с агрегированными данными.

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

2. трудно разнести построение запроса на несколько логически разделённых метода (которые, кстати, могут использоваться для нескольких запросов). Приведенный выше пример занимает всего 30 строчек, но, чем больше условий построения запроса, тем больше разрастается метод. Выделить в таком методе логически независимые части, да ещё с учётом первого пункта, практически невозможно.

3. строчка WHERE gName = '" . $genre . "' говорит сама за себя. Нужно постоянно помнить о подготовке данных перед передачей их в запрос. В СКЛ билдере об этом можно не беспокоиться.

4. Кто назовёт это не лапшёй, пусть первый бросит в меня камнем :)

Мне приходилось работать с разными SQL билдерами. У большинства из них схожие принципы построения запроса. После первого из них, на освоение других у меня уходило не больше 3-4 написанных запроса.

А вот другой пример запроса, по сложности построения превосходящий предыдущий:
PHP:
$query = ezcDbInstance::get()->createSelectQuery();
        $subquery = $query->subSelect();
        $subquery->select($query->expr->count('*'))
            ->from('ClientDiscussion')
            ->where(
                $query->expr->lAnd(
                    $query->expr->eq('cdObject', $query->bindValue('blog')),
                    $query->expr->eq('cdObjectId', 'bpId'),
                    $query->expr->eq('cdEnabled', 1),
                    $query->expr->eq('cdDeleted', 0)
                )
            );

        $query->from('Client')
            ->innerJoin('Artist', 'Artist.ClientId', 'cId')
            ->innerJoin('ArtistBlog', 'aId', 'ArtistId')
            ->innerJoin('BlogPost', 'abId', 'BlogId');

        if ('month' == $this->currentTab) {
            $query->leftJoin('BlogPostRatingMonth', 'bpId', 'BlogPostId');
        } elseif ('all' == $this->currentTab) {
            $query->leftJoin('BlogPostRating', 'bpId', 'BlogPostId');
        } else {
            $query->leftJoin('BlogPostRatingWeek', 'bpId', 'BlogPostId');
        }

        $query->select('aId', 'bpId', 'aSite',
                $query->alias('bpContent', 'text'),
                $query->alias('bpTitle', 'title'),
                $query->alias('bpCreatedAt', 'time'),
                $query->alias('aName', 'nickname'),
                $query->alias('aHasAvatar', 'avatara'),
                $query->alias('BlogPostRatingNormalized', 'rate'),
                $query->alias($subquery, 'comments')
            )
            ->where(
                $query->expr->lAnd(
                    $query->expr->eq('abEnabled', 1),
                    $query->expr->eq('bpEnabled', 1)
                )
            )
            ->orderBy('rate', ezcQuerySelect::DESC)
            ->limit(self::ITEMS_PER_PAGE);
Как вы могли заметить, здесь используется SQL билдер из библиотеки ezc. Надо сказать, не самый "немногословный" билдер, но даже на нём строить запросы легче.

А это SQL билдер, которым я обычно пользуюсь, с более лаконичным синтаксисом.

у нас вообще нет "прямых" запросов к бд - весь "скл" в хранимых pl/sql-процедурах/функциях
Тоже вариант, но, блин, сопровождать такой код жутко неудобно. Да и строить запрос с учётом различных условий сложнее, это всё-таки не язык программирования.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Автор оригинала: whirlwind
строчить километровую SQL-лапшу повторно.
у меня все километры SQL убраны во вьюшки и хранимые процедуры

-~{}~ 27.04.09 12:52:

StUV
у меня тоже
именно поэтому я не понимаю, зачем прятать SQL в OOP, если для этого уже есть средства самого SQL

-~{}~ 27.04.09 13:09:

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

>строить запрос с учётом различных условий сложнее, это всё-таки не язык программирования.
А что по-твоему PLSQL / plpgsql / plperl ? :)

Принцип такой:
Объявляешь тип данных, которую ждет процедура в качестве параметра (вроде структуры на С).
По этим данным процедура формирует и выполняет SQL-запросы.
Твоя логика скриптов формируют данные как массив.
Модель переводит массив в "запись", которую ждет процедура.
Каждую часть пишешь отдельно.
Нет проблем с отладкой, оптимизацией, изменением структуры БД и ограничением доступа.
 

atv

Новичок
Нет проблем с отладкой, оптимизацией, изменением структуры БД и ограничением доступа.
А с контролем версий? А с распространением (deploy) изменений девелоперской БД на продакшн? С MySql я намучался с простым изменением структуры, неговоря уже о триггерах и процедурах.

Не спорю, может в PostgreSql с этим намного проще, в ближайшее время собираюсь попробовать.
 

whirlwind

TDD infected, paranoid
atv нигде это не проще. Проще накуячить хранимок с кучей вложенных вызовов, а как там будут разбираться или отлаживать после тебя - пох. Зато типо оптимизировано.

-~{}~ 27.04.09 15:28:

за что я ненавижу FB, так это за то, что у них есть IDE которая позволяет ваще ненапрягаясь засирать базу кодом.
 

whirlwind

TDD infected, paranoid
Когда доходит до простых формулировок, то до вас доходит. А перечитать мой вопрос и понять что он далеко не про ORM, то ну никак не получается :D
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
не проще
как написал StUV - делать новые процедуры и вьюшки, а старые удалять по мере возможности
схемы позволяют логически распределить горы объектов
а вот удаление поля из таблицы, на которую подвязана куча объектов - это сложно

>понять что он далеко не про ORM, то ну никак не получается
С другой стороны, если 10 человек одновременно поняли тебя неправильно ... может, проблема не в них?
 

whirlwind

TDD infected, paranoid
Проблема как всегда в неспособности углядеть преимущества касательно тех вещей, которые не используешь регулярно на практике. Ничего сверхестественного здесь нет. Дабы холивар не разжигать, откланиваюсь. Что хотел я узнал.
 
Сверху