Pull to refresh

Comments 47

не совсем ясно к чему эти посты?
Мне кажется, метод getRecords можно прекрасно заменить LINQ-подобным запросом при помощи к.-л. библиотеки из указанных мною постов. Может я ошибаюсь, но lдля решения вашей задачи следовало бы взять к.-л. ORM. Или я неверно понял суть реализации…
Да, я с Вами согласен на счет ORM, но это было бы слишком глобально, т.к. эта задача применялась на уже написанной CMS. Думаю использование ORM очень сильно повлияло бы на CMS, а вот с помощью этого способа я просто немного упорядочил работу с базой данных.
if (!is_null($where) && is_array($where))

а как is_array может «пропустить» null? Ну т.е. для чего проверка на нулл?

возможно вы хотели проверить isset(), его ставят чтоб не выпадали notice ошибки в скриптах, но в вашем случае isset тоже не нужен.
проверку на NULL я добавил для того, чтобы можно было входящие параметры метода заменить на NULL, если я их не буду использовать.
1. имхо лучше называть методы аналогично соответствующим SQL функциям — проще, нагляднее.
2. очень полезно было бы вставить mysql_real_escape_string или свой ее аналог для отсечения всяких нехорошестей. И, при беглом просмотре кода, не нашел (плохо искал?) где добавляются кавычки вокруг значений. Что если значение поля будет (без окружающих кавычек) «мама мыла 'раму', но это ей не нравилось»?
3. было бы не лишним добавить "`" для названий полей
4. может быть полезным сделать обертки для getRecords, которые будут возвращать строку как массив, вложенный массив для списка объектов, одно значение, пару значений и т.д.?
SELECT `key`=>`value` FROM `table` => array(key=>value)
SELECT `value` FROM `table` LIMIT 1 => value
SELECT `value` FROM `table` => array(value, value, value)
SELECT * FROM `table` LIMIT 1 => array(...)
SELECT * FROM `table`=> array(array(...), array(...), ...)
Это заметно упрощает работу с библиотекой

PS Лично я (специфика работы) предпочитаю insert/update/delete делать через аналогичную библиотеку, а вот select`ы писать руками — реализация всеобъемлющей функции будет слишком тяжелой: вложенные запросы, сложные условия выборки, вызов хранимых процедур и далее по списку.
1) Спасибо за совет, в будущем учту.
2) Я ее не использовал, т.к. для работы с БД использую PDO. Насколько я знаю, то PDO не нуждается в исп. mysql_real_escape_string.(может я и ошибаюсь)
3) "`" не добавлял, т.к. подразумевается, что поля будут указываться вместе с аллиасами, т.к. специфика базы с которой сейчас работаю требует Join использовать практически для любого запроса.
2. Если используете PDO::prepare, то да, дополнительно эскейпить не нужно. Но я бы сравнил по быстродействию.
3. Чудо-пользователи могут создать табличку с полем where и привет. Никто же не мешает же делать что-то вроде t1.`field1`, где t1 — алиас для таблицы `table1`.

PS Лично мне идея написать обертку для PDO не кажется очень хорошей.
А мне всегда было удобнее писать запросы руками. По-моему, так нагляднее
Да, но если у тебя запрос в 2-4 строки, то наглядность убывает и код становится нечитабельным.
2-4 строки это детский лепет :)
Вот когда отформатированный запрос занимает 36 строчек, это буйство :) И тут ORM скорее в тягость.
В таком случае, при использовании ORM бывает полезно перенести запрос на сторону БД (в хранимую процедуру, или представление), а из ORM уже просто делать вызов соответствующей функции, в которую ORM обернет хранимку.
Это если линейно писать. А если форматировать, то минимум 4 строчки и гораздо лучше воспринимается сознанием)
SELECT
tb.*,
tb2.name,
tb2.body_m,
tb3.catalog_id,
tb4.status_id

FROM product tb

LEFT JOIN ru_product tb2 ON tb2.product_id=tb.id
LEFT JOIN product_catalog tb3 ON tb3.product_id=tb.id
LEFT JOIN product_status_set tb4 ON tb4.product_id=tb.id
WHERE tb.active='1' AND tb3.catalog_id=? AND tb3.product_id!='10'
GROUP BY tb.id
ORDER BY rand()
LIMIT 6

