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

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.

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();

Fastest method to save pageviews in database with doctrine

Using Symfony 4 with doctrine, I want to save pageviews for one entity Program in the database. I want to save this to the database because I want to give some users the rights to view these numbers. What I have done is add a property to the entity like this:
Program.php
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $pageViews;
/**
* #return mixed
*/
public function getPageViews()
{
return $this->pageViews;
}
/**
* #param mixed $pageViews
*/
public function setPageViews($pageViews)
{
$this->pageViews = $pageViews;
}
And in my ProgramController.php in the function showProgram
//...
$program->setPageViews($program->getPageViews()+1);
$em->persist($program);
$em->flush();
This works and adds 1 to the existing number every time the page is refreshed. My question is, is this an acceptable method or are there faster/better alternatives? And does this slow down performance or is that negligible?
Since you don't really need the entity you could directly do this with SQL, using Doctrine's connection:
$connection = $this->getDoctrine()->getConnection();
$connection->executeUpdate('UPDATE page_view_counter SET page_view = page_view+1;');
or using a preprared statement:
$connection = $this->getDoctrine()->getConnection();
$statement = $connection->prepare(
'UPDATE programs SET page_views = page_views + 1 WHERE programs.id = :id'
);
$statement->bindValue('id', $id);
$statement->execute();
This would speed up things a bit by not using some of the more complex features of the ORM that you don't need for your case.
Another alternative for speeding things up could be to switch technologies, like storing the data in a cache like redis. Whether this will actually improve performance (especially under a heavier load) would need to be verified using some measuring tool like JMeter.

Doctrine Results Missing Entity

