概述
在具有由2个外键和一个元数据字段组成的复合主键的联接表中持久化实体集合在某些情况下会失败。代码基于这里的说明:Doctrine文档
问题详情
SUCCESS: 当要持久化的集合中各项的 FOREIGN KEY 1 相同时,且 FOREIGN KEY 2 大于任何现有 PRIMARY KEY 中的 FOREIGN KEY 2 时,实体和相关实体将被正确持久化:
示例:GPA “add val below” 已存在,具有评估值
{"assessment":6,"value":4}
。我们将尝试添加一个新的评估值,其中 assessment_id > GPA “add val below”的任何现有评估值请求有效载荷:
{"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" [] []
FAILURE: 当要持久化的集合中各项的 FOREIGN KEY 1 相同时,且 FOREIGN KEY 2 小于任何现有 FOREIGN KEY 2 时,工作单元尝试插入现有实体,而不是操作新实体。
示例:GPA “add val above” 已存在,具有评估值
{"assessment":8,"value":2}
。我们将尝试添加一个新的评估值,其中 assessment_id < GPA “add val above”的任何现有评估值请求有效载荷:
{"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
代码
migration.sql
创建评估表 ( id 序列大整数 NOT NULL, scale_id 大整数 NOT NULL, title 字符串 NOT NULL, passing 布尔值 NOT NULL, rank 整数,
主键 (id) );
创建评估等级表 ( id 序列大整数 NOT NULL, name 字符串 NOT NULL,
主键 (id) ); -- ...
创建平均绩点表 ( id 序列大整数 NOT NULL, name 字符串 NOT NULL, additional_credit_allowance 数字(4, 2),
主键 (id) );
-- ...
创建GPA评估值表 ( grade_point_average_id 大整数 NOT NULL, assessment_id 大整数 NOT NULL, point_value 数字(4, 2) NOT NULL,
主键 (assessment_id, grade_point_average_id), 外键 (assessment_id) REFERENCES assessment, 外键 (grade_point_average_id) REFERENCES grade_point_average );Model/GradePointAverage.php
命名空间为MyApp\Model。 引入了Doctrine ORM的Entity、Id、GeneratedValue和Column注释,以及Doctrine Common的Collection和ArrayCollection类,以及MyApp\Util\ConstructorArgs和MyApp\Model\GradePointAverage\AssessmentValue两个类。 定义了一个名为GradePointAverage的类,作为实体映射到数据库表中,并指定了其对应的仓库类。 使用了ConstructorArgs trait,该trait提供了一个处理构造函数参数的方法。 定义了一个私有属性$id,作为实体在数据库中的唯一标识符,并且使用了Id和GeneratedValue注释指定了其生成策略和类型为bigint。 定义了一个私有属性$assessmentValues,用于存储与此平均成绩相关的所有评估值,并使用了OneToMany注释表示其与AssessmentValue实体之间是一对多的关系。 定义了构造函数用于初始化对象属性,并将$assessmentValues属性初始化为空ArrayCollection。 定义了获取和设置$assessmentValues属性的方法,以及添加和删除单个评估值的方法。Model/GradePointAverage/AssessmentValue.php
命名空间为MyApp\Model\GradePointAverage; 使用了Doctrine ORM的实体(Entity)和表(Table)注解; 定义了三个属性:$gradePointAverage、$assessment和$value; 其中$gradePointAverage和$assessment属性都是Id,且分别指向MyApp\Model\GradePointAverage和MyApp\Model\Assessment模型; $value属性对应数据库中的point_value字段,类型为float; 定义了构造函数__construct()和一些getter和setter方法。
Model/Assessment.php
命名空间 MyApp\Model;
使用Doctrine\ORM\Mapping\Entity; 使用Doctrine\ORM\Mapping\Id; 使用Doctrine\ORM\Mapping\GeneratedValue; 使用Doctrine\ORM\Mapping\Column; 使用Doctrine\ORM\Mapping\ManyToOne; 使用MyApp\Model\Assessment\Scale; 使用MyApp\Util\ConstructorArgs;
/** * @Entity("MyApp\Repository\AssessmentRepository") */ 类 Assessment { 使用 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
命名空间 MyApp\Repository;
使用 Doctrine\ORM\EntityRepository; // ... 使用 MyApp\Model\GradePointAverage;
类 GradePointAverageRepository 扩展自 BaseRepository 并实现了 GradePointAverageRepositoryInterface 接口。
/** * @param GradePointAverage $gradePointAverage */ public function save(GradePointAverage $gradePointAverage) { $this->getEntityManager()->persist($gradePointAverage); $this->getEntityManager()->flush(); }Repository/GradePointAverage/AssessmentValueRepository.php
命名空间 MyApp\Repository\GradePointAverage;
使用 Doctrine\ORM\EntityRepository; 使用 MyApp\Model\GradePointAverage\AssessmentValue;
类 AssessmentValueRepository 扩展 EntityRepository { /** * @param AssessmentValue $assessmentValue */ public function save(AssessmentValue $assessmentValue) { $this->getEntityManager()->persist($assessmentValue); $this->getEntityManager()->flush(); } }Manager/GradePointAverageManager.php
命名空间 MyApp\Manager;
使用无效参数异常; 使用 Symfony\Component\Validator\ValidatorInterface; 使用 JMS\DiExtraBundle\Annotation\Service; 使用 JMS\DiExtraBundle\Annotation\InjectParams; 使用 JMS\SecurityExtraBundle\Annotation\PreAuthorize; 使用 Knp\Component\Pager\Pagination\PaginationInterface; 使用 MyApp\Repository\GradePointAverageRepository; 使用 MyApp\PaginationFactory\GradePointAveragePaginationFactoryInterface; 使用 MyApp\Model\GradePointAverage;
/** * @Service("grade_point_average_manager") */ 类 GradePointAverageManager { /** * @var GradePointAverageRepository */ private $gradePointAverageRepository;
/** * @var GradePointAveragePaginationFactoryInterface */ private $gradePointAveragePaginationFactory;
/** * @var ValidatorInterface */ private $validator;
/** * @InjectParams * * @param GradePointAverageRepository $gradePointAverageRepository * @param GradePointAveragePaginationFactoryInterface $gradePointAveragePaginationFactory * @param ValidatorInterface $validator */ 公共函数 __construct( GradePointAverageRepository $gradePointAverageRepository, GradePointAveragePaginationFactoryInterface $gradePointAveragePaginationFactory, ValidatorInterface $validator ) { $this->gradePointAverageRepository = $gradePointAverageRepository; $this->gradePointAveragePaginationFactory = $gradePointAveragePaginationFactory; $this->validator = $validator; }
/** * @PreAuthorize("isAllowedToManageTheGradePointAverage(#gradePointAverage)") * @param GradePointAverage $gradePointAverage * @throws InvalidArgumentException */ 公共函数 保存(GradePointAverage $gradePointAverage) { $violationList = $this->validator->validate($gradePointAverage); if ($violationList->count()) { 抛出一个无效参数异常; }
$this->gradePointAverageRepository->save($gradePointAverage); } }Controller/GradePointAverageController.php
命名空间 MyApp\Controller;
使用 Symfony\Component\HttpFoundation\Request; 使用 Symfony\Component\HttpFoundation\Response; 使用 Symfony\Component\HttpKernel\Log\LoggerInterface; 使用 Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 使用 Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 使用 Doctrine\Common\Collections\ArrayCollection; 使用 FOS\RestBundle\View\View; 使用 JMS\DiExtraBundle\Annotation\Service; 使用 JMS\DiExtraBundle\Annotation\InjectParams; 使用 JMS\SecurityExtraBundle\Annotation\PreAuthorize; 使用 Knp\Component\Pager\Pagination\PaginationInterface; 使用 MyApp\Manager\GradePointAverageManager; 使用 MyApp\Model\GradePointAverage; 使用 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:'。$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:'。$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); }
// ... }
GradePointAverage
和Assessment
之间映射一个ManyToMany
,同时忽略点值的元数据字段。 - dyelawnCREATE
命令,并对新实体发出UPDATE
命令,从而导致重复的主键。请参见:http://www.doctrine-project.org/jira/browse/DDC-2879 - dyelawn