Думаю такие запросы даже отформатированные будут смотреться не очень хорошо среди остального кода. Гораздо лучше заменить это все несколькими строчками кода, который сгенерирует этот sql запрос автоматически. Конечно же, это мое IMHO.
Это вы еще не видели как XML с дикой структурой на T-SQL выбирается. У меня там и по двести-триста строк выражения попадались. Зато на выхлопе конфетка.
SELECT
    tb.*, tb2.name, tb2.body_m, tb3.catalog_id, tb4.status_id 
FROM
                         product tb
    LEFT JOIN ru_product tb2 ON tb2.product_id=tb.id
    LEFT JOIN product_catalog tb3 ON tb3.product_id=tb.id
    LEFT JOIN product_status_set tb4 ON tb4.product_id=tb.id
WHERE
    tb.active='1' AND tb3.catalog_id=? AND tb3.product_id!='10'
GROUP BY tb.id
ORDER BY rand()
LIMIT 6


нормально смотрится, если бардак в коде не разводить. К тому же это на много нагляднее.

А с несколькими строчками сложнее — либо оно маленькое, шустрое, но не сильно функциональное, либо вырастает в монструозное чудовище. Вы написали гораздо больше чем несколько строчек кода, а ведь это всего лишь обертка для PDO и она ооочень многого не умеет (OUTER JOIN, подзапросы, вызов хранимых процедур и т.д.).
Ох какой знакомый велосипед. 3 года назад я был на этой же стадии. :)
Мне кажется проще взять готовый query builder, которых на том же phpclasses целый вагон.
Если взять готовый, то как научиться писать свой? ))
//Получаем запись
abstract public function getRecords($what='*',$where=NULL, $limit=NULL, $order=NULL,$join=NULL,$debug=false);

самое интересное начинается, когда сортируем чаще, чем ограничиваем и решаем поменять параметры $limit и $order местами. или когда решаем добавить ещё один параметр, который ставится после $debug. или когда решаем переделать список параметров на параметр в виде массив. или когда…

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

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

не делайте так.
Хм. А казалось, что лучше, т.к. обход параметров будет происходить в массиве, и проблема с тем, чтобы не писать вместо ненужных параметров NULL отпадет.
В любом случае, спасибо за совет.
да пожалуйста. ещё раз повторюсь, что написать своё решение для работы с базой данных — это дело чести для каждого разработчика на пхп. это наверное обязательный этап, чтобы увидеть какие вообще бывают решения, их плюсы/минусы, познакомится с архитектурой и с самим кодом.
Выше вам правильно подсказали и объяснили почему так делать нельзя. Но не подсказали решение получше:

$db->
   what($smt1)->
   where($smt2)->
   limit(23)->
   order($smt3)->
   join($smt4)->
   getRecords($debug) 
;
Формально это решение правильное, но я бы поспорил с удобством его применения на практике. Стремясь уйти от громоздких SQL-запросов мы приходим к громоздкому PHP-коду.
Что проще написать и проще поддерживать?:
  $sql = 'SELECT `smt1` FROM `smt2` ORDER BY `smt3` LIMIT 32';
  $DB->getRecords($sql);

или
  $db->
     what($smt1)->
     where($smt2)->
     limit(23)->
     order($smt3)->
     getRecords($debug);

Добавьте к этому заведомую ограниченность второго варианта.

Мое субъективное имхо: в таких библиотечках самое сложное это понять ту грань, за которую перешагивать не нужно, чтобы не получить неповоротливого монстра.
Абсолютно верно! На вкус и цвет помидоры для каждого свои.
Но во втором варианте мне нравится:
1. Автодополнение
2. Семантика
3. Абстракция, особенно, если в where() передаётся не строка, а некоторый структурированный кусок данных.
4. Обычно, это короче:

$dbNews->select();    // select всё же правильнее, чем getRecords
// VS
$query = 'SELECT * FROM `'. TBL_NEWS .'`';
$mysql->exec($query);


// примерчик посложнее
$filter = array('login'=>$_POST['login'], 'password'=>$_POST['password']);
$dbUsers->where($filter)->selectOneRow();

// VS
$query = 'SELECT * FROM `'. TBL_USERS .'` WHERE
   `login`="'. $mysql->escape((string)$_POST['login']) .'"
    AND `password`="'. $mysql->escape((string)$_POST['password']) .'"';
$mysql->exec($query);


5. Второй вариант локаничнее и транзитивнее, например:
$filter = array('active'=>1);
$cnt = $dbNews->where($filter)->getCount();
$pager = new Pager($cnt, 20);
$items = $dbNews->
    where($filter)->
    limit($pager->getLimit())->
    select()
