Doctrine 2 - How to use objects retrieved from cache in relationships - php

I'm working in a project that use Doctrine 2 in Symfony 2 and I use MEMCACHE to store doctrine's results.
I have a problem with objects that are retrieved from MEMCACHE.
I found this post similar, but this approach not resolves my problem: Doctrine detaching, caching, and merging
This is the scenario
/**
* This is in entity ContestRegistry
* #var contest
*
* #ORM\ManyToOne(targetEntity="Contest", inversedBy="usersRegistered")
* #ORM\JoinColumn(name="contest_id", referencedColumnName="id", onDelete="CASCADE"))
*
*/
protected $contest;
and in other entity
/**
* #var usersRegistered
*
* #ORM\OneToMany(targetEntity="ContestRegistry", mappedBy="contest")
*
*/
protected $usersRegistered;
Now imagine that Contest is in cache and I want to save a ContestRegistry entry.
So I retrieve the object contest in cache as follows:
$contest = $cacheDriver->fetch($key);
$contest = $this->getEntityManager()->merge($contest);
return $contest;
And as last operation I do:
$contestRegistry = new ContestRegistry();
$contestRegistry->setContest($contest);
$this->entityManager->persist($contestRegistry);
$this->entityManager->flush();
My problem is that doctrine saves the new entity correctly, but also it makes an update on the entity Contest and it updates the column updated. The real problem is that it makes an update query for every entry, I just want to add a reference to the entity.
How I can make it possible?
Any help would be appreciated.

Why
When an entity is merged back into the EntityManager, it will be marked as dirty. This means that when a flush is performed, the entity will be updated in the database. This seems reasonable to me, because when you make an entity managed, you actually want the EntityManager to manage it ;)
In your case you only need the entity for an association with another entity, so you don't really need it to be managed. I therefor suggest a different approach.
Use a reference
So don't merge $contest back into the EntityManager, but grab a reference to it:
$contest = $cacheDriver->fetch($key);
$contestRef = $em->getReference('Contest', $contest->getId());
$contestRegistry = new ContestRegistry();
$contestRegistry->setContest($contestRef);
$em->persist($contestRegistry);
$em->flush();
That reference will be a Proxy (unless it's already managed), and won't be loaded from the db at all (not even when flushing the EntityManager).
Result Cache
In stead of using you own caching mechanisms, you could use Doctrine's result cache. It caches the query results in order to prevent a trip to the database, but (if I'm not mistaken) still hydrates those results. This prevents a lot of issues that you can get with caching entities themselves.

What you want to achieve is called partial update.
You should use something like this instead
/**
* Partially updates an entity
*
* #param Object $entity The entity to update
* #param Request $request
*/
protected function partialUpdate($entity, $request)
{
$parameters = $request->request->all();
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($parameters as $key => $parameter) {
$accessor->setValue($entity, $key, $parameter);
}
}
Merge requires the whole entity to be 100% fullfilled with data.
I haven't checked the behavior with children (many to one, one to one, and so on) relations yet.
Partial update is usually used on PATCH (or PUT) on a Rest API.

Related

Symfony4 - How to update a doctrine ArrayCollection ?

I have a member of my entity is an arrayCollection. With a classic form builder is working fine, I can select multiple items and persist it. But when I try to update an object in controller I get the error : "Call to a member function setFaavailability() on array".
A resume of my entity :
/**
* #ORM\ManyToOne(targetEntity="App\Entity\FaAvailability",
inversedBy="faavailability")
* #ORM\JoinColumn(nullable=true)
* #ORM\Column(type="array")
*/
public $faavailability;
/**
* #return mixed
*/
public function getFaavailability()
{
return $this->faavailability;
}
/**
* #param mixed $faavailability
*/
public function setFaavailability($faavailability)
{
$this->faavailability = $faavailability;
}
In my controler :
$varFaavailability = $animal->faperson->getFaavailability();
foreach($varFaavailability as $availability){
if($availability->getName() == $animal->typepet->getName()){
$varFaavailability->removeElement($availability);
$faPerson = $em->getRepository(FaPerson::class) >findById($animal->faperson->getId());
$faPerson->setFaavailability($varFaavailability);
$em->persist($faPerson);
$em->flush();
}
}
Any ideas ?
If I remember well, when you set a field as an ArrayCollection it means that you have a oneToMany relationship between two entities.
From your code, I can tell you that you are trying to persist the data in the wrong entity. You usually add the owning_entity_id(1-to-N) in each item(1-to-N) and persist it. In your code, you are trying to set all the references at once, which is never going to happen. Delete the setFaavailability() or redefine the entities' relationships.
You should never try to mass-add foreign key relationships in one super duper setter function. Cycle through all the items and set the reference to the "parent" entity.
The problem is in this part: $faPerson = $em->getRepository(FaPerson::class)->findById($animal->faperson->getId());
The findBy* methods will try to find multiple entities and return them in a Collection.
If you're looking for a single person, you can use findOneById instead. Or (assuming id is configured as identifier in Doctrine) you can even use the find method: $faPerson = $em->getRepository(FaPerson::class)->find($animal->faperson->getId());
some general comments:
In Doctrine you never have to work with the IDs. Use the entity
objects! You only need to findById if you get the ID from a request parameter for example.
You should reconsider the naming of your variables to make it clear if it is a collection ($availabilities) or a single one ($availability).
Always use the getter/setter methods instead of the fields (typepet vs getTypepet()).
Call flush() one at the end to update all entities in one single transaction.
I've renamned the variables below as I understood them. However I am still not sure what $animal->faperson->getFaavailabilities() returns, since at the beginning you wanto to loop through the results and later set it to a single one via setFaavailability()?
//Should be a Doctrine ArrayCollection
$varFaavailabilities = $animal->faperson->getFaavailabilities();
foreach($varFaavailability as $availability){
if($availability->getName() == $animal->getTypepet()->getName()) {
//Why do you want to remove an element from the current loop?
$varFaavailability->removeElement($availability);
//No need to use Id
$faPerson = $animal->getFaperson();
//A single one?
$faPerson->setFaavailability($availability);
//More than one? addFaavailability should exist.
$faPerson->addFaavailability($availability);
$em->persist($faPerson);
}
}
$em->flush();

