СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Упрощение запросов PDO в черновиках Recovery Mode

Добрый день. Хочу рассказать свой метод работы с PDO. Работать с PDO довольно безопасно чем стандартными средствами но не очень удобно записывать/обновлять большие массивы данных. В этой статья хочу рассказать как я себе облегчил работу с PDO

Простой запрос PDO, это уже четыре строчки.

$sql = "SELECT * FROM `table` WHERE `id` = '1'";
$st = $DB->query($sql);
$st>setFetchMode(PDO::FETCH_OBJ);
$list = $st>fetch(PDO::FETCH_ASSOC);


А запись данных так вообще как одни том книги «Война и мир», и это еще короткий запрос.
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (:email, :password, :registration, :userkod)";
$st = $DB->prepare($sql);
$st->bindValue(":email", $email, PDO::PARAM_STR);
$st->bindValue(":password", $insert_passwrd, PDO::PARAM_STR);
$st->bindValue(":registration", $data, PDO::PARAM_STR);
$st->bindValue(":userkod", $userkod, PDO::PARAM_STR);
$st->execute();

Не буду таить греха но такие я “Копипастю”.

После долгих мучений решил упростить запрос путем написание класса c функцмями SELECT, INSERT, UPDATE

Создадим класс и вставим в него приведенные ниже функции
class DB {
}


SELECT
/*
*  $sql - это запрос к базе
*  $fetch - тип получения результата из базы
*/
function select($sql, $fetch=””){
	//в файле index мы подключемся к БД (можно реализовать несколькими путями, я же создал глобальную переменую)
        global $DB; 

        $st = $DB->query($sql);
        $st->setFetchMode(PDO::FETCH_OBJ);
        if($fetch == '' || $fetch == 'row'){
	    //Возвращает строку
            $fetch_fn = 'fetch';
        }elseif($fetch == 'all'  || $fetch == 'fetchAll'){
	    //Возвращает массив
            $fetch_fn = 'fetchAll';
        }else{
            //вернет ошибку с указанием на неправильный параметр
	    return 'error $fetch';
        }
        //выполняем запрос и возвращаем ответ, за одно решил возвращать и количество строк
        return array('return'=>$st->$fetch_fn(PDO::FETCH_ASSOC), 'count'=>$st->rowCount());
}


Эта функция возвращает результат запроса и количество строк
Теперь запрос выглядит так, намного легче. Правда пришлось отказаться от bindValue, хотя я думаю это не особо критично будет.
$sql = «SELECT * FROM `table` WHERE `id` = '1'»;
$return = DB::select($sql);

INSERT
/*
*  $sql - это запрос к базе
*  $fetch - массив данных где ключами являются колонки в записываемой таблице
*/
function insert($sql, array $data) {
       //в файле index мы подключаемся к БД (можно реализовать несколькими путями, я же создал глобальную переменную)
       global $DB;

       //Проверяем на присутствие всех данных
       if(empty($sql) || empty($data)){
           return false;
       }

       //Создаем два массива с ключами в разном формате
       foreach ($data as $key => $value) {
           $set1[] = "`$key`";
           $set2[] = ":$key";
       }
        
       //Переводим массивы в строку
       $set1_str = implode(', ', $set1);
       $set2_str = implode(', ', $set2);

       //Две строки сливаем в элемент запроса
       $set_str = '('.$set1_str.') VALUES ('.$set2_str.')';

       //заменяем псевдопеременную ?set в запросе на созданный элемент
       $sql = preg_replace("#\?set#is", $set_str, $sql);
       $st = $DB->prepare($sql);

       //Создаем bindValue по ключам и значениям
       foreach ($data as $key => $value) {
           $st->bindValue(":$key", $value);
       }
       //выполняем запрос и возвращаем ответ
       return array('return'=>$st->execute(), 'id'=>$DB->lastInsertId());
}


А в этом запросе bindValue реализованы и создаются путем перебора ключей и значений.

$coll["name"] = $_POST[name];
$coll["email"] = $_POST[email];
$coll["password"] = $_POST[password];
$coll["dateCreated"] = date("Y-m-d H:i:s");
    
$sql = "INSERT INTO `users` ?set";
$return = DB::insert($sql, $coll);


UPDATE
/*
*  $sql - это запрос к базе
*  $fetch - массив данных где ключами являются колонки в обновляемой таблице
*/
function update($sql, array $data) {
        //в файле index мы подключаемся к БД (можно реализовать несколькими путями, я же создал глобальную переменную)
        global $DB;

        //Проверяем на присутствие всех данных
        if(empty($sql) || empty($data)){
            return false;
        }
        //создаем массив с элементами ключей
        foreach ($data as $key => $value) {
            $set[] = "`$key`=:$key";
        }
        //переводим массив в строку
        $set_str = implode(', ', $set);

        //заменяем псевдопеременную ?set в запросе на созданный элемент
        $sql = preg_replace("#\?set#is", $set_str, $sql);
        $st = $DB->prepare($sql);

        //Создаем bindValue по ключам и значениям
        foreach ($data as $key => $value) {
            $st->bindValue(":$key", $value);
        }
        //выполняем запрос и возвращаем ответ
        return array('return'=>$st->execute());
}