;
// $filter был создан один раз и использован дважды


Ещё пример:
class productsController {
   protected $_defaultFilter = array(
        'active'=>1,
        'cnt_images > ' => 0 // картинок больше чем 0
    );
   

   public function indexAction() {
        $filter                 = $this->_defaultFilter;
        $filter['category_id']  = $_POST['catId'];
         // ...
   }
}


Ограниченность — это недоработка. Систему необходимо делать гибкой. Если у кого-то не получается — это его проблемы. Единственное но: некоторые сложные запросы проще написать строкой, чем использовать билдер. Но в моей практике таких запросов не больше 2%. Зато я экономлю своё время в 98% остальных случаев.
Согласен, но Ваши примеры хороши для несложных запросов. Возьмем последний Ваш пример: представьте что Вам нужно выбирать товары, но так чтобы они были только в активных категориях, причем первыми должны идти товары из промо-списка, который хранится в отдельной таблице. Ваши действия?

PS Естественно, свое мнение я не навязываю и абсолютной истиной его не считаю — у каждого свой стиль работы.
Ну лично в моей абстракции, это выглядит так:

$fields = array(
   '*',
   '(SELECT COUNT(*) FROM `promo` WHERE ... ) AS `is_promo`'
);
$order = array('is_promo'=>DESC);


$dbProducts->
    fields($fields)->    // метод fields()  - это то что у автора метод what()
    joinFromRels('categories', ' AND `caterory`.`active`=1')->
    order($order)->
    select()
;


Поясню конструкцию с джоином:
В системе есть глобальный список описывающий все связи (описывает все foreign_keys). Метод joinFromRels($relName) позволяет сгенерировать join для описанной связи. Второй аргумент не обязателен, но он позволяет дополнителнить условие ON [condition] что бы получилось:
ON
  `categories`.`id` = `products`.`cat_id`
  AND `categories`.`active`=1


Естественно, в системе есть более низкоуровневый метод $dbProducts->join(...) — позволяет сгенерировать любой джоин, хотя это сложнее. А вот подцеплять джоины из заранее описанного массива гараздо удобнее :) Можно например так:

$dbContinents->joinFromRels(array(
   'countries' => array(
         'towns' => array(
             'streets',
             'peoples'
        ),
    ),
));
// Для континентов подцепит страны, для стран подцепит города, а для городов подцепит улицы и людей.

Разве это не клёво?
Это здорово. Но представьте себе тихий ужас нового человека, которому нужно сходу влиться в Ваш проект. Или исправить что-то спустя три года, особенно если Вы уже будете работать в другом месте.

По Вашему первому примеру: согласитесь, не очень правильно мешать в одном месте билдер и SQL.

PS Кстати, а список внешних ключей Вы руками обновляете или автоматически?
Представьте тихий ужас секретарши которую попросят написать SQL-запрос… Да, появляется дополнительный порог вхождения так же как и в любой другой ORM. Зато, если вы не меняете персонал каждые 3 месяца, то работа делается быстрее, ведь изучить надо только 1 раз, а потом экономить время на каждом SQL-запросе. Кстати, изучать не так уж сложно. Я вам уже половину системы показал (самую сложную её часть) и надеюсь, всё понятно рассказал.

согласитесь, не очень правильно мешать в одном месте билдер и SQL.

Судя по заданному вопросу, вы уже вникли в суть и ваша догадка абсолютно верна. Система транзитивна, по этому до многих вещей можно додуматься самостоятельно не читая документацию. Например, то что второй параметр может быть массивом $filter :)

AdminCreator всё делает сам :) Я просто говорю: создай мне новую таблицу -> с «такими то» полями… «таких то» типов ..., одно из полей category_id с типом «связь один_ко_многим» к таблице categories -> СГЕНЕРИРОВАТЬ -> БАЦ! получаю готовый php и SQL-код для создания таблицы с foreign keys.

Сразу после этого я могу писать:

dbFactory(MODEL_PRODUCTS)->
  joinFromRels(MODEL_CATEGORIES)->
  select()
;
// Обратите внимание, каждая конструкция автодополняется!
// по этому, печатать приходится только это:
dbF->(MP)->
  JFR(MC)->
 sel()
;
Ну что ж, мне остается только по-доброму позавидовать ))

