Создание приложения для нескольких SQL-серверов

autosoft

Guest
Создание приложения для нескольких SQL-серверов

Каждый сервер имеет свои особенности в использовании SQL. Например, в DML: способ формирования соединений таблиц, способ формирования порционных выборок и т.д.

Предположим, что доступ к серверу осуществляется путем использования ADODB или подобным ему. В статье "Абстрактный доступ к БД с помощью ADODB" опубликованной в PHP Inside №2 говорится, что: "Если бы Вы использовали абстрактный класс доступа к БД - Вам, скорее всего, не пришлось бы менять php-код (только в одном месте указать что используется PostgreSQL). Менять SQL-запросы все равно придется, но и это не всегда необходимо".

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

Идея не нова. Предлагаю её реализацию применительно к PHP.

Например, следующий программный код для PHP 5.0.5 позволит решить такую задачу:

PHP:
$sql =
"SELECT A.*, B.*
FROM
#if (informix)
A, OUTER B WHERE A.ID = B.A_ID
#else
A LEFT JOIN B ON A.ID = B.A_ID
#endif
ORDER BY 1";

$sql_parser = new t_sql_parser("informix");
echo $sql_parser->parse($sql), "<br>";

$sql_parser = new t_sql_parser("firebird");
echo $sql_parser->parse($sql), "<br>";
Результатом выполнения будет:

SELECT A.*, B.* FROM A, OUTER B WHERE A.ID = B.A_ID ORDER BY 1
SELECT A.*, B.* FROM A LEFT JOIN B ON A.ID = B.A_ID ORDER BY 1

Класс t_sql_parser:

PHP:
class t_sql_parser {

    private $f_server_string;
    private $f_system_char;
    private $f_separator_char;
    private $f_index;
    private $f_length;
    private $f_sql;
    private $f_sql_result;

    function __construct($server_string) {

        $this->f_server_string = $server_string;

        $this->f_system_char = array(" ", "(", ")", "+", "-", "/", "*", "&",
        "|", "[", "]", ",", ".", "#", "'", "!", "\"", "\r", "\n");

        $this->f_separator_char = array(" ", "\r", "\n");
    }

    function parse($sql) {

        $this->f_sql = $sql;
        $this->f_sql_result = "";
        $this->f_index = 0;
        $this->f_length = strlen($sql);

        while (strlen($word = $this->word()))
        switch ($word) {

            case "#if":
                $this->parse_if(true);
                break;

            case "#else":
                throw new Exception("Ошибка использования #else");
                break;

            case "#endif":
                throw new Exception("Ошибка использования #endif");
                break;

            default:
                $this->f_sql_result .= $word;
        }

        return $this->f_sql_result;
    }

    private function parse_word($word, $need) {

        if (!strlen($word))
        throw new Exception("Отсутствует '$need'");

        if (strcmp($word, $need))
        throw new Exception("Вместо '$need' указано '$word'");
    }

    private function parse_if($enabled) {

        while (in_array($word = $this->word(), $this->f_separator_char));
        $this->parse_word($word, "(");

        $param = $this->parse_if_param();

        while (in_array($word = $this->word(), $this->f_separator_char));
        $this->parse_word($word, ")");

        while (strlen($word = $this->word())) // #if
        switch ($word) {

            case "#if":
                $this->parse_if($enabled && $param);
                break;

            case "#else":
                break 2;

            case "#endif":
                return;

            default:
                if ($enabled && $param) $this->f_sql_result .= $word;
        }

        while (strlen($word = $this->word())) // #else
        switch ($word) {

            case "#if":
                $this->parse_if($enabled && !$param);
                break;

            case "#else":
                throw new Exception("Ошибка использования #else");
                break;

            case "#endif":
                return;

            default:
                if ($enabled && !$param) $this->f_sql_result .= $word;
        }

        throw new Exception("Отсутствует конструкция #endif");
    }