При таком подходе мы можем перезаписывать большие формы (мне приходилось работать с анкетами состоящими из >50 полей и хранящимися в одной таблице и поверьте это не очень весело)

Пример:
$data[‘name’] = “Иван”;
$data[‘family’] = “Иванов”;
$sql = «UPDATE `users` SET ?set WHERE `id`='$id' LIMIT 1»;
$retDB = DB::update($sql, $data);

Вот в общем то и все. Надеюсь что хоть кому то пригодится эта статья
php, pdo

комментарии (44)

+1
MaximChistov ,   * (был изменён)
А запись данных так вообще как одни том книги «Война и мир», и это еще короткий запрос.
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (:email, :password, :registration, :userkod)";
$st = $DB->prepare($sql);
$st->bindValue(":email", $email, PDO::PARAM_STR);
$st->bindValue(":password", $insert_passwrd, PDO::PARAM_STR);
$st->bindValue(":registration", $data, PDO::PARAM_STR);
$st->bindValue(":userkod", $userkod, PDO::PARAM_STR);
$st->execute();

неа. вот так:
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (:email, :password, :registration, :userkod)";
$st = $DB->prepare($sql); $st->execute([":email"=>$email,":password"=>$insert_passwrd,":registration"=>$data,":userkod"=>$userkod]);

А учитывая, что результат создания стэйтмента вы все равно не проверяете, можно даже так
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (:email, :password, :registration, :userkod)";
 $DB->prepare($sql)->execute([":email"=>$email,":password"=>$insert_passwrd,":registration"=>$data":userkod"=>$userkod]);

Но вообще, фу таким быть пароли в плейнтексте хранить). Надо хэшировать хотя бы так:
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (:email, SHA1(:password), :registration, :userkod)";
 $DB->prepare($sql)->execute([":email"=>$email,":password"=>$insert_passwrd,":registration"=>$data,":userkod"=>$userkod]);
0
MaximChistov ,   * (был изменён)
Простой запрос PDO, это уже четыре строчки.

$sql = "SELECT * FROM `table` WHERE `id` = '1'";
$st = $DB->query($sql);
$st->setFetchMode(PDO::FETCH_OBJ);//WTF??
$list = $st->fetch(PDO::FETCH_ASSOC);//все равно не используете же дефолтное


Вот так выглядит самый простой запрос:
$sql = "SELECT * FROM `table` WHERE `id` = '1'";
$one = $DB->query($sql)->fetch(PDO::FETCH_ASSOC);

Короче никакой костыль не сделает :)
0
TroL929 ,  
Но это не сильно улучшило читаемость кода. Пример брался из рабочего сайта, и пароль там закеширован
0
MaximChistov ,   * (был изменён)
с паролем ок, но что такого нечитаемого в коде
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (:email, :password, :registration, :userkod)";
 $DB->prepare($sql)->execute([":email"=>$email,":password"=>$insert_passwrd,":registration"=>$data,":userkod"=>$userkod]);

По-моему стороннему разработчику он совершенно очевиден, и короче не сделать, если не пихать сам запрос в вызов функции.
+1
TroL929 ,  
А что если в твоем примере надо будет записывать 50 параметров? Ты готов по по четыре раза каждый параметр перечислять?
`userkod`, :userkod, ":userkod" и $userkod. Я думаю это проблемно очень, а еще еще и где то опечатка так вообще можно много времени потратить на поиски ошибки
0
+1 –1
MaximChistov ,  
таблица с 50 полями — архитектурная проблема в 99,99% случаев
+1
TroL929 ,  
Согласен, но бывают случаи когда ты подключаешься на доработку проекта таких как CRM системы, выполненные дилетантом. Я как раз над такой работаю.

Ну тогда пример из 10-20 записей, вполне штатный случай, и уже создает проблему в написании запросов
0
MaximChistov ,  
все беды от криворуких программистов :)
+2
LE0N ,  
выполненные дилетантом.

Честно говоря, ваш код мало чем отличается.
Велосипеды для обучения, конечно, хорошо. Но зачем эту чушь сюда постить то?
–2
TroL929 ,   * (был изменён)
А у вас есть варианты получше? Готов узнать

