Doctrine ORM: Сохраняющиеся коллекции с составными первичными ключами, состоящими из внешних ключей
Я предполагаю, что это ошибка 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);
}
// ...
}
Ответы
Ответ 1
Попробуйте добавить идентификатор в таблицу gpa_assessment_value, а затем используйте командную строку для генерации ваших объектов. Сопоставление файлов orm из базы данных не будет создавать gpa_assessment_value.orm.yml, если вы не добавите поле id.
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
);