Хелпер-обертка для INSERT на PDO

Фанат

oncle terrible
Команда форума
Хелпер-обертка для INSERT на PDO

Ну или не только на PDO.
Какие варианты входящих данных предусматривает ваш метод insert?
Достаточно ли в реальной жизни передать только имена и значения (строковые) полей?
Нужны ли какие-то вариации для, скажем, встроенных функций mysql и вообще других типов данных?

Я пытаюсь написать простой хелпер для добавления в базу, не больше 10 строк.
Простой.
Но чтобы брал максимум работы на себя.
Получится ли вообще сделать одной функцией, или надо еще одну для подготовки?

Напишите, плиз, как вы реализуете это дело?
Желательно чтобы это было от рабочей системы, а не придумано на ходу. На ходу я и сам умею... Но хочется, чтобы оно было обстрелянное, и по результатам боёв доведенное.

Это вообще можно сделать без многоэтажных setopt()?

Главное, чтобы оно было простое. Для фака, который совсем про другое.
 

ran

Новичок
Re: Хелпер-обертка для INSERT на PDO

Нашел в старых скриптах свои же примеры, писал для себя простенькую обертку для того, чтобы начать работу с pdo. Строк, конечно не 10, но и не 100.

PHP:
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
$dbh = new PDO("mysql:host=localhost;dbname=tests", "root", "12345");
$dbh -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbh -> exec("SET NAMES UTF8");
$fields = array('id'=>PDO::PARAM_INT, 'some_field'=>PDO::PARAM_INT, 'field2'=>PDO::PARAM_STR);
insert($dbh, 'tests', array('some_field'=>rand(0,22)), $fields);
insert($dbh, 'tests', array('some_field'=>rand(23, 44), 'field2'=>'xxx = '.rand(45, 66)), $fields);
    
    
function insert($dbh, $table, $assoc_data, $fields_list){
	try{
		$query = 'INSERT INTO '.$table.' SET ';
		$values = array();
		$k = 0;
		foreach ($assoc_data as $field => $value){
			if (isset($fields_list[$field])){
				$query .= ($k > 0 ? "," : "") . " `".$field."` = ? ";
				$k++;
			}
			
		}
		$sth = $dbh -> prepare($query);
		$k = 0;
		foreach($assoc_data as $field => $value){
			$k++;
			$sth -> bindParam($k, $value, $fields_list[$field]);
		}
		$sth -> execute();
		$lastId = $dbh -> lastInsertId();
	} catch(PDOException $e){
		throw new Exception($e -> getMessage() . "  " . get_class($this).' -> '.__METHOD__);
	}
	return (int)$lastId;
}
?>
Вкратце:
- есть ресурс подключения dbh,
- $table - название таблицы
- $assoc_data - это массив с данными, где ключ - это поле, а значение - собственно значение вставляемой ячейки
- $fields - это массив, где ключ - название поля, а значение - тип поля в константах PDO.

Впоследствии $dbh перееехал в драйвер для работы с БД, а функции и параметры (названия таблиц, поля и значения) в TableGateway и стало все выглядеть для использования так:

PHP:
<?php
 $gw = new TestsGateway;
 $gw -> bind(array('some_field'=>rand(23, 44), 'field2'=>'xxx = '.rand(45, 66)));
 $gw -> insert();
?>
но это уже другая история...
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
>Нужны ли какие-то вариации для, скажем, встроенных функций mysql и вообще других типов данных?

мне для постгреса бывает нужно - массивы, кастомные типы

я юзаю ф-ю, которая получает массив (поле=>значение) и готовит часть запроса после имени таблицы:
$sql = 'insert into table '.$this->DB->prepareRowTypes($data, $expected,'insert');
удобно для работы с данными, полученными из форм - в массиве $data могут быть ненужные поля

в простых случаях тип ставится по типу значения элемента в $data, а попадает туда из filter_input (строгая типизация непривычна, но удобна)
 

Фанат

oncle terrible
Команда форума
Ну вот да. для mysql у меня есть похожее.

