API Platform中reviews操作时自动更新mainObjectStats统计实体的方案
API Platform中评论操作时自动更新关联统计数据的解决方案
问题背景
我在API Platform中定义了三个实体:
- mainObject(被评分项):包含ID(int UUID)、title(char)、type(int)、year(int)、user(int KEY users)、created(date)字段;
- mainObjectStats(与mainObject一对一关联的统计实体):包含ID(int UUID)、mainObjectID(int KEY mainObject)、countReviews(char)、countTotalRatingsStar(int)字段;
- reviews(与mainObject多对一关联的评论实体):包含ID(int UUID)、mainObjectID(int KEY mainObject)、userID(int KEY users)、body(char)、ratings、created(date)字段。
目标是在对reviews发起PUT、POST或DELETE请求时,自动更新关联mainObject对应的mainObjectStats数据,曾尝试自定义Normalizer但未达到预期效果。具体需求:
- 用户对mainObject进行评分;
- 该mainObject的统计数据自动更新;
- 无需查询并分组所有评论即可展示统计数据。
优雅解决方案
方案1:Doctrine生命周期回调(轻量直接)
在reviews实体中添加生命周期注解,在评论创建、更新、删除时触发统计更新逻辑,这是最贴合ORM逻辑的方案:
- 先确保实体关联关系配置正确,比如
reviews关联到mainObject,mainObject关联到mainObjectStats:
// src/Entity/Reviews.php use Doctrine\ORM\Mapping as ORM; use App\Entity\MainObject; use Doctrine\ORM\EntityManagerInterface; /** * @ORM\Entity * @ORM\HasLifecycleCallbacks */ class Reviews { // ... 现有字段定义 /** * @ORM\ManyToOne(targetEntity=MainObject::class, inversedBy="reviews") * @ORM\JoinColumn(nullable=false) */ private $mainObject; private $entityManager; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; } // ... getter和setter方法 /** * @ORM\PostPersist * @ORM\PostUpdate * @ORM\PostRemove */ public function syncMainObjectStats(): void { $mainObject = $this->getMainObject(); // 计算评论数和总评分 $reviewCount = $this->entityManager->getRepository(Reviews::class)->count(['mainObject' => $mainObject]); $totalRatings = $this->entityManager->getRepository(Reviews::class) ->createQueryBuilder('r') ->select('SUM(r.ratings)') ->where('r.mainObject = :mainObject') ->setParameter('mainObject', $mainObject) ->getQuery() ->getSingleScalarResult() ?? 0; // 获取或创建统计实体 $stats = $mainObject->getMainObjectStats(); if (!$stats) { $stats = new MainObjectStats(); $stats->setMainObject($mainObject); $this->entityManager->persist($stats); } // 更新统计数据 $stats->setCountReviews((string)$reviewCount); $stats->setCountTotalRatingsStar((int)$totalRatings); $this->entityManager->flush(); } }
方案2:API Platform事件监听器(解耦业务逻辑)
利用API Platform的事件系统,监听reviews的POST、PUT、DELETE事件,在请求完成后执行统计更新,适合需要和API请求流程深度结合的场景:
- 创建事件监听器类:
// src/EventListener/ReviewStatsSyncListener.php namespace App\EventListener; use ApiPlatform\Core\EventListener\EventPriorities; use App\Entity\MainObjectStats; use App\Entity\Reviews; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\KernelEvents; class ReviewStatsSyncListener implements EventSubscriberInterface { private $entityManager; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; } public static function getSubscribedEvents() { return [ KernelEvents::VIEW => ['syncStats', EventPriorities::POST_WRITE], ]; } public function syncStats(ViewEvent $event): void { $review = $event->getControllerResult(); $requestMethod = $event->getRequest()->getMethod(); // 只处理reviews实体的POST/PUT/DELETE请求 if (!$review instanceof Reviews || !in_array($requestMethod, ['POST', 'PUT', 'DELETE'])) { return; } $mainObject = $review->getMainObject(); // 计算统计数据 $reviewCount = $this->entityManager->getRepository(Reviews::class)->count(['mainObject' => $mainObject]); $totalRatings = $this->entityManager->getRepository(Reviews::class) ->createQueryBuilder('r') ->select('SUM(r.ratings)') ->where('r.mainObject = :mainObject') ->setParameter('mainObject', $mainObject) ->getQuery() ->getSingleScalarResult() ?? 0; // 更新统计实体 $stats = $mainObject->getMainObjectStats() ?? new MainObjectStats(); if (!$stats->getId()) { $stats->setMainObject($mainObject); $this->entityManager->persist($stats); } $stats->setCountReviews((string)$reviewCount); $stats->setCountTotalRatingsStar((int)$totalRatings); $this->entityManager->flush(); } }
- 在
services.yaml中注册监听器:
services: App\EventListener\ReviewStatsSyncListener: tags: - { name: kernel.event_subscriber }
方案3:数据库触发器(性能最优)
如果追求极致性能,直接在数据库层面创建触发器,脱离PHP代码逻辑,适合统计规则简单且对性能要求高的场景:
以MySQL为例:
-- 创建更新统计的存储过程 DELIMITER // CREATE PROCEDURE update_main_object_stats(IN target_main_object_id INT) BEGIN DECLARE review_count INT; DECLARE total_ratings INT; SELECT COUNT(*) INTO review_count FROM reviews WHERE mainObjectID = target_main_object_id; SELECT COALESCE(SUM(ratings), 0) INTO total_ratings FROM reviews WHERE mainObjectID = target_main_object_id; -- 插入或更新统计数据 INSERT INTO main_object_stats (mainObjectID, countReviews, countTotalRatingsStar) VALUES (target_main_object_id, review_count, total_ratings) ON DUPLICATE KEY UPDATE countReviews = review_count, countTotalRatingsStar = total_ratings; END // DELIMITER ; -- 创建INSERT触发器 CREATE TRIGGER after_review_insert AFTER INSERT ON reviews FOR EACH ROW CALL update_main_object_stats(NEW.mainObjectID); -- 创建UPDATE触发器 CREATE TRIGGER after_review_update AFTER UPDATE ON reviews FOR EACH ROW CALL update_main_object_stats(NEW.mainObjectID); -- 创建DELETE触发器 CREATE TRIGGER after_review_delete AFTER DELETE ON reviews FOR EACH ROW CALL update_main_object_stats(OLD.mainObjectID);
为什么自定义Normalizer不行?
Normalizer的核心作用是序列化/反序列化数据,也就是把实体转成响应格式(如JSON),或者把请求数据转成实体对象,它不负责业务层面的数据更新逻辑,所以之前的方向不符合这个组件的定位。
内容的提问来源于stack exchange,提问作者AlexMassy
相关产品推荐
相关产品推荐

