Моя цель — выполнить обновление управляемого объекта, используя данные из объекта того же класса, но не управляемого Doctrine.
Это было бы здорово, если бы можно было выполнить «простое обновление» при замене атрибутов, но на самом деле, если я уберу ArrayCollection
старые данные, кажется, не удаляются (даже если я очищаю все ссылки на скрипке от элементов ArrayCollection
или если orphanRemoval
установлено в true).
Но давайте введем конкретный пример. я имею эта сущность с множеством отношений OneToOne / OneToMany для представления играть на скрипке. Я могу импортировать образцы скрипки (ранее экспортированные как json из другой среды) с помощью команды Symfony2.
Если образец уже существует, как я могу его правильно обновить?
Я строю свою сущность, используя следующий код (сокращенно):
$fiddle = new Fiddle();
$fiddle->setHash($this->get($json, 'hash'));
$fiddle->setRevision($this->get($json, 'revision'));
$context = $fiddle->getContext();
$context->setFormat($this->get($json, 'context', 'format'));
$context->setContent($this->get($json, 'context', 'content'));
$fiddle->clearTemplates();
$jsonTemplates = $this->get($json, 'templates') ? : array ();
foreach ($jsonTemplates as $jsonTemplate)
{
$template = new FiddleTemplate();
$template->setFilename($this->get($jsonTemplate, 'filename'));
$template->setContent($this->get($jsonTemplate, 'content'));
$template->setIsMain($this->get($jsonTemplate, 'is-main'));
$fiddle->addTemplate($template);
}
// ...
Теперь я могу сохранить свою сущность после удаления, если она уже существует:
$check = $this
->getContainer()
->get('doctrine')
->getRepository('FuzAppBundle:Fiddle')
->getFiddle($fiddle->getHash(), $fiddle->getRevision());
if (!is_null($check->getId()))
{
$em->remove($check);
$em->flush();
}
$em->persist($fiddle);
$em->flush();
Но это создаст DELETE + INSERT вместо UPDATE, если образец уже существует. Это странно, потому что пользователи могут ставить закладки на скрипты, а связь устанавливается по id.
Сначала я получаю свою скрипку, и если она уже существует, я очищаю ее и заполняю ее новыми данными … Код работает хорошо, но действительно ужасно, Вы можете проверить это Вот.
Как образец, проверьте tags
свойство: поскольку теги могли быть удалены / изменены, я должен правильно установить новые теги, заменив старые на новые.
// remove the old tags
foreach ($fiddle->getTags() as $tag)
{
if (\Doctrine\ORM\UnitOfWork::STATE_MANAGED === $em->getUnitOfWork()->getEntityState($tag))
{
$em->remove($tag);
$em->flush();
}
}
// set the new tags
$tags = new ArrayCollection();
$jsonTags = $this->getFromArray($json, 'tags');
foreach ($jsonTags as $jsonTag)
{
$tag = new FiddleTag();
$tag->setTag($jsonTag);
$tags->add($tag);
}
$fiddle->setTags($tags);
Поскольку на теги ссылаются с помощью идентификатора fiddle, я могу использовать ->remove
даже если это безобразно Это нормально, но если идентификаторы были сгенерированы автоматически, то должны быть лучшие решения.
Я также попытался просто установить идентификатор старой скрипки на новый и объединить, но это привело к следующему исключению:
[Symfony\Component\Debug\Exception\ContextErrorException]
Notice: Undefined index: 00000000125168f2000000014b64e87f
Больше, чем простая «функция импорта», я хочу использовать этот стиль обновления для привязки форм к неуправляемым объектам и обновления существующих объектов только при необходимости. Поэтому моя цель — сделать что-то общее, применимое ко всем видам сущностей.
Но я, конечно, не ожидаю весь код. Хорошая практика для борьбы с ArrayCollection
обновлений и некоторых подсказок / предупреждений о том, что я должен рассмотреть, прежде чем кодировать эту функцию, должно быть достаточно.
Обновлять существующие объекты только при необходимости.
Это может быть достигнуто довольно просто с Doctrine:
То, что вы ищете, это политика отслеживания изменений Отложенный Явный.
Доктрина по умолчанию будет использовать Политику отслеживания изменений Отложенный неявный. Это означает, что когда вы звоните $em->flush()
Учение пройдет все его управляемых объектов для расчета наборов изменений. Тогда все изменения сохраняются.
При использовании Политики отслеживания изменений Отложенный Явный и позвонить $em->flush()
Учение будет только просмотрите сущности, которые вы явно назвали $em->persist()
на. Другими словами: вы можете иметь тысячи управляемых объектов, называемых $em->persist()
на 2 из них, и Doctrine рассчитает только наборы изменений этих 2 (и сохранит изменения при необходимости).
Политика отслеживания изменений может быть установлена на уровне класса объекта. Так что, если вы хотите использовать определенный класс сущностей Отложенный Явный, просто добавьте аннотацию к блоку doc класса:
/**
* @Entity
* @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
*/
class Fiddle
{
Тогда это просто вопрос вызова $em->persist($fiddle)
когда тебе действительно нужно.
Вероятно, целесообразно установить одинаковую политику отслеживания изменений для всего агрегата (корневого объекта и всех его дочерних элементов).
PS: есть также третья политика отслеживания изменений, названная Поставить в известность, это немного больше работы для настройки, но дает вам еще более детальный контроль над тем, что сохраняется при вызове $em->flush()
, Но я не думаю, что вам нужно заходить так далеко.
Видя код, который вы используете для обновления сущности Fiddle, я думаю, что вы можете кое-что улучшить.
Сначала перенесите ответственность за управление ассоциациями обратно на предприятие:
/**
* @Entity
* @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
*/
class Fiddle
{
// ...
/**
* @return FiddleTag[]
*/
public function getTags()
{
return $this->tags->toArray();
}
/**
* @param FiddleTag $tag
*/
public function addTag(FiddleTag $tag)
{
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
$tag->setFiddle($this);
}
}
/**
* @param FiddleTag $tag
*/
public function removeTag(FiddleTag $tag)
{
if ($this->tags->contains($tag)) {
$this->tags->removeElement($tag);
$tag->setFiddle(null);
}
}
/**
* @param FiddleTag[] $newTags
*/
public function replaceTags(array $newTags)
{
$currentTags = $this->getTags();
// remove tags that are not in the new list of tags
foreach ($currentTags as $currentTag) {
if (!in_array($currentTag, $newTags, true)) {
$this->removeTag($currentTag);
}
}
// add tags that are not in the current list of tags
foreach ($newTags as $newTag) {
if (!in_array($newTag, $currentTags, true)) {
$this->addTag($newTag);
}
}
}
// ...
}
Теперь код в вашем ImportCommand может выглядеть примерно так:
$jsonTags = $this->getFromArray($json, 'tags');
$newTags = [];
foreach ($jsonTags as $jsonTag) {
$tag = $tagRepo->findOneByTag($jsonTag);
if ($tag === null) {
$tag = new FiddleTag();
$tag->setTag($jsonTag);
}
$newTags[] = $tag;
}
$fiddle->replaceTags($newTags);
Затем, когда все в порядке и может быть сохранено, выполните:
$em->persist($fiddle);
foreach ($fiddle->getTags() as $tag) {
$em->persist($tag);
}
$em->flush();
Когда вы настроили cascade=persist
в ассоциации вы должны быть в состоянии пропустить цикл, который вручную сохраняет теги.
Вы могли бы взглянуть на Сериализатор JMS библиотека, а сверток это интегрирует его в Symfony.
Других решений пока нет …