马里奥传说 / 蓝色点
纯 SQL 数据库抽象层
Requires
- php: >=7.0 || <=7.2
- symfony/var-dumper: ^4.0
- symfony/yaml: ^4.2
Requires (Dev)
- fzaninotto/faker: ^1.7
- kint-php/kint: ^3.2
- phpunit/phpunit: ^7
- dev-master
- 2.0.5
- 1.4
- 1.3
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.0
- 0.0.60
- 0.0.59
- 0.0.58
- 0.0.57
- 0.0.56
- 0.0.55
- 0.0.54
- 0.0.53
- 0.0.52
- 0.0.51
- 0.0.50
- 0.0.49
- 0.0.48
- 0.0.47
- 0.0.46
- 0.0.45
- 0.0.44
- 0.0.42
- 0.0.41
- 0.0.40
- 0.0.39
- 0.0.38
- 0.0.37
- 0.0.36
- 0.0.35
- 0.0.34
- 0.0.33
- 0.0.32
- 0.0.31
- 0.0.30
- 0.0.29
- 0.0.28
- 0.0.27
- 0.0.25
- 0.0.24
- 0.0.23
- 0.0.22
- 0.0.21
- 0.0.20
- 0.0.19
- 0.0.18
- 0.0.17
- 0.0.16
- 0.0.15
- 0.0.14
- 0.0.13
- 0.0.12
- 0.0.11
- 0.0.10
- 0.0.9
- 0.0.8
- dev-dev
- dev-add-license-1
This package is auto-updated.
Last update: 2024-09-22 08:22:26 UTC
README
纯 SQL 数据库抽象层
内容
- 简介
- 安装
- 基础知识
- 初始配置
- 数据库连接
- 术语
- 简单语句
- 基本示例
- 参数说明
- 与模型一起工作
- 场景语句
- 基本示例
- 参数说明
- 'use' 配置选项
- 'foreign_key' 配置选项
- 'if_exists' 和 'if_not_exists' 配置选项
- 服务语句
- 准备执行
- 仓库
- 语句构建器
- Promise 接口和获取结果
- 简介
- PromiseInterface
- 实体结果对象
- 过滤器
- 可能性列表
- 使用配置过滤器
- 导入
- 结论
- 设置测试
1. 简介
BlueDot 是一个 MySQL 数据库抽象层,它可以与纯 SQL 一起工作,但返回您可以与之交互的域对象。它基于配置,并且只需要最少的努力和设置就可以开始使用。我创建这个工具的原因很简单,那就是空闲时间和需要一个更好的工具来处理纯 SQL。希望有人会发现它很有用。
您可以在小型到中型项目中使用此工具,但我不建议在企业项目中使用它。我在一个中型项目中使用过它,只在某些地方使用,效果非常好。不要将其作为唯一的工具来处理 MySQL,而应将其作为 Doctrine 或 Propel 的补充,在您需要快速数据库查找的情况下使用。
您还可以在数据库密集型迁移中使用它,其中包含数千万行数据,并且您需要尽可能快地更改它,同时消耗最少的内存。由于 BlueDot 使用纯 SQL,并且不跟踪对象或先前执行的查询,如 Doctrine 或 Propel,因此它是 Phinx 等工具的快速替代品。
您还可以在博客等应用上的简单 CRUD 操作中使用它,或者在工作与大量数据一起的场合使用它。
本文档的编写方式是,您将首先学习如何执行 SQL 查询,但获取结果和操作它在第 12 章“Promise 接口和获取结果”中有详细介绍。
2. 安装
BlueDot 需要 PHP 7.0 或更高版本
使用 composer 安装
composer require mario-legenda/blue-dot
BlueDot 的当前版本为 2.0.4。
3. 基础知识
3.1 创建 BlueDot 和初始配置
BlueDot 通过解析配置 .yml 文件并在稍后执行它来工作,但您也可以不使用文件来实例化它。这是因为您只能使用 BlueDot 的 StatementBuilder(我们将在稍后讨论 StatementBuilder)。您还可以稍后注入配置文件和 MySQL 连接。这使得 BlueDot 轻巧且可配置。
use BlueDot\BlueDot;
$blueDot = new BlueDot();
// OR
$blueDot = new BlueDot('/path/to/file.yml');
请注意,.yml 配置必须是该文件的绝对路径。
如果您希望稍后注入配置,可以使用 BlueDot::setConfiguration() 方法。
// the path to the file must be an absolute path
$blueDot->setConfiguration('/path/to/file.yml');
您不能多次调用 BlueDot::setConfiguration()。BlueDot 依赖于仓库的概念,每个文件都是一个仓库,您可以根据需要切换仓库,但不能加载现有的仓库(已加载的 .yml 文件)。关于仓库的更多信息将在后面介绍。
3.2. 数据库连接
这就是您设置连接信息的方式。
configuration:
connection:
host: localhost
database_name: world
user: user
password: password
此外,在您的.yml配置中设置数据库不是必需的。您可以通过接受一个 BlueDot\Database\Connection 对象的 BlueDot::setConnection() 方法来设置连接。
use BlueDot\Kernel\Connection\ConnectionFactory;
use BlueDot\Kernel\Connection\Connection;
use BlueDot\BlueDot;
$blueDot = new BlueDot();
/** @param Connection $connection */
$connection = ConnectionFactory::createConnection([
'host': 'localhost',
'database_name': 'world',
'user': 'user',
'password': 'password'
]);
$blueDot->setConnection($connection);
Connection 对象还具有设置dsn值的方法,例如 Connection::setDatabaseName()、Connection::setHost() 等... 此外,还有一个 Connection::setAttribute() 方法,您可以使用它来设置用于建立连接的PDO属性。
$connection->setAttribute( \PDO::ATTR_ERRMODE, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION);
注意:Errormode属性已经设置,同时还有持久连接和utf8字符集。
如果您需要关闭数据库连接,请使用 Connection::close() 方法。
$blueDot->getConnection()->close();
请注意,当您创建Connection对象实例时,并不会立即建立到MySQL的实际连接,而是在BlueDot执行 Connection::connect() 时才会建立连接。该方法仅在BlueDot知道所有检查和验证都成功完成时才会执行。
如果您已经有一个建立的PDO对象,在从BlueDot创建Connection对象之后,您可以通过 Connection::setPdo() 方法设置自己的PDO对象。
4. 术语
在下文中,我指的是 语句。语句是一个配置值,它存储要执行的SQL查询的配置。
例如...
simple:
select:
find_users:
sql: 'SELECT * FROM users'
simple.select.find_users 是一个语句,而 'SELECT * FROM users' 是一个SQL查询。所以当我说到 语句 时,我指的是 simple.select.find_users,但当我提到SQL查询时,我指的是 'SELECT ...',实际的SQL查询。
在 BlueDot 中,有三种类型的语句
- 简单
- 场景
- 服务
因此,当我说到语句时,我指的是这三种中的一种。
5. 简单语句
5.1 基本示例
简单语句是在配置中定义并执行的单一SQL查询。
例如...
configuration:
connection:
host: localhost
database_name: world
user: root
password: root
simple:
select:
find_users:
sql: "SELECT * FROM users"
注意:从现在起,我将不会包括连接参数
在您的代码中,实例化 BlueDot 并使用 BlueDot::execute() 方法以及 simple.select.find_users 语法运行。
$blueDot->execute('simple.select.find_users');
这一行代码将执行语句 simple.select.find_users 的SQL查询。您将在稍后看到如何获取此语句的实际结果。
简单语句有4种类型
- 选择
- 插入
- 更新
- 删除
为了扩展前面的示例,一个更新简单语句可能看起来像这样
simple:
select:
find_users:
sql: "SELECT * FROM users"
update:
update_user:
sql: "UPDATE users SET name = 'Mary' WHERE id = 6"
delete 和 insert 语句的设置方式和执行方式相同。
现在,结果是。BlueDot::execute() 方法的产物是一个 promise。它与JavaScript中的promises无关。它只是我对结果的包装,称之为promise。没有什么特别的。关于promises和查询结果的更多内容将在后续章节中介绍。
5.2 参数说明
PHP PDO可以使用PDO::prepare()绑定参数。BlueDot以略微不同的方式支持此功能。
注意:如果您在配置中提供了参数但在代码中没有提供,反之亦然,则会抛出异常。
要将参数绑定到语句,您需要在配置和代码中提供该参数。根据代码中提供的参数的性质和数量,BlueDot决定是仅执行语句一次还是多次。
看看这个语句
simple:
select:
find_user:
sql: "SELECT * FROM users WHERE id = :id"
parameters: [id]
$blueDot->execute('simple.select.find_users', array(
'id': 6,
));
此语句仅执行一次,并返回一个id为6的用户。
但如果您需要执行一个单一的SQL查询多次并使用不同的参数呢?
simple:
insert:
create_users:
sql: "INSERT INTO users (name) VALUES (:name)"
parameters: [name]
如果您提供与上一个示例中相同的参数,则此语句将只执行一次。
$blueDot->execute('simple.insert.create_users', array(
'name': 'Peter',
));
但如果您提供多个参数作为多个数组,则此语句将根据参数的数量执行多次。
$blueDot->execute('simple.insert.create_users', array(
array('name' => 'Mary'),
array('name' => 'Jean'),
array('name' => 'Zoey'),
array('name' => 'Jennifer'),
));
simple.insert.create_users 将执行 4 次,因为向 execute 方法提供了 4 个参数。
当只有一个参数要绑定到 SQL 查询时,有一种执行多个语句的快捷方式。在我们的 simple.insert.create_users 语句中,只需要绑定一个 name 参数,因此可以使用这种快捷方式。
$blueDot->execute('simple.insert.create_users', array(
'name' => array(
'Mary',
'Jean',
'Zoey',
'Jennifer',
),
));
这种快捷方式 仅 当只有一个参数要绑定到 SQL 查询时才有效。如果 SQL 查询需要绑定多个参数,这种方法将不起作用,并且您将收到一个异常。例如,如果上面的 SQL 查询需要绑定一个 name 参数和一个 id 参数。
总结来说,一个语句将执行与该语句参数数量相同的次数。如果您提供多个参数,则该语句将根据参数的数量执行。如果您只提供一个参数,则语句将只执行一次。
5.3 与模型协同工作
如 Doctrine 这样的数据库工具使用模型来简化与数据库的通信并使其更具描述性。简单的语句也提供了这个特性。
例如,假设我们有一个 language 表,包含 id 和 language 列。我们的模型可能看起来像这样...
namespace App\Model;
class Language
{
private $id;
private $language;
public function setId($id) : Language
{
$this->id = $id;
return $this;
}
public function getId()
{
return $this->id;
}
public function setLanguage($language) : Language
{
$this->language = $language;
return $this;
}
public function getLanguage()
{
return $this->language;
}
}
根据之前的示例,我们可以通过使用此模型创建一个新的语言
simple:
insert:
create_language:
sql: "INSERT INTO languages (name) VALUES (:language)"
parameters: [language]
$language = new Language();
$langauge->setLanguage('french');
$blueDot->execute('simple.insert.create_language', $language);
BlueDot 从配置中推断出您想要将 language 参数绑定到 SQL 语句查询。然后,它推断出您提供了一个对象作为参数,并在该对象上查找 Language::getLanguage() 方法。如果找到,则将该方法返回的值绑定到 SQL 查询的 language 参数。
重要的是要说,模型上必须有一个 get 方法来绑定您想绑定的参数。例如,如果您还需要绑定一个 name 参数,则在 Language 模型上必须有 Language::getName() 方法。
模型绑定是一个双向过程,并且可以用来从数据库获取模型。例如,为了扩展我们的 users 示例,您可能有一个具有 id、name、username 和 password 字段的 User。您希望从数据库返回填充了用户的数组。
simple:
select:
find_users:
sql: "SELECT * FROM users"
model:
object: App\Model\User
$blueDot->execute('simple.select.find_user');
BlueDot 将返回一个包含 id、name、username 和 password 值的填充好的 User 对象数组。
您可以将这两种方法结合起来以查找特定用户...
simple:
select:
find_user:
sql: "SELECT * FROM users WHERE id = :id"
parameters: [id]
model:
object: App\Model\User
$userId = 6;
$user = new User();
$user->setId($userId);
$blueDot->execute('simple.select.find_user', $user);
BlueDot 将方法 User::getId() 的返回值绑定到 id 参数,并返回一个包含所有返回值的新的 User 对象。
BlueDot 与列名协同工作。如果您有一个 last_name 列名和一个对象作为参数提供,BlueDot 将搜索 User::getLastName()/User::setLastName() 方法。您还可以将表列命名为 lastName,模型绑定也将生效。如果对象没有对应于该列的 get 和 set 方法,BlueDot 不会将返回的列值绑定到提供的模型。例如,如果一个表包含一个 date_created 列,但模型没有 Model::setDateCreated(),则不会将那个列的值绑定到提供的模型。
如果您有一个与模型属性不同的列名,您可以使用 properties 配置。
simple:
select:
find_user:
sql: "SELECT * FROM users WHERE id = :id"
parameters: [id]
model:
object: App\Model\User
properties: { find_user.created_on: dateCreated }
在这个例子中,用户对象有一个属性dateCreated,它有相应的set和get方法,但列名为created_on。 BlueDot将搜索User::setDateCreated()方法,并将created_on列的值保存。如果BlueDot在模型或properties中找不到属性,则将跳过该列,不会将其放入模型中。例如,如果表中包含一个名为updated_on的列,但模型中没有名为setUpdatedOn()的方法,并且您没有在properties配置中提供替换,则将跳过该列。
重要
model配置属性用于告诉BlueDot将返回列值绑定到该模型。如果您提供模型作为参数,则不需要该配置。model配置属性仅用于返回模型。
6. 场景语句
6.1 基本示例
场景语句是一组一起执行、以原子方式执行的语句。这意味着,如果这些语句中的任何一个失败,则不会执行任何语句。它们可以描述应用程序上的搜索功能或需要大量数据库流量和存储在许多表中的不同信息的计算功能。
让我们从一个现实世界的例子中创建一个基本示例。在一个用户注册场景中,您首先搜索具有注册用户名/电子邮件的用户,然后创建新用户。
scenario:
create_user:
atomic: true
statements:
find_user_by_username:
sql: "SELECT * FROM users WHERE username = :username"
parameters: [username]
create_user:
sql: "INSERT INTO users (name, username, password) VALUES (:name, :username, :password)"
parameters: [name, username, password]
if_exists: find_user_by_username
$blueDot->execute('scenario.create_user', array(
'find_user_by_username' => array(
'username' => 'John',
),
'create_user => array(
'name' => 'Jennifer',
'username' => 'jennifer@gmail.com',
'password' => 'someweakpassword',
),
));
关于这个简单示例,有一些话要说。
此场景的名称为create_user。find_user_by_username和create_user是它的语句。语句按它们在配置中出现的顺序执行,除了use、foreign_key和if_exists/if_not_exists选项。这些选项在包含这些选项的语句之前执行。
让我解释一下。create_user语句有一个if_exists选项。BlueDot按配置中语句出现的顺序开始执行语句。首先,它执行find_user_by_username。然后,它转到执行create_user。它看到create_user有一个具有要检查其存在性的语句名称的if_exists选项。然后,它检查if_exists语句是否已执行。如果没有,它将执行它,然后执行create_user。
在我们的例子中,当BlueDot想要执行create_user时,它看到if_exists语句已执行,跳过其执行并执行create_user。
您可能已经注意到了atomic选项。atomic选项告诉BlueDot所有语句都在事务中执行。这意味着如果其中一个语句失败,整个场景将回滚,并且没有任何语句影响数据库。如果您将此选项设置为false,则将逐个执行语句,如果其中一个失败,其他语句将影响数据库。
这是场景可以做什么的基本示例。在这个例子中,我介绍了if_exists选项。if_exists/if_not_exists选项检查包含这些选项的语句是否存在或不存在。根据该条件,具有这些选项的语句将执行或不执行。有关场景选项的更多信息,请参阅本章后面的内容。
6.2 参数说明
场景的参数与简单语句在大多数情况下相似,但有一些不同。您必须提供场景语句的名称作为数组键,以及参数数组作为其值。
在之前的示例中
$blueDot->execute('scenario.create_user', array(
'find_user_by_username' => array(
'username' => 'John',
),
'create_user => array(
'name' => 'Jennifer',
'username' => 'jennifer@gmail.com',
'password' => 'someweakpassword',
),
));
您有两个语句,find_user_by_username和create_user。您通过命名它们作为键和参数数组来为这些语句提供参数。参数的规则与简单语句相同,有两个例外:您不能提供模型作为参数,并且可以分配null作为参数。
如果您将 null 作为场景语句的参数分配,则该语句将不会执行。这在例如您有一个不希望在某些情况下执行但希望在另一些情况下执行的删除或更新查询时很有用。
6.3 'use' 配置选项
use 选项是一个强大的场景功能。使用它,您可以绑定一个参数到另一个语句的返回值。
例如,如果一篇博客可以保存为许多语言(本地),在保存时,您需要检查您正在保存的语言是否存在,然后保存博客的文本。
scenario:
save_blog:
atomic: true
statements:
find_language:
sql: "SELECT id FROM locales WHERE locale = :locale"
parameters: [locale]
save_block:
sql: "INSERT INTO blogs (locale_id, blog_text) VALUES(:locale_id, blog_text)"
if_exists: find_language
use:
statement_name: find_language
values: { find_language.id: locale_id }
$blueDot->execute('scenario.save_blog', array(
'find_language' => array(
'locale' => 'en',
),
'save_block' => array(
'blog_text' => 'Some cool blog text',
),
));
BlueDot 按配置中出现的顺序执行语句。首先,它执行 find_language。接下来是 save_block。BlueDot 首先看到的是 if_exists。此选项告诉 BlueDot 只有当 find_language 语句返回一些结果时(即找到了一个 en 本地化),才执行 save_block。如果找到了,它会看到它有一个 use 选项语句。
use 选项为您提供将参数与来自其他语句的返回值绑定在一起的机会。在上面的例子中,您已将语句 find_language.id 返回的 save_block.locale_id 参数绑定。因此,find_language.id 绑定到 save_block.locale_id。
如果 use 语句的结果没有执行,BlueDot 将执行它,然后才执行 save_block 语句。
尽管 use 选项是一个有用的功能,但它有自己的限制。某些其他语句中的 use 选项只能是一个 select sql 查询,并且它必须返回单行。在上面的例子中,如果 find_language 返回多行,BlueDot 将抛出异常。
您放置 use 选项语句的顺序无关紧要。在上面的例子中,find_language 可以配置在 save_block 之下。在这种情况下,save_block 会看到它有一个 use find_language 语句,并且该语句将被执行。到执行 find_language 时,它已经执行,所以将被跳过。
6.4 'foreign_key' 配置选项
当您希望将某个语句的参数绑定到插入语句的 last_insert_id 时,可以使用 foreign_key 选项。最好的方法是看看例子。
scenario:
create_word:
atomic: true
statements:
create_word:
sql: "INSERT INTO words (word) VALUES (:word)"
parameters: [word]
create_translations:
sql: "INSERT INTO translations (word_id, translation) VALUES (:word_id, :translation)"
parameters: [translation]
foreign_key:
statement_names: [create_word]
bind_them_to: [word_id]
$blueDot->execute('scenario.create_word', array(
'create_word' => array(
'word' => 'some word',
),
'create_translations' => array(
'translation' => 'some translation of a word',
),
));
这是一个经典的 一对一 关系。translations 表有一个 word_id 字段,它接受我们要与之连接的翻译单词的 id。在经典的 PHP PDO 使用中,您会执行 create_word sql 查询,并调用 PDOConnection::lastInsertId() 方法来获取该查询的最后一个插入的 id。然后,您会执行 create_translations sql 查询,并将该 last_insert_id 绑定到参数 word_id。
上面的例子描述了一个 一对一 关系,但您可以通过相同的场景配置轻松地将此关系转换为 一对多。
$blueDot->execute('scenario.create_word', array(
'create_word' => array(
'word' => 'some-word',
),
'create_translations' => array(
'translations' => array('translation 1', 'translation 2', 'translation 3'),
)
));
通过将 create_translations 语句的参数类型更改为,我们已告诉 BlueDot 将 3 条翻译语句插入到 create_word 语句的 last_insert_id。
您会注意到 statement_names 和 bind_them_to 是复数形式。这是因为它们作用于多个语句。这意味着您可以绑定任意多的语句。考虑以下示例
normalized_user_insert:
atomic: true
statements:
insert_user:
sql: "INSERT INTO user (username, name, lastname) VALUES (:username, :name, :lastname)"
parameters: [username, name, lastname]
insert_address:
sql: "INSERT INTO addresses (user_id, address) VALUES (:user_id, :address)"
parameters: [address]
foreign_key:
statement_names: [insert_user]
bind_them_to: [user_id]
create_reference_user:
sql: "INSERT INTO reference_user (user_id, address_id) VALUES (:user_id, :address_id)"
foreign_key:
statement_names: [insert_user, insert_address]
bind_them_to: [user_id, address_id]
insert_user 和 insert_address 的 last_insert_id 值绑定到 create_reference_user 的 :user_id 和 :address_id 参数。顺序很重要。这是 非常 重要的。在 statement_names 数组中的第一个语句结果对应于 bind_them_to 数组的第一个值。
这是一个简单的例子,但如果需要多个插入语句,这可能会变得繁琐。使用场景,这只是一个简单的任务。
6.5 'if_exists' 和 'if_not_exists' 配置选项
您已经看到了if_exists和if_not_exists选项的示例。这些选项告诉BlueDot在存在其他语句时执行或不执行一个语句。
这两个选项可以与任何SQL查询一起使用,但有一个限制。在MySQL中,如果更新没有改变任何信息,BlueDot无法知道一个语句是否执行。内部,对于insert、update或delete SQL查询,它调用PDOStatement::rowCount()方法来查看有多少行受到影响。如果没有行受到影响,这种语句将被视为不存在。使用这些SQL查询时,不要忘记这一点。
7. 服务语句
服务语句是一个扩展BlueDot\Configuration\Flow\Service\BaseService的对象。在那个对象中,您将接收到BlueDot实例和参数数组,您可以用作依赖注入容器。最好通过示例来了解它。
service:
my_service:
class: Some\Namespace\MyClass
class MyService extends use BlueDot\Configuration\Flow\Service\BaseService;
{
public function run()
{
// fetch some parameter
$someParameter = $this->parameters['some-parameter'];
// execute some simple statement
$this->blueDot->execute('simple.select.some_statement');
// execute some scenario
$this->blueDot->execute('simple.select.some_scenario');
}
}
$blueDot->execute('callable.my_callable', array(
'some-parameter' => new SomeObject(),
'other-parameter' => 'some string',
'number-parameter' => 6,
));
服务有一个run()方法,BlueDot会执行它。服务的作用是将许多场景或简单语句组合在一起。服务的返回值是run()方法返回的任何内容,但封装在Promise中。关于Promise的更多信息将在后面介绍。
8. 准备执行
准备执行允许您将多个语句作为单个原子MySQL查询运行,原子意味着如果其中之一失败,则它们都不会成功,并且成功查询的结果将回滚。这可以用来执行在您构建的应用程序中不相关的查询,但在某些情况下,它们可以。
准备执行可以用来在一个原子操作中执行多个场景和简单语句,如果您单独使用场景或单个语句,则无法获得这些操作。
您可以使用与BlueDot::execute()方法相同的签名,通过BlueDot::prepareExecution()方法将要在以后执行的语句添加进去。它接受相同的参数;语句的名称和查询的参数。
您可以使用方法BlueDot::executePrepared()执行所有准备好的语句。
例如
$blueDot->prepareExecution('simple.insert.user', [
'name' => 'Billie',
'last_name' => 'Holliday',
'email' => 'billieholliday@bestjazz.com'
]);
$blueDot->prepareExecution('simple.insert.blog', [
'blog' => 'text of the blog',
]);
$blueDot->prepareExecution('simple.insert.some_statement_that_has_nothing_to_do_with_user_or_blog');
$blueDot->executePrepared();
9. 仓库
仓库是一种按它们旨在服务的逻辑来组织查询的方法。
例如,特定用户的查询将位于user.yml中,而特定博客的查询将位于blog.yml中。它与Doctrines仓库类似,其中User和Blog对象都有自己的仓库。
BlueDot默认使用仓库。如果您有一个包含您构建BlueDot的configuration.yml配置,这意味着您正在使用底层的configuration仓库。仓库名称是从.yml文件的名称派生出来的,不带.yml扩展名。
在我们的user和blog示例中,我们将有两个文件
user.yml
configuration:
select:
get_all_users:
sql: SELECT * FROM users;
blog.yml
configuration:
select:
get_all_blogs:
sql: SELECT * FROM blogs;
在您创建BlueDot实例后,首先,告诉BlueDot您有一个新的要使用的仓库
$blueDot->repository()->putRepository('/path/to/user.yml');
$blueDot->repository()->putRepository('/path/to/blog.yml');
这将创建两个仓库:user和blog。要使用仓库,请使用BlueDot::useRepository()方法
$blueDot->useRepository('blog');
NOTE: The name of the repository is the name of the file, minus the .yml extension
现在,您可以使用blog.yml配置中定义的查询。
仓库是将查询组织到逻辑部分的好方法,但它们有一个限制;一次只能使用一个仓库。这意味着,如果您目前正在使用blog仓库,您不能执行use仓库中的查询。您必须使用BlueDot::useRepository()方法来切换仓库。
$blueDot->useRepository('user');
$blueDot->execute('simple.select.get_all_users');
// this one throws an exception since it cannot find the statement
$blueDot->execute('simple.select.get_all_blogs');
$blueDot->useRepository('blog');
// Now OK
$blueDot->execute('simple.select.get_all_blogs');
您是否打算将查询分组到多个文件作为仓库,还是在一个文件中,这取决于您。
10. 语句构建器
语句构建器是一个用于执行单次SQL语句的独立工具,这些语句尚未配置或BlueDot无法执行。
// when using statement builder, you don't need configuration, only connection
$blueDot = new BlueDot(null, $connection);
$this->blueDot
->createStatementBuilder()
->addSql(sprintf('SELECT word_id, translation FROM translations WHERE word_id IN (1, 60, 150, 78, 345)'))
->execute()
->getResult();
此外,BlueDot::createStatementBuilder() 接收一个 BlueDot\Scenario\Connection,因此您可以与多个连接(例如数据库)一起使用。
语句构建器还支持返回模型作为结果。
$this->blueDot
->createStatementBuilder()
->addSql(sprintf('SELECT word_id, translation FROM translations WHERE word_id IN (1, 60, 150, 78, 345)'))
->addModel(Translation::class)
->execute()
->getResult();
如果您不带任何参数(不带配置和连接)实例化 BlueDot,那么,您只能在提供语句构建器连接的情况下使用语句构建器。
$this->blueDot
->createStatementBuilder($connection)
->addSql(sprintf('SELECT word_id, translation FROM translations WHERE word_id IN (1, 60, 150, 78, 345)'))
->addModel(Translation::class)
->execute()
->getResult();
11. Promise 接口和获取结果
11.1 简介
在 BlueDot 中,结果通过 Promise 对象访问,该对象由 BlueDot::execute() 方法返回。
/** @param BlueDot\Entity\PromiseInterface */
$promise = $blueDot->execute('simple.select.get_all_users');
Promise 有以下方法
- getArrayResult()
- getEntity(): 实体
- onResultReady(\Closure)
在本章中,我将尽量保持简单。我将给出一个示例,说明结果可能看起来是什么样子,以及您可以用来获取结果特定部分的 Entity 对象上的方法。
首先,让我们谈谈 Promise 对象。
11.2 PromiseInterface
让我们运行一个查询,该查询将从数据库中返回所有用户。
/** BlueDot\Entity\PromiseInterface */
$promise = $blueDot->execute('simple.select.get_all_users');
PromiseInterface::getEntity() 获取包含查询实际结果的 Entity 对象。
PromiseInterface::getResultAsArray() 将结果作为数组返回。
PromiseInterface::onResultReady() 接收一个匿名函数作为参数,而匿名函数又接收 EntityInterface 对象作为其参数
/** BlueDot\Entity\PromiseInterface */
$promise = $blueDot->execute('simple.select.get_all_users');
$promise->onResultReady(function(EntityInterface $entity) {
// manipulate the resulting entity here
});
我喜欢匿名函数,因为它们引入了一个新的作用域,您可以在其中声明变量而不用担心它们在其他地方被声明。PHP 中关于作用域的讨论不多,但应该有。例如
// lets presume that up to this point you have created
// a lot of variables here. That would be bad pratice, but follow
// me here for the sake of the argument.
$someVar = 'string';
$promise->onResultReady(function(EntityInterface $entity) {
// new scope inside the anonymous function
// $someVar has nothing to do with $someVar declared
// outside of this function
$someVar = 'string'
});
var_dump($someVar); // prints 'string';
现在,让我们谈谈 EntityInterface 和该接口的 Entity 实现。
11.3 实体结果对象
BlueDot\Entity\Entity 对象是对结果的包装,它具有各种方法,可以帮助您从最终结果中获取数据,并使用筛选方法操作这些数据。根据查询,某些信息可能可用或不可用。例如,如果您执行一个选择查询,您将有一个数据属性来保存获取的数据,但您还将有一个行数 row_num。在 delete、update 或 insert 查询中,data 属性将不可用。
另一方面,在插入查询中,您将获得 last_insert_id 和 row_count,以及 inserted_ids 字段,其中包含所有插入的 ID(如果查询多次执行,每次带有多个参数)。
此外,场景语句将返回相同的数据,但数据位于场景名称的键下。它将不会是一个 Entity 对象,而是一个 BlueDot\Entity\EntityCollection 对象,该对象将保存与每个执行的场景关联的所有实体。
简单语句
给定此结果
[
0 => ['name' => 'Billie', 'last_name' => 'Holliday'],
1 => ['name' => 'Dolly', 'last_name' => 'Parton'],
2 => ['name' => 'Katie', 'last_name' => 'Melua']
]
结果 Entity 对象将包含以下数据
[
'rows_num' => 3
'type' => 'simple',
'data' => // the data from the upper example
]
您可以使用 Entity::getRowCount() 方法获取 row_count。
您可以使用 Entity::getType() 方法获取语句的类型。
但是没有方法可以从 Entity 对象中获取数据。这是因为 insert、update 和 delete 语句没有任何数据可以获取。
为了获取数据,您必须调用 Entity::toArray() 方法,并在 data 索引上获取数据。
$blueDot
->execute('simple.select.get_all_users')
->onResultReady(function(EntityInterface $entity) {
$data = $entity->toArray()['data'];
});
// OR
$data = $promise->getEntity()->toArray()['data'];
场景语句
场景语句与简单语句相同,但仅与某个特定的场景语句相关。
例如
$promise = $blueDot->execute('scenario.my_scenario');
/** BlueDot\Entity\EntityCollection */
$entityCollection = $promise->getEntity();
$scenarioEntity = $entityCollection->getEntity('scenario_name');
其他一切都是与简单语句相同。
服务语句
服务语句用于在逻辑位置执行多个场景、简单语句或多个服务语句。因此,您从 run() 方法返回的任何内容,都将是 Entity 对象的 data 属性,在这方面,与场景结果或简单语句结果没有区别。
12. 筛选器
筛选器是在执行某个语句之后过滤结果的一种方式。您可以使用它来在条目列表中查找特定的条目,从条目列表中提取单个列,或者在多对一关系中将列链接起来。
请注意,过滤器不适用于对象,而只适用于纯数组。
您可以在“实体”对象的结果中找到过滤器。
12.1 可能性列表
非常重要
每个实体对象都是不可变的。这意味着在应用过滤器后,您将得到一个新的实体对象,该对象保留原始结果。
Entity::findBy($columns:string, $value:any): array
给定一个$column名称和列的值,返回找到的值的数组。
例如
如果查询的结果是
[
0 => ['name' => 'Natalia', 'last_name' => 'Natalie' ... other fields ]
1 => ['name' => 'Katie', 'last_name' => 'Melua' ... other fields ]
2 => ['name' => 'Billie', 'last_name' => 'Holiday' ... other fields ]
3 => ['name' => 'Dolly', 'last_name' => 'Parton' ... other fields ]
4 => ['name' => 'Natalia', 'last_name' => 'Natalie' ... other fields ]
]
如果您调用$entity->findBy('name', 'Natalia'),此方法将返回一个列表,其中包含所有名称列中包含'Natalia'的结果。重要的是要说明,即使只有一个条目,此方法也将返回一个数组,其中包含按数字索引的条目。
在这个例子中,它将返回2个条目。
Entity::find($column:string, $value:any): array
此方法仅在结果数组中存在单个条目时返回单个条目。如果我们有一个像这样的结果...
[
0 => ['name' => 'Natalia', 'last_name' => 'Natalie' ... other fields ]
1 => ['name' => 'Katie', 'last_name' => 'Melua' ... other fields ]
2 => ['name' => 'Billie', 'last_name' => 'Holiday' ... other fields ]
3 => ['name' => 'Dolly', 'last_name' => 'Parton' ... other fields ]
4 => ['name' => 'Natalia', 'last_name' => 'Natalie' ... other fields ]
]
$entity->find('name', 'Natalia')将抛出异常,因为有2行名称列的值为字符串'Natalia'。将此方法包裹在try/catch子句中,以避免异常。
Entity::extractColumn($column:string): array
Entity::extractColumn()返回一个列的所有条目。
[
0 => ['name' => 'Natalia', 'last_name' => 'Natalie' ... other fields ]
1 => ['name' => 'Katie', 'last_name' => 'Melua' ... other fields ]
2 => ['name' => 'Billie', 'last_name' => 'Holiday' ... other fields ]
3 => ['name' => 'Dolly', 'last_name' => 'Parton' ... other fields ]
4 => ['name' => 'Natalia', 'last_name' => 'Natalie' ... other fields ]
]
$entity->extractColumn('name')将返回一个包含所有名称属性列表,如下所示...
[
'name' => ['Natalia', 'Katie', 'Billie', 'Dolly', 'Natalia']
]
Entity::normalizeJoinedResult($grouping:array, $columns:array): array
这是一个专门处理MySQL表之间关系的特殊方法。如果您有一个一对一关系,请使用此方法以自然方式对字段进行分组。
例如,如果我们有一个名为words的表和一个名为translations的表,并且translations表与words表具有多对一关系,执行连接SQL查询将导致translations表的行数,即使words表对于搜索词只有一个行。
SELECT w.id, w.name, t.translation FROM words AS w INNER JOIN translations AS t ON w.id = t.word_id AND w.id = 1;
如果translations对于单个词有10行,此查询将返回10行,每行都具有来自words表行的相同信息,但具有不同的翻译。这不是我们想要的。
要分组此结果,请使用
$entity->normalizeJoinedResult([
'linking_column' => 'id',
'columns' => ['translations']
]);
linking_column告诉我们行之间的关系。在上面的例子中,id对于所有行都是相同的,因为它是words表中的id。columns是您希望分组的列。结果是
[
'id' => 1,
'name' => 'word_name',
'translations' => [
'translation1',
'translation2',
... other translations
]
]
您可以选择您喜欢的任意多列。
链式过滤器
可以将过滤器链接起来以获取所需的结果。如果我们想要从Entity::normalizeJoinedResult()示例中提取所有翻译到一个单独的关联数组中,我们可以使用
$entity
->normalizeJoinedResult([
'linking_column' => 'id',
'columns' => ['translations'],
])
->extractColumn('translations')
->normalizeIfOneExists()
这些方法的最终结果将是一个包含所有找到的翻译的单个数组。
您也可以用这个例子和这个SQL查询一起使用
SELECT w.id, w.name, t.translation FROM words AS w INNER JOIN translations AS t ON w.id = t.word_id;
这是与Entity::normalizeJoinedResult()相同的例子,但我们去掉了AND id = 1,这样我们就得到了单个词的所有翻译。在新例子中,我们想要所有有翻译的词的翻译。例如,如果我们有3个词,每个词有10个翻译,上面的查询将返回30行,每行都有一个翻译。
因此,首先我们将结果标准化,将特定词的所有翻译分组到单个translations字段中。然后,我们找到id = 2的词,并标准化结果以返回一个基于单个字符串键的数组。
$entity
->normalizeJoinedResult([
'linking_column' => 'id',
'columns' => ['translations'],
])
->find('id', 2);
->normalizeIfOneExists()
12.2 使用配置过滤器
您也可以在配置中使用所有过滤器,但有一个缺点。如果您已经阅读了关于过滤器的整个章节,您知道使用过滤器与Entity对象一起的代码会保留原始的Entity对象。每个过滤器方法都返回一个新的对象,这使得Entity对象不可变。
配置实体的情况并非如此。如果您选择在配置中应用过滤器(或链式过滤器),则原始结果将丢失。
您可以在配置中使用过滤器,如下所示
normalize_joined_result_find_all_users:
sql: SELECT * FROM users
filter:
by_column: 'id'
by_column 当在代码中使用时是 Entity::extractColumn() 的别名。这将与调用 Entity::extractColumn() 有相同的效果。
您也可以在配置中链式调用过滤器
normalize_joined_result_find_all_users:
sql: select.find_all_users
filter:
normalize_joined_result:
linking_column: 'id'
columns: ['username']
find: [id, 7]
normalize_if_one_exists: true
请注意,normalize_if_one_exists 必须接收布尔值 true 或 false
在配置中使用时,所有过滤器都会被分组并发送到一个观察者模式,该模式不关心原始结果,只关心从下一个执行的过滤器获取的结果。
在代码中使用过滤器或在配置中使用过滤器的选择取决于您。如果您不需要保留原始结果,请使用配置过滤器。如果您需要原始结果以及过滤器的结果,请使用代码中的过滤器,作为 Entity 对象的方法。选择权在您手中。
13. 导入
导入是一种将所有 SQL 查询集中到一个 .yml 文件中的方式。该文件的路径通过 sql_import 配置选项注入。
sql_import: sqls.yml
该文件必须是相对于给定配置值的相对路径。该文件应如下所示...
your_unique_name: "some sql"
some_unique_namespace:
another_unique_namespace:
yet_another_unique_namespace:
sql_1: "some_sql"
sql_2: "some_sql"
您可以在 sql 配置选项下使用点分隔的名称来指定导入。回到先前的例子
sql_import: relative_path_config.yml
scenario:
atomic: true
return_data: ['select_user.name', 'select_user.lastname', 'select_user_prefs.purchase_history']
select_user_data:
statements:
select_user:
sql: "SELECT id, name, lastname, username FROM user WHERE user_id = :id"
parameters: [id]
select_user_pref:
sql: "SELECT * FROM user_preferences WHERE id = :id"
use:
statement_name:
values: {select_user.id: id}
SQL 查询可以表示如下...
my_scenarious:
user_queries:
select_user: "SELECT id, name, lastname, username FROM user WHERE user_id = :id"
select_user_prefs: "SELECT * FROM user_preferences WHERE id = :id"
在配置中,这个导入将如下所示...
sql_import: relative_path_config.yml
scenario:
atomic: true
return_data: ['select_user.name', 'select_user.lastname', 'select_user_prefs.purchase_history']
select_user_data:
statements:
select_user:
sql: my_scenarious.user_queries.select_user
parameters: [id]
select_user_pref:
sql: my_scenarious.user_queries.select_user_prefs
use:
statement_name:
values: {select_user.id: id}
14. 结论
虽然 BlueDot 使得执行 SQL 查询变得简单,但它并不是用来取代 Doctrine 或类似工具的。我建议在您必须在使用 DBAL 时进行复杂 SQL 查询时使用 BlueDot。 BlueDot 可以用来创建完整的应用程序,但并非每个应用程序都应该仅使用 BlueDot。例如,如果您有一个需要插入和更新大量表单的应用程序,BlueDot 可能不适合您。但是,如果您有一个复杂的搜索功能,需要从多个表中选择大量数据,请使用它。
您还可以将 BlueDot 与 Doctrine 或类似工具一起使用。Doctrine 会创建自己的 PDO 连接。如果您省略了 connection 配置值,您可以使用 Doctrine 的连接初始化 BlueDot 并与 BlueDot 一起使用。
use BlueDot\Kernel\Connection\ConnectionFactory;
$doctrinePdoConnection = // get the PDO object from doctrine here
$connection = ConnectionFactory::create([]);
$connection->setPDO($doctrinePdoConnection);
$blueDot->setConnection($connection);
现在,BlueDot 和 Doctrine 都使用了相同的连接。如果 BlueDot 在调用 BlueDot::setConnection() 之前使用了来自其他地方的先前 PDO 对象,那么该连接将在您调用 BlueDot::setConnection() 之后。
15. 设置测试
为了设置测试,创建一个名为 blue_dot 的数据库,用户名为 root,密码为 root。在本地,我使用简单的密码。如果您已经设置了其他密码,请进入 tests/config/result 目录,并找到以下文件
prepared_execution_test.yml simple_statement_test.yml scenario_statement_test.yml
然后在这些文件的 configuration > connection 节点下更改连接设置,然后您就可以在本地上运行测试了。