Обработка сложных предложений WHERE с помощью PHP Query Builder - PullRequest
11 голосов
/ 18 декабря 2009

Существует несколько библиотек построителей запросов в стиле ActiveRecord. Некоторые автономны , а некоторые встроены в фреймворки . Однако у них действительно возникают проблемы с предложениями WHERE и HAVING, когда речь идет о сложном SQL. Оставляя в стороне другие базы данных - я пытаюсь найти метод WHERE (), совместимый с MySQL и PostgreSQL, который мог бы исправить эти текущие ошибки метода.

Ниже приводится длинный список идей и примеров, показывающих лучшее, что я смог придумать. Тем не менее, я не могу решить все варианты использования , и я чувствую, что мое частичное решение небрежно. Любой, кто может ответить чем-то, что решит все эти проблемы, не только ответит на этот вопрос, но и будет отвечать за исправление проблемы, которая преследовала реализации PHP в течение нескольких лет.

Общие операторы

    =   Equal
    <>  Not Equal
    >   Greater Than
    <   Less Than
    >=  Greater Than Or Equal
    <=  Less Than Or Equal
    BETWEEN between values on right 
    NOT logical NOT 
    AND logical AND 
    OR  logical OR

Пример, где оговорки

SELECT ... FROM table...
    WHERE column = 5
    WHERE column > 5
    WHERE column IS NULL
    WHERE column IN (1, 2, 3)
    WHERE column NOT IN (1, 2, 3)
    WHERE column IN (SELECT column FROM t2)
    WHERE column IN (SELECT c3 FROM t2 WHERE c2 = table.column + 10)
    WHERE column BETWEEN 32 AND 34
    WHERE column BETWEEN (SELECT c3 FROM t2 WHERE c2 = table.column + 10) AND 100
    WHERE EXISTS (SELECT column FROM t2 WHERE c2 > table.column)

Существует много распространенных форматов ActiveRecord, которые предложение where () использует в различных текущих библиотеках.

$this->db->where(array('session_id' => '?', 'username' => '?'));
$this->db->fetch(array($id, $username));

// vs with is_int($key)
$this->db->where(array('session_id', 'username'));
$this->db->fetch(array($id, $username));

// vs with is_string($where)
$this->db->where('session_id', '?');
$this->db->where('username');
$this->db->fetch(array($id, $username));

// vs with is_array($value)
$this->db->where('session_id', '?');
$this->db->where('username', array('Sam', 'Bob'));
$this->db->fetch(array($id));

Вот последний формат, который у меня есть. Он должен обрабатывать группу (...) AND (...), а также подготовленные параметры, связанные с оператором ("?" & ": Name").

function where($column, $op = '=', $value = '?', $group = FALSE){}


// Single line

$this->db->where('column > 5');
$this->db->where('column IS NULL');

// Column + condition

$this->db->where('column', '=');
// WHERE column = ?     (prepared statement)
$this->db->where('column', '<>');
// WHERE column <> ?    (prepared statement)

// Column + condition + values

$this->db->where('column', '=', 5);
// // WHERE column = 5
$this->db->where('column', 'IN', '(SELECT column FROM t2)');
// WHERE column IN (SELECT column FROM t2)
$this->db->where('column', 'IN', array(1,2,3));
// WHERE column IN (1, 2, 3)
$this->db->where('column', 'NOT IN', array(1,2,3));
// WHERE column NOT IN (1, 2, 3)

// column + condition + values + group
$this->db->where(
    array(
        array('column', '<', 20), 
        array('column', '>', 10)
    ),
    NULL,
    NULL,
    $group = TRUE
);
// WHERE (column < 20 AND column > 10)

: UPDATE:

В ходе моего вопроса я понял, что условия WHERE и HAVING усложняются только по мере углубления. Попытка абстрагировать даже 80% функций приведет к огромной библиотеке только для WHERE и HAVING. Как указывает Билл, это просто неразумно для языка сценариев, такого как PHP.

Решение состоит в том, чтобы просто обработать ГДЕ часть вашего запроса. Пока вы используете " вокруг своих столбцов, вы можете использовать один и тот же запрос WHERE в Postgre, SQLite и MySQL, поскольку они используют почти одинаковый синтаксис SQL. (Для MySQL вы должны str_replace() отметить их галочкой).