Кстати, обращу внимание на важность документирования и изучения документации новыми сотрудниками — как правило идет процесс «деградации» понимания системы. Разработчики, используя в своей работе только базовые функции ORM, забывают / не знают о более хитрых инструментах и особенностях их использования.
Завидовать — плохо, даже по доброму. Лучше приходите к нам работать ))

Пока с этим проблем нет. Наверное, потому что система относительно простая. Да, возможно некоторые забывают про существование низкоуровневых методов типа join(), потому что используют только joinFromRels(), но быстро освежают память при необходимости. PHPDoc-комментов в коде больше, чем самого кода — это облегчает процесс. Есть tdd-тесты в которых можно посмотреть интерфейс методов. Есть документация.
Спасибо за приглашение, конечно, но ездить далековато.
А как с быстродействием, между прочим? И нет ли паразитных запросов (тот же список внешних ключей ведь в базе хранится)?
В отличие от других ORM, наша ничего не знает о структуре БД и сама никогда не выполняет никаких запросов. Иногда это плохо, иногда хорошо. Но никакого оверхеда нет. Все SQL-запросы максимально примитивны. Единственный оверхеад,- это сгенерировать из массива $filter кусок SQL-запроса, но всё это делается на стороне php очень быстро.

Например, в TDD-тестах выполняется больше 1300 запросов к БД. Эти тесты проходят за ~5 сек (на моём компе под виндой). Так что оверхеад совершенно незаметен.

А связи описываются в bootstrap.php в виде php-кода:

My_Db_Rels_GlobalList::addHasMany( ... );
My_Db_Rels_GlobalList::addMany2Many( ... ...);

Таким образом, foreign keys-ов может и не существовать. Более того, так получилось, что система поддерживает шардинг (хотя мы к этому и не стремились) — т.е. разные таблицы могут храниться на разных mysql-серверах, но при этом, между ними будет связь. Использовать join-ы уже не получится, но у нас есть ещё один более удобный механизм выцепления связанных данных который будет отлично работать
А данные в bootstrap.php вписываются ручками или программно?
Это палка о двух концах.
Раньше вписывались ручками. Но AdminCreator не мог дописывать в середину файла новые строки, по этому, пришлось сделать отдельную папку и написать скрипт который рекурсивно находит все файлы и подключает классы:
/libs/My/Module/***.class.php

БуутСтрап автоматом для всех делает:

new My_Module_News();
new My_Module_Users();
new My_Module_Catalog_Categories();
new My_Module_Catalog_Products();
...


Соответственно, каждый модуль может инициализировать свои собственные константы и связи.

Теперь AdminCreator для каждой таблицы создаёт отдельный модуль, но из-за этого получается небольшой оверхеад (совсем не ощутимый). Однако, на высоко нагруженном проекте все модули можно объединить в один файл, что бы не было оверхеда.
Может быть стоит подумать о том чтобы автоматически выдергивать связи из самой базы и писать в один файл, а после изменения структуры БД генерировать этот файл заново? Впрочем в чужой монастырь, как говорится.
как доказали на практике товарищи из лагеря .NET, это — самое лучшее решение. А еще в нем работают рефакторинги, ни в одном другом решении такого не будет.
Когда-то давно создал класс для работы с БД, до сих пор пользуюсь.
суть: вставка\обновление — вызов метода с параметрами: Таблица, Массив данных. Помимо этого есть методы для выборки данных, тоже в виде надстроек над PDO.
Пример:

$folder_id = self::insert_array('##_contacts', array('note' => $post['folder_new'],
'ownerid' => $player_id,
'isfolder' => 1,
'parentid' => 0));

/**
* Вставка записи в таблицу
* @param string $prmTable идентификатор таблицы
* @param array $prmData массив данных (ключ соответствует названию колонки)
* return mixed ID добавленной записи
*/
public function insert_array($prmTable, $prmData)
{
$fields = "";
$fieldsVals = "";
foreach (array_keys($prmData) as $key) {
$fields .= (($fields == "")? '': ','). $key;
$fieldsVals .= (($fieldsVals == "")? ':': ',:'). $key;
}
$query = «insert into ». self::parse_dbprefix($prmTable). " ({$fields}) values ({$fieldsVals})";
self::execute($query, $prmData);
return $this->db()->handle()->lastInsertId();
}

Что вам мешало взять любой ORM?
Ответ на Ваш вопрос находится в самом начале комментариев.
Sign up to leave a comment.

Articles