четверг, 31 января 2019 г.

Безопасный и удобный поиск в mySQL

Безопасный и удобный поиск в mySQL

Автор:Дмитрий Лебедев 
Краткая справка по реализации поиска: Обработка строки, вырезание служебных символов, составление запроса к базе, логика, постраничный вывод, релевантность.
1.   Общие ведомости
2.   Кратко о релевантности
3.   Упражнения c релевантностью
4.   Продолежение начатого
Часть 1: Общие ведомости
Обработка строки
Первым делом надо порезать ручками строку.
$ search = substr ($ search, 0, 64);
64 символа пользователю будет достаточно для поиска. Теперь каленым железом выжжем все "ненормальные" символы.
$ search = preg_replace ("/ [^ \ w \ x7F- \ xFF \ s] /", "", $ search);
По идее, нельзя давать пользователю возможности искать по слишком коротким словам - кроме всего прочего, это сильно загружает сервер. Итак, разрешим искать только по словам, которые длиннее двух букв (если ограничение больше, надо заменить "{1,2}" на "{1, кол-во символов}").
$ good = trim (preg_replace ("/ \ s (\ S {1,2}) \ s /", "", ereg_replace ("+", "", "$ search")));
А после замены плохих слов - надо сжать двойные пробелы (они были сделаны специально для корректного поиска коротких слов).
$ good = ereg_eplace ("+", "", $ good);
Логика
Допустим, мы хотим предоставить пользователю возможность выбирать логику поиска - искать все слова или только одно из нескольких. Если вы хотите сделать как в Яндексе - два амперсанта означают "И" (слово1&&слово2&&слово3) или как-то еще, то я не советчик. Шаманство со строками на небольшом сайте imho не оправдывает затраченного времени. Поэтому форму для поиска рисуем так:
<form name = "some">
 <input type = text name = "stroka">
 <select name = "logic">
  <option value="OR">искать любое из слов
  <option value="AND">искать все слова
 </ Выберите>
</ Форма>
А в поисковом скрипте лишний раз проверяем, что пользователь ввел:
if (($ logic! = "AND") && ($ logic! = "OR"))

  $ logic = "ИЛИ";
Как будет использоваться логика — ниже.
Статистика поиска
Неплохо будет сразу информировать пользователя, сколько он нашел строк таблицы. Для этого делается дополнительный запрос в базу:
$ query = "SELECT id ИЗ таблицы, ГДЕ поле LIKE '%". str_replace ("", "% 'OR field LIKE'%", $ good). "%";
Для статистики по отдельным словам можно сделать следующее:
$ word = explode ("", $ search);
while (список ($ k, $ v) = каждый ($ слово)) {
  if (strlen ($ v)> 2)
    $ Стат [] = "$ v:". 
      mysql_num_rows (mysql_query ("ВЫБЕРИТЕ идентификатор ИЗ таблицы, ГДЕ поле LIKE"% $ v% '"));
  еще
    $ stat [] = "$ v: <font color = # cc0000> короткое </ font>";
  };
$ word_stats = "Статистика слов:". implode ("", $ stat). "<br>";
снята с охраны ($ стат);
Постраничный вывод результатов
Ну, когда у нас есть макет для поиска и количество строк результата поиска, сделать постраничный поиск - пара пустяков. Проверяем переменную $page (не меньше 0, не больше $results_amount/$rows_in_page).В запрос, который подсчитывает количество строк (смотри выше), пишем нужные нам поля и поля для сортировки. А потом дописываем
если ($ page == 0)
   $ request. = "LIMIT $ lines_in_page";
еще
   $ request. = "LIMIT". $ Страница * $ rows_in_page. "". $ Rows_in_page;
                  
(синтаксис: LIMIT <кол-во строк> либо LIMIT <кол-во строк отступа>, <кол-во строк>)
В результате выполнения подобного запроса мы получим именно те самые строки, которые надо выводить на странице.Для навигации можно либо рисовать ссылки на следующую и предыдущую страницы, либо, что сложнее, делать панель навигации на несколько страниц.
если ($ page> 0)
  print ("<a href = search.php? search =". rawurlencode ($ good). "& page =". ($ page-1). 
  ">предыдущая страница</a>");

если ($ page <$ results_amount / $ lines_in_page)
  print ("<a href = search.php? search =". rawurlencode ($ good). "& page =". ($ page + 1). 
  ">следующая страница</a>");
Подсветка
Чтобы подсвечивать светом или жирным шрифтом искомые слова в тексте, надо сделать всего лишь следующее:
$ highlight = str_replace ("", "|", $ good);
Пробелы (а они у нас между словами стоят поодиночке, и нигде двойной пробел не встречается, к тому же с концов строки мы их тоже вырезали) достаточно заменить на вертикальную черту - разделитель вариантов в регулярных выражениях. "Плохие" слова мы не подсвечиваем, потому что в базе их не ищем :). В коде, который выводит текст пишем:
$ row ["text"] = ereg_replace ($ highlight, "<font color = # cc0000> \\ 0 </ font>", $ row ["text"]);
После написания выпуска я кинулся, было, писать и себе "подсветку". Не тут-то было! У меня в тексте встречаются теги HTML, поэтому пришлось много подумать... Получилась вот такая вещь (строка со словами для подсветки есть):
$ text = eregi_replace ("> ([^ <] *) ($ words)", "> \\ 1 <font color = # cc0000> \\ 2 </ font>", $ text);
Приходится смотреть, нет в теге ли это слово. Однако тут встает проблема ресурсоемкости такой замены (мой K6-266 над текстом в 5 килобайт думал целых семь секунд). Печально.
Итог
Применяя такие приемы, можно, во-первых, ограничить свободу действий пользователя и не дать ему а) узнать программную структуру сайта б) вызвать перегрузку сервера (например, отправив мегабайт текста, состоящего из слов длиной в три буквы (фраза получилась двусмысленная, но переписывать не буду :), чтобы скрипт 250 тысяч раз лазил в базу) в) увидеть сообщение об ошибке в результате попадания в строку спецсимволов языка запросов. Во-вторых, некоторое удобство для пользователя - постраничный вывод и подсветка.
Помнится в статье "Безопасный и удобный поиск" была такая фраза
Часть 2. Кратко о релевантности
Для вывода результатов поиска по релевантности необходимо:
  • Требуемые поля VARCHAR, либо любые из разновидностей полей TEXT (SMALLTEXT, MEDIUMTEXT и т.п.) сделать ключами FULLTEXT:
    Таблица ALTER TABLE ADD FULLTEXT (поле)
  • Дальше — еще проще:
    $ query = "SELECT *, поле MATCH AGAINST ('$ searchwords') как релевантное из таблицы ORDER BY релевантным DESC"
    Далее можно навешивать всякие LIMIT'ы и прочее для удобного вывода.
