Doctrine ORM:使用由外键组成的复合主键来保留集合

时间:2013-12-31 18:05:52

标签: symfony orm doctrine-orm doctrine-orm-postgres

我猜这是一个Doctrine bug(我在JIRA问题跟踪器上提交了一个问题),但是如果只是用户错误我决定在这里发布。

概要

在具有复合主节点的连接表中保留实体集合 在某些情况下,由2个外键和一个元数据字段组成的密钥失败。代码基于此处的说明:Doctrine docs

问题详情

  1. 成功:当FOREIGN KEY 1在要保留的集合中的项目相同时, 和FOREIGN KEY 2在任何现有的PRIMARY KEY(实体)中大于FOREIGN KEY 2 集合中的相关实体正确保留:

    • 示例:GPA“添加以下值”存在且评估值为{"assessment":6,"value":4} 我们将尝试在assessment_id>处添加新的评估值。任何现有的 GPA评估值“加上以下”

    • 请求有效负载:{"name":"add val below","courses":[],"assessmentValues":[{"assessment":6,"value":4},{"assessment":7,"value":3}]}

    • 调试日志:

      [2013-12-31 11:48:48] app.INFO: GPA ID PRESAVE IN CONTROLLER:9 [] []
      [2013-12-31 11:48:48] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL ASSESSMENT ID:7 [] []
      [2013-12-31 11:48:48] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL POINTS:3 [] []
      [2013-12-31 11:48:48] app.INFO: GPA ID PRESAVE IN CONTROLLER:9 [] []
      [2013-12-31 11:48:48] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL ASSESSMENT ID:6 [] []
      [2013-12-31 11:48:48] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL POINTS:4 [] []
      [2013-12-31 11:48:48] doctrine.DEBUG: "START TRANSACTION" [] []
      [2013-12-31 11:48:48] doctrine.DEBUG: INSERT INTO gpa_assessment_value (point_value, grade_point_average_id, assessment_id) VALUES (?, ?, ?) {"1":3,"2":"9","3":"7"} []
      [2013-12-31 11:48:48] doctrine.DEBUG: UPDATE gpa_assessment_value SET point_value = ? WHERE grade_point_average_id = ? AND assessment_id = ? [4,9,6] []
      [2013-12-31 11:48:48] doctrine.DEBUG: "COMMIT" [] []

  2. FAILURE:当FOREIGN KEY 1在集合中的项目之间相同时,FOREIGN KEY 2小于任何现有的FOREIGN KEY 2,工作单元尝试INSERT现有实体并且不对新实体进行操作。

    • 示例:GPA“添加上面的值”存在且评估值为{"assessment":8,"value":2} 我们将尝试添加一个新的评估值,其中assessment_id<任何现有的 GPA的评估值“加上以上”

    • 请求有效负载:{"name":"add val above","courses":[],"assessmentValues":[{"assessment":6,"value":4},{"assessment":8,"value":2}]}

    • 调试日志:

      [2013-12-31 11:53:59] app.INFO: GPA ID PRESAVE IN CONTROLLER:10 [] []
      [2013-12-31 11:53:59] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL ASSESSMENT ID:8 [] []
      [2013-12-31 11:53:59] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL POINTS:2 [] []
      [2013-12-31 11:53:59] app.INFO: GPA ID PRESAVE IN CONTROLLER:10 [] []
      [2013-12-31 11:53:59] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL ASSESSMENT ID:6 [] []
      [2013-12-31 11:53:59] app.INFO: PRESAVE IN CONTROLLER ASSESSMENT VAL POINTS:4 [] []
      [2013-12-31 11:53:59] doctrine.DEBUG: "START TRANSACTION" [] []
      [2013-12-31 11:53:59] doctrine.DEBUG: INSERT INTO gpa_assessment_value (point_value, grade_point_average_id, assessment_id) VALUES (?, ?, ?) {"1":2,"2":"10","3":"8"} []
      [2013-12-31 11:53:59] doctrine.DEBUG: "ROLLBACK" [] []
      [2013-12-31 11:53:59] request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\DBALException: "An exception occurred while executing 'INSERT INTO gpa_assessment_value (point_value, grade_point_average_id, assessment_id) VALUES (?, ?, ?)' with params [2, "10", "8"]:
      SQLSTATE[23505]: Unique violation: 7 ERROR:  duplicate key value violates unique constraint "gpa_assessment_value_pkey"

  3. CODE

    migration.sql

    CREATE TABLE assessment
    (
        id       bigserial NOT NULL,
        scale_id bigint    NOT NULL,
        title    varchar   NOT NULL,
        passing  boolean   NOT NULL,
        rank     int,
    
        PRIMARY KEY (id)
    );
    
    CREATE TABLE assessment_scale
    (
        id   bigserial NOT NULL,
        name varchar   NOT NULL,
    
        PRIMARY KEY (id)
    );
    -- ...
    
    CREATE TABLE grade_point_average
    (
        id                         bigserial       NOT NULL,
        name                       varchar         NOT NULL,
        additional_credit_allowance numeric(4, 2),
    
        PRIMARY KEY (id)
    );
    
    -- ...
    
    CREATE TABLE gpa_assessment_value
    (
        grade_point_average_id bigint        NOT NULL,
        assessment_id          bigint        NOT NULL,
        point_value            numeric(4, 2) NOT NULL,
    
        PRIMARY KEY (assessment_id, grade_point_average_id),
        FOREIGN KEY (assessment_id) REFERENCES assessment,
        FOREIGN KEY (grade_point_average_id) REFERENCES grade_point_average
    );
    
    

    型号/ GradePointAverage.php

    namespace MyApp\Model;
    
    use Doctrine\ORM\Mapping\Entity;
    use Doctrine\ORM\Mapping\Id;
    use Doctrine\ORM\Mapping\GeneratedValue;
    use Doctrine\ORM\Mapping\Column;
    //...
    use Doctrine\Common\Collections\Collection;
    use Doctrine\Common\Collections\ArrayCollection;
    use MyApp\Util\ConstructorArgs;
    use MyApp\Model\GradePointAverage\AssessmentValue;
    // ...
    
    /**
     * @Entity("MyApp\Repository\GradePointAverageRepository")
     */
    class GradePointAverage
    {
        use ConstructorArgs;
    
        /**
         * @Id
         * @GeneratedValue
         * @Column(type="bigint")
         *
         * @var int
         */
        private $id;
    
        // ...
    
        /**
         * @OneToMany(targetEntity="MyApp\Model\GradePointAverage\AssessmentValue", mappedBy="gradePointAverage", cascade="persist")
         *
         * @var Collection
         */
        private $assessmentValues;
    
        // ...
    
        /**
         * @param array $args
         */
        public function __construct(array $args = [])
        {
            $this->assessmentValues = new ArrayCollection;
            // ...
            $this->handleArgs($args);
        }
    
        // ...
    
        /**
         * @return Collection
         */
        public function getAssessmentValues()
        {
            return $this->assessmentValues;
        }
    
        /**
         * @param ArrayCollection $assessmentValues
         */
        public function setAssessmentValues(ArrayCollection $assessmentValues)
        {
            $this->assessmentValues = $assessmentValues;
        }
    
        /**
         * @param AssessmentValue $assessmentValue
         */
        public function addAssessmentValue(AssessmentValue $assessmentValue)
        {
            $this->assessmentValues->add($assessmentValue);
        }
    
        /**
         * @param AssessmentValue $assessmentValue
         */
        public function removeAssessmentValue(AssessmentValue $assessmentValue)
        {
            $this->assessmentValues->removeElement($assessmentValue);
        }
    
        // ...
    }
    

    型号/ GradePointAverage / AssessmentValue.php

    namespace MyApp\Model\GradePointAverage;
    
    use Doctrine\ORM\Mapping\Entity;
    use Doctrine\ORM\Mapping\Table;
    use Doctrine\ORM\Mapping\Column;
    use Doctrine\ORM\Mapping\Id;
    use Doctrine\ORM\Mapping\GeneratedValue;
    use Doctrine\ORM\Mapping\ManyToOne;
    use Doctrine\ORM\Mapping\JoinColumn;
    use MyApp\Model\GradePointAverage;
    use MyApp\Model\Assessment;
    use MyApp\Util\ConstructorArgs;
    
    /**
     * @Entity("MyApp\Repository\GradePointAverage\AssessmentValueRepository")
     * @Table("gpa_assessment_value")
     */
    class AssessmentValue
    {
        use ConstructorArgs;
    
        /**
         * @Id
         * @ManyToOne(targetEntity="MyApp\Model\GradePointAverage")
         */
        private $gradePointAverage;
    
        /**
         * @Id
         * @ManyToOne(targetEntity="MyApp\Model\Assessment")
         */
        private $assessment;
    
        /**
         * @Column("point_value")
         *
         * @var float
         */
        private $value;
    
        /**
         * @param array $args
         */
        public function __construct(array $args = [])
        {
            $this->handleArgs($args);
        }
    
        /**
         * @return GradePointAverage
         */
        public function getGradePointAverage()
        {
            return $this->gradePointAverage;
        }
    
        /**
         * @param GradePointAverage $gradePointAverage
         */
        public function setGradePointAverage(GradePointAverage $gradePointAverage)
        {
            $this->gradePointAverage = $gradePointAverage;
        }
    
        /**
         * @return Assessment
         */
        public function getAssessment()
        {
            return $this->assessment;
        }
    
        /**
         * @param Assessment $assessment
         */
        public function setAssessment(Assessment $assessment)
        {
            $this->assessment = $assessment;
        }
    
        /**
         * @return float
         */
        public function getValue()
        {
            return $this->value;
        }
    
        /**
         * @param float $value
         */
        public function setValue($value)
        {
            $this->value = $value;
        }
    
        /**
         * @return AssessmentScale
         */
        public function getAssessmentScale()
        {
            return $this->assessment->getScale();
        }
    }
    

    型号/ Assessment.php

    namespace MyApp\Model;
    
    use Doctrine\ORM\Mapping\Entity;
    use Doctrine\ORM\Mapping\Id;
    use Doctrine\ORM\Mapping\GeneratedValue;
    use Doctrine\ORM\Mapping\Column;
    use Doctrine\ORM\Mapping\ManyToOne;
    use MyApp\Model\Assessment\Scale;
    use MyApp\Util\ConstructorArgs;
    
    /**
     * @Entity("MyApp\Repository\AssessmentRepository")
     */
    class Assessment
    {
        use ConstructorArgs;
    
        /**
         * @Id
         * @GeneratedValue
         * @Column(type="bigint")
         *
         * @var int
         */
        private $id;
    
        // ...
    
        /**
         * @param array $args
         */
        public function __construct(array $args = [])
        {
            $this->handleArgs($args);
        }
    
        /**
         * @return int
         */
        public function getId()
        {
            return $this->id;
        }
    
        // ...
    }
    

    存储库/ GradePointAverageRepository.php

    namespace MyApp\Repository;
    
    use Doctrine\ORM\EntityRepository;
    // ...
    use MyApp\Model\GradePointAverage;
    
    class GradePointAverageRepository extends BaseRepository implements GradePointAverageRepositoryInterface
    {
        // ...
    
        /**
         * @param GradePointAverage $gradePointAverage
         */
        public function save(GradePointAverage $gradePointAverage)
        {
            $this->getEntityManager()->persist($gradePointAverage);
            $this->getEntityManager()->flush();
        }
    }
    

    存储库/ GradePointAverage / AssessmentValueRepository.php

    namespace MyApp\Repository\GradePointAverage;
    
    use Doctrine\ORM\EntityRepository;
    use MyApp\Model\GradePointAverage\AssessmentValue;
    
    class AssessmentValueRepository extends EntityRepository
    {
        /**
         * @param AssessmentValue $assessmentValue
         */
        public function save(AssessmentValue $assessmentValue)
        {
            $this->getEntityManager()->persist($assessmentValue);
            $this->getEntityManager()->flush();
        }
    }
    

    经理/ GradePointAverageManager.php

    namespace MyApp\Manager;
    
    use InvalidArgumentException;
    use Symfony\Component\Validator\ValidatorInterface;
    use JMS\DiExtraBundle\Annotation\Service;
    use JMS\DiExtraBundle\Annotation\InjectParams;
    use JMS\SecurityExtraBundle\Annotation\PreAuthorize;
    use Knp\Component\Pager\Pagination\PaginationInterface;
    use MyApp\Repository\GradePointAverageRepository;
    use MyApp\PaginationFactory\GradePointAveragePaginationFactoryInterface;
    use MyApp\Model\GradePointAverage;
    
    /**
     * @Service("grade_point_average_manager")
     */
    class GradePointAverageManager
    {
        /**
         * @var GradePointAverageRepository
         */
        private $gradePointAverageRepository;
    
        /**
         * @var GradePointAveragePaginationFactoryInterface
         */
        private $gradePointAveragePaginationFactory;
    
        /**
         * @var ValidatorInterface
         */
        private $validator;
    
        /**
         * @InjectParams
         *
         * @param GradePointAverageRepository $gradePointAverageRepository
         * @param GradePointAveragePaginationFactoryInterface $gradePointAveragePaginationFactory
         * @param ValidatorInterface $validator
         */
        public function __construct(
            GradePointAverageRepository $gradePointAverageRepository,
            GradePointAveragePaginationFactoryInterface $gradePointAveragePaginationFactory,
            ValidatorInterface $validator
        )
        {
            $this->gradePointAverageRepository = $gradePointAverageRepository;
            $this->gradePointAveragePaginationFactory = $gradePointAveragePaginationFactory;
            $this->validator = $validator;
        }
    
        /**
         * @PreAuthorize("isAllowedToManageTheGradePointAverage(#gradePointAverage)")
         * @param GradePointAverage $gradePointAverage
         * @throws InvalidArgumentException
         */
        public function save(GradePointAverage $gradePointAverage)
        {
            $violationList = $this->validator->validate($gradePointAverage);
            if ($violationList->count()) {
                throw new InvalidArgumentException;
            }
    
            $this->gradePointAverageRepository->save($gradePointAverage);
        }
    }
    

    控制器/ GradePointAverageController.php

    namespace MyApp\Controller;
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\HttpKernel\Log\LoggerInterface;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
    use Doctrine\Common\Collections\ArrayCollection;
    use FOS\RestBundle\View\View;
    use JMS\DiExtraBundle\Annotation\Service;
    use JMS\DiExtraBundle\Annotation\InjectParams;
    use JMS\SecurityExtraBundle\Annotation\PreAuthorize;
    use Knp\Component\Pager\Pagination\PaginationInterface;
    use MyApp\Manager\GradePointAverageManager;
    use MyApp\Model\GradePointAverage;
    use MyApp\Model\GradePointAverage\AssessmentValue;
    
    /**
     * @Service("grade_point_average_controller", parent="app.controller.abstract")
     * @Route("/gpa", service="grade_point_average_controller")
     */
    class GradePointAverageController extends BaseController
    {
        /**
         * @var GradePointAverageManager
         */
        private $gradePointAverageManager;
    
        private $logger;
    
        /**
         * @InjectParams
         *
         * @param GradePointAverageManager $gradePointAverageManager
         * @param LoggerInterface $logger
         */
        public function __construct(GradePointAverageManager $gradePointAverageManager, LoggerInterface $logger)
        {
            $this->gradePointAverageManager = $gradePointAverageManager;
            $this->logger = $logger;
        }
    
        // ...
    
        /**
         * @Route("/{id}", name="gpa.edit", requirements={"id" = "\d+"})
         * @Method("PUT")
         *
         * @param Request $request
         * @param GradePointAverage $gpa
         * @return View
         */
        public function editAction(Request $request, GradePointAverage $gpa)
        {
            $form = $this->formFactory->createNamed(null, 'gpa', $gpa, [
                'method' => 'PUT',
            ]);
            $form->handleRequest($request);
    
            foreach ($gpa->getAssessmentValues() as $av) {
                $this->logger->info('GPA ID PREVALIDATE IN CONTROLLER:'.$gpa->getId());
                $this->logger->info('PREVALIDATE IN CONTROLLER ASSESSMENT VAL ASSESSMENT ID:'.$av->getAssessment()->getId());
                $this->logger->info('PREVALIDATE IN CONTROLLER ASSESSMENT VAL POINTS:'.$av->getValue());
            }
    
            /*
            // try reversing the order of the collection to see if that helps
            $assessmentVals = $gpa->getAssessmentValues()->toArray();
            $reversed = array_reverse($assessmentVals);
            $reversedColl = new ArrayCollection($reversed);
            $gpa->setAssessmentValues($reversedColl);
            */
    
            if ($form->isValid()) {
                foreach ($gpa->getAssessmentValues() as $av) {
                    $this->logger->info('GPA ID PRESAVE IN CONTROLLER:'.$gpa->getId());
                    $this->logger->info('PRESAVE IN CONTROLLER ASSESSMENT VAL ASSESSMENT ID:'.$av->getAssessment()->getId());
                    $this->logger->info('PRESAVE IN CONTROLLER ASSESSMENT VAL POINTS:'.$av->getValue());
                }
                $this->gradePointAverageManager->save($gpa);
    
                return new View($gpa, 204);
            }
    
            return new View($form);
        }
    
        // ...
    }
    

1 个答案:

答案 0 :(得分:1)

尝试在表格gpa_assessment_value中添加ID,然后使用命令行生成实体。如果不添加id字段,则从数据库映射orm文件将不会创建gpa_assessment_value.orm.yml。

CREATE TABLE gpa_assessment_value
(
    id                     bigserial       NOT NULL,
    grade_point_average_id bigint        NOT NULL,
    assessment_id          bigint        NOT NULL,
    point_value            numeric(4, 2) NOT NULL,

    PRIMARY KEY (id,assessment_id, grade_point_average_id),
    FOREIGN KEY (assessment_id) REFERENCES assessment,
    FOREIGN KEY (grade_point_average_id) REFERENCES grade_point_average
);