-
Notifications
You must be signed in to change notification settings - Fork 1
运行以下命令以完成安装
composer require free2one/hyperf-doctrine
发布配置
php bin/hyperf.php vendor:publish free2one/hyperf-doctrine
下面我们从用户实体开始,举例说明在Doctrine中如何关联实体与数据库,并最终完成数据写入。
首先,我们需要创建实体类User
,其包含了用户标识、名称及性别。
<?php
namespace App\Domain\Entity;
use App\Domain\Entity\ValueObject\Gender;
class User
{
private int $id;
private string $userName;
private Gender $gender;
}
接着,我们需要创建性别枚举类Gender
。
<?php
namespace App\Domain\Entity\ValueObject;
enum Gender: int
{
case Male = 1;
case Female = 2;
}
下一步我们需要使用元数据语言(metadata language
)向Doctrine描述实体的结构。元数据语言将定义实体、属性和引用应该如何被持久化以及应该对它们应用什么约束。
这里我们先对User
对应的表结构进行展示。
随后我们开始在User
中对定义相关元数据
<?php
namespace App\Domain\Entity;
use App\Domain\Entity\ValueObject\Gender;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'user')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private int $id;
#[ORM\Column(name: 'name', type: Types::STRING)]
private string $userName;
#[ORM\Column(name: 'gender', type: Types::SMALLINT, enumType: Gender::class)]
private Gender $gender;
}
类级别的注解我们定义了ORM\Entity
,表明它是一个实体。同时使用了ORM\Table
,描述了实体与之关联的表user
。
属性级别的注解我们逐个进行说明:
-
id
:我们定义了ORM\Id
来表明该列为主键,并且通过ORM\GeneratedValue
声明其使用了自动生成策略(具体由数据库供应商决定,如Mysql的AUTO_INCREMENT)。另外,我们还使用了ORM\Column
来描述关联的列信息。 -
userName
:同样使用了ORM\Column
对关联的列信息进行了描述。需要注意的是,属性userName
与列名并不相等。name
选项描述的是对应的列名称,不填写时Doctrine将默认列名与属性名相同。 -
gender
:因该成员类型为枚举,因此在ORM\Column
中我们额外使用了enumType
来对其进行描述。 更多介绍见官网文档基本映射 - Doctrine Object Relational Mapper (ORM) (doctrine-project.org)。
下面我们将实例化一个User
对象,并将其存储到数据库中。
<?php
use App\Domain\Entity\User;
use App\Domain\Entity\ValueObject\Gender;
use Hyperf\Doctrine\EntityManagerFactory;
$user = new User();
$user
->setUserName('小明')
->setGender(Gender::Male);
$em = EntityManagerFactory::getManager(); //获取实体管理器
$em->persist($user); //持久化实体
$em->flush(); //刷新至数据库中
首先我们从工厂类中获取到实体管理器EntityManager
,随后调用persist()
方法来通知EntityManager
该实体需要被写入到数据库中。最后显式调用flush()
方法,此时将启动一个事务,并把user
对象真正写入到数据库中。
persist 和 flush 之间的这种区别允许将所有数据库写入(INSERT、UPDATE、DELETE)聚合到一个事务中,该事务在flush()
调用时执行。使用这种方法,写入性能明显优于在每个实体上单独执行写入的场景。
假设用户存在多个地址信息,我们尝试增加新的实体类UserAddress
,同时修改原有的User
以建立新的关联关系。
地址实体UserAddress
<?php
namespace App\Domain\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'user_address')]
class UserAddress
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private int $id;
#[ORM\Column(name: 'uid', type: Types::INTEGER)]
private int $uid;
#[ORM\Column(type: Types::STRING)]
private string $region;
#[ORM\Column(type: Types::STRING)]
private string $address;
#[ORM\Column(name: 'update_time', type: Types::DATETIME_MUTABLE)]
private \DateTime $updateTime;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'addresses')]
#[ORM\JoinColumn(name: 'uid', referencedColumnName: 'id')]
private ?User $user = null;
}
新增地址关联后的User
<?php
namespace App\Domain\Entity;
use App\Domain\Entity\ValueObject\Gender;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'user')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private int $id;
#[ORM\Column(name: 'name', type: Types::STRING)]
private string $userName;
#[ORM\Column(name: 'gender', type: Types::SMALLINT, enumType: Gender::class)]
private Gender $gender;
/** @var Collection<int, UserAddress> */
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserAddress::class)]
private Collection $addresses;
}
在上述例子中User
与UserAddress
是一对多的关系,我们需要在类中增加对应的元数据定义,详见User
中addresses
以及UserAddress
中user
的对应注解。
除了OneToMany
,关联关系还有OneToOne及ManyToOne,更多详细说明请详见官网文档。关联映射 - Doctrine Object Relational Mapper (ORM) (doctrine-project.org)
完成以上工作后,我们在进行查询时Doctrine将会自动关联地址信息至结果集中。
use App\Domain\Entity\User;
use Hyperf\Doctrine\EntityManagerFactory;
$em = EntityManagerFactory::getManager();
/** @var User $user */
$user = $em->find(User::class, 1); //查询id为1的用户信息
foreach ($user->getAddresses() as $address) {
var_dump($address);
}
响应结果如下:
^ App\Domain\Entity\UserAddress^ {#814
-id: 1
-uid: 1
-region: "中国广东省深圳市"
-address: "福田区xxx大院"
-updateTime: DateTime @1671445263 {#817
date: 2022-12-19 18:21:03.0 Asia/Shanghai (+08:00)
}
-user: App\Domain\Entity\User^ {#838
-id: 1
-userName: "小明"
-gender: App\Domain\Entity\ValueObject\Gender^ {#836
+name: "Male"
+value: 1
}
......
}
}
^ App\Domain\Entity\UserAddress^ {#811
-id: 2
-uid: 1
-region: "中国广东省东莞市"
-address: "A区xxx01大院"
-updateTime: DateTime @1671445319 {#812
date: 2022-12-19 18:21:59.0 Asia/Shanghai (+08:00)
}
-user: App\Domain\Entity\User^ {#838
-id: 1
-userName: "小明"
-gender: App\Domain\Entity\ValueObject\Gender^ {#836
+name: "Male"
+value: 1
}
......
}
}
注意,默认关联对象的获取策略为LAZY
(懒加载)。即当访问对象时,才真正从数据库中获取相关数据。
除了懒加载外,还存在EAGER
(饥饿)模式,在此模式下SQL将不再进行拆分,而是汇总成一条联表查询语句,并于find()
方法内完成查询。EAGER对应的注解调整如下:
<?php
namespace App\Domain\Entity;
#[ORM\Entity]
#[ORM\Table(name: 'user')]
class User
{
......
/** @var Collection<int, UserAddress> */
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserAddress::class, cascade: null, fetch: 'EAGER')]
private Collection $addresses;
}
需要注意的是,当批量获取对象时,无论是EAGER
或LAZY
模式Doctrine均无法进行SQL语句拼接,查询语句仍会以实体维度进行拆分。考虑以下场景,系统需要获取100个用户信息及其地址,最终查询将会被拆分成1+100条SQL语句。
在介绍更新操作前,我们先对UserAddress
进行修改。我们会新增一个状态
属性以描述地址是否激活中。同时,实体内也会新增deactivate()
方法,通过调用其来完成状态更新。
<?php
namespace App\Domain\Entity\ValueObject;
enum Status: int
{
case Activated = 1;
case Unactivated = 2;
}
<?php
namespace App\Domain\Entity;
use App\Domain\Entity\ValueObject\Status;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'user_address')]
class UserAddress
{
.....
#[ORM\Column(name: 'status', type: Types::SMALLINT, enumType: Status::class)]
private Status $status;
.....
public function deactivate(): void
{
$this->status = Status::Unactivated;
$this->updateTime->setTimestamp(time());
}
}
在完成上述更改后,我们看看代码内如何变更用户地址至非激活状态。
$em = EntityManagerFactory::getManager();
/** @var User $user */
$user = $em->find(User::class, 1);
foreach ($user->getAddresses() as $address) {
$address->deactivate();
}
$em->flush();
在上述代码中,我们并没有显式的调用persist()
方法来通知EntityManager
。这是因为当完成数据检索时,对应的数据已经插入到Doctrine 的 UnitOfWork内的 IdentityMap中。当调用flush()
时,EntityManager 遍历身份映射中的所有实体,并比较最初从数据库中检索的值与实体当前具有的值。如果这些属性中至少有一个不同,则实体将针对数据库安排更新。此时仅会仅更新更改的列,与更新所有属性相比,这提供了相当好的性能改进。
默认情况下,每个实体都拥有默认的仓储(Kkguan\KkDoctrine\ORM\EntityRepository
),并提供一系列方便的方法,你可以使用这些方法来查询该实体的实例。
<?php
$em = EntityManagerFactory::getManager();
$user = $em->getRepository(User::class)->findOneBy(['userName' => '小明']);
Doctrine
原生支持实体级别的仓储自定义扩展,我们在适配Hyperf的过程中,针对常规DDD项目的代码架构进行了部分调整。下面我们将为User
建立自己的仓储类,并使用其来获取激活中的用户地址信息。
首先,我们新建仓储类UserRepository
。
<?php
namespace App\Domain\Repository;
use App\Domain\Entity\User;
use App\Domain\Entity\UserAddress;
use App\Domain\Entity\ValueObject\Status;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Query\Parameter;
use Hyperf\Doctrine\EntityManagerFactory;
use Hyperf\Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
protected string $entityName = User::class;
public function getUserAddressesByStatus(int $uid, Status $status)
{
$em = EntityManagerFactory::getManager();
$builder = $em->createQueryBuilder()
->select('address')
->from(UserAddress::class, 'address')
->andWhere('address.uid = :uid')
->andWhere('address.status = :status')
->setParameters(
new ArrayCollection([
new Parameter('uid', $uid),
new Parameter('status', $status),
])
);
return $builder->getQuery()->getResult();
}
}
修改原有的User
类中的ORM\Entity
注解,增加仓储相关的描述。
<?php
namespace App\Domain\Entity;
.....
use App\Domain\Repository\UserRepository;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'user')]
class User
{
......
}
当完成上述流程后,我们便可通过EntityManager
获取对应仓储,从而使用getUserAddressesByStatus
方法进行查询。
$em = EntityManagerFactory::getManager();
$userAddresses = $em->getRepository(User::class)->getUserAddressesByStatus(1, Status::Activated);
在进行上述调用时,你的代码可能会出现以下报错信息。
ErrorException: require(/xxxxxxx/runtime/doctrine/__CG__AppDomainEntityUser.php): Failed to open stream: No such file or directory
那是因为Doctrine在此查询中需要使用到代理类,而代理类的默认生成策略为Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_NEVER
,即永远不自动生成。你可以更改配置文件中的isDevMode
为true
使其每次都自动生成。但请切记不要在生产环境中使用自动生成策略,你应该通过命令来手动生成代理类。
php bin/hyperf.php doctrine:generate:proxies
以上的仓储设计未必是正确的,我们更多是为了展示仓储的使用以及可能出现的问题。若你未在User
及UserAddress
中定义关联关系时,将不会出现以上的报错,因为此时Doctrine并不会使用到代理类。
在实际业务场景中,我们要慎重考虑是否真的需要使用实体间的关联关系。更多时候,你应该考虑在代码中进行手动管理而不是完全依赖orm。