Заметки:
  • По умолчанию установлен поиск слов, содержащих не менее 4 символов. Правится установкой #define MIN_WORD_LEN 4 в исходнике ft_static.c, хотя на мой взгляд править это не нужно.
  • Недоступны символы % в поисковой фразе, слова в поисковой фразе парсятся с использованием списка разделетелей.
  • Список разделителей слов правится в исходнике ft_static.c.
  • Необходимо минимум десяток записей в таблице для начала вычисления релевантности.
  • Нельзя поле relev использовать в клаузе WHERE:
    SELECT *, поле MATCH против ('$ searchwords') как релевантное из таблицы ГДЕ релевантное> 0 ЗАКАЗАТЬ по релевантному DESC
    хотя можно:
    SELECT *, поле MATCH против («$ searchwords») как релевантное из таблицы ГДЕ поле MATA против («$ searchwords»)> 0 ЗАКАЗАТЬ по релевантному DESC
  • Скорость достаточно высокая — даже в некоторых случаях быстрее like поиска
  • Все вышесказанное работает начиная с версии MySQL 3.23.23
При создании индексов FULLTEXT по нескольким полям возможны 2 варианта:
CREATE TABLE стол
 (
   field1 VARCHAR (255),
   field2 TEXT,
   FULLTEXT (поле1, поле2)
 )
CREATE TABLE стол
 (
  field1 VARCHAR (255),
  field2 TEXT,
  FULLTEXT (поле1),
  FULLTEXT (поле 2)
 )
В первом случае возможен запрос:
SELECT *, MATCH field1, field2 ПРОТИВ ('$ searchwords') как релевантные из таблицы ORDER BY релевантные DESC
релевантность вычисляется у всех полей сразу. Во втором случае такой запрос выдаст ошибку. Здесь вычисляем релевантность следующим образом:
SELECT *, MATCH field1 AGAINST ('$ searchwords') + MATCH field2 AGAINST ('$ searchwords') как релевантные из таблицы ORDER BY релевантной DESC
Второй вариант несколько сложнее в запросах, однако, на мой взгляд лучше, т.к. увеличивается гибкость поиска — к каждому из полей можно задать, например, коэффициент значимости и при суммировании релевантностей полей умножать их на этот коэффициент. Поисковая фраза будет "больше" искаться в полях с большим коэффициентом. Например, если мы делаем поиск по проиндексированным страницам каталога ресурсов, то поле имени страницы обычно задают с большим коэффициентом, чем поля мета-тегов описаний или ключевых слов.
Часть 3: Упражнения c релевантностью
Сначала как добавить FULLTEXT-индекс:
mysql> изменить таблицу articlea add fulltext (ztext);
ОШИБКА 1073: столбец BLOB 'ztext' не может использоваться в спецификации ключа с использованием
тип таблицы

mysql> изменить таблицу articlea type = myisam;
Запрос в порядке, затронуто 36 строк (0,60 с)
Записи: 36 Дубликаты: 0 Предупреждения: 0