    private function parse_if_param() {

        while (in_array($word = $this->word(), $this->f_separator_char));

        if (!strcmp($word, "(")) {

            $result = $this->parse_if_param();

            while (in_array($word = $this->word(), $this->f_separator_char));
            $this->parse_word($word, ")");

        } else
        if (!strcmp($word, "!")) $result = !$this->parse_if_param();
        else {

            if (!strcmp($word, "||") || !strcmp($word, "&&") ||
            in_array($word, $this->f_system_char))
            throw new Exception("Ошибка использования $word");

            $result = !strcmp($this->f_server_string, $word);
        }

        $index_save = $this->f_index;

        while (in_array($word = $this->word(), $this->f_separator_char));
        switch ($word) {

            case "||":
                $result = $result | $this->parse_if_param();
                break;

            case "&&":
                $result = $result & $this->parse_if_param();
                break;

            default:
                $this->f_index = $index_save;
        }

        return $result;
    }

    private function item() {

        $result = "";

        if ($this->f_index < $this->f_length) {

            while (($this->f_index < $this->f_length) &&

            (!in_array($char = $this->f_sql{$this->f_index}, $this->f_system_char))) {

                $result .= $char;
                $this->f_index++;
            }

            if (!strlen($result)) {

                $result = $char;
                $this->f_index++;
            }
        }

        return $result;
    }

    private function word() {

        switch ($result = $this->item()) {

            case "#":
                if (strcmp($next = $this->item(), "#")) $result .= $next;
                break;

            case "|":
                $index_save = $this->f_index;
                if (!strcmp($this->item(), "|")) $result = '||';
                else $this->f_index = $index_save;
                break;

            case "&":
                $index_save = $this->f_index;
                if (!strcmp($this->item(), "&")) $result = '&&';
                else $this->f_index = $index_save;
                break;
        }

        return $result;
    }
}
В целом синтаксическую конструкцию использования такого "препроцессора" можна описать следующим образом:

запрос
[#if (перечисление_серверов)
запрос
[#else
запрос]
#endif]
запрос

перечисление_серверов ::= [!] имя_сервера [|| перечисление_серверов | && перечисление_серверов]

"Служебными" являются конструкции: #if, #else, #endif. Все остальные игнорируются.
Служебные конструкции являются регистрозависимыми.

Конструкция #if ... #endif может указываться в любом месте запроса.
Уровень вложенности конструкций #if ... #endif неограничен.

Особенность: если в запросе требуется явное использование символа '#' то его необходимо дублировать.

Пока могу отметить только одно возможное улучшение - PHP это компилируемо-транслируемый язык, поэтому если такой препроцессор SQL-запросов реализовать в виде расширения PHP, можно существенно повысить скорость окончательной подготовки SQL-запроса.

Какие ещё преимущества и недостатки есть у такого подхода?
 

vitus

мимо проходил
проще можно быть, товарищ:

$sql['informix']="SELECT A.*, B.* FROM A, OUTER B WHERE A.ID = B.A_ID ORDER BY 1";

$sql['default']="SELECT A.*, B.* FROM A LEFT JOIN B ON A.ID = B.A_ID ORDER BY 1";

но всёравно - малаца!
 

filter

Новичок
С преимуществом понятно - обеспечить поддержку разных диалектов.
Думаю с самого начала надо подумать, а наступит ли когда-нибуть такой день, что надо будет посадить проект на другую базу.

Недостатки на мой взгляд по реализации - усложняется код. По производительности - надо смотреть и мерять.

Можно попробовать например такое вариант.
1) Использовать абстрактный доступ к БД
2) Вынести сами запросы из кода, и реализовать что-то на подобие словаря с таким интерфейсом.
$sql = $dictionary->getQuery("SELECT_CANDIDATE_QUERY");

3) Проинициализировать словарь нужными данными при старте, + указать какую реализацию брать для абстрактного доступа.
 

svetasmirnova

маленький монстрик
>если такой препроцессор SQL-запросов реализовать в виде расширения PHP, можно существенно повысить скорость окончательной подготовки SQL-запроса.

pdo смотрел?
 

filter

Новичок
svetasmirnova

PDO provides a data-access abstraction layer, which means that, regardless of which database you're using, you use the same functions to issue queries and fetch data. PDO does not provide a database abstraction; it doesn't rewrite SQL or emulate missing features. You should use a full-blown abstraction layer if you need that facility.
 

BeGe

Вождь Апачей, блин (c)
Напиши один класс для работы с данными, забудь что у тебя есть mysql_query или pdo->query

Но у тебя есть методы
$data->getUsers
$data->updateUser($userid, array $user_data)

Это как вариант.... для средних и маленьких приложений
 

