问题描述
长话短说。
我使用 Doctrine 的单表继承映射来映射一个公共实体的三个不同上下文(类):NotActivatedCustomer、DeletedCustomer 和 Customer.此外,还有一个 AbstractCustomer 包含以下内容:
App\Identity\Domain\Customer\AbstractCustomer:
type: entity
inheritanceType: SINGLE_TABLE
discriminatorColumn:
name: discr
type: string
discriminatorMap:
Customer: App\Identity\Domain\Customer\Customer
NotActivatedCustomer: App\Identity\Domain\Customer\NotActivatedCustomer
DeletedCustomer: App\Identity\Domain\Customer\DeletedCustomer
table: customer
id:
id:
type: customer_id
unique: true
generator:
strategy: CUSTOM
customIdGenerator:
class: Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator
fields:
email:
type: email
length: 180
unique: true
子类型定义示例:
<?PHP
declare(strict_types=1);
namespace App\Identity\Domain\Customer;
use App\Identity\Domain\User\Email;
class DeletedCustomer extends AbstractCustomer
{
public const TYPE = 'DeletedCustomer';
public function __construct(CustomerId $id)
{
$this->_setId($id);
$this->_setEmail(new Email(sprintf('%s@mail.local',$id->value())));
}
}
用例:
<?PHP
declare(strict_types=1);
namespace App\Identity\Application\Customer\UseCase\DeleteCustomer;
use App\Identity\Application\Customer\CustomerEntityManager;
use App\Identity\Application\User\AuthenticatedCustomer;
use App\Identity\Domain\Customer\DeletedCustomer;
use App\Shared\Application\ImageManager;
final class DeleteCustomerHandler
{
private CustomerEntityManager $customerEntityManager;
private AuthenticatedCustomer $authenticatedCustomer;
private ImageManager $imageManager;
public function __construct(AuthenticatedCustomer $authenticatedCustomer,CustomerEntityManager $customerEntityManagerByActiveTenant,ImageManager $customerPhotoManager)
{
$this->customerEntityManager = $customerEntityManagerByActiveTenant;
$this->authenticatedCustomer = $authenticatedCustomer;
$this->imageManager = $customerPhotoManager;
}
public function handle(): void
{
$customer = $this->authenticatedCustomer->customer();
$photo = (string) $customer->photo();
$deletedCustomer = new DeletedCustomer($customer->id());
// Todo OR return DeletedCustomer that way
// $deletedCustomer = $customer->deactive();
// entityManager->merge() called here
$this->customerEntityManager->sync($deletedCustomer);
// simple entityManager->flush() under the hood
$this->customerEntityManager->update();
// that's a raw query to update discriminator field,hackish way I'm using
// UPDATE customer SET discr = ? WHERE id = ?
$this->customerEntityManager->updateInheritanceType($customer,DeletedCustomer::TYPE);
if ($photo) {
$this->imageManager->remove($photo);
}
}
}
因此,如果您已经有一个现有的 Customer 持久化并运行 DeleteCustomerHandler,Customer 将被更新,但它的鉴别器字段不会! 谷歌搜索,没有办法更新鉴别器字段,而不是像我那样采用某种黑客方式(手动运行原始查询来更新字段)。
此外,我需要使用 EntityManager->merge() 方法将手动初始化的 DeletedCustomer 添加到内部 UnitOfWork。看起来也有点脏,而且它是 Doctrine 3 不推荐使用的方法,所以问题还有没有更好的方法来处理我的情况?
所以,总结所有问题:
- 我是否将 Customer's 状态更改为 DeletedCustomer 完全错误?我只是想避免客户上帝对象,区分这个实体的有界上下文,有点那样。
- 如何避免 EntityManager->merge() 出现? AuthenticatedCustomer 来自会话 (JWT)。
解决方法
我认为您希望避免客户变成上帝对象是绝对正确的。继承是一种方法,但将其用于不同状态的客户可能会导致问题。
我的经验中的两个关键问题:
- 随着新状态的出现,您会继续添加不同的继承实体吗?
- 当您让一位客户经历两种不同的状态时会发生什么,例如一位曾是
NotActivatedCustomer
但现在是DeletedCustomer
的客户?
因此,只有当继承的类型真正是更具体的类型时,我才会保留继承,其中给定的实体在其整个生命周期中永远只是这些类型之一。例如,汽车不会变成摩托车。
我有两种模式来解决与您不同的问题。我倾向于从第一个开始,然后转向第二个。
interface DeletedCustomer
{
public function getDeletedAt(): DateTime;
}
interface NotActivatedCustomer
{
public function getCreatedAt(): DateTime;
}
class Customer implements DeletedCustomer,NotActivatedCustomer
{
private $id;
private $name;
private DateTime $deletedAt;
private bool $isActivated = false;
public function getDeletedAt(): DateTime {...}
public function getCreatedAt(): DateTime {...}
}
class DeletedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.deletedAt IS NOT NULL
>>>)->getQuery()->getResults();
}
}
class NotActivatedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.isActivated = false
>>>)->getQuery()->getResults();
}
}
class DeletedCustomerService
{
public function doTheThing(DeletedCustomer $customer) {}
}
这减少了耦合,这是神对象的主要问题之一。因此,当列开始激增时,我可以将它们移到加入 Customer
的真实实体中。引用 DeletedCustomer
的组件仍会收到一个。
第二种模式是 event-sourcing-lite - 与“CustomerLifecycleEvent”实体具有多对一的关系。根据客户是否有“已删除”事件进行查询。第二种方法要复杂得多,无论是更新还是查询。您仍然可以拥有返回实体(如 DeletedCustomer
)的专用存储库,但您需要做更多的样板工作。