mysql> изменить таблицу articlea add fulltext (ztext);
Запрос в порядке, затронуто 36 строк (10,00 с)
Записи: 36 Дубликаты: 0 Предупреждения: 0
Текстовые индексы можно делать только в таблицах типа MyISAM. Тексты берутся из таблицы и скидываются в файл индекса, и растёт объём базы. По поводу запросов. Нельзя поле relev использовать в клаузе WHERE:
SELECT *, поле MATCH против ('$ searchwords') как релевантное из таблицы ГДЕ релевантное> 0 ЗАКАЗАТЬ по релевантному DESC
хотя можно:
SELECT *, поле MATCH против («$ searchwords») как релевантное из таблицы ГДЕ поле MATA против («$ searchwords»)> 0 ЗАКАЗАТЬ по релевантному DESC
Вычисленное поле, конечно же, нельзя использовать в WHERE по всем правилам синтаксиса, но можно использовать в HAVING:
SELECT *, поле MATCH AGAINST ('$ searchwords') как релевантное из таблицы ИМЕЮЩЕЕ> 0 ЗАКАЗАТЬ по релевантному DESC
Поиск через MATCH, как писал Олег, делается только по слову целиком. ...Впрочем, по релевантности можно только сортировать, а выбирать по LIKE (это, конечно, скажется на производительности, даже не знаю, насколько).
Убираем условие "relev>0", оставляем сортировку. Остальное, как и раньше — рубим полученную строку и превращаем в запрос с несколькими операторами LIKE:
SELECT *, поле MATCH против ('$ searchwords') как релевантно из таблицы ГДЕ поле LIKE '% $ word1%' ИЛИ ​​поле LIKE '% $ word2%' ЗАКАЗАТЬ по релевантному DESC, поле даты DESC
Часть 4: Продолежение начатого
Продолжаю начатую в сентябре тему поиска с сортировкой по релевантности в базе MySQL.
MySQL предлагает в последних версиях базы данных использовать для полнотекстового поиска индексацию FULLTEXT и конструкцию MATCH field AGAINST. Однако не на всех серверах стоит последняя версия MySQL, и не все хостинг-провайдеры хотят обновлять софт по соображениям надежности системы.
В своё время я предполагал, что поиск с сортировкой по релевантности надо будет делать в несколько запросов, и, следовательно, лучше вовсе не браться за это. Мысли, что релевантность можно подсчитывать в самом запросе отдалённо меня посещали, но я боялся и представить такую конструкцию.
Однако же, работник одной из сайтостроительных фирм Н-ска похвастался мне системой поиска, которую они применяют на своих сайтах. Я точно не запомнил запрос, попробую так воспроизвести его:
ВЫБЕРИТЕ title, date_format (material_date, '% e.% C.% Y') AS date1, IF (текст похож на '% word1 word2 word3%', 3 * 10, 0) + IF (текст LIKE '% word1%', 9, 0) + IF (текст LIKE '% word2%', 9, 0) + IF (текст LIKE '% word3%', 9, 0) КАК релевантность из таблицы ГДЕ текст LIKE '% word1%' ИЛИ ​​текст LIKE ' % word2% 'OR text LIKE'% word3% 'ORDER BY по релевантности DESC, material_date DESC
Ужасно выглядит, но работает даже на старых версиях MySQL. Попробовал сравнить скорость работы с вот таким запросом:
ВЫБЕРИТЕ title, date_format (material_date, '% e.% C.% Y') КАК date1, СООТВЕТСТВУЕТ тексту ПРОТИВ ('word1 word2 word3') КАК релевантность ИЗ таблицы ГДЕ текст LIKE '% word1%' ИЛИ ​​текст LIKE '% word2% 'OR text LIKE'% word3% 'ORDER BY по релевантности DESC, material_date DESC
В среднем скорость универсального запроса в два раза меньше, чем использующего новые конструкции. Что вполне логично — чем больше универсальность, тем больше ресурсоёмкость.
Попробуем построить такой запрос автоматически. Отрезаем длинную строку, а так же все неправильные символы и короткие слова. Рисуем запрос.
$ query = "SELECT title, date_format (material_date, '% e.% c.% y') AS date1, IF (текст похож на '%". $ good_words. "%',". (substr_count ($ good_words, "" ) + 1). "* 10, 0) + IF (текст LIKE '%". Str_replace ("", "%', 9, 0) + IF (текст LIKE"% ", $ good_words)."% ', 9, 0) КАК релевантность из таблицы ГДЕ текст LIKE '% ". str_replace ("", "% 'OR text LIKE'%", $ good_words). "% 'ORDER BY по релевантности DESC, material_date DESC";
Не очень-то сложно. Для надёжности и защиты от флуда можно ограничить количество слов в запросе.
Некоторые дополнения к прежним публикациям
Общее количество найденных строк в таблице. Для вывода результатов поиска, разумеется, надо пользоваться оператором LIMIT (чтобы не писать каждый раз формирование этого параметра, пользуйтесь готовыми функциями). Если никаких операций группировки в запросе не делается, лучше подсчитать количество строк сразу в запросе — COUNT(*), а не через функцию php mysql_num_rows(). Можете проверить на больших таблицах. Если производятся групповые операции, делаем запрос с COUNT(DISTINCT(<поле, по которому группируем>)), но без GROUP BY.
Подсветка. Если в текстах не бывает html-тегов, жить проще
$ text = preg_replace ("/ word1 | word2 | word3 / i", "<b> \\ 0 </ b>", $ text);
Если в тексте теги используются, то есть три варианта а) не делать подсветку б) поскольку теги пользователь не видит (разве что очень любопытный пользователь), то можно сделать поле индекса, в котором не будет тегов а символы [^\w\x7F-\xFF\s] будут заменены на пробелы (именно эти символы вырезаются из поисковой строки в самом начале, так что поиск по ним не производится). Поиск и подсветку в таком случае сделать именно по индексу. в) делать подсветку текста из обычного поля, предварительно вырезав теги функцией srip_tags().

Полная версия поискового кода, как всегда, в списке файлов.

Построение таблиц «Один-к-разным»

Построение таблиц «Один-к-разным»

Передо мною встала задача — «объединить» несколько типов объектов с разными свойствами в одной таблице для «глобального» поиска. Я перепробовал несколько решений такой задачи. Возможно, вы предложите что-то новое, чего я не смог разглядеть в потенциале SQL.

Рассмотрим задачу более конкретно: 
Необходимо разместить в космосе несколько разнородных объектов. Например, планеты, корабли и порталы. Таких, различных типов объектов могут быть десятки, не похожих на других.
В основном, требуется получать данные по всем этим объектам:
  • по их координатам в заданном квадрате.
  • по ID, который не должен пересекаться c ID других объектов.
Предлагаю такие решения:

Строковые параметры для объектов

