Обработка сложных предложений WHERE с помощью PHP Query Builder

Существует несколько библиотек построителей запросов 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:

В ходе моего вопроса я понял, что ГДЕ и УСЛОВИЯ ОБСТОЯТЕЛЬСТВА только усложняются, чем глубже вы идете. Попытка абстрагировать даже 80% функций приведет к массивной библиотеке только для WHERE и HAVING. Как отмечает Билл, это просто не разумно для языка сценариев, такого как PHP.

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

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

Ответ 1

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

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

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

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

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

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


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

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

Ответ 2

Это часть моего класса 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().

Ответ 3

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

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

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

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

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

Библиотеки, которые я написал до сих пор, составляют около 600 строк кода (~ 170 строк - обработка ошибок). Он касается объединений ISO, подзапросов (в предложениях select, FROM и where), любое предложение 2-стороннего сравнения, 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') 
      )

Наряду с массивом bind:

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)

Ответ 4

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

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

Ответ 5

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

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