Наступает момент, когда абстракция причиняет больше вреда, чем помогает, ГДЕ условия являются одним из таких мест.

Ответы [ 5 ]

8 голосов
/ 18 декабря 2009

Я немного поработал над библиотекой Zend_Db, которая включает в себя класс PHP для построения SQL-запросов . Я решил попытаться обработать каждый мыслимый синтаксис SQL в предложениях WHERE и HAVING по нескольким причинам:

  • PHP - это язык сценариев, который анализирует и компилирует код при каждом запросе (если только вы не используете кэш байт-кода). Таким образом, среда PHP чувствительна к громоздким библиотекам кода - в большей степени, чем Java, C #, Python или что-то еще. Поэтому очень важно, чтобы библиотеки были как можно меньше.

    Вся библиотека Zend_Db, над которой я работал, содержала около 2000 строк PHP-кода. Напротив, Java Hibernate имеет порядок 118 тыс. Строк кода. Но это не такая уж большая проблема, поскольку библиотека Java предварительно скомпилирована и ее не нужно загружать при каждом запросе.

  • Выражения SQL следуют за генеративной грамматикой, которая более компактна, проще для чтения и поддержки, чем любая из построенных вами конструкций на основе PHP. Изучение грамматики выражений SQL намного проще, чем изучение API, который может имитировать ее. В итоге вы поддерживаете «упрощенную грамматику». Или же вы начнете таким образом, и ваше сообщество пользователей заставит вас Feature Creep , пока ваш API не станет необычайно сложным.

  • Для отладки приложения, использующего такой API, вам неизбежно понадобится доступ к окончательному выражению SQL, так что речь идет о самой протекающей абстракции , которую вы можете иметь.

  • Единственное преимущество использования интерфейса на основе PHP для выражений SQL состоит в том, что он помогает выполнять автозавершение кода в интеллектуальных редакторах и IDE. Но когда так много операторов и операндов используют строковые константы, такие как '>=', вы портите любой интеллект завершения кода.


обновление: Я только что прочитал хорошую статью в блоге " Прощание с ORM ." Автор, Альдо Кортези, предлагает использовать язык выражений SQL в SQLAlchemy Python. Синтаксический сахар и перегрузка операторов, стандартная в Python (но не поддерживаемая в PHP), делают это очень эффективным решением для генерации запросов.

Вы также можете посмотреть на DBIx :: Class в Perl, но в итоге он выглядит довольно уродливо.

2 голосов
/ 14 мая 2012

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

После изучения я обнаружил, что проблема с Zend-Db и другими подобными движками заключается в том, что они пытаются быть всем для всех. Чтобы обратиться к самой большой аудитории, им нужно предложить наиболее общую функциональность, которая, насколько я вижу, сама по себе уничтожает их (и, как мастерски объяснил Билл Карвин).

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

При создании движка SQL первое, что нужно сделать, это ограничить область действия SQL, которую может создать ваш движок. Вы не должны позволять ему производить select * from table например; движок должен требовать от разработчика явного определения каждого столбца select, where и having. В качестве другого примера, часто бывает полезно, чтобы у каждого столбца был псевдоним (обычно это не требуется для базы данных).

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

Библиотеки, которые я написал до сих пор, содержат около 600 строк кода (~ 170 строк из которых обрабатывают ошибки). Он имеет дело с соединениями ISO, вложенными операторами (в предложениях SELECT, FROM и WHERE), любыми предложениями двустороннего сравнения, IN, EXISTS и BETWEEN (с вложенными операторами в предложение WHERE). Он также неявно создает привязки, вместо непосредственного введения значений в SQL.

Ограничения (кроме уже упомянутых): SQL написан специально для Oracle. Не проверено на любой другой платформе базы данных.

Я готов поделиться кодом, при условии, что все улучшения отправлены обратно.

В качестве примера того, что позволяют мне создавать библиотеки, я надеюсь, что следующее достаточно просто, чтобы быть интуитивно понятным, и в то же время достаточно сложным, чтобы продемонстрировать потенциал расширяемости:

<?php
$substmt = new OraSqlStatement;
$substmt->AddVarcharCol ('value','VALUE')
        ->AddVarcharCol ('identity','UID',false)
        ->AddVarcharCol ('type','info_type',false)
        ->AddFrom ('schemaa.user_propertues','up')
        ->AddWhere ('AND')
        ->AddComparison ('UID', '=', 'e.identity', 'column')
        ->AddComparison ('info_type', '=', 'MAIL_ADDRESS');

$stmt = new OraSqlStatement;
$stmt->AddVarcharCol ('company_id', 'Company')
     ->AddVarcharCol ('emp_no',     'Emp Id')
     ->AddVarcharCol ('person_id',  'Pers Id')
     ->AddVarcharCol ('name',       'Pers Name')
     ->AddDateCol ('employed_date', 'Entry Date')
     ->AddDateCol ('leave_date', 'Leave Date')
     ->AddVarcharCol ('identity',   'User Id')
     ->AddVarcharCol ('active', 'Active')
     ->AddVarcharCol ($substmt, 'mail_addy')
     ->AddFrom ('schemab.employee_tab', 'e')
     ->AddFrom ('schemaa.users_vw','u','INNER JOIN','u.emp_no=e.emp_number')
     ->AddWhere ('AND')
     ->AddComparison ('User Id', '=', 'my_user_id')
     ->AddSubCondition ('OR')
     ->AddComparisonNull ('Leave Date', false)
     ->AddComparisonBetween ('Entry Date', '2011/01/01', '2011/01/31');

echo $stmt->WriteSql();
var_dump($stmt->GetBindArray());
?>

Который производит:

SELECT 
  company_id "Company", emp_no "Emp Id", person_id "Pers Id", name "Pers Name", 
  employed_date "Entry Date", leave_date "Leave Date", identity "User Id", active "Active", 
  ( SELECT value "VALUE" FROM schemaa.user_propertues up 
    WHERE  upper(identity) = upper(e.identity)
      AND  upper(TYPE) = upper (:var0) 
  ) "mail_addy" 
FROM 
  schemab.employee_tab e 
      INNER JOIN schemaa.users_vw u ON u.emp_no = e.emp_number 
WHERE 
        upper (identity) = upper (:var1)
  AND ( leave_date IS NOT NULL OR
        employed_date BETWEEN to_date (:var2,'YYYY/MM/DD') AND to_date (:var3,'YYYY/MM/DD') 
      )

Вместе с массивом связывания:

array
  0 => string 'MAIL_ADDRESS' (length=12)
  1 => string 'my_user_id' (length=10)
  2 => string '2011/01/01' (length=10)
  3 => string '2011/01/31' (length=10)
2 голосов
/ 18 декабря 2009

Это часть моего класса ActiveRecord, я не обрабатываю подзапросы (я даже не беспокоюсь):

public function Having($data, $operator = 'LIKE', $merge = 'AND')
{
    if (array_key_exists('query', $this->sql) === true)
    {
        foreach ($data as $key => $value)
        {
            $this->sql['having'][] = ((empty($this->sql['having']) === true) ? 'HAVING' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value);
        }
    }

    return $this;
}

public function Where($data, $operator = 'LIKE', $merge = 'AND')
{
    if (array_key_exists('query', $this->sql) === true)
    {
        foreach ($data as $key => $value)
        {
            $this->sql['where'][] = ((empty($this->sql['where']) === true) ? 'WHERE' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value);
        }
    }

    return $this;
}

Еще одна вещь, которую вы можете рассмотреть, - это методы customHaving () и customWhere ().

1 голос
/ 18 декабря 2009
API

SQLAlchemy - лучший, с которым я когда-либо работал. Это библиотека Python, но вы все равно можете вдохновиться ею. Это не только для WHERE-предложений - весь SQL-запрос (будь то выбор или DML) выражается структурой данных, которую легко изменить.

(я имею в виду его SQL-инструментарий, а не ORM-части.: -)

0 голосов
/ 09 декабря 2014

Вы можете рассмотреть SQLBuilder, написанный на PHP, и он может генерировать кроссплатформенный SQL для MySQL и PostgreSQL, устанавливая разные драйверы запросов.

Вариант использования здесь: https://github.com/c9s/SQLBuilder/blob/2.0/tests/SQLBuilder/Query/SelectQueryTest.php

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...