Создать таблицу таким образом:
CREATE TABLE `space` (
 `ID` int(11) NOT NULL auto_increment,
 `x` int(11) NOT NULL default '0',
 `y` int(11) NOT NULL default '0',
 `type` char(2) NOT NULL default '',
 `variables` text NOT NULL,
 PRIMARY KEY (`ID`),
);
* This source code was highlighted with Source Code Highlighter.

После чего все поля объекта записывать и хранить, например, в формате JSON, XML (или просто implode ',') в поле variables.

Минусы:
  • Работа с такой струтурой возможна только в языках, поддерживающих неопределенные типы переменных. Например, PHP.
  • Нет возможности поиска по полям объектов, в случае необходимости
Плюсы:
  • Необходим всего один запрос для выборки всех нужных объектов
@Vile, 3starkXaocCPS предлагают определить в базе вместо сомнительного text поле xml, которое linq to sql позволит замапить в XML-объект в ORM с полной поддержкой всего нужного вам xml-функционала.

Параметры всех объектов в одной таблице

Предыдущий метод, только все поля по всем объектам лежат в одной таблице. При добавлении нового параметра, в эту таблицу добавляется новое поле.
Минусы:
  • Таблица будет занимать много места
Плюсы:
  • Необходим всего один запрос для выборки всех нужных объектов
  • Есть возможность поиска по любым полям объектов, в случае необходимости
  • Нет пересечения в космосе по ID

Каждый объект в своей таблице

То есть, получаем таблицы с разделенными параметрами и разными стартовыми ID, чтобы поиск объектов по ID был возможен:
— 1я таблица
CREATE TABLE `ship` (
 `ID` int(11) NOT NULL auto_increment,
 `x` int(11) NOT NULL default '0',
 `y` int(11) NOT NULL default '0',
 `armor` float NOT NULL default '0',
 `maxarmor` float NOT NULL default '0',
 …
 PRIMARY KEY (`ID`),
)AUTO_INCREMENT=0;

— 2я таблица
CREATE TABLE `planet` (
 `ID` int(11) NOT NULL auto_increment,
 `x` int(11) NOT NULL default '0',
 `y` int(11) NOT NULL default '0',
 `radius` float NOT NULL default '0',
 …
 PRIMARY KEY (`ID`),
)AUTO_INCREMENT=0x10000000;

— 3я таблица
CREATE TABLE `alien` (
 `ID` int(11) NOT NULL auto_increment,
 `x` int(11) NOT NULL default '0',
 `y` int(11) NOT NULL default '0',
 `damage` float NOT NULL default '0',
 …
 PRIMARY KEY (`ID`),
)AUTO_INCREMENT=0x20000000;
* This source code was highlighted with Source Code Highlighter.
Минусы:
  • Необходимо осуществить несколько запросов, при пересечении полей, в каждой Таблице, для получения всех объектов (например, координаты)
  • Необходимо следить за пересечениями по ID между таблицами
  • В случае десятков объектов, получатся десятки таблиц
Плюсы:
  • Данные занимают столько, сколько и должны
  • Есть возможность поиска по любым полям объектов, в случае необходимости
clorz предлагает воспользоваться функциями GUID или UUID. Она возвращает уникальный идентификатор. Можно использовать его вместе с обычным autoincrement ID. Тогда не придется следить за пересечением последних.

Параметры объектов в отдельной таблице
(предложили khizhaster mcedonskiy gribunin)

Когда БД представляется таблицей и состоит из ключей и параметров:
CREATE TABLE `space_id` (
 `ID` int(11) NOT NULL auto_increment,
 `object_id` int(11) NOT NULL default '0',
 `parameter_name` varchar(32) NOT NULL default '',
 `value` text NOT NULL,
 PRIMARY KEY (`ID`),
);
* This source code was highlighted with Source Code Highlighter.
Минусы:
  • Более сложная обработка запроса и сложность разобраться сторонним людям
  • Необходимость держать `value` в индексах при поиске
  • Чем больше параметров в условии запроса, тем сложнее сам запрос и больше итераций приходится делать БД
Плюсы:
  • Самая универсальная структура, подходящая под любые нужды

Каждый параметр объектов в отдельной таблице
(предложили Keenn ArtemS @Vile Pilot34)

БД разбита на таблицы, которые при запросе объеденяются с помощью left join в таблицы, с (null), при отсуствии параметров (как во втором примере, только на ходу):
— 1я таблица
CREATE TABLE `space` (
 `ID` int(11) NOT NULL auto_increment,
 `x` int(11) NOT NULL default '0',
 `y` int(11) NOT NULL default '0',
 `type` varchar(8) NOT NULL default '0',
 …
 PRIMARY KEY (`ID`),
) ENGINE=InnoDB, AUTO_INCREMENT=0;

