Я предполагаю, что это ошибка Doctrine (и я отправил сообщение об ошибке JIRA), но в случае, если это просто ошибка пользователя, я решил опубликовать ее здесь.
СИНТАКСИС
Сохранение коллекции объектов в таблице соединений с составной первичной Ключ, состоящий из 2 внешних ключей и одного поля метаданных, не выполняется в определенных ситуациях. Код был основан на инструкциях здесь: Doctrine docs
ДЕТАЛИ ВЫДАЧИ
-
УСПЕХ: Когда FOREIGN KEY 1 является одинаковым для элементов в коллекции, которая должна быть сохранена, и FOREIGN KEY 2 больше, чем FOREIGN KEY 2 в любом существующем PRIMARY KEY, субъект и связанные объекты в коллекции сохраняются правильно:
-
Пример: существует GPA "add val ниже" и имеет оценочное значение
{"assessment":6,"value":4}
Мы попытаемся добавить новое оценочное значение, где valu_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" [] []
-
-
НЕИСПРАВНОСТЬ: Когда FOREIGN KEY 1 одинаково для элементов в коллекции, а FOREIGN KEY 2 меньше, чем любой существующий FOREIGN KEY 2, блок работы пытается вставить INSERT существующий объект и не работает с новым объектом.
-
Пример: существует GPA "add val above" и имеет оценочное значение
{"assessment":8,"value":2}
Мы попытаемся добавить новое значение оценки, любой существующей значение оценки для 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"
-
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; } // ... }
Repository/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(); } }
Repository/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); } // ... }