Symfony2 表单:如何持久保存具有可为空关联的实体?
保存表单提交数据时,我在持久保存新实体实例时遇到问题,其中该实体与另一个实体具有可为空的关联,并且我尝试将其设置为空。为表单创建新的实体实例,将提交的请求绑定到表单并持久化和刷新实体实例,具体取决于我填充关联实体的属性的方式,我要么得到
UnexpectedTypeException: Expected argument of type "对象或数组”、“NULL”给定
(如果设置为 null)或InvalidArgumentException:通过关系“AccessLog#document”找到了一个新实体,该实体未配置为级联持久操作实体
(如果设置为相关实体的新的空实例,我不想保留该实例)。
如果我设置级联持久化,它会尝试在相关表中创建一条记录(数据库中的数据模型不允许这样做),即使没有数据可供持久化。如果设置级联持久是可行的方法,那么如何防止它尝试创建新记录?处理这个问题的最佳方法是什么?
请注意,无论关联设置为单向还是双向,行为都是相同的。
详细信息:
我有一个与另一个实体(缩写)具有多对一关联的实体:
/** @Entity */
class AccessLog
{
/** @Id @Column(type="integer") */
private $access_log_id;
/** @Column(type="integer", nullable=true) */
private $document_id;
/**
* @ManyToOne(targetEntity="Document", inversedBy="access_logs", cascade={"persist"})
* @JoinColumn(name="document_id", referencedColumnName="document_id")
*/
private $document;
// plus other fields
// plus getters and setters for all of the above...
}
相关实体没什么奇特的:
/** @Entity */
class Document
{
/** @Id @Column(type="integer") */
private $document_id;
/** @Column(length=255) */
private $name;
/** @OneToMany(targetEntity="AccessLog", mappedBy="document") */
private $access_logs;
// plus other fields
// plus getters and setters for all of the above...
}
我有一个 Symfony 表单,用于为新的 AccessLog 记录输入数据:
class AccessLogFormType extends AbstractType
{
public function getName()
{
return 'access_log_form';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'AccessLog');
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('access_log_id', 'hidden');
$builder->add('document_id', 'hidden', array(
'required' => false
));
$builder->add('document', new DocumentType(), array(
'label' => 'Document',
'required' => false
));
//...
}
}
使用以下支持类型定义:
class DocumentType extends AbstractType
{
public function getName()
{
return 'document';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Document');
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name', 'text', array(
'required' => false
));
}
}
我的控制器包括以下内容:
public function save_access_log_action()
{
$request = $this->get('request');
$em = $this->get('doctrine.orm')->getEntityManager();
$access_log = null;
if ($request->getMethod() === 'POST') {
$data = $request->request->get('access_log_form');
if (is_numeric($data['access_log_id'])) {
$access_log = $em->find('AccessLog', $data['access_log_id']);
} else {
$access_log = new AccessLog();
}
if (is_numeric($data['document_id'])) {
$document = $em->find('Document', $data['document_id']);
$access_log->set_document($document);
} else {
// Not calling set_document() since there shouldn't be a
// related Document entity.
}
$form = $this->get('form.factory')
->createBuilder(new AccessLogFormType(), $access_log)
->getForm();
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($access_log);
$em->flush();
}
} else {
// ... (handle get request)
}
return $this->render('access_log_form.tpl', array(
'form' => $form->createView()
));
}
当更新现有访问日志条目或创建新条目并且在表单中选择文档时,上述代码可以正常工作,但如果未选择任何文档,则无法正常工作。
假设数据模型无法更改,如何在不保留新文档实体的情况下保留新的 AccessLog 实体?
When saving form submission data, I'm having trouble persisting a new entity instance where the entity has a nullable association with another entity and I attempt to set it to null. After creating a new entity instance for the form, binding the submitted request to the form and persisting and flushing the entity instance, depending on how I populate the property for the associated entity, I either get
UnexpectedTypeException: Expected argument of type "object or array", "NULL" given
(if set to null), orInvalidArgumentException: A new entity was found through the relationship 'AccessLog#document' that was not configured to cascade persist operations for entity
(if set to a new, empty instance of the related entity, which I don't want to persist).
If I set up cascade persist, it tries to create a record in the related table (which is not allowed by the data model in the db), even though there's no data for it to persist. If setting up cascade persist is the way to go, how do I prevent it from trying to create a new record? What's the best way to handle this?
Note, the behavior is the same whether the association is set up as unidirectional or bidirectional.
Details:
I have an entity with a many-to-one association with another entity (abbreviated):
/** @Entity */
class AccessLog
{
/** @Id @Column(type="integer") */
private $access_log_id;
/** @Column(type="integer", nullable=true) */
private $document_id;
/**
* @ManyToOne(targetEntity="Document", inversedBy="access_logs", cascade={"persist"})
* @JoinColumn(name="document_id", referencedColumnName="document_id")
*/
private $document;
// plus other fields
// plus getters and setters for all of the above...
}
The related entity is nothing fancy:
/** @Entity */
class Document
{
/** @Id @Column(type="integer") */
private $document_id;
/** @Column(length=255) */
private $name;
/** @OneToMany(targetEntity="AccessLog", mappedBy="document") */
private $access_logs;
// plus other fields
// plus getters and setters for all of the above...
}
I have a Symfony form for entering data for new AccessLog records:
class AccessLogFormType extends AbstractType
{
public function getName()
{
return 'access_log_form';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'AccessLog');
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('access_log_id', 'hidden');
$builder->add('document_id', 'hidden', array(
'required' => false
));
$builder->add('document', new DocumentType(), array(
'label' => 'Document',
'required' => false
));
//...
}
}
With the following supporting type definition:
class DocumentType extends AbstractType
{
public function getName()
{
return 'document';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Document');
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name', 'text', array(
'required' => false
));
}
}
My controller includes the following:
public function save_access_log_action()
{
$request = $this->get('request');
$em = $this->get('doctrine.orm')->getEntityManager();
$access_log = null;
if ($request->getMethod() === 'POST') {
$data = $request->request->get('access_log_form');
if (is_numeric($data['access_log_id'])) {
$access_log = $em->find('AccessLog', $data['access_log_id']);
} else {
$access_log = new AccessLog();
}
if (is_numeric($data['document_id'])) {
$document = $em->find('Document', $data['document_id']);
$access_log->set_document($document);
} else {
// Not calling set_document() since there shouldn't be a
// related Document entity.
}
$form = $this->get('form.factory')
->createBuilder(new AccessLogFormType(), $access_log)
->getForm();
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($access_log);
$em->flush();
}
} else {
// ... (handle get request)
}
return $this->render('access_log_form.tpl', array(
'form' => $form->createView()
));
}
The above code works fine when updating an existing access log entry or creating a new one and a document is selected in the form, but not if no document is selected.
Assuming the data model cannot be changed, how do I go about persisting a new AccessLog entity without persisting a new Document entity?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
解决方案似乎是将 set_document(null) 调用移到持久操作之前,因为将请求绑定到表单会导致将空 Document 对象附加到 AccessLog 对象的 $document 属性。
该解决方案在删除级联持续并使关联成为单向后也有效。
感谢@greg0ire 的帮助。
[更新] 当文档实体定义继续尝试更新与 AccessLog 记录关联的文档记录时,例如在删除现有的记录时,还需要将 @ChangeTrackingPolicy("DEFERRED_EXPLICIT") 添加到文档实体定义中。关联(这导致文档上的 $name 属性设置为 null,然后在数据库中设置为 null)。确实令人沮丧的是,即使未指定级联持久,默认行为也是在删除关联时擦除/修改数据。
It appears that the solution was to move the set_document(null) call to right before the persist operation, since binding the request to the form caused an empty Document object to be attached to the $document property of the AccessLog object.
This solution also works after removing the cascade persist and making the association unidirectional.
Thanks to @greg0ire for his help.
[update] It also became necessary to add
@ChangeTrackingPolicy("DEFERRED_EXPLICIT")
to the Document entity definition as it continued to try to update the Document record associated with the AccessLog record, for example when removing an existing association (which caused the $name property to be set to null on the Document and then in the db). It's really frustrating that the default behavior is to erase/modify data when an association is removed, even when cascade persist is not specified.我认为您的问题可能是您在 AccessLog 中定义文档设置器的方式。
如果你这样做:
你将永远无法
setDocument(null)
,因为 null 不是 Document 的实例。为此,I think your problem could be the way you defined your document setter in
AccessLog
.if you do it like this:
you will never be able to
setDocument(null)
since null is not an instance of Document. To do so,这已在 Symfony2 master 中修复。空表单现在不再产生空对象,而是返回 null。
This was fixed in Symfony2 master. Empty forms now don't result in empty objects anymore, but return null instead.
您是否阅读过 有关传递持久性/级联操作的 Doctrine2 文档?我认为如果文档存在,您首先需要
删除
它,如果不存在,那么您不应该调用setDocument()
。为了避免第二个错误,§ 解释了如何配置级联(请参阅最后一个代码片段)。Have you read the page of the Doctrine2 documentation about Transitive persistence / Cascade Operations? I think that if the document exists, you first need to
remove
it, and if it doesn't, then you should just not callsetDocument()
. To avoid the second error, the § explains how you can configure the cascade (see the last code snippet).在您的 AccessLogFormType 中,您应该更改
为:
这允许您在不设置文档的情况下保存实体。
In your AccessLogFormType you should change
to:
This allows you to save your entity without setting document.