PHP:
$id = intval($_POST['id']);
$fields = explode(" ","name surname lastname address zip fax phone");
$_POST['date'] = $_POST['y']."-".$_POST['m']."-"$_POST['d'];
$query  = "UPDATE $table SET ".dbSet($fields)." stamp=NOW() WHERE id=$id";

function dbSet($fields) {
  $set='';
  foreach ($fields as $field) {
    if (isset($_POST[$field])) {
      $set.="`$field`='".mysql_real_escape_string($_POST[$field])."', ";
    }
  }
  return substr($set, 0, -2); 
}
такая тупая ручная сборка, которая, тем не менее, позволяет довольно эффективно управляться с запросами на вставку/

Но ПДО, что удивительно(!), нам при этом УСЛОЖНЯЕТ задачу! :)
Поскольку кроме prepare есть еще и execute, которому надо передать массив данных, который тоже надо сформировать - $_POST ему не подпихнешь.
Вот сиди и думай, как делать, блин.
На стаковерфлоу чувак так мне и предложил, т.е. в твоем случае это будет типа
PHP:
$this->DB->prepareRowTypes($data, $expected,'insert',&$values);
где в $values формируется массив для, собственно, execute

еще вариант - сделать конкретно $this->DB->insert
и разрешить передавать в него куски сырого запроса. Но это блин уже ваще...
 

zerkms

TDD infected
Команда форума
Эм, а зачем вообще в такие методы сувать прям $_POST?
Почему бы $_POST не пропустить через валидатор, который на выходе выдаст нам чистый и правильный массив?
 

Фанат

oncle terrible
Команда форума
В данном случае речь не о "почему бы не", а о том, что без такой валидации в принципе невозможно обойтись :)
в случае со старым мускулем мы должны получить только строку, а с ПДО - строку И массив
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
>&$values
да, вариант, или костыль

zerkms
$data - массив с провалидированными данными из поста и сессии
его удобно отправлять подряд нескольким запросам подряд.
упрощенно:
$data['order_id'] = addOrder($data);
addOrderQueue($data);

function addOrderQueue(){
$expected = array('order_id'=>PDO::INT,...)
$sql = 'insert ...'. $DB->prepareRowTypes($data, $expected,'insert',&$values);
$DB->exec($sql,$values);
}
 

zerkms

TDD infected
Команда форума
я не понимаю о чём вы говорите :-S

мой недавно реализованный интерфейс поверх oci_ выглядит примерно так:

::insert($table, array $fields)

где $table - имя таблицы
array $fields - массив вида field => $value

Массивом я передаю УЖЕ ОТВАЛИДИРОВАННЫЕ И С ПРИМЕНЕНИЕМ ФИЛЬТРОМ данные. А вот зачем вы придумываете себе проблемы и пишете функции dbSet, которые работают с базой и знают о существовании $_POST мне категорически непонятно.
 

zerkms

TDD infected
Команда форума
$post = Validate::factory($_POST)->rule('a', 'numeric')->rule('b', 'not_empty');

if ($post->check())
$db->insert('table', $post);

(kohana)

ps: валидатор реализует интерфейс ArrayAccess (и некоторые другие) - так что в данном случае можем передавать прям объект валидатора.
 

Фанат

oncle terrible
Команда форума
мнда
получается, что как там реализовано внутри - плейсхолдерами или прямой подстановокой - никакого значения не имеет...

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

Вурдалак

Продвинутый новичок
PHP:
public function insert($tbl, $data)
{
    // ...

    foreach($data as $field => $value) {
        if( $value instanceof Database_Expr ) {
            $set .= "`{$field}` = {$value}";
        } else {
            $set .= "`{$field}` = '" . $this->quote($value) . "'";
        }
    }

    // ...
}
?
 

Mols

Новичок
хм.
Я в общем то не ленюсь делать так.
PHP:
db::execute('INSERT INTO '.$this->getTable() 
                .' (`description`, `angle`, `time_marker`, )'
                .' VALUES(?,       RADIANS(?),  NOW())'
                , array(
                    $sText,
                    $dAngle)
                , array(db::PARAM_STRING,  db::PARAM_DOUBLE)
           );