I've been trying to figure this out for a while now. Let's start with the basic information, I have a client table and a contact table. The client table has a OneToMany and OneToOne relation with contact
class Client
{
/**
* #var int
* #Id
* #Column(type="integer", nullable=false, unique=true, options={"comment":"Auto incrementing client_id of each client"})
* #GeneratedValue
*/
protected $pid;
/**
* #OneToMany(targetEntity="Contact", mappedBy="client")
* #JoinColumn(name="contact_id", referencedColumnName="pid")
* #var Contact[]
*/
protected $contact;
/**
* #OneToOne(targetEntity="Contact")
* #JoinColumn(name="defaultcontact_id", referencedColumnName="pid", nullable=true)
* #var Contact
*/
protected $default_contact;
The contact table has a ManyToOne relation with Client:
class Contact
{
/**
* #var int
* #Id
* #Column(type="integer", nullable=false, unique=true, options={"comment":"Auto incrementing user_id of each user"})
* #GeneratedValue
*/
protected $pid;
/**
* #ManyToOne(targetEntity="Client", inversedBy="contact")
* #JoinColumn(name="client_id", referencedColumnName="pid")
*/
protected $client;
Here's the query that I've been using:
$qb = $entityManager->createQueryBuilder();
$qb->select("cn as contact", "cl as client")
->from('DB\Contact', 'cn')
->innerJoin('cn.client', 'cl')
->where(
$qb->expr()->andX(
$qb->expr()->eq('cl.client_name', '?1'),
$qb->expr()->eq('cn.pid', '?2')
)
)
->setParameter(1, $client)
->setParameter(2, $contact);
try
{
$result = $qb->getQuery()->getOneOrNullResult();
}
I want both the contact and the client. And this is where I'm having problems: array_keys($result) ends up outputting:
Array
(
[0] => contact
)
I wanted something like this:
[0] => contact
[1] => client
In other words, the Client Entity is missing. Flipping the SELECT FROM from the Contact to the Client repository yielded the reverse situation, contact was missing.
I've checked over the previous code, while entityManager was reused from the login step, this is the first time the Client and Contact repository are accessed so I don't believe it's a caching problem.
Here's the SQL statement being executed:
Executing SQL:
SELECT c0_.pid AS pid0, c0_.caller AS caller1, c0_.address_1 AS address_12, c0_.address_2 AS address_23,
c0_.unit AS unit4, c0_.city AS city5, c0_.state AS state6, c0_.zip_code AS zip_code7, c0_.phone AS phone8,
c0_.email AS email9, c0_.is_active AS is_active10, c0_.date_created AS date_created11,
c0_.date_last_modified AS date_last_modified12, c1_.pid AS pid13, c1_.client_name AS client_name14,
c1_.is_active AS is_active15, c1_.date_created AS date_created16, c1_.date_last_modified AS date_last_modified17,
c0_.client_id AS client_id18, c0_.created_by_id AS created_by_id19, c0_.last_modified_by_id AS last_modified_by_id20,
c1_.defaultcontact_id AS defaultcontact_id21, c1_.created_by_id AS created_by_id22,
c1_.last_modified_by_id AS last_modified_by_id23
FROM contacts c0_
INNER JOIN clients c1_ ON c0_.client_id = c1_.pid
WHERE c1_.client_name= ? AND c0_.pid = ?
As a sidenote, if I alter the select so that the missing entity accesses a specific column, I'll get the desired values.
e.g.
$qb->select("cn as contact", "cl.pid as client")
->from('RGAServ\DB\Contact', 'cn')
will have the following array_keys($result):
Array
(
[0] => contact
[1] => client
)
So I can assure you that the client does exist in the database and it should be properly attached to the contact, it's just that under the first select statement where I want the whole entity and not just one column, the entity ends up not being pushed into the result array.
Why is this? Are there too many columns in the Sql statement? Am I forgetting something in the annotations?
First: You can't have different entities in the resulting array: every row in the result must be in the same format.
Second: If you examine your SQL query closely you'll notice that the one row that is returned contains both the contact (c0_) and the client (c1_).
Try executing the SQL query in the database to see the result.
After looking through a bunch of stack overflow questions, I've come to the following conclusion: It looks like this is how doctrine handles "Fetch Joins".
The big clue comes from here:
Doctrine Regular vs Fetch join
With supporting evidence for this behavior's existence coming from:
Doctrine - entities not being fetched
Doctrine join bypass lazy loading
Doctrine2 query with select on multiple entities from different Symfony2 bundles
More specifically, this quote from the doctrine documentation starts to make sense (http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html#joins):
When Doctrine hydrates a query with fetch-join it returns the class in
the FROM clause on the root level of the result array. In the previous
example an array of User instances is returned and the address of each
user is fetched and hydrated into the User#address variable. If you
access the address Doctrine does not need to lazy load the association
with another query.
In layman's terms, during a FETCH JOIN, contact (or client if I'm SELECTing FROM the client repository) is designated as the root entity. Whatever is found for Client will then be pushed into the contact's $client variable to be retrieved afterwards using a getter accessor.
The retrieval itself will not need a follow-up database query to fetch the client entity. I'll need to do a little testing, but it looks like this behavior was for the situation when multiple results are returned during the join. Instead of cluttering up the results, they're organized under an intuitive location.
In other words, I had the wrong expectations and was looking in the wrong spot. The client entity did indeed come back, but it wasn't placed in results. It was filed under contact. Retrieving it separately is, therefore, a given but at least it won't need another database call.
At least now, I believe I know why when I had Client in the from field, I was getting one specific contact instead of all of them when I tried to use the getContact() accessor.

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

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.

Doctrine2.0: Error - Column specified twice

In Doctrine2.0.6, I keep getting an error: "Column VoucherId specified twice".
The models in question are:
Basket
BasketVoucher
Voucher
Basket links to BasketVoucher.
Voucher links to BasketVoucher.
In Voucher and BasketVoucher, there is a field called VoucherId. This is defined in both models and exists with the same name in both DB tables.
The error occurs when saving a new BasketVoucher record:
$basketVoucher = new BasketVoucher;
$basketVoucher->setVoucherId($voucherId);
$basketVoucher->setBasketId($this->getBasket()->getBasketId());
$basketVoucher->setCreatedDate(new DateTime("now"));
$em->persist($basketVoucher);
$em->flush();
I've checked the models and VoucherId is not defined twice. However, it is used in a mapping. Is this why Doctrine thinks that the field is duplicated?
Here's the relevant code - I haven't pasted the models in their entirety as most of the code is get/set.
Basket
/**
* #OneToMany(targetEntity="BasketVoucher", mappedBy="basket")
* #JoinColumn(name="basketId", referencedColumnName="BasketId")
*/
private $basketVouchers;
public function getVouchers()
{
return $this->basketVouchers;
}
BasketVoucher
/**
* #ManyToOne(targetEntity="Basket", inversedBy="basketVouchers")
* #JoinColumn(name="basketId", referencedColumnName="BasketId")
*/
private $basket;
public function getBasket()
{
return $this->basket;
}
/**
* #OneToOne(targetEntity="Voucher", mappedBy="basketVoucher")
* #JoinColumn(name="voucherId", referencedColumnName="VoucherId")
*/
private $voucherEntity;
public function getVoucher()
{
return $this->voucherEntity;
}
Voucher
/**
* #OneToOne(targetEntity="BasketVoucher", inversedBy="voucherEntity")
* #JoinColumn(name="voucherId", referencedColumnName="VoucherId")
*/
private $basketVoucher;
public function getBasketVoucher()
{
return $this->basketVoucher;
}
Any ideas?
EDIT: I've found that the same issue occurs with another model when I save it for the first time. I am setting the primary key manually. The main issue appears to be saving a relationship within an entity.
In this case, I have a field - DraftOrderId - which is used as the primary key on three models. The first model - DraftOrder - has DraftOrderId as a primary key, which is an auto incrementing value. The other two models - DraftOrderDeliveryAddress, and DraftOrderBillingAddress - also use DraftOrderId as a primary key, but it isn't auto incremented.
What's happening is one of the following issues:
If I save the delivery address entity with a draft order id and set it to persist, I get an error: Column DraftOrderId specified twice. Code:
try {
$addressEntity->getDraftOrderId();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
if ($addressType == "delivery") {
$addressEntity = new Dpp\DraftOrderDeliveryAddress;
} elseif ($addressType == "billing") {
$addressEntity = new Dpp\DraftOrderBillingAddress;
}
$addressEntity->setDraftOrderId($draftOrder->getDraftOrderId());
$em->persist($addressEntity);
}
(It would also help to know if there's a better way of checking if a related entity exists, rather than trapping the exception when trying to get a value.)
If I remove the line that sets the draft order id, I get an error: Entity of type Dpp\DraftOrderDeliveryAddress is missing an assigned ID.
If I keep the line that sets the draft order id but I remove the persist line, and I also keep the lines later on in the code that sets the name and address fields, I don't get an error - but the data is not saved to the database. I am using flush() after setting all the fields - I'm just not using persist(). In the previous examples, I do use persist() - I'm just trying things out to see how this can work.
I can paste more code if it would help.
I think I've fixed it! A couple of findings:
For a primary key that is not an auto-incrementing value, you need to use:
#generatedValue(strategy="IDENTITY")
You also have to explicitly set the mapped entities when creating them for the first time. At first, I was trying to create the address entity directly, but I wasn't setting the mapped entity within the parent model to reference the address entity. (if that makes any sense)
I'm fairly sure it was mostly due to the lack of the IDENTITY keyword, which for some reason was either saying the key wasn't set, or saying it was set twice.

Categories