autosoft

Guest
vitus
Спасибо :)

filter
По-поводу SELECT_CANDIDATE_QUERY.
Я как раз думал над подобным решением. Идея с внешним словарем хорошая. Только вопрос возникает - где его хранить и как к нему обращаться. Если вдуматься, то в таком словаре могут храниться абсолютно все возможные запросы. Например, у меня большой проект (система учета хозяйственной деятельности предприятия) и я даже затрудняюсь сказать сколько там таких запросов (больше 1000 точно). Теперь самое главное – для каждого отдельного обращения нужны, как правило, всего 2 - 3 запроса – остальные не нужны вовсе. Но загружать придется весь словарь, а это время. Есть идеи как данный процесс оптимизировать?

svetasmirnova
Спасибо за подсказку о PDO. Насколько я понимаю это аналог dbExpress в Delphi. С такими вещами переход с одного сервера на другой выполняется на порядок легче. Я посмотрел документацию к PDO, но ничего о "совмещении" диалектов SQL там не нашел. Пришел к выводу о возможности добавить к PDO описанный выше класс t_sql_parser.

Вот что получилось:
PHP:
class ext_PDO extends PDO {

    private $sql_parser;

    function ext_PDO($dsn, $username = "", $password = "", $driver_options = array()) {

        if (is_array($server_string = split(":", $dsn, 2)) &&
            count($server_string) == 2)
        $this->sql_parser = new t_sql_parser($server_string[0]);
        else throw new Exception("Ошибка определения сервера");

        parent::__construct($dsn, $username, $password, $driver_options);
    }    

    function exec($statement) {
        return parent::exec($this->sql_parser->parse($statement));
    }

    function query($statement) {
        return parent::query($this->sql_parser->parse($statement));
    }

    function prepare($statement, $driver_options) {
        return parent::prepare($this->sql_parser->parse($statement), $driver_options);
    }
}
А также описанный выше пример:
PHP:
$dsn = "firebird:dbname=localhost:base.gdb";
$user = "SYSDBA";
$password = "masterkey";

if ($dbh = new ext_PDO($dsn, $user, $password)) {

    $dbh->setAttribute(PDO_ATTR_ERRMODE, PDO_ERRMODE_EXCEPTION);

    $sql = "SELECT A.*, B.* 
            FROM 
            #if (informix) 
            A, OUTER B WHERE A.ID = B.A_ID 
            #else 
            A LEFT JOIN B ON A.ID = B.A_ID 
            #endif 
            ORDER BY 1";

    foreach ($dbh->query($sql) as $row) ...
}
Или все таки в PDO есть свои варианты?

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

svetasmirnova

маленький монстрик
>Или все таки в PDO есть свои варианты?
Да я в PDO особо не играла. Информацию см. в архивах листа pecl-dev
 

BeGe

Вождь Апачей, блин (c)
autosoft запутаться будет очень сложно, потому что ты будешь работать не с базой данных, а с данными, хотя тогда становиться вопрос, я работаю с данными а нужен класс для работы с базой данных, что бы работать с данными.

Я поставил условие работе в малых и срудних приложениях, где у тебя идёт до 10 объектов например, тогда для работы с данными тебе надо будет 30 + N методов, на каждый объект добавить, удалить, обновить. И n методов для сложных выборок, когда надо получить одновременно данных о 2 и более объектах.

А потом можно подумать и сделать ещё проще.... чаще всего у тебя будут запросы простого селекта, тогда написать одну функцию - select($table, array $fields); delete($table,string $where); update($table, array $fields, array $data, string $where)

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

filter

Новичок
Автор оригинала: autosoft
Теперь самое главное – для каждого отдельного обращения нужны, как правило, всего 2 - 3 запроса – остальные не нужны вовсе. Но загружать придется весь словарь, а это время. Есть идеи как данный процесс оптимизировать?
Идеального решения нет. Все равно прийдется выбирать, что важнее для твоей задачи.

Как вариант оптимизации данного подхода, толково сгрупировать запросы по нескольким словарям. И при разработке конкретной бизнес логики, подгружать только то, что требуется. Да, ты возможно подгрузишь лишние 50 запросов, но не все 1000.

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

autosoft

Guest
Сравнение производительности:

