ramphor/sql

SQL查询字符串构建器

1.0.0 2020-07-12 02:29 UTC

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::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 / %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 查询变得更大、更复杂时,它真正开始发光;特别是在 INSERTUPDATE 查询上。查询越大,好处就越大;这正是它的设计目的。通过简单地在你想要放置变量的地方放置 ?,就消除了“转义”、“引号”和连接字符串的复杂性和繁琐工作,这个库会处理其余的部分。

所以当您发现自己处理 '对象关系阻抗不匹配' 时;因为您有一个 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

一些函数如 leftJoinwhere 支持变量参数的 prepare/sprintf 风格,而其他如 selectfrom 则更方便地使用 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();

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

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

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查询。这通常是INSERTUPDATEDELETE语句。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离开...