Doctrine 2 - Persist Entity with Joined Table's Foreign Key Opposed to Foreign Entity

I'm trying to persist a TradeEntity. A TradeEntity has a OneToOne relationship with a CurrencyEntity.
/**
* #ORM\OneToOne(targetEntity="Repositories\Currency\CurrencyEntity")
* #ORM\JoinColumn(name="currency", referencedColumnName="id")
*
* #var CurrencyEntity
*/
protected $currency;
I have recieved a CurrencyEntity from another object which I'm trying to insert in this new TradeEntity and persist it to the database but get an exception:
Type: Doctrine\ORM\ORMInvalidArgumentException
Message: Expected value of type "Repositories\Currency\CurrencyEntity"
for association field "Repositories\Trade\TradeEntity#$currency", got "integer" instead.
Is there no other way of me persisting TradeEntity without fetching the CurrencyEntity from the database and setting it that way?
In light of my recent discovery, i felt the need to update this answer.
Reading about Doctrine's advanced configuration, i came across Reference Proxies.
The method EntityManager#getReference($entityName, $identifier) lets you obtain a reference to an entity for which the identifier is known, without loading that entity from the database.
This is useful, for example, as a performance enhancement, when you want to establish an association to an entity for which you have the identifier. You could simply do this:
<?php
// $em instanceof EntityManager, $cart instanceof MyProject\Model\Cart
// $itemId comes from somewhere, probably a request parameter
$item = $em->getReference('MyProject\Model\Item', $itemId);
$cart->addItem($item);
Old answer:
What you can do is (it defeats the purpose of an ORM, but it is possible):
$conn = $entityManager->getConnection();
$conn->insert(
'table_name',
array(
'column_name' => 'column_value',
// define all the columns here
)
);
See Doctrine's Data Retrieval And Manipulation

Correct Symfony-doctrine inheritance mapping

