rds / 氢气
更快、更便捷的 Doctrine ORM 抽象层
Requires
- php: >=7.1
- doctrine/orm: ~2.5
- illuminate/support: >=5.5
Requires (Dev)
- fzaninotto/faker: ~1.8
- phpunit/phpunit: ~6.1
README
简介
Hydrogen 为使用 Doctrine 查询提供了一个美丽、方便且简单的实现。它不会以任何方式影响现有代码,甚至可以在已构建的生产应用中使用。
安装
服务器要求
Hydrogen 库有几个系统要求。请确保您的服务器满足以下要求
- PHP >= 7.1.3
- PDO PHP 扩展
- Mbstring PHP 扩展
- JSON PHP 扩展
- doctrine/orm >= 2.5
- illuminate/support >= 5.5
安装 Hydrogen
Hydrogen 使用 Composer 来管理其依赖项。因此,在使用 Hydrogen 之前,请确保您的机器上已安装 Composer。
稳定版
composer require rds/hydrogen
开发版
composer require rds/hydrogen dev-master@dev
使用方法
Hydrogen 与 Doctrine 的存储库交互。为了利用额外的功能,您需要将主要特质添加到现有的存储库实现中。
<?php use Doctrine\ORM\EntityRepository; use RDS\Hydrogen\Hydrogen; class ExampleRepository extends EntityRepository { use Hydrogen; }
之后,您将获得对查询构建器的完全访问权限。
检索结果
检索所有实体
您可以使用存储库上的 ->query()
方法开始查询。此方法返回给定存储库的流畅查询构建器实例,允许您将更多约束添加到查询中,然后最终使用 ->get()
方法获取结果
<?php use RDS\Hydrogen\Hydrogen; use Doctrine\ORM\EntityRepository; class UsersRepository extends EntityRepository { use Hydrogen; public function toArray(): iterable { return $this->query->get(); } }
get()
方法返回一个包含结果的 array
,其中每个结果都是与指定存储库相关联的对象(实体)的实例
foreach ($users->toArray() as $user) { \var_dump($user); }
此外,您还可以使用 collect()
方法获取一个与 ArrayCollection 兼容的集合
<?php use RDS\Hydrogen\Hydrogen; use Doctrine\ORM\EntityRepository; use RDS\Hydrogen\Collection\Collection; class UsersRepository extends EntityRepository { use Hydrogen; public function toCollection(): Collection { return $this->query->collect(); } }
$users->toCollection()->each(function (User $user): void { \var_dump($user); });
注意:直接访问 Hydrogen 构建,而不是 Doctrine 提供的现有方法,完全 忽略 所有关系(如:
@OneToMany(..., fetch="EAGER")
)。
获取单个实体
如果您只需要从数据库表中检索一行,您可以使用第一种方法。此方法将返回一个单个实体对象
$user = $repository->query->where('name', 'John')->first(); echo $user->getName();
如果您甚至不需要整行,您可以使用 ->first()
方法的附加参数从记录中提取单个值。此方法将直接返回列的值
[$name, $email] = $repository->query->where('name', 'John')->first('name', 'email'); echo $name . ' with email ' . $email;
获取字段值列表
如果您希望检索包含单个实体字段值的数组或集合,您可以使用 ->get()
或 ->collect()
方法的附加参数。在此示例中,我们将检索用户ID和名称的集合
$users = $repository->query->get('id', 'name'); foreach ($users as ['id' => $id, 'name' => $name]) { echo $id . ': ' . $name; }
聚合和标量结果
查询构建器还提供各种聚合方法,如 count
、max
、min
、avg
和 sum
。您可以在构建查询后调用这些方法中的任何一种
$count = $users->query->count(); $price = $prices->query->max('price');
当然,您可以与其他子句结合使用这些方法
$price = $prices->query ->where('user', $user) ->where('finalized', 1) ->avg('price');
如果您的数据库支持其他函数,则可以直接使用 ->scalar()
方法使用这些方法
->scalar()
方法的第一参数需要指定应包含在结果中的字段。第二个可选参数允许您将类型转换为所需的类型。
$price = $prices->query ->select('AVG(price) as price') ->scalar('price', 'int');
允许的类型
类型 | 描述 |
---|---|
int |
返回一个整数值 |
float |
返回一个浮点值 |
string |
返回一个字符串值 |
bool |
返回布尔值 |
callable |
返回闭包实例 |
object |
返回一个对象 |
array |
返回一个数组 |
iterable |
array 的别名 |
查询调用
方法 | 描述 |
---|---|
get |
返回实体数组 |
collect |
返回实体集合 |
first |
返回第一个结果 |
scalar |
返回单个标量值 |
count |
返回给定字段的计数 |
sum |
返回给定字段的总和 |
avg |
返回给定字段的平均值 |
max |
返回给定字段的最大值 |
min |
返回给定字段的最小值 |
选择
使用 select()
方法,您可以指定查询的自定义选择子句
['count' => $count] = $users->query ->select(['COUNT(id)' => 'count']) ->get(); echo $count;
此外,此表达式可以简化并重写为以下方式
$result = $users->query ->select(['COUNT(id)' => 'count']) ->scalar('count'); echo $result;
附加字段
实体
您会发现,如果我们指定选择,则在响应中我们得到选择的数据,忽略实体。为了在响应中获得任何实体,我们应该使用 withEntity
方法
['messages' => $messages, 'user' => $user] = $users->query ->select(['COUNT(messages)' => 'messages']) ->withEntity('user') ->where('id', 23) ->first();
原始列
有时某些字段可能不在实体中,例如,关系键。在这种情况下,我们别无选择,只能直接选择这些列,绕过实体的结构
$messages = $query ->select([$query->column('user_id') => 'user_id']) ->withEntity('message') ->get('message', 'user_id'); foreach ($messages as ['message' => $message, 'user_id' => $id]) { echo $message->title . ' of user #' . $id; }
Where 子句
简单的 Where 子句
您可以使用查询构建实例上的 where 方法向查询中添加 where 子句。最基本的 where 调用需要三个参数。第一个参数是列名。第二个参数是运算符,可以是数据库支持的所有运算符之一。最后,第三个参数是要与列进行比较的值。
例如,以下是一个验证 "votes" 实体字段值等于 100 的查询。
$users = $repository->query->where('votes', '=', 100)->get();
为了方便,如果您想验证列是否等于某个给定值,可以直接将值作为 where 方法的第二个参数传递。
$users = $repository->query->where('votes', 100)->get();
当然,在编写 where 子句时,您可以使用各种其他运算符。
$users = $repository->query ->where('votes', '>=', 100) ->get(); $users = $repository->query ->where('votes', '<>', 100) ->get(); $users = $repository->query ->where('votes', '<=', 100) ->get();
或语句
您可以将 where 约束链接起来,也可以向查询中添加 or
子句。orWhere
方法接受与 where 方法相同的参数。
$users = $repository->query ->where('votes', '>', 100) ->orWhere('name', 'John') ->get();
或者,您可以使用 ->or
魔术方法。
$users = $repository->query ->where('votes', '>', 100) ->or->where('name', 'John') ->get();
其他 where 子句
whereBetween
whereBetween
方法验证实体字段值是否介于两个值之间。
$users = $repository->query ->whereBetween('votes', 1, 100) ->get(); $users = $repository->query ->where('name', 'John') ->orWhereBetween('votes', 1, 100) ->get();
whereNotBetween
whereNotBetween
方法验证实体字段值是否不在两个值之间。
$users = $repository->query ->whereNotBetween('votes', 1, 100) ->get(); $users = $repository->query ->where('name', 'John') ->orWhereNotBetween('votes', 1, 100) ->get();
whereIn / whereNotIn
whereIn
方法验证给定实体字段值是否包含在给定的数组中。
$users = $repository->query ->whereIn('id', [1, 2, 3]) ->get(); $users = $repository->query ->where('id', [1, 2, 3]) // Equally: ->whereIn('id', [1, 2, 3]) ->orWhere('id', [101, 102, 103]) // Equally: ->orWhereIn('id', [101, 102, 103]) ->get();
whereNotIn
方法验证给定实体字段值是否不包含在给定的数组中。
$users = $repository->query ->whereNotIn('id', [1, 2, 3]) ->get(); $users = $repository->query ->where('id', '<>', [1, 2, 3]) // Equally: ->whereNotIn('id', [1, 2, 3]) ->orWhere('id', '<>', [101, 102, 103]) // Equally: ->orWhereNotIn('id', [101, 102, 103]) ->get();
whereNull / whereNotNull
whereNull
方法验证给定实体字段值是否为 NULL
。
$users = $repository->query ->whereNull('updatedAt') ->get(); $users = $repository->query ->where('updatedAt', null) // Equally: ->whereNull('updatedAt') ->orWhereNull('deletedAt', null) // Equally: ->orWhereNull('deletedAt') ->get();
whereNotNull
方法验证实体字段值是否不为 NULL
。
$users = $repository->query ->whereNotNull('updatedAt') ->get(); $users = $repository->query ->whereNotNull('updatedAt') ->or->whereNotNull('deletedAt') ->get();
like / notLike
like
方法验证给定实体字段值是否与给定值相似。
$messages = $repository->query ->like('description', '%some%') ->orLike('description', '%any%') ->get(); $messages = $repository->query ->where('description', '~', '%some%') ->orWhere('description', '~', '%any%') ->get();
notLike
方法验证给定实体字段值是否不与给定值相似。
$messages = $repository->query ->notLike('description', '%some%') ->orNotLike('description', '%any%') ->get(); $messages = $repository->query ->where('description', '!~', '%some%') ->orWhere('description', '!~', '%any%') ->get();
参数分组
有时您可能需要创建更复杂的 where 子句,如 "where exists" 子句或嵌套参数分组。Hydrogen 查询构建器可以处理这些情况。要开始,让我们看看一个分组约束的例子。
$users = $repository->query ->where('name', 'John') ->where(function (Query $query): void { $query->where('votes', '>', 100) ->orWhere('title', 'Admin'); }) ->get();
如你所见,传递一个 Closure
到 where
方法指示查询构建器开始一个约束组。这个 Closure
将接收一个查询构建实例,您可以使用它来设置应包含在括号组内的约束。上面的例子将生成以下 DQL。
SELECT u FROM App\Entity\User u WHERE u.name = "John" AND ( u.votes > 100 OR u.title = "Admin" )
此外,除了 where
或 orWhere
方法外,您还可以使用其他选项。方法 or
和 and
会做同样的事情。
$users = $repository->query ->where('name', 'John') ->and(function (Query $query): void { $query->where('votes', '>', 100) ->orWhere('title', 'Admin'); }) ->get(); // SELECT u FROM App\Entity\User u // WHERE u.name = "John" AND ( // u.votes > 100 OR // u.title = "Admin" // ) $users = $repository->query ->where('name', 'John') ->or(function (Query $query): void { $query->where('votes', '>', 100) ->where('title', 'Admin'); }) ->get(); // SELECT u FROM App\Entity\User u // WHERE u.name = "John" OR ( // u.votes > 100 AND // u.title = "Admin" // )
排序
orderBy
orderBy
方法允许您根据给定的列对查询结果进行排序。orderBy
方法的第一个参数应该是您想要排序的列,而第二个参数控制排序的方向,可以是 asc 或 desc。
$users = $repository->query ->orderBy('name', 'desc') ->get();
此外,您还可以使用快捷方式 asc()
和 desc()
来简化代码。
$users = $repository->query ->asc('id', 'createdAt') ->desc('name') ->get();
latest / oldest
最新和最老的方法允许您轻松根据日期对结果进行排序。默认情况下,结果将按 createdAt
实体字段排序。或者,您也可以传递您想要排序的列名。
$users = $repository->query ->latest() ->get(); $posts = $repository->query ->oldest('updatedAt') ->get();
分组
groupBy
groupBy
方法可用于对查询结果进行分组。
$users = $repository->query ->groupBy('account') ->get();
您可以向 groupBy
方法传递多个参数,以按多个列进行分组。
$users = $repository->query ->groupBy('firstName', 'status') ->get();
having
having
方法的签名与where
方法类似
$users = $repository->query ->groupBy('account') ->having('account.id', '>', 100) ->get();
限制和偏移量
跳过/获取
要限制查询返回的结果数量,或要跳过查询中给定数量的结果,可以使用skip()
和take()
方法
$users = $repository->query->skip(10)->take(5)->get();
或者,您也可以使用limit
和offset
方法
$users = $repository->query ->offset(10) ->limit(5) ->get();
之前/之后
通常在数据库高负载期间,当向表中插入新记录时,offset
可能会移动。在这种情况下,使用before()
和after()
方法以确保后续样本严格遵循前一个样本是值得的。
让我们以获取id为15之后10篇文章的例子来说明
$articles = $repository->query ->where('category', 'news') ->after('id', 15) ->take(10) ->get();
范围
您可以使用range()
方法来指定您想要作为结果接收的确切记录
$articles = $repository->range(10, 20)->get();
内嵌对象
内嵌对象是那些不是实体的类,但它们嵌入在实体中,并且也可以通过Hydrogen进行查询。您通常会使用它们来减少重复或分离关注点。例如,日期范围或地址这样的值对象是此功能的主要用例。
<?php /** * @ORM\Entity(repositoryClass=UsersRepository::class) */ class User { /** * @ORM\Embedded(class=Address::class) */ private $address; } /** * @ORM\Embeddable() */ class Address { /** * @ORM\Column(type="string") */ private $city; /** * @ORM\Column(type="string") */ private $country; }
要使用查询来管理内嵌对象,您可以使用点运算符(.
)
<?php class UsersRepository extends EntityRepository { use Hydrogen; public function findAllOrderedByCountry(): iterable { return $this->query->asc('address.country')->get(); } }
关系
Doctrine ORM提供了几种不同类型的关系:@OneToOne
、@OneToMany
、@ManyToOne
和@ManyToMany
。并且“贪婪”加载这些关系是在实体的元数据级别设置的。Doctrine不提供在查询期间管理关系和加载它们的机制,因此在检索数据时,您可能会遇到使用DQL时的N+1
查询,特别是在@OneToOne
关系上,因为根本就没有其他加载选项。
Hydrogen允许您在查询级别灵活地管理如何获取关系,以及这些关系的数量和适用于这些关系的附加聚合函数
<?php /** * @ORM\Entity() */ class Customer { /** .... */ /** * @ORM\OneToOne(targetEntity=Cart::class, mappedBy="customer") */ private $cart; } /** * @ORM\Entity() */ class Cart { /** .... */ /** * @ORM\OneToOne(targetEntity=Customer::class, inversedBy="cart") * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") */ private $customer; }
如果您对存储库创建了一个基本查询,在这种情况下,您将得到相同的N+1
,对于每个元素,对于每个相关实体,都会生成一个额外的查询块。
连接
在Doctrine中处理关系的一个选项是连接。为了使查询在一个查询中请求具有关系的数据,您应使用join
(使用INNER JOIN
)或leftJoin
(使用LEFT JOIN
)方法
$customers = $customerRepository->query ->join('cart') ->get(); foreach ($customers as $customer) { echo $customer->cart->id; }
请注意,在使用连接时,您不能使用
limit
,因为它会影响响应中的总数据量(即,包括关系),而不是父实体的数量。
连接子查询
我们还可以对依赖实体执行其他操作。例如,我们想获取资产负债表上有超过100卢布的用户(客户)列表
$customers = $customerRepository->query ->join(['cart' => function (Query $query): void { $query->where('balance', '>', 100) ->where('currency', 'RUB'); }]) ->get();
注意:使用
join
的操作会影响底层查询。
嵌套关系
因此,如果我们需要所有订购过电影票等商品的客户,我们需要进行一个简单的请求
$customers = $customerRepository->query ->join(['cart.goods' => function (Query $query): void { $query->where('category', 'tickets') ->where('value', '>', 0); }]) ->get();
查询作用域
有时构建整个查询需要很长时间,其中某些部分已经重复了。在这种情况下,我们可以使用作用域的机制,该机制允许您向查询添加一组方法,这些方法将返回我们需要的查询部分。
<?php class UsersRepository extends EntityRepository { use Hydrogen; public function banned(bool $positive = true): Query { return $positive ? $this->query->whereNotNull('bannedAt') : $this->query->whereNull('bannedAt'); } public function findBanned(): iterable { // We supplement the query, call the existing method "banned" return $this->query->banned->get(); } public function findActive(): iterable { // We supplement the query, call the existing method "banned" with additional argument "false" return $this->query->banned(false)->get(); } }
集合
由于基础内核使用了Illuminate Collections,但增加了一些新功能。
高阶消息传递
在使用全局函数时,模式"_
"用于指定高阶消息传递中函数参数中代理的位置。
use RDS\Hydrogen\Collection; $data = [ ['value' => '23'], ['value' => '42'], ['value' => 'Hello!'], ]; $example1 = Collection::make($data) ->map->value // ['23', '42', 'Hello!'] ->toArray(); // // $example1 = \array_map(function (array $item): string { // return $item['value']; // }, $data); // $example2 = Collection::make($data) ->map->value // ['23', '42', 'Hello!'] ->map->intval(_) // [23, 42, 0] ->filter() // [23, 42] ->toArray(); // // // $example2 = \array_map(function (array $item): string { // return $item['value']; // }, $data); // // $example2 = \array_map(function (string $value): int { // return \intval($value); // ^^^^^ - pattern "_" will replaced to each delegated item value. // }, $example1); // // $example2 = \array_filter($example2, function(int $value): bool { // return (bool)$value; // }); // // $example3 = Collection::make($data) ->map->value // ['23', '42', 'Hello!'] ->map->mbSubstr(_, 1) // Using "mb_substr(_, 1)" -> ['3', '2', 'ello!'] ->toArray();
解构
use RDS\Hydrogen\Collection; $collection = Collection::make([ ['a' => 'A1', 'b' => 'B1' 'value' => '23'], ['a' => 'A2', 'b' => 'B2' 'value' => '42'], ['a' => 'A3', 'b' => 'B3' 'value' => 'Hello!'], ]); // Displays all data foreach($collection as $item) { \var_dump($item); // [a => 'A*', b => 'B*', value => '***'] } // Displays only "a" field foreach ($collection as ['a' => $a]) { \var_dump($a); // 'A' }
贝多芬同意。