Skip to content
free2one edited this page Jul 13, 2023 · 1 revision

安装

运行以下命令以完成安装

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对应的表结构进行展示。

image

随后我们开始在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以建立新的关联关系。

image

地址实体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;  
}

在上述例子中UserUserAddress是一对多的关系,我们需要在类中增加对应的元数据定义,详见Useraddresses以及UserAddressuser的对应注解。 除了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(懒加载)。即当访问对象时,才真正从数据库中获取相关数据。

image

除了懒加载外,还存在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;  
}

需要注意的是,当批量获取对象时,无论是EAGERLAZY模式Doctrine均无法进行SQL语句拼接,查询语句仍会以实体维度进行拆分。考虑以下场景,系统需要获取100个用户信息及其地址,最终查询将会被拆分成1+100条SQL语句。

实体更新

在介绍更新操作前,我们先对UserAddress进行修改。我们会新增一个状态属性以描述地址是否激活中。同时,实体内也会新增deactivate()方法,通过调用其来完成状态更新。

<?php  

namespace App\Domain\Entity\ValueObject;  
  
enum Status: int  
{  
    case Activated = 1;  
    case Unactivated = 2;  
}
Status状态枚举
<?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());
    }
}
UserAddress

在完成上述更改后,我们看看代码内如何变更用户地址至非激活状态。

$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,即永远不自动生成。你可以更改配置文件中的isDevModetrue使其每次都自动生成。但请切记不要在生产环境中使用自动生成策略,你应该通过命令来手动生成代理类。

php bin/hyperf.php doctrine:generate:proxies

以上的仓储设计未必是正确的,我们更多是为了展示仓储的使用以及可能出现的问题。若你未在UserUserAddress中定义关联关系时,将不会出现以上的报错,因为此时Doctrine并不会使用到代理类。 在实际业务场景中,我们要慎重考虑是否真的需要使用实体间的关联关系。更多时候,你应该考虑在代码中进行手动管理而不是完全依赖orm。

Clone this wiki locally