I recently started using Symfony2-Doctrine2. I'm not getting how to save data in inheritance mapping.
My requirements:
For learning exercise:
I'm making a library application for testing (Requirements might not be practical).
At high level, library may contain many different type of items like books, articles, manuals as example for now.
They have some common fields like name, publish year etc and some item specific details like book has IDBN, publisher; Manual have company, product.
Again to make problem little more complex, there is another 'item_content' table to have some description in different language.
To quickly visualize, I've following structure:
I achieved above structure as per doctrine docs for inheritance mapping & Bidirectional one to many relation
My Question: How to save data using Symfony2 (I've proper routing/actions running, just need code to write in controller or better in repository). While saving data (say for manual) I want to save data in Item, Manual and ItemContect table but getting confused due to discr field in database. I didn't find code for saving data in above structure. I don't need full code, just few hints will be sufficient. My Item class is as follow (Other classes have proper inverse as mentioned in doctrine docs):
/**
* Article
*
* #ORM\Table(name="item")
* #ORM\Entity(repositoryClass="Test\LibraryBundle\Entity\ItemRepository")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"book" = "Book", "manual" = "Manual", "article" = "Article"})
*/
class Item
{
//...
/**
* For joining with ItemContent
*
* #ORM\OneToMany(targetEntity="ItemContent", mappedBy="item")
**/
private $itemContents;
public function __construct()
{
$this->itemContents = new ArrayCollection();
}
//...
}
The discriminator field will be automatically filled by Doctrine
$em = $this->getDoctrine()->getManager();
$item = new Manual(); // discr field = "manual"
$itemContent = new ItemContent();
$item->addItemContent($itemContent);
$itemContent->setItem($item);
$em->persist($item);
$em->persist($itemContent);
$em->flush();
Is that the answer you're waiting ?

How to use Merge on partly serialized document in Doctrine ODM?

Here is the use case. Some fields on the document are serializable/deserializable, others don't(see #JMS\ReadOnly).
/**
* #JMS\Groups({"board_list", "board_details"})
* #JMS\Type("string")
* #MongoDB\string
*/
protected $slug;
/**
* #JMS\Groups({"board_list", "board_details"})
* #JMS\ReadOnly
* #MongoDB\Increment
*/
protected $views;
When in the controller I do an action to update the document:
/**
* [PUT] /boards/{slug} "put_board"
* #ParamConverter("board", converter="fos_rest.request_body")
* #Rest\Put("/boards/{slug}")
* #Rest\View(statusCode=204)
*/
public function putBoardAction($slug, Board $board)
{
$dm = $this->get('doctrine_mongodb')->getManager();
$board = $dm->merge($board);
$dm->flush();
return true;
}
If the views field had some value before the action, after the action it gets reset to 0. How to avoid it? Is there a work-around Merge or Persist?
If the $views property is read-only, and not set upon deserialization, it will be 0 at the time the action is invoked. When merging, ODM is first going to try to look up the Board document by its identifier. When it finds it in the database, its $views property will be the current value stored in the database. That document now becomes the managed copy that merge() will ultimately return. From there, we proceed to copy values from the Board document passed to merge(). In doing so, $views is set to 0, over-writing whatever positive number it may have stored. When ODM goes to flush this change, it calculates the differences between the new and original values (likely the original view count multiplied by -1) and uses that for an $inc. That update brings the database value back to zero.
My advice would be to issue a separate update to increment $views, perhaps using the query builder. Even if $views was not read-only for the JMS Serializer service, you could still inadvertently decrement the counter if a Board with $views less than the corresponding database value was sent into the API.

Doctrine detaching, caching, and merging

I'm on Doctrine 2.3. I have the following query:
$em->createQuery('
SELECT u, c, p
FROM Entities\User u
LEFT JOIN u.company c
LEFT JOIN u.privilege p
WHERE u.id = :id
')->setParameter('id', $identity)
I then take that, get the result (which is an array, I just take the first element), and run detach $em->detach($result);.
When I go to fetch from the cache (using Doctrine's APC cache driver), I do:
$cacheDriver = new \Doctrine\Common\Cache\ApcCache();
if($cacheDriver->contains($cacheId))
{
$entity = $cacheDriver->fetch($cacheId);
$em->merge($entity);
return $entity;
}
My hope was that this would re-enable the relationship loading on the entity as there are many other relationships tied to the User object other than what's shown in that query.
I'm trying to create a new entity like such:
$newEntity = new Entities\ClientType();
$newEntity['param'] = $data;
$newEntitiy['company'] = $this->user['company'];
$em->persist($newEntity);
$em->flush();
When I do this, I get an error:
A new entity was found through the relationship 'Entities\ClientType#company' that was not configured to cascade persist operations for entity:
Entities\Company#000000005d7b49b500000000dd7ad743.
To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
If you cannot find out which entity causes the problem implement 'Entities\Company#__toString()' to get a clue.
This works just fine when I don't use the company entity under the user entity that I got from cache. Is there any way to make this work so I don't have to refetch the company entity from the database every time I want to use it in a relationship with a new entity?
Edit:
This is what I have in my User entity dealing with these two relationships:
/**
* #ManyToOne(targetEntity="Company" , inversedBy="user", cascade={"detach", "merge"})
*/
protected $company;
/**
* #ManyToOne(targetEntity="Privilege" , inversedBy="user", cascade={"detach", "merge"})
*/
protected $privilege;
I am still getting the same error.
Second edit:
Trying a $em->contains($this->user); and $em->contains($this->user['company']); both return false. Which sounds... wrong.
When merging a User, you want the associated Company and Privilege to be merged as well, correct?
This process is called cascading:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html#transitive-persistence-cascade-operations
In your User entity put cascade={"merge"} in the #ManyToOne annotation (or another type of association-definition you are using) for $company and $privilege.
And if you want the detach call to be cascaded too (which is recommended), put in cascade={"detach", "merge"}.
p.s.: Don't put such cascades on both sides of one association, you'll create an endless loop ;)
Edit:
This piece of code:
$entity = $cacheDriver->fetch($cacheId);
$em->merge($entity); // <-
return $entity;
should be:
$entity = $cacheDriver->fetch($cacheId);
$entity = $em->merge($entity); // <-
return $entity;
The thing with merge() is that it leaves the entity you pass as an argument untouched, and returns a new object that represents the managed version of the entity. So you want to use the return-value, not the argument you passed.

Categories