krugozor-old/database

数据库 — PHP 类库,用于简单、方便、快速、安全地与 MySQL 数据库进行交互,使用 PHP mysqli 扩展和预处理查询的模拟。

V2.0 2021-06-09 21:33 UTC

This package is auto-updated.

Last update: 2024-09-20 16:33:48 UTC


README

您可以通过以下方式获取它:下载其压缩包(点击此处),从本站克隆或通过 composer 安装(packagist.org 链接

composer require krugozor/database

变更记录

v2.0 - 2021年2月25日

  • 支持 sleep/wakeup
  • 修改了类 Statement 中的方法名称(由于历史原因,旧方法未使用 CamelCase 风格命名)
  • 修改了命名空间

v1.0

  • 不再支持,建议升级到 v2.0 版本

什么是 Database?

Database 是 PHP >= 5.3 的类库,用于简单、方便、快速、安全地与 MySQL 数据库进行交互,使用 PHP mysqli 扩展。

为什么需要自定义的 MySQL 类,PHP 中已经有了 PDO 抽象和 mysqli 扩展?

所有 PHP 数据库操作库的主要缺点是

  • 冗长
    • 为了防止 SQL 注入,开发人员有两种选择
    • 两种方法都有巨大的缺点
      • 预处理查询非常冗长。要使用 PDO 抽象或 mysqli 扩展,没有将所有方法聚合以从数据库获取数据的抽象层,因此要从表中获取值,至少需要编写 5 行代码!而且每个查询都是这样!
      • 手动转义参数,这在 SQL 查询体中使用,甚至都不需要讨论。优秀的程序员是懒惰的程序员。一切都应该尽可能自动化。
  • 无法获取 SQL 查询进行调试
    • 为了了解程序中为什么 SQL 查询不起作用,需要对其进行调试 — 找到逻辑错误或语法错误。为了找到错误,需要“看到”数据库对哪个查询“发火”,即包含在查询体中的参数。也就是说,需要有一个完整的 SQL。如果开发人员使用 PDO 和预处理查询,那么做这件事...是不可能的!原生库中没有任何最大化的便利机制来做到这一点没有预见到。只能做些扭曲的事情,或者查看数据库日志。

解决方案:Database - 用于操作 MySQL 的类

  1. 避免了冗长 — 使用原生库执行一个查询需要 3 行以上代码,而使用 Database,您只需编写一行。
  2. 根据指定的填充符类型转义所有查询体中的参数,提供可靠的 SQL 注入防护。
  3. 不替代原生 mysqli 驱动程序的功能,而是对其进行补充。

Database 库不是什么?

大多数数据库驱动器的包装都是一堆无用的代码和糟糕的架构的堆砌。它们的作者没有理解自己的包装的实际目的,把它们变成了查询构建器(sql builder)、ActiveRecord库和其他ORM解决方案的类似物。

Database库不是上述任何一种。它只是一个方便的工具,用于在MySQL数据库中处理普通的SQL——仅此而已!

什么是占位符(placeholders)?

占位符(英文:placeholders)——是专门类型化的标记,它们在SQL查询字符串中代替显式值(查询参数)。而实际值则在稍后通过执行SQL查询的主要方法作为后续参数传递。

<?php
// Предположим, что установили библиотеку через composer 
require  './vendor/autoload.php';

use Krugozor\Database\Mysql;

// Соединение с СУБД и получение объекта-"обертки" над "родным" mysqli
$db = Mysql::create("localhost", "root", "password")
      // Выбор базы данных
      ->setDatabaseName("test")
      // Выбор кодировки
      ->setCharset("utf8");

// Получение объекта результата Statement
// Statement - "обертка" над "родным" объектом mysqli_result
$result = $db->query("SELECT * FROM `users` WHERE `name` = '?s' AND `age` = ?i", "Василий", 30);

// Получаем данные (в виде ассоциативного массива, например)
$data = $result->fetchAssoc();

// Не работает запрос? Не проблема - выведите его на печать:
echo $db->getQueryString();

通过占位符系统处理过的SQL查询参数,将根据占位符的类型由专门的机制进行转义处理。也就是说,您现在不需要将变量放在mysqli_real_escape_string()等转义函数中,或者像以前那样将它们转换为数值类型。

<?php
// Раньше перед каждым запросом в СУБД мы делали
// примерно это (а многие и до сих пор `это` не делают):
$id = (int) $_POST['id'];
$value = mysql_real_escape_string($_POST['value'], $link);
$result = mysql_query("SELECT * FROM `t` WHERE `f1` = '$value' AND `f2` = $id", $link);

现在写查询变得容易、快捷,最重要的是,Database库完全防止了任何可能的SQL注入。

占位符类型和SQL查询参数类型

下面将描述占位符类型及其用途。在了解占位符类型之前,您需要了解Database库的工作原理。

 $db->query("SELECT ?i", 123); 

转换模板后的SQL查询

SELECT 123

在执行此命令的过程中,库会检查参数123是否为整数类型。整数类型的占位符?i代表符号?(问号)和单词integer的首字母。如果参数确实表示整数类型数据,则模板中的SQL查询占位符?i将被替换为123,然后SQL查询将被执行。

由于PHP是一种弱类型语言,因此上述表达式等价于以下表达式

 $db->query("SELECT ?i", '123'); 

转换模板后的SQL查询

SELECT 123

即,无论是以自己的类型还是以string的形式表示的数字(整数和浮点数)——在库看来都是等价的。

转换为占位符类型

 $db->query("SELECT ?i", '123.7'); 

转换模板后的SQL查询

SELECT 123

在此示例中,整数类型的占位符期望接收integer类型的值,而传入的是double。默认情况下,库以类型转换模式运行,这导致double类型最终被转换为int类型。

库的工作模式和强制类型转换

库有两种工作模式

  • Mysql::MODE_STRICT —— 严格匹配占位符类型和参数类型。在严格模式下,参数必须与占位符类型匹配。例如,尝试将55.5'55.5'作为参数传递给整数类型的占位符?i将引发异常。
// устанавливаем строгий режим работы
$db->setTypeMode(Mysql::MODE_STRICT);
// это выражение не будет исполнено, будет выброшено исключение:
// Попытка указать для заполнителя типа int значение типа double в шаблоне запроса SELECT ?i
$db->query('SELECT ?i', 55.5);
  • Mysql::MODE_TRANSFORM —— 当占位符类型和参数类型不匹配时,将参数转换为占位符类型的工作模式。默认情况下,设置为MODE_TRANSFORM模式,这是一种“宽容”模式——当占位符类型和参数类型不匹配时,不会生成异常,而是尝试通过PHP本身将参数转换为所需的占位符类型。顺便说一句,作为库的作者,我总是使用这个模式,严格模式(Mysql::MODE_STRICT)我只是出于好玩而实现,在实际工作中从未使用过。

MODE_TRANSFORM模式下允许以下转换

  • 转换为int类型(占位符?i
    • 表示为string或类型double的浮点数
    • bool TRUE转换为int(1),FALSE转换为int(0)
    • null转换为int(0)
  • 转换为double类型(占位符?d
    • 表示为string或类型int的整数
    • bool TRUE转换为float(1),FALSE转换为float(0)
    • null转换为float(0)
  • 转换为string类型(占位符?s
    • bool TRUE转换为string(1) "1",FALSE转换为string(1) "0"。这种行为与PHP中将布尔类型转换为int的行为不同,因为在实践中,布尔类型通常被记录为数字。
    • 类型为 numeric 的值根据 PHP 转换规则转换为字符串
    • NULL 转换为 string(0) ""
  • 将任何类型的数据转换为 null (填充符 ?n)
    • 所有参数。
  • 对于数组、对象和资源,不允许转换。

数据库库中提供了哪些填充符类型?

?i — 整数填充符

$db->query('SELECT * FROM `users` WHERE `id` = ?i', $value); 

注意! 如果您处理的数字超出了 PHP_INT_MAX,那么

  • 只能在程序中将它们作为字符串操作。
  • 不要使用该填充符,请使用字符串填充符 ?s (见下文)。因为超出 PHP_INT_MAX 的数字,PHP 将其解释为浮点数。库解析器会尝试将参数转换为 int 类型,最终“结果将是未定义的,因为 float 的精度不足以返回正确的结果。在这种情况下,既不会发出警告,也不会有任何提示!” — php.net

?d — 浮点数填充符

$db->query('SELECT * FROM `prices` WHERE `cost` = ?d', $value); 

注意! 如果您使用库处理 double 类型数据,请确保设置正确的区域设置,以使 PHP 和数据库层面的整数和小数部分分隔符一致。

?s — 字符串类型填充符

使用 PHP 函数 mysqli_real_escape_string() 对参数值进行转义

 $db->query('SELECT "?s"', "Вы все пидарасы, а я - Д'Артаньян!");

转换模板后的SQL查询

SELECT "Вы все пидарасы, а я - Д\'Артаньян!"

?S — 用于 SQL LIKE 操作的字符串类型填充符

使用 PHP 函数 mysqli_real_escape_string() 对参数值进行转义,并转义 LIKE 操作符中使用到的特殊字符(%_

 $db->query('SELECT "?S"', '% _'); 

转换模板后的SQL查询

SELECT "\% \_"

?nNULL 类型填充符

忽略任何参数值,在 SQL 查询中将填充符替换为 NULL 字符串

 $db->query('SELECT ?n', 123); 

转换模板后的SQL查询

SELECT NULL

?A* — 从关联数组生成序列对的关联集合填充符

示例:"key_1" = "val_1", "key_2" = "val_2", ..., "key_N" = "val_N"

其中 * 后的填充符是一个类型

  • i(整数填充符)
  • d(浮点数填充符)
  • s(字符串类型填充符)

转换和转义规则与上面描述的单个标量类型相同。示例

$db->query('INSERT INTO `test` SET ?Ai', ['first' => 123, 'second' => 1.99]);

转换模板后的SQL查询

INSERT INTO `test` SET `first` = "123", `second` = "1"

?a* — 从简单(或关联)数组生成序列值的集合填充符

示例:"val_1", "val_2", ..., "val_N"

其中 * 后的填充符是一个类型

  • i(整数填充符)
  • d(浮点数填充符)
  • s(字符串类型填充符)

转换和转义规则与上面描述的单个标量类型相同。示例

 $db->query('SELECT * FROM `test` WHERE `id` IN (?ai)', [123, 1.99]);

转换模板后的SQL查询

 SELECT * FROM `test` WHERE `id` IN ("123", "1")

?A[?n, ?s, ?i, ...] — 明确指定类型和数量的关联集合填充符,生成序列对 键 = 值

示例

 $db->query('INSERT INTO `test` SET ?A[?i, "?s"]', ['first' => 1.3, 'second' => "Д'Артаньян"]);

转换模板后的SQL查询

 INSERT INTO `test` SET `first` = 1,`second` = "Д\'Артаньян"

?a[?n, ?s, ?i] — 明确指定类型和数量的集合填充符,生成序列值

 $db->query('SELECT * FROM `test` WHERE `value` IN (?a[?i, "?s"])', [1.3, "Д'Артаньян"]);

转换模板后的SQL查询

 SELECT * FROM `test` WHERE `value` IN (1, "Д\'Артаньян")

?f — 表名或字段名填充符

此填充符适用于在查询中通过参数传递表名或字段名的情况。字段名和表名用单引号括起来

 $db->query('SELECT ?f FROM ?f', 'name', 'database.table_name');

转换模板后的SQL查询

 SELECT `name` FROM `database`.`table_name`

限制引号

要求 开发者遵循 SQL 语法。这意味着以下查询将不会正常工作

$db->query('SELECT CONCAT("Hello, ", ?s, "!")', 'world');

— 字符串填充符 ?s 需要放在单引号或双引号内

$db->query('SELECT concat("Hello, ", "?s", "!")', 'world');

转换模板后的SQL查询

SELECT concat("Hello, ", "world", "!")

对于习惯使用 PDO 的开发者来说,这可能看起来有些奇怪,但实现一个机制来确定在某种情况下是否需要将填充符的值放在引号内或外是一个非常复杂的问题,需要编写整个解析器。

库使用示例

// Предположим, что установили библиотеку через composer 
require  './vendor/autoload.php';

use Krugozor\Database\Mysql;

// Подключение к СУБД, выбор кодировки и базы данных.
$db = Mysql::create('localhost', 'root', '')
           ->setCharset('utf8')
           ->setDatabaseName('test');
// Создаем таблицу пользователей с полями:
// Первичный ключ, имя пользователя, возраст, адрес
$db->query('
    CREATE TABLE IF NOT EXISTS users(
        id int unsigned not null primary key auto_increment,
        name varchar(255),
        age tinyint,
        adress varchar(255)
    )
');

以下示例用于理解填充符的概念

不同的 INSERT 方式

通过不同类型的填充符插入简单数据
$db->query("INSERT INTO `users` VALUES (?n, '?s', ?i, '?s')", null, 'Иоанн Грозный', '54', 'в палатах');

转换模板后的SQL查询

INSERT INTO `users` VALUES (NULL, 'Иоанн Грозный', 54, 'в палатах')
通过字符串类型的关联集合填充符插入数据
$user = array('name' => 'Пётр', 'age' => '30', 'adress' => "ООО 'Рога и Копыта'");
$db->query('INSERT INTO `users` SET ?As', $user);

转换模板后的SQL查询

INSERT INTO `users` SET `name` = "Пётр", `age` = "30", `adress` = "ООО \'Рога и Копыта\'"
通过明确指定类型和数量的关联集合填充符插入数据
$user = array('name' => "Д'Артаньян", 'age' => '19', 'adress' => 'замок Кастельмор');
$db->query('INSERT INTO `users` SET ?A["?s", ?i, "?s"]', $user);

转换模板后的SQL查询

INSERT INTO `users` SET `name` = "Д\'Артаньян",`age` = 19,`adress` = "замок Кастельмор"

不同的 SELECT 方式

指定一个不正确的数值参数 - double 类型的值
$db->query('SELECT * FROM `users` WHERE `id` = ?i', '1.00');

转换模板后的SQL查询

SELECT * FROM `users` WHERE `id` = 1
---
 $db->query(
    'SELECT id, adress FROM `users` WHERE `name` IN (?a["?s", "?s", "?s"])',
    array('Василий', 'Иван', "Д'Артаньян")
); 

转换模板后的SQL查询

SELECT id, adress FROM `users` WHERE `name` IN ("Василий", "Иван", "Д\'Артаньян")
数据库的名称、表和字段与请求参数的传递方式相同。不要对'.users.name'字段名感到惊讶 - 这是MySQL允许的语法
$db->query(
    'SELECT * FROM ?f WHERE ?f IN (?as) OR `id` IN (?ai)',
    '.users', '.users.name', array('Василий'), array('2', 3.000)
);

转换模板后的SQL查询

SELECT * FROM .`users` WHERE .`users`.`name` IN ("Василий") OR `id` IN ("2", "3")

API的一些功能

使用queryArguments()方法 - 参数以数组的形式传递。这是在query()方法之后,第二个用于向数据库查询的方法
$sql = 'SELECT * FROM `users` WHERE `name` = "?s" OR `name` = "?s"';
$arguments[] = "Василий";
$arguments[] = "Д'Артаньян";
$result = $db->queryArguments($sql, $arguments);
// Получим количество рядов в результате
$result->getNumRows(); // 2
插入记录,获取自增字段的最后值和影响的行数
if ($db->query("INSERT INTO `users` VALUES (?n, '?s', '?i', '?s')", null, 'тест', '10', 'тест')) {
    echo $db->getLastInsertId(); // последнее значение автоинкрементного поля
    echo $db->getAffectedRows(); // количество задействованных рядов
}
以关联数组的形式获取所有内容
// Получить все...
$res = $db->query('SELECT * FROM users');
// Последовательно получать в виде ассоциативных массивов
while (($data = $res->fetchAssoc()) !== null) {
    print_r($data);
}
从查询中获取单个值
echo $db->query('SELECT 5 + ?d', '5.5')->getOne(); // 10.5
获取当前连接的所有SQL查询
print_r($db->getQueries());