Reputation: 420
I have a project based on Symfony 2.6. It has the following structure:
Customer->@OneToMany->Orders->@OneToMany->Domains->@OneToMany->SubDomains
same in reverse order:
SubDomains->@ManyToOne->Domains->@ManyToOne->Orders->@ManyToOne->Customer
Doctrine creates some additional pretty cool "magic" columns like customer_id
(in domains), order_id
(in domains) and domain_id
(in subdomains
).
It think it would be perfect, if all these *_id
cols are filled with the IDs of the parent table. But in my case, that will not match all the time.
Only for tables customer
and order
, the *_id
cols are filled properly.
My PHP code creates new Domain Entities. It also sets the ID, if the record exists in DB, and it adds all the subdomains
as ArrayCollection
.
Here is my storeOrder()
method:
protected function storeOrder(Order $order, array $domains) {
if ($order->getDomains()->count() === 0) {
// insert
foreach ($domains as $domain) {
$order->getDomains()->add($this->createDomainFromArray($domain));
}
} else {
// remove/add
$order->getDomains()->clear();
foreach ($domains as $domain) {
$order->getDomains()->add($this->createDomainFromArray($domain));
}
}
//$updatedOrder = $this->entityManager->merge($order);
// starts the commit process incl. transactions and prepared queries
$this->entityManager->persist($order);
$this->entityManager->flush();
// clear all cached entities in em. That speeds up processing enormous
$this->entityManager->clear();
$this->amountOfDomains += count($domains);
}
/**
* create a domain object from delivered data array
*
* @param array $data
* @return Domain
*/
protected function createDomainFromArray($data) {
$domain = new Domain();
$domain->setOrderNumber($this->orderNumber);
$domain->fromArray(ArrayUtility::removeSubEntriesFromArray($data));
$this->addIdToDomainIfFoundInDatabase($domain);
$this->addSubDomainsToDomain($domain, $data['subdomain']);
return $domain;
}
/**
* To prevent duplicate domains in database we have to set the id,
* if we have found such domain already in DB
* @param Domain $domain
*/
protected function addIdToDomainIfFoundInDatabase(Domain $domain) {
/** @var Domain|NULL $domainFromDatabase */
$domainFromDatabase = $this->findDomainBySeid($domain->getSeid());
if ($domainFromDatabase instanceof Domain) {
$domain->setId($domainFromDatabase->getId());
}
}
/**
* find domain by seid
*
* @param int $seid
* @return Domain|NULL
*/
protected function findDomainBySeid($seid) {
// do not use empty, because findOneBy can also return NULL as result
if (!isset($this->cache['domainFromDatabase']) || (is_object($this->cache['domainFromDatabase']) && $this->cache['domainFromDatabase']->getSeid() !== $seid)) {
$this->cache['domainFromDatabase'] = $this->domainRepository->findOneBy(array(
'seid' => $seid,
'orderNumber' => $this->orderNumber
));
}
return $this->cache['domainFromDatabase'];
}
/**
* add/override subdomains to domain object
*
* @param Domain $domain
* @param array $subDomains These are the subDomains from request
*/
protected function addSubDomainsToDomain(Domain $domain, array $subDomains) {
$first = TRUE;
$this->amountOfSubDomains += count($subDomains);
$domainFromDatabase = $this->findDomainBySeid($domain->getSeid());
if ($domainFromDatabase instanceof Domain) {
$subDomainsFromDatabase = $domainFromDatabase->getSubDomains();
} else {
$subDomainsFromDatabase = new ArrayCollection();
}
foreach ($subDomains as $subDomain) {
// create new SubDomain
$subDomainObject = new SubDomain();
$subDomainObject->setOrderNumber($this->orderNumber);
$subDomainObject->fromArray($subDomain);
// add ID to subDomain, if we have one already in DB
// It's not perfect, but as long as we can have fully equal records in subdomain table
// I don't see any chance to change that
if ($first) {
$subDomainFromDatabase = $subDomainsFromDatabase->first();
$first = FALSE;
} else {
$subDomainFromDatabase = $subDomainsFromDatabase->next();
}
if ($subDomainFromDatabase instanceof SubDomain) {
$subDomainObject->setId($subDomainFromDatabase->getId());
}
// add Order to Customer
$domain->addSubDomain($subDomainObject);
}
}
Here are the annotation from order entity:
/**
* Order -> domains
*
* @var ArrayCollection
* @ORM\OneToMany(targetEntity="FQCN\Domain", mappedBy="order", cascade={"all"})
*/
protected $domains;
Here are the annotations from subdomain entity:
/**
* relation from domain to order
*
* @ORM\ManyToOne(targetEntity="FQCN\Order", inversedBy="domains")
*/
protected $order = NULL;
/**
* domain -> subDomains
*
* @var ArrayCollection
* @ORM\OneToMany(targetEntity="FQCN\SubDomain", mappedBy="domain", cascade={"all"})
*/
protected $subDomains;
If I use only flush()
, the *_id
fields will not be filled. If I call detach($order)
, the fields will be filled properly, but now I have a second (duplicate) of $order
in my database. If I use merge()
everything works on INSERT, but not if I want to update all my ArrayCollections
.
What I'm doing wrong? How to UPDATE ArrayCollections
the right way without removing and creating them again? How to get the *_id
field filled?
Stefan
Upvotes: 1
Views: 1628
Reputation: 420
Thank you all. Here my solution:
I have added a new function to my domain model, to get the original subDomain object from database:
public function getSubDomain($seid) {
foreach ($this->subDomains as $subDomain) {
if ($subDomain->getSeid() === $seid) {
return $subDomain;
}
}
}
In my DomainSynchronization-Object I decide between updated or new record:
$domainFromDatabase = $order->getDomain((int)$domain['seid']);
if ($domainFromDatabase instanceof Domain) {
// update
$this->updateDomainFromArray($domainFromDatabase, $domain);
} else {
// insert
$order->getDomains()->add($this->createDomainFromArray($domain, $order));
}
And, as @redbirdo already mentioned, I add the domain object to subDomains manually now:
protected function addSubDomainsToDomain(Domain $domain, array $subDomains) {
$this->amountOfSubDomains += count($subDomains);
foreach ($subDomains as $subDomain) {
$subDomainObject = $domain->getSubDomain((int)$subDomain['seid']);
if ($subDomainObject instanceof SubDomain) {
// update
$subDomainObject->fromArray($subDomain);
} else {
// insert
$subDomainObject = new SubDomain();
$subDomainObject->setDomain($domain);
$subDomainObject->setOrderNumber($this->orderNumber);
$subDomainObject->fromArray($subDomain);
$domain->addSubDomain($subDomainObject);
}
}
}
Now I don't need a ->persist() or ->merge() anymore. It's enough to call:
$this->entityManager->flush();
Any I can see the parent UIDs now in subdomain table, too.
Thank you all
Upvotes: 1
Reputation: 4957
Firstly, I notice that when you add a domain to an order you are calling:
$order->getDomains()->add($this->createDomainFromArray($domain));
Your order class should have these methods, then you can use addDomain():
public function addDomain(Domain $domain)
{
$this->domains[] = $domain;
return $this;
}
public function removeDomain(Domain $domain)
{
$this->domains->removeElement($domain);
}
Adding those methods may be enough to fix your issue. However, I usually find it necessary to explicitly set both ends of a relationship, in this case between Order and Domain. I presume you aren't doing this since when you create a Domain you do not pass in the Order:
$order->getDomains()->add($this->createDomainFromArray($domain));
Try this instead:
$domain = $this->createDomainFromArray($domain);
$domain->setOrder($order);
$order->addDomain($domain);
You will need to do something similar with your SubDomains.
I can't see why you should need to use $em->detach() or $em->merge() although from the code in the question I don't know where the Order passed into storeOrder() is coming from.
Upvotes: 0