— 2я таблица
CREATE TABLE `ship` (
 `ID` int(11) NOT NULL,
 `armor` float NOT NULL default '0',
 `maxarmor` float NOT NULL default '0',
 …
 FOREIGN KEY (ID) REFERENCES `space`(ID) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

— 3я таблица
CREATE TABLE `planet` (
 `ID` int(11) NOT NULL,
 `radius` float NOT NULL default '0',
 …
 FOREIGN KEY (ID) REFERENCES `space`(ID) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

— 4я таблица
CREATE TABLE `alien` (
 `ID` int(11) NOT NULL,
 `damage` float NOT NULL default '0',
 …
 FOREIGN KEY (ID) REFERENCES `space`(ID) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
* This source code was highlighted with Source Code Highlighter.
Минусы:
  • В случае десятков объектов, получатся десятки таблиц
  • Необходимо несколько запросов для выборки всех нужных объектов
Плюсы:
  • Данные занимают столько, сколько и должны
  • Есть возможность поиска по любым полям объектов, в случае необходимости



среда, 30 января 2019 г.

Абстрактные классы БД

Абстрактные классы БД

Автор: Дмитрий Лебедев
Классы, позволяющие работать с разными базами данных через один стандартный набор функций. Полезны при смене базы данных и работе с несколькими БД одновременно.
Тема эта регулярно всплывает в форуме. Собственно, зачем они нужны, эти абстрактные классы баз данных? Большинство сайтов по-хорошему обойдутся и без таких средств унификации. Они работают либо без базы, либо на MySQL, чего для большей части проектов вполне достаточно. Да, да! Достаточно! Когда нагрузки возрастут, вот тогда купят и поставят Оракл, а чтобы скрипты не переписывать, пусть пользуются абстрактными классами. :) Про бесплатные и фирменные базы данных разговор отдельный, грозит превратиться в пустой флейм (а конференциях именно этим все и заканчивается), поэтому не буду его продолжать.
Абстрактные классы позволяют разработчикам писать код без оглядок на то, какая база данных стоит — только на ее возможности и особенности работы. Очень полезно, если работа идет с несколькими базами одновременно (не надо вспоминать, например, что писать: mysql_fetch_array или OCIFetchInto). Смена базы данных тоже многократно облегчится — надо будет всего лишь изменить тип базы в инициализации объекта. Разумеется, все эти красивые вещи оборачиваются большей ресурсоемкостью программ.
Сразу отмечу, что, первое, эти классы я нашел не в каких-либо дебрях сети, а прямо "на поверхности" — hotscripts.com. И второе — написать свой собственный класс — не проблема. И, конечно же, всех классов не опишешь, поэтому если вы считаете, что я пропустил здесь что-то полезное и хорошее, пишите.
Немного о схеме работы с абстрактными классами.
Нормальная схема — это два класса: соединение с базой и результат запроса. В принципе, идентификатор результата запроса можно было держать и внутри объекта БД (например в массиве результатов), но порядка и удобства ради результаты вынесены в отдельный класс.
Итак, найдено пять классов. Описываю в порядке убывания удобства (удобства на мой взгляд).
ADOdb (по имеющимся сведениям — переписанный в php из asp)
Самый расписанный, самый документированный. Есть примеры использования. А оно достаточно простое. Настраиваете в файле adodb.inc.php параметры базы данных, которые будут использоваться по умолчанию. А можно и не настраивать.
Пример из документации:
включают в себя ( "adodb.inc.php");
ADOLoadCode ( 'MySQL');
$ db = NewADOConnection ();
$ db-> Connect ("localhost", "root", "password", "mydb");
$ result = $ db-> Execute ("SELECT * FROM сотрудников");
if ($ result === false) die ("fail");
while (! $ result-> EOF) {
  для ($ i = 0, $ max = $ result-> FieldCount (); $ i <$ max; $ i ++)
    выведите $ result-> fields [$ i]. ' «;
  $ Result-> MoveNext ();
  печать "<br>
«;
  }
Если нужна база не MySQL, а Oracle, меняем вторую строку на
ADOLoadCode ( 'оракула');
Программа сама подключает функцией include_once файл с классом для нужной базы данных.
Поддержка разных баз осуществляется через дочерние классы.
В общем, все работает и пишется хорошо, и требования тоже "хорошие": объем подключаемого кода для БД MySQL равняется 34 килобайтам кода. Для справки: код для этого сайта включая класс шаблона и сами шаблоны "весит" 66 килобайт.
DAC (Класс доступа к базе данных).
В использовании — примерно то же, что и AdoDB. Только в "комплекте" нет руководства. :) Разные базы поддерживаются дочерними классами, которые, насколько я понял, подключаются автоматически. Но здесь функций для выполнения запроса несколько — select, insert, update и execute. Не знаю, может, это и удобнее с точки зрения обработки результата, но писать программы по моим прикидкам не будет ни удобнее, ни сложнее (по крайней мере, мне не приходилось сталкиваться с ситуацией, когда тип запроса выбирался в зависимости от ситуации).
class.DBI (Database independent class, говорят, что переписан с Perl-овой версии).
Описано хорошо, но на сайте. Функции разделены, но на две (выборка и остальные запросы). Чтобы достать из архива какие-либо файлы, надо проявить хорошую смекалку. Потому что архив не распаковывается — там в именах файлов двоеточия, и система грязно ругается. Заходим в файл class.DBI-....tgz, копируем оттуда в нужную директорию файл class.DBI-0.3.8.tar и открываем его в UltraEdit. Народным методом copy/paste достаем фрагмент кода под нужную базу. Фуф!
В отличие от предыдущих классов здесь запрос отправляется в базу конструктором класса запроса. В одном из классов нашел ошибку — вместо str_replace используется ereg_replace.
DIOW (Оболочка независимого от базы данных объекта).
А вот этим классом настоятельно не советую пользоваться. Разделение на разные базы сделано самым глупым способом — если переменная с типом базы, и такая конструкция:
if ($ database_type == 1) {
  ...
  }
elseif ($ database_type == 2) {
  ...
  };
А в ней — самое страшное. Попробуйте найти это сами.
if ($ database_type == 2) {
  $ this-> my_temp_resultID = mysql_query ($ someSQL, $ this-> my_connection);
  $ this-> my_temp_result_object-> numrows = mysql_num_rows ($ this-> my_temp_resultID);
  $ this-> my_temp_result_object-> numcols = mysql_num_fields ($ this-> my_temp_resultID);

  // заполняем столбцы из результирующего набора
  для ($ j = 0; $ j <$ this-> my_temp_result_object-> numcols; $ j ++) {
    $ this-> my_temp_result_object-> column_name [$ j] = 
 mysql_fieldname ($ this-> my_temp_resultID, $ j);
    };

  // заполняем элементы данных из набора результатов
  для ($ i = 0; $ i <$ this-> my_temp_result_object-> numrows; $ i ++) {
    $ x = mysql_fetch_row ($ this-> my_temp_resultID);
    для ($ j = 0; $ j <$ this-> my_temp_result_object-> numcols; $ j ++) {
      $ this-> my_temp_result_object-> element [$ i] [$ j] = $ x [$ j];
      };
    };

  return $ this-> my_temp_result_object;
  mysql_free_result ($ this-> my_temp_resultID);
  };
Перед return есть два вложенных цикла, которые тупо берут данные из базы и скидывают их в массив. Конечно же, потом с этим массивом делать можно что угодно, "хоть веревки вейте, хоть ездовую собаку делайте", но это стоит дополнительной занимаемой памяти.
Конечно же, иногда требуется произвести операцию, которая не по силам базе данных, но это один случай из ста. В остальных же нужно просто получать данные и выводить их в документ. При этом сразу после вывода массив строки заменяется на новый, почти такой же. Здесь же этого не происходит.
Тут, конечно, мне можно возразить, мол, сам шаблонами пользуешься, и до конца держишь все в переменной (финал моих скриптов такой: $root->ugh(); — это выводится готовый документ :). Ну, шаблоны — это совсем другое дело. А если я прикручу этот класс к своим шаблонам или к другому скрипту, в котором держать все данные в массиве не нужно, памяти будет требоваться еще больше.
Если же кому-то нужно именно скидывать в один массив все результаты запроса, пользуйтесь другими классами — всего-то четыре строки своего кода.
К тому же, как вы понимаете, освобождения результата в конце функции не произойдет, потому что команда на завершение работы функции — return — уже дана. Если скрипт, который вызывает несколько "хороших" запросов, одновременно вызовет много пользователей, ой, как плохо станет серверу!

Active Directory Sync

  • .NET
По долгу службы пришлось разбираться с Active Directory. Пришлось почитать, поэкспериментировать с классами, но всё в результате заработало превосходно.

В первую очередь хотелось бы описать немного Directory Synchronization объект, который появился в .net framework 2.0. О нём, и о других преимуществах 2го framework вы сможете почитать на сайте microsoft (http://msdn.microsoft.com/en-us/magazine/cc188700.aspx ). Лично мне статья помогла разобраться, хотя я обилия информации в сети на предмет dyrSync нет.


Для синхронизации я использую Web-Service, действия происходят следующим образом:

1. Веб сервис получает LDAP путь к запросу. 
  [WebMethod]
  public void Synchronize (string DomainPath, string Filter, string EntryPath)
  {

        DirectoryEntry de = new DirectoryEntry (DomainPath);
        using (SqlConnection conn = new SqlConnection(Globals.ConnectionString))
        {
          conn.Open();
          SqlCommand command = conn.CreateCommand();
          command.CommandText = «INSERT INTO SyncTable (Snapshot, OU) VALUES (@Snapshot, @OU, @ExchangeServer)»;
          command.Parameters.AddWithValue("@Snapshot", GetSyncData(de, filter));
          command.Parameters.AddWithValue(" @OU", EntryPath);

          command.ExecuteNonQuery();
          conn.Close();
        }

  }

2. GetSyncData возвращает AD cookie:
    public byte[] DirectorySync (DirectoryEntry DomainDE, string Filter)
    {
      ADInit.Init();
      try
      {
        using (DirectorySearcher srch = new DirectorySearcher(DomainDE, Filter))
        {
          srch.SearchScope = SearchScope.Base;
          srch.DirectorySynchronization = new DirectorySynchronization();

          foreach (SearchResult se in srch.FindAll())
          {
          }

          MemoryStream ms = new MemoryStream();
          BinaryFormatter bFormat = new BinaryFormatter();
          bFormat.Serialize(ms, srch.DirectorySynchronization.GetDirectorySynchronizationCookie());
          ms.Close();

          return ms.GetBuffer();         
        }
      }
      catch (Exception Ex)
      {
        throw Ex;
      }
    }

Приходится использовать Домен в качестве точки входа, потому как DirSynch работает с корневым элементом дерева. Фильтр может быть в виде: OU=myOU или что-либо в этом роде. В моём случае я достаточно просто обошел эту особенность: создал иерархическую структуру, из которой можно выбрать и LDAP путь до домена, и LDAP путь до данного объекта. Фильтр же – через имя объекта, который нужно синхронизировать.

3. Обратная синхронизация + сверка
    [WebMethod]
    public bool GetRegionEntry (string Domain, string Filter, string EntryPath)
    {
      DirectoryEntry de = new DirectoryEntry (Domain);

      RegionInfo _regionInfo = regionEntry.GetEntry();
      using (SqlConnection conn = new SqlConnection(Globals.ConnectionString))
      {
        conn.Open();
        SqlCommand command = conn.CreateCommand();
        command.CommandText = «SELECT * FROM SyncTable WHERE OU= @OU»;
        command.Parameters.AddWithValue(" @OU", EntryPath);

        SqlDataReader reader = command.ExecuteReader();
        if (reader.Read())
        {
          return regionEntry.GetSyncDelta(de, Filter, (byte[])reader[«Snapshot»]);
        }
        else
        {
          throw new Exception (“Can’t read Sync Cookie from Database!”);

        }
        conn.Close();
      }
      return _regionInfo;
    }

Здесь поступайте на своё усмотрение, либо получите bool (есть разница – нет разницы), либо получите дельту в полном объеме.
    ///public Dictionary<string, object> GetSyncDelta (DirectoryEntry DomainDE, string Filter, byte[] _cookie)

    public bool GetSyncDelta (DirectoryEntry DomainDE, string Filter, byte[] _cookie)
    {
      Dictionary<stringobject> _delta = new Dictionary<stringobject>();

      BinaryFormatter bf = new BinaryFormatter();
      byte[] cookie = (byte[]) bf.Deserialize(new MemoryStream(_cookie));

      DirectorySynchronization dirSync = new DirectorySynchronization(cookie);
      DirectorySearcher srch = new DirectorySearcher(DomainDE, Filter);
      srch.DirectorySynchronization = dirSync;

      foreach(SearchResult sr in srch.FindAll())
      {
        foreach (string attrName in sr.Properties.PropertyNames)
        {
          _delta.Add( attrName, sr.Properties[attrName]);
        }
        return true;
      }

      return false;
      //return _delta;
    }

(Dictionary<stringobject> не сериализируется стандартными методами в XML, попробуйте использовать свои массивы с keyvaluepair, либо перепишите сериализацию).

4. Для того, чтобы сохранить всю историю изменений записи, создайте еще одну таблицу, добавьте к ней поле-идентификатор и поставьте триггер на INSERT / UPDATE:
ALTER TRIGGER [SyncTableTrg]
  ON [dbo].[SyncTable]
  AFTER INSERTUPDATE
AS
BEGIN
  INSERT INTO SyncTableLog (OU, Snapshot)
    SELECT OU, Snapshot FROM inserted
END

Таким образом получите неплохую историю о каждой из записей, которые синхронизируете. Хотя это – не единственный способ.

спасибо за внимание

з.ы. не судите строго. прежде не блоггил( first experience

вторник, 29 января 2019 г.

Работа с Oracle в PHP

Работа с Oracle в PHP

Автор: Frank Naude
Перевод: Александр Войцеховский
Обсуждаемые вопросы:

Что такое PHP и что необходимо иметь для работы с Oracle?
PHP - рекурсивный акроним "PHP Hypertext Preprocessor". Это интерпретируемый язык программирования с открытым исходным кодом. Он предназначен для генерации веб-документов на сервере.
Этот документ описывает, как PHP взаимодействует с базой данных Oracle. Подразумевается, что у Вас уже установлен и настроен PHP. Проверить его работоспособность можно следующим скриптом:
<html>
<p>If PHP is working, you will see "Hello World" below:<hr>
<? php
   
echo "Hello world";
   
phpinfo();  // Print PHP version and config info ?> </html>
Попробуйте выполнить этот код из командной строки либо откройте тестовую страницу в Вашем браузере. Если приведенный выше пример не работает, отложите этот документ и добейтесь корректной работы PHP.
В чем разница между модулями OCI и ORA?
В дистрибутив PHP входят два модуля, предназначенных для работы с PHP:
  • Стандартные Oracle-функции (ORA)
  • Интерфейс для доступа к функциям Oracle (OCI)
    Если есть возможность выбора, предпочтение стоит отдать модулю OCI, так как он более оптимизирован и имеет большую функциональность. К примеру, модуль ORA не поддерживает CLOB, BLOB, BFILE, ROWID.

    Как собрать PHP с поддержкой Oracle?
    Для того, что бы получить работоспособную связку PHP & Oracle, необходимо выполнить следующие шаги:
  • Скачать дистрибутив PHP с официального сайта www.php.net, установить его на сервер и убедиться в его работоспособности
  • Установить клиент Oracle на Ваш сервер и настроить подключение к Вашей базе данных
  • Если у Вас Windows-сервер, раскомментируйте следующие строки в файле php.ini:
    ; extension = php_oci8.dll
    ; extension = php_oracle.dll
      
    Также убедитесь в том, чтобы extension_dir указывала на директорию, содержащую указанные модули.
    Если у Вас Linux-сервер, пересоберите PHP с одной из опций:
    --with-оракул = / путь / к / оракул / дома / реж
    --with-oci8 = / путь / к / оракул / дома / реж
      
  • Напишите небольшой тестовый скрипт, чтобы убедится в том, что установка завершилась успешно. В качестве тестового скрипта можно взять пример из следующего вопроса.
Как подключится к базе данных?
Если вы используете модуль OCI, воспользуйтесь следующим кодом:
<?php if ($c=OCILogon("scott""tiger""orcl")) {
  echo 
"Successfully connected to Oracle.\n";
  
OCILogoff($c);
} else {
  
$err OCIError();
  echo 
"Oracle Connect Error " $err[text];
?>
Если вы используете модуль ORA, попробуйте следующее:
<?php if ($c=ora_logon("scott@orcl","tiger")) {
  echo 
"Successfully connected to Oracle.\n";
  
ora_commitoff($c);
  
ora_logoff($c);
} else {
  echo 
"Oracle Connect Error " ora_error();
?>
Замечание: если у Вас не установлены необходимые переменные окружения, поместите следующие строки в начале каждого скрипта, работающего с Oracle:
<?php
  PutEnv
("ORACLE_SID=ORCL");
  
PutEnv("ORACLE_HOME=/app/oracle/product/9.2.0");
  
PutEnv("TNS_ADMIN=/var/opt/oracle"); 
В случае, если Вы несколько раз в пределах одного скрипта попробуете подключится к базе данных, используя одинаковую комбинацию логин/пароль, Вы получите одно и то же соединение. Если Вам необходимы различные подключения к базе, используйте функцию OCINLogon(). Если вам нужны постоянные соединения, используйте функцию OCIPLogon().
Почему возникает ошибка "Call to undefined function: ora_logon()/ ocilogon()"?
Вероятнее всего, у Вас не включена поддержка Oracle. Вернитесь к вопросу "Как собрать PHP с поддержкой Oracle?". Вам необходимо пересобрать php, если у Вас Linux или раскомментировать загрузку соответствующей библиотеки, если у вас Windows.
Как выполнять запросы SELECT, INSERT, UPDATE и DELETE посредством PHP?
Приведенный ниже исходный код демонстрирует, как удалять/создавать новые таблицы, записывать/извлекать из них данные в PHP.
<?php
  $c
=OCILogon("scott""tiger""orcl");
  if ( ! 
$c ) {
    echo 
"Невозможно подключится к базе: " var_dumpOCIError() );
    die();
  }

  
// Удаляем старую табоицу
  
$s OCIParse($c"drop table tab1");
  
OCIExecute($sOCI_DEFAULT);

  
// Создаем новую таблицу
  
$s OCIParse($c"create table tab1 (col1 number, col2 varchar2(30))");
  
OCIExecute($sOCI_DEFAULT);

  
// Заносим строку в только что созданную таблицу
  
$s OCIParse($c"insert into tab1 values (1, 'Frank')");
  
OCIExecute($sOCI_DEFAULT);

  
// Заносим данные в таблицу используя конструкцию "bind"
  
$var1 2;
  
$var2 "Scott";
  
$s OCIParse($c"insert into tab1 values (:bind1, :bind2)");
  
OCIBindByName($s":bind1"$var1);
  
OCIBindByName($s":bind2"$var2);
  
OCIExecute($sOCI_DEFAULT);

  
// Производим выборку из базы данных
  
$s OCIParse($c"select * from tab1");
  
OCIExecute($sOCI_DEFAULT);
  while (
OCIFetch($s)) {
    echo 
"COL1=" ociresult($s"COL1") .
       
", COL2=" ociresult($s"COL2") . "\n";
  }

  
// Выполняем commit;
  
OCICommit($c);

  
// Отключаемся от базы данных
  
OCILogoff($c); ?>
Как получить доступ к механизму транзакций из PHP?
При использовании модуля OCI каждый раз после удачного выполнения ociexecute() автоматически происходит commit, и, таким образом, транзакция сразу же завершается. Вы можете управлять этим процессом, указывая дополнительный параметр OCI_COMMIT_ON_SUCCESS либо OCI_DEFAULT при вызове функции ociexecute(). В случае использования OCI_DEFAULT вы сможете полностью управлять механизмом транзакций, для этого используйте функции OCICommit() и OCIRollback().
Следует учесть, что использование OCI_DEFAULT в одном из вызовов ociexecute() автоматически наследуется для всего подключения к базе данных и будет использовано при дальнейших операциях с базой данных. Если вы не хотите использовать автоматический и ручной механизм управления транзакциями одновременно, используйте OCINLogon().
В случае, если Вы используете модуль ORA, управление транзакциями выглядит немного иначе. Используйте функции ORA_CommitOn() и ORA_CommitOff() для переключения между ручным и автоматическим механизмом управления транзакциями. Для завершения транзакции и отката используйте функции ORA_Commit() и ORA_Rollback() соответственно.
В случае, если после завершения работы скрипта ни разу не выполнялся ни commit, ни rollback, PHP завершит транзакцию командой commit.
Как корректно обрабатывать возникающие ошибки?
В случае, если вы используете модуль OCI, используйте функцию OCIError() для получения массива, содержащего детальную информацию о возникающих ошибках. Если подключений несколько, OCIError() позволяет получить информацию об ошибке в каждом их них индивидуально. Если данная функция вызывается без параметров, она возвращает массив с информацией о последней произошедшей ошибке.
<?php
  $err 
OCIError();
  
var_dump($err);

  print 
"\nError code = "     $err[code];
  print 
"\nError message = "  $err[message];
  print 
"\nError position = " $err[offset];
  print 
"\nSQL Statement = "  $err[sqltext]; ?>
В случае, если вы используете модуль ORA, воспользуйтесь функциями ora_error() и errorcode() для получения информации о последней произошедшей ошибке.
<?php
  
print "\nError code = "    ora_errorcode();
  print 
"\nError message = " ora_error(); ?>
Как вызвать из PHP хранимую процедуру?
Приведенный ниже пример демонстрирует создание и вызов хранимой процедуры.
<?php
  
// Подключаемся к базе данных
  
$c=OCILogon("scott""tiger""orcl");
  if ( ! 
$c ) {
     echo 
"Unable to connect: " var_dumpOCIError() );
     die();
  }

  
// Создаем хранимую процедуру
  
$s OCIParse($c"create procedure proc1(p1 IN number, p2 OUT number) as " .
                    
"begin" .
                    
"  p2 := p1 + 10;" .
                    
"end;");
  
OCIExecute($sOCI_DEFAULT);

  
// Вызываем процедуру
  
$in_var 10;
  
$s OCIParse($c"begin proc1(:bind1, :bind2); end;");
  
OCIBindByName($s":bind1"$in_var);
  
OCIBindByName($s":bind2"$out_var32); // 32 is the return length
  
OCIExecute($sOCI_DEFAULT);
  echo 
"Procedure returned value: " $out_var;

  
// Отключаемся от базы данных
  
OCILogoff($c); ?>
Где можно найти больше информации о работе с Oracle в PHP?
Для получения более детальной информации посетите следующие сайты: