В качестве класса для работы с БД используется немного доработанная библиотека DBSimple Дмитрия Котерова
Библиотека имеет поддержку placeholder-ов: для вставки данных в строку SQL-запроса используются специальные маркеры, например, "?", а сами данные передаются позже: query('SELECT * FROM tbl WHERE id=?', $id). Это позволяет почти полностью возложить на библиотеку заботу о корректности передаваемых в запрос параметров и обезопасить приложение от SQL-инъекций.
Ниже приведена сокращенная версия руководства, которая позволит разрабатывать модули для LAB CMS.
Чтобы избежать самой популярной среди скриптописателей проблемы с безопасностью — SQL Injection, существует хороший способ: использовать в запросах placeholder-ы и переложить обработку «небезопасных» данных на плечи библиотеки работы с СУБД. Иными словами, там, где «плохой» программист пишет «дырявый» код:
«хороший» применит placeholder-ы:
и, тем самым, гарантированно избавит себя от дыр вида SQL Injection. Основными для DbSimple являются placeholder-ы ? (вставка строки) и ?a (вставка списка или массива). Они используются в подавляющем большинстве случаев.
Это самый простой вид placeholder-а, используемый в подавляющем большинстве случаев. Вставляемое на его место значение обрамляется апострофами ('), при этом все символы, которые СУБД считает служебными, экранируются в соответствии с правилами этой СУБД (в MySQL перед апострофами вставляется \).
Правильнее было бы назвать данный placeholder не строковым, а бинарным, т.к. с его помощью в БД можно вставлять произвольные бинарные данные (в том числе — картинки, исполняемые файлы и т. д.).
Если значение подставляемого параметра равно null, вместо обрамления его апострофами вставляется ключевое слово SQL NULL. Это же верно и для всех остальных типов placeholder-ов.
Данный вид placeholder-ов удобно использовать для составления IN-выражений в SQL — при условии, что в программе имеется список с перечисленными значениями:
Помните, что в случае передачи пустого списка вы получите ошибочный SQL-запрос:
Если в качестве параметра передан ассоциативный массив (ключи массива не целочисленные, а строковые), DbSimple заменяет ?a набором пар ключ=значение. Это удобно использовать в UPDATE-запросах:
Для удобства работы DbSimple поддерживает еще целый набор видов placeholder-ов, которые будут описаны далее. Они используются значительно реже и, как правило, позволяют просто сократить письмо. Если вы думаете, что все это слишком сложно для понимания, — не используйте дополнительные placeholder-ы.
Часто все имена таблиц, используемых в программе, имеют один и тот же префикс. Например, в форуме phpBB этот префикс, как правило, равен phpbb_, и таблицы называются phpbb_users, phpbb_sessions и т. д. Это делается для того, чтобы в одной базе данных можно было хранить сразу несколько наборов таблиц для разных форумов, избегая конфликтов имен.
Префиксный placeholder заменяется на некоторое фиксированное значение, ранее установленное для объекта базы данных:
Ключевые слова SQL, такие как date, int и т. д., не могут использоваться в качестве имен полей и таблиц. Например, у вас не получится создать в таблице столбец с именем date. Тем не менее, многие СУБД предлагают способы, позволяющие все же ссылаться на подобные объекты. Имена идентификаторов следует окружить теми или иными ограничителями:
Идентификаторный placeholder заставляет СУБД воспринимать значение как идентификатор:
Конечно, использование идентификаторного placeholder-а полностью защищает от уязвимостей вида SQL Injection. Передаваемый параметр обрамляется «идентификаторными кавычками», а если они встречаются в нем самом, то экранируются специфичным для СУБД образом.
Уже знакомый нам идентификаторный placeholder ?# может принимать в качестве значения не только строку, но также и массив (список значений). Это очень удобно для формирования INSERT-запросов в соответствии со стандартом SQL'92:
В зависимости от того, передаете вы placeholder-у ?# массив или строку, он "развернется" в список идентификаторов или в единственный идентификатор соответственно.
Переданный параметр преобразуется в целое цисло и вставляется без обрамления апострофами. В случае ошибки конвертирования вставляется 0.
Вещественный placeholder можно использовать для передачи дробных (вещественных) чисел в СУБД. В зависимости от локальных настроек для разделения компонент PHP использует либо точку, либо запятую, в то время как стандарт SQL требует обязательного использования точки безотносительно к локальным настройкам. Чтобы не связываться с локальными настройками, просто применяйте дробный placeholder.
В большинстве таблиц, с которыми приходится работать, присутствует целочисленный столбец с именем ID — первичный ключ данной таблицы. На этот столбец устанавливают уникальный индекс и "навешивают" auto_increment, чтобы при вставке очередной записи в таблицу она автоматически получала уникальный номер (как правило, отличный от нуля).
На первичный ключ очень удобно ссылаться из другой (или из той же самой) таблицы, такие ссылки называют внешними ключами. Например, у нас может быть таблица forest с полями (ID, PARENT_ID, NAME), определяющая множество деревьев. Чтобы не нарушать ссылочной целостности, корневой элемент каждого дерева должен иметь PARENT_ID=NULL.
Предположим, мы пишем скрипт, который вставляет в forest новую запись. ID родительского узла передается так: http://example.com/tree.php?parent=123. Что делать, когда нам нужно передать NULL в качестве идентификатора родителя?
Теперь можно передать в GET-параметр parent значение "" или 0 для вставки NULL.
Ссылочный placeholder позволяет немного упростить письмо:
Подставляемое значение преобразуется в целое число. Если это число равно нулю, то вместо него подставляется NULL, иначе — оно само.
Библиотека DbSimple имеет в своем имени слово simple ("простой") потому, что она максимально упрощает процедуру выполнения запросов к базе данных.
Данный метод является самым простым и универсальным. С его помощью вы можете выполнить запрос к базе данных и (если это SELECT-запрос) получить в двумерный массив все строки результата операции.
Если вам не нужны все строки результата, ограничьте выборку стаднартными средствами SQL — например, предложением LIMIT в MySQL:
Результат выборки, который попал в $rows (см. предыдущий пример), является списком массивов. Иными словами, ключи $rows — целые числа, большие либо равные нулю и идущие по порядку.
В ряде случаев оказывается удобным индексировать результат не целыми числами, а ассоциативными значениями, взятыми в одном из столбцов выборки. Например, если мы выбираем пользователей, в качестве ключа может быть использован их primary key в базе (идентификатор).
Чтобы DbSimple сформировал ассоциативный массив, а не список, используйте для столбца фиксированное имя ARRAY_KEY:
По наличию столбца с именем ARRAY_KEY библиотека определит, какой формат данных вы ожидаете получить, и произведет соответствующие преобразования. Т.к. этот столбец является служебным, он сам в результат выборки не попадет (см. последнюю строчку примера).
Если результат выборки необходимо оформить в виде многомерного ассоциативного массива, используйте следующий синтаксис:
На выходе получится двумерный ассоциативный массив: $messagesByForumsAndTopics[topicId][messageId] = messageData. Поле message_topic_id, объявленное как ARRAY_KEY_1, стало первым индексом массива, а поле message_id (ARRAY_KEY_2) — вторым.
Существует и специальный вариант данного синтаксиса, позволяющий формировать индексы массивов в возрастающем порядке, а не на основе величины, полученной из БД:
В данном примере будет получен массив списков пользователей $usersByCity[cityId][] = userData. Т.е. каждый элемент массива, соответствующий некоторому городу, содержит обычный список записей с данными пользователей. Мы достигли получения обычного списка, передав NULL в качестве ARRAY_KEY_2.
Иногда в таблице хранится древовидная структура: каждая запись содержит поле parent_id, ссылающееся на ID родительского элемента. DbSimple облегчает выборку и такой структуры, формируя вложенные древовидным образом массивы:
Строго говоря, на выходе в $forest мы получаем не дерево, а лес — набор однокоренных деревьев. Дело в том, что в результатах выборки могут присутствовать сразу несколько элементов, не имеющих родителей. Все такие элементы объявляются вершинами дерева, а их "дети" строятся по правилу: PARENT_KEY "ребенка" равен ARRAY_KEY "родителя".
У каждого элемента результирующего массива, помимо его собственных данных (в нашем случае это * — все поля записи), имеется запись с ключом childNodes. В ней-то и содержится массив всех "детей" текущего элемента (или пустой массив, если это листовая вершина).
Выше мы видели, что, используя один-единственный метод select(), можно осуществлять любые выборки из базы данных, по желанию форматируя их в виде ассоциативного массива или дерева.
Часто бывают случаи, когда выборка гарантировано состоит из одной записи. Предположим, у нас есть ID некоторого объекта, и мы хотим получить данные его полей. Можно для этого воспользоваться методом select(), а потом взять первую строку результата, однако удобнее будет применить метод selectRow():
Иногда нам нужны данные в еще более простом формате, чем выдает selectRow(). Например, мы хотим получить имя пользователя, зная его ID, и при этом совершенно не интересуемся всеми остальными его полями. Метод selectCell() подходит здесь как нельзя лучше:
Последний вид форматирования результатов выборки — получение одного столбца. Метод selectCol() трактует результат как массив-столбец и возвращает данные в виде списка:
Можно также индексировать массив ассоциативными значениями, а не целыми числами. Это делается при помощи служебного поля с уже знакомым именем ARRAY_KEY:
Организация страничного навигатора по некоторому результату выборки может оказаться настоящей головной болью, если не знать, как оптимальнее всего подойти к этому вопросу. Помимо выполнения запроса на получение строк очередной страницы нужно дополнительно подсчитывать общее число записей. Иными словами, нам нужен один запрос с предложением LIMIT, и один — с выборкой COUNT(*).
Метод selectPage() сводит эти две операции к одному вызову. С его помощью вы передаете СУБД запрос с необходимыми вам LIMIT-ограничениями, а библиотека дополнительно производит еще и COUNT-запрос:
Обратите особое внимание, что первый параметр метода является ссылочным: в переменную, указанную на его месте, записывается общее число попадающих под запрос записей, без учета LIMIT-предложения.
Как видите, до сих пор повествование "крутилось" вокруг различных вариаций метода select(), предназначенного для организации выборок из базы данных (SELECT). Но ведь существуют еще и команды вставки (INSERT) и обновления (UPDATE) данных. Как быть с ними?
Библиотека DbSimple поддерживает метод query(), который удобно использовать именно для подобных запросов. А теперь — сюрприз: query() является ни чем иным, как полным синонимом для пресловутого select(). А называется он по-другому, чтобы не пришлось думать: как это — INSERT в select().
Метод query() (а значит, и select() тоже!) возвращает различные значения для INSERT и UPDATE-запросов:
Вот несколько примеров:
При возникновении ошибки запроса вызывается зарегистрированный ранее обработчик ошибок. Он, как правило, завершает работу скрипта и выдает исчерпывающую информацию о контексте вызова запроса. В большинстве случаев это поведение — самое разумное, однако для некоторых запросов может понадобиться временно отключить стандартный механизм и обработать ошибку вручную, непосредственно в коде программы.
Библиотека DbSimple позволяет позапросно отключать обработку ошибок, используя для этого стандартную нотацию PHP — оператор @. Иными словами, поставив @ перед вызовом любого из методов DbSimple, вы заставите этот метод вернуть null в случае возникновения проблем. Далее вы можете извлечь контекст выполнения запроса из свойства error (краткое описание ошибки — из errmsg) и поступить с этой информацией так, как вам нужно.
Типичный пример: мы хотим вставить некоторую запись в таблицу, но, если она там уже имеется (нарушение уникальности), не завершать работу программы, а выводить пользователю аккуратное предупреждение в форме. Это можно сделать так:
Каждый, кто писал скрипты со сложными запросами к СУБД, знает, какие проблемы начинаются, если запрос требуется составлять динамически. Например, если нам требуется добавить в выражение WHERE некоторое ограничение, если пользователь поставил галочку в форме, и не выполнять его в противном случае. Традиционно в таких случаях применяют динамическое составление SQL-запросов, формируя их в виде строки:
Теперь представьте, что на activated_at наложено более сложное условие, учитывающее также и его величину:
В примере выше мы используем всего одно динамическое поле, но на практике их может быть значительно больше. В результате читабельность кода резко снижается, не говоря уж о читабельности генерируемых SQL-запросов...
К счастью, данная проблема относится к классу беспроигрышно-разрешимых. А именно, имеется такой синтаксис, который позволяет создавать динамические SQL-запросы без какого-либо снижения читабельности кода! Он используется в DbSimple:
Обратите внимание на блок, обрамленный фигурными скобками ({}-блок). Нетрудно догадаться, как он работает: если хотя бы один placeholder, используемый в этом блоке, имеет специальное значение DBSIMPLE_SKIP, то весь блок удаляется из запроса, в противном случае удаляются только обрамляющие фигурные скобки (точнее, они заменяются на пробелы, чтобы сформированный SQL-запрос хорошо читался).
Начав однажды пользоваться {}-макросами, через некоторое время перестаешь понимать, как же обходился без них раньше. Вот еще примеры запросов с макроподстановками:
В последнем примере применены два интересных приема.
Отсюда мораль: не всегда бесполезные на первый взгляд условия действительно не имеют смысла. Иногда их очень удобно использовать совместно с условными блоками.
Транзакции поддерживаются методами Db::i()->transaction(), commit() и rollback(). У каждого соединения в любой момент времени может существовать только одна текущая транзакция.
Каждый запрос может быть снабжен одним или несколькими атрибутами, являющимися некоторыми указаниями для DbSimple. Они оформляются в виде SQL-комментариев, идущих перед телом запроса, и имеют формат:
Если BLOB-ы очень большие, можно работать с ними как с объектами, выполняя read и write "кусками".
Для того, чтобы получить blob-поля в виде объектов, а не в виде строки, используйте синтаксис:
В результате в $row['blob_field'] окажется не строка, равная содержимому blob-а, а объект DbSimple_*_Blob, у которого есть метод read().
Кэширование осуществляется в предположении, что один и тот же запрос, выполняемый через небольшой промежуток времени, вернет одинаковый результат. Для управления этим промежутком используется синтаксис:
Здесь "10h 20m 30s" - промежуток времени, в течение которого запрос будет браться из кэша. (Если указано только число, то оно трактуется как промежуток времени в секундах.)
Вы можете также использовать дополнительные условия для управления инвалидацией кэша. Например, если одна из таблиц, участвующих в запросе, изменилась, следует считать кэш недействительным. Вы можете сообщить об этом DbSimple при помощи следующей конструкции:
Здесь предполагается, что в таблице forum имеется столбец с именем modified, хранящий дату последнего изменения записи (аналогично и с таблицей topic). Как только в указанных таблицах появляется новая запись, библиотека это обнаруживает, делая запрос SELECT MAX(forum.modified) FROM forum, и очищает кэш.
Всего комментариев: 0