twister/sql

SQL查询字符串构建器

dev-master 2017-08-26 21:31 UTC

This package is not auto-updated.

Last update: 2024-09-29 02:23:55 UTC


README

原始SQL查询构建器 ~ 原始SQL查询的瑞士军刀

简介

当我们使用ORM(对象关系映射)或Doctrine DBAL等管理或抽象数据库层时,我们已经有一些优秀的工具。大多数ORM允许你在需要更大的/自定义的灵活性或功能时,执行原始SQL查询。

然而,当处理原始/原生SQL查询的纯文本字符串时,你有什么工具呢?你有很多字符串拼接、implode()PDO::preparePDO::quotesprintf(对于勇敢者)和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 %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 Builder 类 的内容时;它是为少数但自豪的、喜欢编写原生 SQL 查询的程序员而设计的!拥有巨大的力量...

它最初被设计用来弥合 ORM 查询构建器和原生 SQL 查询之间的差距;通过使用熟悉的 ORM 风格的 '流畅接口',但将语法尽可能接近 SQL。

速度和安全

这个库不是为了执行速度或完全防止 SQL 注入而设计的,但它确实会做得比手动转义字符串好;但只有真正的 '预处理语句' 才能提供防止 SQL 注入的保护;然而,它们增加了更多的复杂性并且有许多限制。在现实中,几乎不可能仅使用真正的/真实的预处理语句来编写整个网站,所以您不可避免地要编写许多 '未准备' 的语句;这就是这个类能帮到您的地方;通过编写更安全的 '未准备' 语句!它会 '引用' 和 '转义' 字符串,检测正确的数据类型使用;但它不进行语法检查、语法解析、查询/语法验证等。主要任务是替换您的占位符与相应的数据,具有自动检测数据类型的能力。

简化复杂

这个类对于像 'SELECT * FROM users WHERE id = ' . $id; 这样的小/静态查询来说并不特别有用或必要。

但是,当您的 SQL 查询变得更大更复杂时,它真的开始发光;在 INSERTUPDATE 查询上真正发光。查询越大,好处越大;这正是它的设计目的。所有 '转义'、'引用' 和连接字符串的复杂性和繁琐工作都通过在需要变量的地方简单地放置 ? 来消除,这个库会处理其余的工作。

所以当您发现自己处理 '对象关系阻抗不匹配';因为您有一个 400+ 张表的数据库,6000+ 列/字段,一个有 156 个数据字段的表,10 个有超过 100 个字段的表,24 个有超过 50 个字段的表,1000+ 个 varchar/char 字段,就像我一样;只需记住,这个库被设计来帮助减少一些这种复杂性!特别是在几个月或几年后回到查询时,仍然具有(半)可读性,这使得它非常易于使用。

真正预处理语句的限制

一个限制是您不能这样做: WHERE ? = ?,而这在这个类中是可以的,另一个限制是您基本上不能使用NULL值(有解决方案)。另外,您不能使用动态的列/表/字段名,例如 SELECT ? FROM ?,所有这些都可以用这个类实现;您可以在 $db->query($sql) 中做的任何事情,都可以在这里做!

安装

Composer

composer require twister/sql

手动

/* composer.json */
	"require": {
		"php": ">=5.6",
		"twister/sql": "*"
	}

或从GIT

https://github.com/twister-php/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()

注意:“numeric”值(如上面的“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

一些函数(如leftJoinwhere)支持prepare/sprintf风格的变量参数,而其他函数(如selectfrom)则更方便地编码为仅使用implode您的值。

多种调用约定

代码支持camelCase、snake_case和UPPER_CASE语法;以及简短的形式语法

构造函数

use Twister;

$sql = new sql();
$sql = new Sql();
$sql = new SQL();

// or

$sql = new \Twister\Sql();

方便的sql()函数

function sql($stmt = null, ...$params)
{
	return new Twister\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资源连接。

要充分利用类提供的所有功能,必须设置连接。

\Twister\Sql::setConnection($conn);

一旦设置了连接,类(以及您之后创建的所有sql()实例)将使用您的连接来'转义'和'引用'字符串,如果您愿意,您还有直接从类执行查询的能力。使用类执行查询完全是可选的,但非常方便!

query()、exec()、fetchAll()、lookup()

您可以直接从sql对象调用4个非常轻量级的函数。所有连接类型都已统一。

fetchAll()

$array = sql('SELECT ...')->fetchAll();

返回一个包含所有结果集行的关联数组。

基于PDOStatement::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;
};

查找函数

$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"
}

查找函数将返回一行数据,或者一个值,这取决于你选择了多少列。如果你选择了一列,你将直接在$value中得到一个值或null。如果你选择了多列,它们将作为一个关联数组返回,其中键是列名。只有第一行被返回,其他任何结果都将被丢弃。

此函数的工作方式与SQLite3::querySingle相似,除了结果'mode'是自动检测的,这对应于SQLite3::querySingle中的$entire_row值。

查询函数

$recset = sql(...)->query();

查询函数将使用你的数据库连接的查询函数执行SQL查询,并返回相同的结果。这是一个非常薄的包装器,使其使用起来非常快和方便;它比从依赖注入/IoC容器(如$container->db()->query($sql);)中获取连接对象要方便得多;它只是sql(..)->query()

执行函数

$affected_rows = sql('DELETE FROM ...')->exec();

执行函数执行一个不期望有'查询'结果的SQL查询。这通常是一个INSERTUPDATEDELETE语句。MySQL和SQLite3返回的受影响行数值与从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 Twister\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先...