You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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但未达到预期效果。具体需求:

  1. 用户对mainObject进行评分;
  2. 该mainObject的统计数据自动更新;
  3. 无需查询并分组所有评论即可展示统计数据。

优雅解决方案

方案1:Doctrine生命周期回调(轻量直接)

在reviews实体中添加生命周期注解,在评论创建、更新、删除时触发统计更新逻辑,这是最贴合ORM逻辑的方案:

  1. 先确保实体关联关系配置正确,比如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请求流程深度结合的场景:

  1. 创建事件监听器类:
// 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();
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 06:45:30