ramphor / sql
SQL查询字符串构建器
Requires
- php: >=5.6.4
- ext-mbstring: *
Suggests
- filp/whoops: Interactive error and exception handling
- jdorn/sql-formatter: SQL query highlighting
- symfony/var-dumper: Pretty print complex values better with var-dumper available
- vrana/adminer: Database management in a single PHP file.
This package is auto-updated.
Last update: 2024-09-13 11:20:52 UTC
README
原始SQL查询构建器 ~ 原始SQL查询的瑞士军刀
介绍
当我们使用ORM或Doctrine DBAL等管理或抽象数据库层时,我们已经有一些很好的工具。大多数ORM允许你在需要更大的/自定义的灵活性或功能时,执行原始SQL查询。
但是,当你处理原始/本地的SQL查询的纯文本字符串时,你有什么工具呢?你有很多字符串连接,implode()
,PDO::prepare
,PDO::quote
,sprintf
(对于勇敢者)和mysqli::real_escape_string
(因为第一个版本不够真实或名字不够长)。
统治他们的那一环,绑定他们的那一环
介绍《原始SQL查询构建器》(SQLQB);结合了所有具有占位符(如?
、:id
、%s
、%d
)的功能,以及ORM风格的流畅接口(方法return $this
用于方法链)以及更多。
它是介于$sql = '...';
和$db->query($sql)
之间的粘合剂。这是你在原始SQL查询字符串中连接、'转义'、'引用'、'准备'和'绑定'值的部分。
这不仅仅是一个ORM或ORM的替代品,当你需要使用占位符创建原始SQL查询字符串时,它是你使用的工具。它不会像PDO::prepare
那样'准备'或'执行'你的查询;但它确实支持使用?
或:id
作为占位符的熟悉语法。它还支持sprintf
的子集的%s
/ %d
语法。
此外,它支持使用@
插入'原始'字符串(无需引号或转义),例如sql('dated = @', 'NOW()')
,甚至可以替换列名或表名,以及自动使用[]
将数组implode()
,例如sql('WHERE id IN ([])', $array)
echo sql('SELECT * FROM @ WHERE @ = ? OR name IN ([?]) OR id IN ([]) AND created = @', 'users', 'name', 'Trevor', ['Tom', 'Dick', 'Harry'], [1, 2, 3], 'NOW()');
无需转义,无需引号,无需处理数组,也无需连接...
输出
SELECT * FROM users WHERE name = "Trevor" OR name IN ("Tom", "Dick", "Harry") OR id IN (1, 2, 3) AND created = NOW()
描述
SQLQB 实质上只是一个针对 SQL 查询字符串的“美化”字符串包装器,提供了多种执行相同操作的方式,取决于您的个人喜好或编码风格(支持多种命名约定,具有 camelCase 和 snake_case 函数名称,或者您可以在构造函数中编写语句)。设计为 100% 多字节兼容(UTF-8,取决于您的 mb_internal_encoding() 设置,所有函数都使用 mb_* 内部),支持任何数据库(数据库连接是可选的,它只是一个字符串连接器,您以自己的方式编写数据库/驱动程序的查询)和 支持任何框架(无需框架或外部依赖),轻量级(一个变量)但 功能丰富,无状态(不了解查询,不解析或验证查询),使用 原生 SQL 语言 编写,学习曲线 为零(只需了解 SQL 语法)和针对 快速编写、设计、测试、构建、开发和原型设计 原生/原始 SQL 查询字符串的功能。您可以构建 完整的 SQL 查询 或 部分 SQL 片段,甚至 非 SQL 字符串。
历史
我从阅读有关 MyBatis SQL 构建器类 的内容中获得了最初的灵感;它致力于少数但自豪的热爱编写原生 SQL 查询的程序员!拥有巨大的力量...
最初的设计是为了在 ORM 查询构建器和原生 SQL 查询之间架起桥梁;通过使用熟悉的 ORM 风格的 '流畅接口',但将语法保持得尽可能接近 SQL。
速度和安全
这个库不是为了执行速度或完全防止 SQL 注入而设计的,但它将比手动转义字符串做得更好;但是只有真正的“预处理语句”才能提供防止 SQL 注入的保护;然而,它们增加了更多的复杂性和许多限制。实际上,几乎不可能仅使用真正的/真实的预处理语句构建整个网站,因此您不可避免地要编写许多“未准备”的语句;这就是这个类能帮助您的地方;通过编写更安全的“未准备”语句!它将“引号”和“转义”字符串,检测应使用的数据类型;但它不进行语法检查、语法解析、查询/语法验证等。主要任务是替换占位符与对应的数据,具有自动检测数据类型的能力。
简化复杂
这个类对于像 'SELECT * FROM users WHERE id = ' . $id;
这样的小型/静态查询并不是特别有用或必要。
但是,当您的 SQL 查询变得更大、更复杂时,它真正开始发光;特别是在 INSERT
和 UPDATE
查询上。查询越大,好处就越大;这正是它的设计目的。通过简单地在你想要放置变量的地方放置 ?
,就消除了“转义”、“引号”和连接字符串的复杂性和繁琐工作,这个库会处理其余的部分。
所以当您发现自己处理 '对象关系阻抗不匹配' 时;因为您有一个 400+ 个表、6000+ 个列/字段、一个具有 156 个数据字段的表、10 个具有 100 个以上字段的表、24 个具有 50 个以上字段的表、1000+ 个 varchar/char 字段的数据库,就像我一样;只要记住这个库是为了帮助减少一些复杂性而设计的!特别是当您几个月或几年后再次返回这些查询时,仍然具有(半)可读性,这使得它非常易于使用。
“真实”预处理语句的限制
其中一个限制是不能这样做: WHERE ? = ?
,而在这个类中你可以这样做,另一个限制是你基本上不能使用NULL值(有解决方案)。此外,你不能使用动态的列/表/字段名,例如 SELECT ? FROM ?
,所有这些你都可以在这个类中使用;你可以在$db->query($sql)
中做的任何事情都可以在这里做!
安装
Composer
composer require ramphor/sql
手动
/* composer.json */ "require": { "php": ">=5.6", "ramphor/sql": "*" }
或从GIT
https://github.com/ramphor/sql
要求(类似于Laravel)
PHP 5.6+ (for ...$args syntax)
Multibyte mb_* extension
你好,世界
echo sql('Hello @', 'World');
Hello World
echo sql('Hello ?', 'World');
Hello "World"
你好,SQL世界
echo sql('SELECT ?, ?, ?, ?, @', 1, "2", null, 'Hello World', 'NOW()');
SELECT 1, 2, NULL, "Hello World", NOW()
注意:'数字'值(如上面的"2")不使用引号(即使它们是字符串值)。PHP null
值变为SQL NULL
值。
更多示例
echo sql('?, ?, ?, ?, ?, ?, ?', 4, '5', "Trevor's", 'NOW()', true, false, null);
4, 5, "Trevor\'s", "NOW()", 1, 0, NULL,
"NOW()"
是一个SQL函数,它将不会执行,使用 @
来输出原始字符串
echo sql('@, @, @, @, @, @, @', 4, "5", "Trevor's", 'NOW()', true, false, null);
4, 5, Trevor's, NOW(), 1, 0, NULL
"Trevor's"
未使用 @
转义,会产生SQL错误
流畅式风格
echo sql()->select('u.id', 'u.name', 'a.*') ->from('users u') ->leftJoin('accounts a ON a.user_id = u.id AND a.overdraft >= ?', 5000) ->where('a.account = ? OR u.name = ? OR a.id IN ([])', 'BST002', 'foobar', [1, 2, 3]) ->orderBy('u.name DESC') ->limit(5, 10);
SELECT u.id, u.name, a.* FROM users u LEFT JOIN accounts a ON a.user_id = u.id AND a.overdraft >= 5000 WHERE a.account = "BST002" OR u.name = "foobar" OR a.id IN (1, 2, 3) ORDER BY u.name DESC LIMIT 5, 10
查询包括额外的空白字符用于格式化和显示,可以通过调用 Sql::singleLineStatements()
来删除。可以通过调用 Sql::lowerCaseStatements()
将SQL关键词转换为小写
其他功能
数组
echo sql('WHERE id IN ([])', [1, 2, 3]);
WHERE id IN (1, 2, 3)
echo sql('WHERE name IN ([?])', ['joe', 'john', 'james']);
WHERE name IN ("joe", "john", "james")
echo sql('WHERE id = :id OR name = :name OR dob = :dob:raw', ['id' => 5, 'name' => 'Trevor', 'dob' => 'NOW()']);
WHERE id = 5 OR name = "Trevor" OR dob = NOW()
范围
echo sql('WHERE id IN (1..?) OR id IN (?..?)', 3, 6, 8);
WHERE id IN (1, 2, 3) OR id IN (6, 7, 8)
文本过滤器
例如:trim、pack(合并内部空白)以及截取20个字符
echo sql('SET description = %s:pack:trim:crop:20', "Hello World's Greatest");
SET description = "Hello World\'s Greate"
初学者指南
编写查询有两种主要方式;要么像 sprintf
函数一样使用构造函数(例如 sql('?', $value)
),要么通过调用 '流畅接口'(方法链)来使用,例如 sql()->select(...)->from(...)->where(...)
等。
流畅接口
其基本思想非常简单;当你调用一个函数时,它本质上只是将函数/语句名(例如 select(...)
,from(...)
,where(...)
)(附带一些额外的空白字符)附加到内部的 $sql
字符串变量上,并返回 $this
以实现方法链。
伪代码
class Sql { function select(...$cols) { $this->sql .= 'SELECT ' . implode(', ', $cols); return $this; } function from(...$tables) { $this->sql .= PHP_EOL . 'FROM ' . implode(', ', $tables); return $this; } function leftJoin($stmt, ...$params) { return $this->prepare(PHP_EOL . 'LEFT JOIN ', ...$params); } function where($stmt, ...$params) { return $this->prepare(PHP_EOL . 'WHERE ', ...$params); } function prepare($stmt, ...$params) { // the magic happens here // processing `?`, `@`, escaping, quoting etc. } }
echo sql()->select('*') ->from('users u') ->leftJoin('accounts a ON a.user_id = u.id') ->where('u.id = ?', 5);
SELECT * FROM users u LEFT JOIN accounts a ON a.user_id = u.id WHERE u.id = 5
一些函数如 leftJoin
和 where
支持变量参数的 prepare
/sprintf
风格,而其他如 select
和 from
则更方便地使用 implode
来编写代码。
多种调用约定
代码支持camelCase、snake_case和UPPER_CASE语法;以及简短的形式
构造函数
use Ramphor; $sql = new sql(); $sql = new Sql(); $sql = new SQL(); // or $sql = new \Ramphor\Sql();
方便的 sql()
函数
function sql($stmt = null, ...$params) { return new Ramphor\Sql($stmt, ...$params); } $sql = sql(); $sql = Sql(); $sql = SQL();
camelCase
->select('col1', 'col2', 'col3') ->from('table t') ->join('table2 t2 ON ... = ?', $var) ->leftJoin('table3 t3 ON ... = ?', $var) ->where('foo = ?', 'bar') ->groupBy('t.col1', 't2.col2') ->orderBy('t.col1 DESC') ->limit(5, 10); // other common functions ->selectDistinct(..) ->insert(..) ->insertInto(..) ->values(..) ->set(..) ->delete(..) ->deleteFrom(..) ->having(..) ->union(..)
snake_case
->select('col1', 'col2', 'col3') ->from('table t') ->join('table2 t2 ON ... = ?', $var) ->left_join('table3 t3 ON ... = ?', $var) ->where('foo = ?', 'bar') ->group_by('t.col1', 't2.col2') ->order_by('t.col1 DESC') ->limit(5, 10); // other common functions ->select_distinct(..) ->insert(..) ->insert_into(..) ->values(..) ->set(..) ->delete(..) ->delete_from(..) ->having(..) ->union(..)
UPPER_CASE
->SELECT('col1', 'col2', 'col3') ->FROM('table t') ->JOIN('table2 t2 ON ... = ?', $var) ->LEFT_JOIN('table3 t3 ON ... = ?', $var) ->WHERE('foo = ?', 'bar') ->GROUP_BY('t.col1', 't2.col2') ->ORDER_BY('t.col1 DESC') ->LIMIT(5, 10); // other common functions ->SELECT_DISTINCT(..) ->INSERT(..) ->INSERT_INTO(..) ->VALUES(..) ->SET(..) ->DELETE(..) ->DELETE_FROM(..) ->HAVING(..) ->UNION(..)
简短语法
->s('col1', 'col2', 'col3') // s = SELECT ->f('table t') // f = FROM ->j('table2 t2 ON ... = ?', $var) // j = JOIN ->lj('table3 t3 ON ...?', $var) // lj = LEFT JOIN ->w('foo = ?', 'bar') // w = WHERE ->gb('t.col1', 't2.col2') // gb = GROUP BY ->ob('t.col1 DESC') // ob = ORDER BY ->l(5, 10); // l = LIMIT // other common functions ->sd(..) // sd = SELECT DISTINCT ->i(..) // i = INSERT ->ii(..) // ii = INSERT INTO ->v(..) // v = VALUES ->d(..) // d = DELETE ->df(..) // df = DELETE FROM ->h(..) // h = HAVING
补充说明
设置连接
连接只需要为所有创建的sql()
对象设置一次。你不需要新的连接,你只需将你的正常连接对象提供给类;它将提取一个'驱动'。连接存储在静态类变量中,所以类的所有实例共享相同的连接。
连接类型会自动检测:PDO、MySQLi或SQLite3对象,或PostgreSQL/MySQL资源连接。
设置连接是充分利用类所提供所有功能的必要条件。
\Ramphor\Sql::setConnection($conn);
一旦设置了连接,类(以及之后创建的所有 sql()
实例)将使用你的连接来'转义'和'引用'字符串,并且如果你想要的话,你可以直接从类中执行查询。使用类执行查询是可选的,但非常方便!
query(),exec(),fetchAll(),lookup()
你可以直接从 sql
对象调用4个非常轻量级的函数。所有连接类型都进行了统一。
fetchAll()
$array = sql('SELECT ...')->fetchAll();
返回一个包含所有结果集行的关联数组。
PDO代码示例
function ($sql) use ($conn) { $recset = $conn->query($sql); if ( ! $recset) { throw new \Exception('PSO::query() error: ' . $conn->errorInfo()[2]); } $result = $recset->fetchAll(\PDO::FETCH_ASSOC); $recset->closeCursor(); return $result; };
lookup()
$value = sql('SELECT 999')->lookup(); echo $value;
999
$data = sql('SELECT 1 AS id, "Trevor" AS name')->lookup(); var_dump($data);
array(2) { ["id"] => string(1) "1" ["name"] => string(6) "Trevor" }
lookup()
将返回单行数据,或根据您选择的列数返回单个值。如果您选择一列,您将直接在 $value 中获得值或一个空值。如果您选择了多个列,它们将作为关联数组返回,其中键是列名。只返回第一行,其他任何结果都将被丢弃。
此函数与SQLite3::querySingle类似,除了结果'mode'是自动检测的,这与SQLite3::querySingle
中的$entire_row
值相对应
query()
$recset = sql(...)->query();
query()
将使用数据库连接的query()
函数执行SQL查询,并返回相同的结果。这是一个非常薄的包装,使其非常快速且易于使用;它比从依赖注入/IoC容器(如$container->db()->query($sql);
)获取连接对象要方便得多;只需sql(..)->query()
exec()
$affected_rows = sql('DELETE FROM ...')->exec();
exec()
执行您不期望“查询”结果的SQL查询。这通常是INSERT
、UPDATE
或DELETE
语句。MySQL和SQLite3返回affected rows
值,与从PDO::exec()
返回的相同。
此函数根据您的连接类型内部调用PDO::exec()
、MySQLi->real_query()
和SQLite3::exec()
。
文字?和@
PDO将在未来的版本中支持??
作为文字?
;如PHP 7.2的PDO标准所提议的此处
此类还支持在您的代码中为文字?
使用??
,以及为文字@
和%
使用@@
和%%
特性
- 单个文件:没有其他类、接口、特质或自定义异常
- 5,000+行代码(包括注释)和完整的文档
- 200字符的多字节正则表达式为替换引擎提供动力(使用600行的函数mb_ereg_replace_callback())
- ORM风格的'Fluent interface'
- 旨在弥合ORM的'Fluent interface'和原生SQL语句之间的差距
- 查询按自然SQL字符串连接顺序构建,只需将它们附加到内部
$sql
字符串变量即可 - 任何复杂性和任何数量的自定义命令的查询都可以通过SQLQB表达。
- PHP 5.6+(大量使用...$arg语法)
- 除了(mb_*)扩展外,没有外部依赖。在任意PHP应用程序或框架中使用SQLQB。
- 支持多种函数调用/代码风格,SELECT()或select(),leftJoin()或left_join()或LEFT_JOIN()
- 简单的全局包装函数
$sql = sql();
而不是调用$sql = new Ramphor\Sql();
- 广泛使用PHP魔术方法(__toString(), __get(), __invoke(), __call())
- 添加少量额外的空白来格式化查询以供显示
- 最小化SQL抽象
- 完全数据库中立和无关;但PDO、MySQLi、PostgreSQL和SQLite是主要目标。
- 内置PDO、MySQLi、PostgreSQL、SQLite和MySQL(旧版)的驱动程序。驱动程序是嵌入的,不是独立的类。
它不做什么
- 不会解析您的字符串
- 不会验证您的字符串
- 不会验证您的字符串
- 不会检查语法错误
- 不会尝试从您那里抽象出原生/原生SQL,只是为您提供工具来更快、更安全地编写它
- 不尝试替换原始/原生SQL编写
- 不重新排序或更改SQL语句的自然顺序
- 不更改传统SQL语句的名称、意义或意图
- 不使用反射或注解
- 不重新结构/重新格式化/重新对齐/重新排列您的语句(除了添加一些空白以提高可读性外)
- 不进行输入/参数验证/验证,除了字符串转义外
- 不检查列类型是否与数据库模式匹配
- 不使用任何模式/模型/实体/映射/配置/YAML/XML/临时/缓存文件
- 不内部存储抽象SQL语句接口,它构建的一切都是可见的
- 没有任何外部依赖,只有一个文件,PHP 5.6+ 和 mb_*
- 不添加任何其他类(除了Sql),不添加接口、特性、不添加新的异常类等。
- 不保证您的字符串/查询不受SQL注入的影响(如真正的'预编译语句'); 然而,它仍然比编写原始/原生/传统SQL字符串安全得多!
结论
我的目标是让您(和我)能够比传统的连接方式更快、更安全、更清晰地编写SQL查询。
它们可能不会执行得更快,尤其是在正则表达式引擎启动时,但您将节省的时间,以及几个月或几年后回到您的代码,立即能够阅读和理解它,是无价的!在大多数情况下,代码的可读性应该排在首位,尤其是在大型查询/项目中。
我相信这个代码和解决方案是独特的;因为我之前没有找到过类似的东西;根本就没有其他库具有相同的功能和特性集;而且非常少有工具能帮助您更快地编写原始SQL查询。
我希望您喜欢这个努力,它花费了我很多周(数百小时)的业余时间来编写这个代码和文档。
我很乐意听到其他人使用这个代码的消息!功能、建议、赞扬、问题、评论和感谢都欢迎!
Trevor离开...