Ну и 4 пользователя поставили статью в избранное, значит хоть кому то она показалась полезной или интересной
+1
Delphinum ,  
Есть ORM, есть ActiveRecord.
Почему все, кто первый раз сталкиваются с PDO (не в обиду, но сложилось такое мнение), сразу стараются писать для него класс-обертку?
Почему вы в классе используете глобальные переменные?
В вашем последнем примере:
$data[‘name’] = “Иван”;
$data[‘family’] = “Иванов”;
$sql = «UPDATE `users` SET ?set WHERE `id`='$id' LIMIT 1»;
$retDB = DB::update($sql, $data);

откуда метод update берет параметр $id?
0
boodda ,   * (был изменён)
нельзя отходить когда начал писать комментарий.
Хотите что бы просто писалось и не мазолило глаза, откройте для себя дивный мир ORM(EloquentORM, Doctrine).
А про апдейт это вы вообще сморозились, а вдруг апдейт тоже по гигантскому where и какой запрос у вас будет?..

Вы собрали в коде аж несколько антипаттернов.
1.Если методы из вашего класса DB вызываются статично, то они должны быть объявлены как public static function.
2.Глобальная переменная $db нагло врывается в методы класса, хотя её никто не знает, и что делать если баз несколько и сразу несколько коннектов к разным базам.
3.Если вы работаете с MySQL то у него у INSERT и UPDATE одинаковый синтаксис.(INSERT INTO table SET a=1,b=2,…
4.
$sql = «UPDATE `users` SET ?set WHERE `id`='$id' LIMIT 1»; 

Надеюсь $id у вас сначала хотя бы intval($id) и второе почему очевидно цифровое значение вставляется как строковое в запрос.

Итог статьи, вы переставили колесо на велосипеде, ехать стало как будто бы приятнее, из за того, что вы убрали «фатальный недостаток», но я думаю вас все равно преследует какое странное чувство что не все в порядке.
0
MaximChistov ,  
можно еще сильнее сократить, тут правда уже читабельность страдает, но для больших форм ничего такого в этом.
$sql = "INSERT INTO `users` (`email`, `password`, `registration`, `userkod`) VALUES (?, SHA1(?), ?, ?)";
 $DB->prepare($sql)->execute([$email,$insert_passwrd,$data,$userkod]);
0
TroL929 ,   * (был изменён)
Можно и так, но он остался таким же усложненным. Я же хотел написать что то на подобии goDB
0
runcore ,  
ну а почему бы вам не пойти еще дальше в размышлениях, и сделать както так:

$data[‘name’] = “Иван”;
$data[‘family’] = “Иванов”;
$retDB = DB::update('users', $data); 

а внутри DB::update() уже смотрите что пришло в первом параметре. если одно слово, то строим сами стандартный запрос на обновление. Если более одного слова = используем переданный запрос.
0
TroL929 ,  
Не совсем понял что Вы имеете ввиду
0
runcore ,  
ну вы чего хотите добиться своими функциями?
как я понял — облегчить себе работу, уменьшить кол-во кода, т.е. всю рутинную работу делать автоматом.
зачем тогда писать постоянно запросы вручную? сделайте чтобы внутри функции запрос строился ВЕСЬ сам. в большинстве случаев это возможно сделать. путей много. можно сделать класс наследник с описанием полей талицы.
в общем автоматизируйте дальше. в ваших вариантах — по прежнему много тупого копипаста. и мало автоматики
0
+1 –1
korzhik ,  
Пожалуйста, никогда так не делайте. Спасибо.
0
+1 –1
TroL929 ,  
А как тогда делать?
0
maximkou ,   * (был изменён)
Интересно, почему вы решили использовать глобальную переменную $DB и отказались от более приемлемых методов?

Вот так к примеру.
0
Delphinum ,  
А может все таки разделить ответственность за создание соединения с БД и за конструирование и посылку запросов к ней на два разных класса? Один будет конструктором подключения (зачем он нужен вообще, почему не создать соединение собственноручно?), который можно даже сделать Singleton, а второй будет конструктором SQL, м?
0
maximkou ,   * (был изменён)
Я полностью с вами согласен, мной просто приведен пример того, как проблему решил кто-то другой — модифицировать его не составит никаких проблем.

P.S. Ответ на SO писался не мной, был найден методом гугления.
0
TroL929 ,  
в комментариях к коду я указал "(можно реализовать несколькими путями, я же создал глобальную переменную)". Я понимаю что это далеко не идеальный вариант но статья не о подключение к БД, а о варианте упрощения запросов к ней
+1
boodda ,  
Ну вы же ничего не упростили. Если бы вы упростили, то любой кто видит код в первый раз сразу бы понял что да как, потому как, чтобы упростить PDO, надо очень постараться.
0
TroL929 ,  
Я упрощал сам запрос к БД, а не код его запроса
–1
zelenin ,  
Теперь запрос выглядит так, намного легче. Правда пришлось отказаться от bindValue, хотя я думаю это не особо критично будет.


тут я заржал
0
TroL929 ,  
это хорошо. Так что там смешного?
+2
zelenin ,  
либо вы защищаете от инъекций, либо нет
0
TroL929 ,   * (был изменён)
Почему я отказался от bindValue? Потому что с ним будет тяжко делать запросы с большими условиями и INNER-ами, а моя цель была упростить запрос
0
zelenin ,  
я не спрашивал «почему». Я лишь прокомментировал, что вы делаете обертку над PDO и не используете его важное преимущество, создав бесполезную обертку. Бесполезную не потому, что она плоха (хотя она плоха), а потому что она уязвима.
0
Aliance ,   * (был изменён)
Рассмотрим вашу функцию select класса DB:

1) зачем-то использована конструкция if/elseif/else, хотя можно было бы упростить до более наглядного switch.
2) значение по-умолчанию пустое, допустимые значения — захардкоденные строки, а не константы.
3) в случае успеха возвращается массив, в случае ошибки — строка, а не кидается исключение.