сам метод
PHP:
/**
* Выполняет запрос.
* 
* Парметр $query - это запрос с плейсхолдерам (плейсхолдер - знак вопроса. То есть "?"). 
* Вместо плейсхолдеров будут подставлены значения из  $arrVars.
*  При подстановке значений будет попытка определить тип подставляемой переменной. 
* Для этого используется массив $arrTypes. 
* (Если в массиве $arrTypes есть элемент с таким же ключем как и в $arrVars,
*  то значение этого элемента будет интерпретироваться как тип соответвующего элемента из $arrVars)
*  
* @param string
* @param array
* @param array
* 
* @return mysqli_result  
*/           
public static function execute($query, $arrVars=array(), $arrTypes = null)
{
    if(!$query) throw new Exception('Пустой запрос');
    $queryAsArray =  explode('?', $query);
    $queryParams = count($queryAsArray) - 1;
    if($queryParams != count($arrVars))
        throw new Exception("Ожидаемое количество параметров не соответсвует существующему. $queryParams != ".count($arrVars));
    $paramNum = 0;
    $queryForExecute = $queryAsArray[0];
    // Склеиваем запрос и подставляем значения
    if($arrVars && is_array($arrVars)){
        foreach($arrVars as $kV  =>$V){
             $queryForExecute .= self::escape($V,($arrTypes && is_array($arrTypes) && isset($arrTypes[$kV]) ? $arrTypes[$kV] : db::PARAM_STRING)).$queryAsArray[++$paramNum];     
        }
    }
    self::getConnection()->real_query($queryForExecute);
    if(self::getConnection()->errno)
        throw new Exception("Ошибка при выполнении запроса \n $queryForExecute \n ".self::getConnection()->error); 	
    return self::getConnection()->store_result();			
}
Все вопросы с функциями и выражениями решаются довольно просто.
PHP:
db::execute('UPDATE '.$this->getTable()
            .' SET `comments_count` = ? '
            .',`last_comment_time_marker` = IF( `last_comment_time_marker` < NOW(), ?, `last_comment_time_marker`)'
            .' WHERE id = ?'
            , array(
                $iCount
                ,$tTime
                ,$iItemId
            )
            , array(
                db::PARAM_INT
                ,db::PARAM_STRING
                ,db::PARAM_INT
            )
Понятно, что в запросе поля написаны ручками...
Если есть желание можно конечно их передавать как ключи здесь $arrVars.
Но тогда нужно, чтобы модель много знала о полях. Набор этих полей, что можно не ставить в запрос их тип и прочие штуки...
Но если это всё надо - Доктрину вам в руки.
Если надо попроще, я остановился на таком варианте.
мне очень удобно.
Внутрениий метод escape принимает значение и параметр константу по которому обрабатывается тип значения.
Туда можно запросто добавить что угодно.
Хоть db::pARAM_ARRAY или db::pARAM_VECTOR... ну или что захочется.
Если не найден тип - обрабатываю как строку.
Исключение одно.
PHP:
    public static function escape($value, $type)
    {
        if(is_null($value)) return 'NULL';
......
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Автор оригинала: zerkms
$post = Validate::factory($_POST)->rule('a', 'numeric')->rule('b', 'not_empty');

$db->insert('table', $post);
проблема в том, что указание типов данных бывает обязательно
пример - в соседней ветке, где дата рассматривается как строка, и поведение различается
другой пример - финансовые операции

и тем более это касается pg, с кастомными типами и традицией явного указания
 

findnext

Новичок
а у меня валидация данных происходит отдельно, а метод инсерт очень похож на метол товарища run
 

Фанат

oncle terrible
Команда форума
Я все думаю над этой фигней.
Отчаянно не хватает спарринг-партнера идеи проверить.

Проблему с тем, что передаваемый в функцию массив нужно специально готовить, я, кажется, решил.
вместо передачи подготовленного массива в execute, мы делать bindValue в цикле, пробегая по массиву $expected.

но сделать все с помощью только одной функции не получается.
только если делать макроподстановку в запрос, типа
PHP:
$db->run("INSERT INTO $table SET :SET: WHERE id=:id",$expected,&$data);
но смысла особого делать так нет. Был бы смысл, если бы оно унифицировалось с SELECT-ом, но селекту не нужен массив $expected.
Ну или то же самое с хелпером
PHP:
$set = prepareSET($expected);
$db->run("INSERT INTO $table SET $set",$expected,&$data);
вообще, если задавать $expected отдельно, то получится унифЕкация.
PHP:
$db->expected = array("name","pass");
$db->query("INSERT INTO $table SET $set",&$data);
а при отсутствии expected брать его из table schema.
но тогда статикой такое не вызвать.
и все равно надо обозначать, что вызываем именно insert. Тогда запрос писать вообще не имеет смысла.

Уж очень не хочется совсем SQL прятать. Я понимаю, что поклонниками ормов это противно слышать, но блин читабельность и гибкость SQL - тоже не хрен собачий.

или забить на все хелпервы писать тупо руками.

-~{}~ 02.10.10 14:49:

На самом деле все наоборот.
Вот как раз специальная подготовка массива с данными и решает проблему.

То есть если данных много и лень писать сет руками, по используем с минимальными изменениями тот же хелпер,
PHP:
function pdoSet($fields, &$values, $source = array()) {
  $set = '';
  $values = array();
  if (!$source) $source = &$_POST;
  foreach ($fields as $field) {
    if (isset($_POST[$field])) {
      $set.="`$field`=:$field, ";
      $values[$field] = $source[$field];
    }
  }
  return substr($set, 0, -2); 
}
и в итоге получается практически то же самое, что и раньше
если делать голым апи, то получается вот так
PHP:
$fields = array("login","password");
$_POST['password'] = MD5($_POST['login'].$_POST['password']);
$stmt = $dbh->prepare("UPDATE users SET ".pdoSet($fields,$values)." WHERE id = :id");
$values["id"] = $_POST['id'];
$stmt->execute($values);
для сравнения, как по-старинке:
PHP:
$id = intval($_POST['id']);
$fields = array("login","password");
$_POST['password'] = MD5($_POST['login'].$_POST['password']);
$sql = "UPDATE users SET ".dbSet($fields,$values)." WHERE id = $id";
mysql_query($sql) or trigger_error(mysql_error().$sql);
В принципе, завернуть это дело в отдельный метод можно, но тут вопрос - нужно ли
 

Mols

Новичок
Столкнётесь с проблемой когда надо будет делать
UPDATE users SET field = function(param1, param2,.... paramN)

-~{}~ 02.10.10 23:33:

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

grigori

( ͡° ͜ʖ ͡°)
Команда форума
долго думал и перечитывал :)

>Проблему с тем, что передаваемый в функцию массив нужно специально готовить
imho или orm, или данные специально готовим, иначе немножко беременные,
общее решение 1й строкой создаст больше проблем, чем пользы

pdoSet() не делает валидацию данных, а мне она нужна

>при отсутствии expected брать его из table schema
у меня в этом редко есть смысл, т.к. id - автоинкрементный, поле времени заполняться должно само, и т.д.

-~{}~ 04.10.10 01:28:

Mols
с хранимыми процедурами все в принципе намного проще
$set = array(
$DBh->quote($_POST['field1']),
int_val($_POST['field2']),
isset($_POST['field1'])?$DBh->quote($_POST['field1']):'NULL',
);
$sql = 'update users SET field = ('.implode(',',$set).')';

>типов данных чето я не заметил
убрали из рассмотрения для краткости

-~{}~ 04.10.10 01:55:

вот валидацию и подготовку данных можно автоматизировать, но может быть нужна схема сопоставления имен полей на имена таблицы
PHP:
$manager = updatePassFormManager($ValidatedPostData);

$stmt = $dbh->prepare("UPDATE users SET ".$manager->pdoSet()." WHERE id = :id");
$stmt->execute($manager);
//или
$manager->bindWithTypes($stmt);
$stmt->execute();

class updatePassFormManager extends FormFieldMapper /* implements ArrayAccess */
{
$fields = array("login"=>PDO::PARAM_STR,"password"=>PDO::PARAM_STR);
function getOffset($key){
    switch($key ) {
        case 'password': 
            return md5(parent::getOffset('login').parent::getOffset('password') ;
        default: return parent::getOffset($key);
    }
}
}
 
Сверху