1. ОС: WindowsXP Pro SP1, Процессор: Intel Celeron 950 Мгц, ОЗУ: 512 Мб.
Без парсинга: 0.0003 секунд.
С парсингом: 0.3179 секунд.

2. ОС: FreeBSD 5.3, Процессор: Intel Pentium 2400 Мгц, ОЗУ: 1024 Мб.
Без парсинга: 0.0001 секунд.
С парсингом: 0.1221 секунд.

BeGe
И никто и не говорил, что будет быстрее.
Нужно учесть, что это время подготовки 50-и запросов. На практике бывает гораздо меньше - редко больше 10.
По поводу потери функциональности: в чем она?!

Указанные данные получены следующим образом:
PHP:
function getmicrotime() {
    list($usec, $sec) = explode(" ", microtime());
    return ((float)$usec + (float)$sec);
}

$t0 = getmicrotime();

for ($i = 0; $i < 50; $i++) {

    $sql = 
    "SELECT A.*, B.* 
    FROM 
    #if (informix) 
    A, OUTER B WHERE A.ID = B.A_ID 
    #else 
    A LEFT JOIN B ON A.ID = B.A_ID 
    #endif 
    ORDER BY 1";
}

$t1 = getmicrotime();

$parser = new t_sql_parser("server");

for ($i = 0; $i < 50; $i++) {

    $sql = $parser->parse(
    "SELECT A.*, B.* 
    FROM 
    #if (informix) 
    A, OUTER B WHERE A.ID = B.A_ID 
    #else 
    A LEFT JOIN B ON A.ID = B.A_ID 
    #endif 
    ORDER BY 1");
}

$t2 = getmicrotime();

printf("%.4f\n%.4f\n", $t1 - $t0, $t2 - $t1);
 

filter

Новичок
autosoft

Производительность - спорный вопрос и этой теме посвящено много обсуждений в форуме.
Я могу и ошибаться, но мне кажется что парсинг запросов будет не самым тонким местом в твоем приложении. Это просто чтобы не переводить обсуждение в другую плоскость.

По поводу измерений времени парсинга :) - vitus тебе предложил более простой вариант. Померяй :)

Выводы делать тебе, анализируя поставленные требования.

PS. Keep it simple stupid.
 

vitus

мимо проходил
:) я не понял, а чего вы все улыбаетесь, когда говорите о том, что я написал?

имхо оно всётаки более наглядно и работет быстро и оптимизировать sql можно как хочешь и голова не болит,
появилась новая схема, протестил, добавил где нужно новые строки с запросами без головной боли и вперёд и песней.
 

filter

Новичок
Автор оригинала: vitus
:) я не понял, а чего вы все улыбаетесь, когда говорите о том, что я написал?
Это ты не правильно понял. Смайл никакого отношения не имеет к твоему решению.

Если выбирать между твоим вариантом и и autosoft, то +1 в твою пользу по тем же соображениям.
 

autosoft

Guest
А я вот подумал, что ничего не мешает хранить словарь в отдельных файлах.
Например, вместо "SELECT_CANDIDATE_QUERY" указывать "tovar/insert". Где: tovar – директорий в словаре, insert - файл в этой директории.
Тоже вроде ничего получится, но vitus лучший я согласен :)
 

BeGe

Вождь Апачей, блин (c)
Потеря функциональности базы данных
Те функции, которые есть в PostgreSQL те типы данных, которы есть в таблицы, ты не надёшь в MySQL, то что есть в MySQL ты не найдёшь в PostgreSQL и в MSSQL, те возможности что есть в MSSQL ты не найдёшь в MySQL и в PostgreSQL. А использовать кастрировный MySQL или MSSQL никто не хочет и не целесобразно.
 

filter

Новичок
Автор оригинала: autosoft
А я вот подумал, что ничего не мешает хранить словарь в отдельных файлах.
Например, вместо "SELECT_CANDIDATE_QUERY" указывать "tovar/insert". Где: tovar – директорий в словаре, insert - файл в этой директории.
Возможно. Я предложил лишь общую идею, а не конкретную реализацию.
 

svetasmirnova

маленький монстрик
BeGe
>А использовать кастрировный MySQL или MSSQL никто не хочет и не целесобразно.
Что-то мне подсказывает, что ты ошибаешься

-~{}~ 25.11.05 12:50:

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