Все это не ошибки, но вещи, не соотносящиеся в моей голове с понятием best practise. У каждого, конечно, свой code style, но советую вам обратить внимание на мои замечания.

И это лишь по качеству написанного одного метода, не буду повторяться на счет лишних вызовов методов, использование глобальной переменной, да и в целом попытки «прооптимизировать то, что уже итак оптимизировано».
0
TroL929 ,  
Да, с вами я согласен что лучше использовать switch. На счет лишних вызовов методов, думаю я понимаю о чем Вы и сделано это специально, для наглядности. А про прооптимизацию считаю что я добился то что хотел, хоть и не в лучшем качестве.
0
Aliance ,  
Видимо, вы другое поняли под моим «лишних вызовов методов». Имелось ввиду, что вы пишите
$st->setFetchMode(PDO::FETCH_OBJ);

а потом используете
$st->$fetch_fn(PDO::FETCH_ASSOC)


Искренне надеюсь, что это копипаст из какого-то источника, и вы просто использовали функцию, назначение которой не понимаете. Иначе мне сложно объяснить, зачем вы грубо говоря говорите, что дефолтным методом доставания данных будет объектная нотация, а потом все же при доставании данных явно указываете иную.
0
TroL929 ,  
Да, это где то, когда то нашел на просторах интернета когда только узнал про PDO, смущала меня эта строчка, но разобраться в ее «нужности» всё время откладывал
0
SazereS ,   * (был изменён)
С учетом habrahabr.ru/post/242473/, астрологи объявили неделю упрощения запросов к БД. Количество оберток над SQL увеличено вдвое.
0
Aliance ,  
Нужно больше обёрток!
0
TroL929 ,  
Да, я читал статью и увидев ее я решил наконец то дописать свою. Только мне не ясно почему моя так много минусов собирает.
0
zelenin ,  
потому что это третьесортный велосипед, ненужный сообществу
0
SazereS ,  
По моему, в комментариях выше это вполне доступно, да и не единожды, объясняли. Ваша статья не несет полезной информации. Даже наоборот, совсем начинающих она может толкнуть в неправильную сторону.
Я не пытаюсь вас унизить или обидеть, но то, что вы написали, можно смело приводить в качестве примера «как не надо делать».
0
akubintsev ,  
Почему бы сперва не залезть на github или packagist и не посмотреть готовые проверенные временем и сообществом обёртки, прежде чем писать свой велосипед?
0
printercu ,  
gist.github.com/printercu/7b3a7ddf635c5cc3ad09 давно когда-то сделал.
Примеры использования:
$collections = $sql->reset()->select('collections', 'collections.*')
    ->joinOn('brands', 'brands.id = collections.id_brand AND brands.visible')
    ->where('collections.visible')
    ->group('collections.id')
    ->order('brands.fullname, collections.fullname')
    ->fetchAll();
/* ... */
$this->sql->reset()
  ->update( 'postRates' )
  ->set([
    'postRateValue' => $rate,
    'postRateDate'  => $this->date()
  ])
  ->where->eq([
    static::$ID   => $id,
    'id_profiles' => $idProfiles
  ])
  ->execute();


Для всего CRUD вроде хэлперы есть. Если что, могу больше примеров поискать.
0
Delphinum ,  
Я писал вот такую фабрику:
$delete = Delete::getInstance()->table('people')->where('name', '=', 'ivan')->create('OID', '<', '5')->orC('OID', '>', '10')->last('AND')->delete->get();
echo $delete->interpretation('mysql);

Умеет создавать валидный SQL для различных СУБД (DB2, PostgreSQL, MySQL, Firebird, MSSQL, OracleDB).
0
TroL929 ,  
Отличный вариант. Если бы мне карму не испортили то я бы плюсанул.
А вообще это не то что планировал сделать "'postRateValue' => $rate," таких записей сколько вы готовы делать если идет запись с большим количеством данных?
0
printercu ,  
Извините, вопрос не до конца понял.