Symfony2 Doctrine trying to insert record twice? Race condition? - symfony

We have a survey building application built in Symfony2 (think Survey Monkey or Google Forms). When a member of public accesses the survey, the various questions are built dynamically using Symfony's Form Builder and their answers are persisted using Doctrine. We currently collect an average of 1000 completed surveys a day without any trouble, but, every now and again, we get an Doctrine\DBAL exception.
Each answer to a question is persisted in a table which has a unique key consisting of the ID of the person filling in the survey and the ID of the question they are answering. Sometimes, when Doctrine tries to INSERT their answer, this key is violated and the query fails. As I mentioned, this happens fairly infrequently and we have been unable to replicate it on our test environment.
Things are made more complicated by the fact that all the Symfony Forms are build dynamically. Here is the code that actually does the persisting of the form data
public function save(\Symfony\Component\Form\Form $form, \Our\Namspace\Entity\Participant $participant)
{
/**
* $surveyReponse
*
* #var \Our\Namespace\Entity\SurveyResponse
*/
foreach ($form->getData() as $surveyResponseKey => $surveyResponse) {
$subQuestionId = $this->getQuestionIdFromSurveyResponseKey($surveyResponseKey);
$subQuestion = $this->getSubQuestionSettingsBySubQuestionId($subQuestionId);
if ($surveyResponse) {
$surveyResponse->setParticipant($participant)->setSubQuestion($subQuestion);
$participant->addResponse($surveyResponse);
$this->em->persist($surveyResponse);
}
$this->em->flush();
}
return true;
}
You can see that we encode the ID of the question (which is known as a SubQuestion in our domain) in the Form data in order to get the (sub)Question entity, then we set the Participant and the SubQuestion on the SurveyResponse object before persisting it. $this->em is just the Doctrine Entity Manager.
Without seeing all the code, it is probably difficult for anyone to figure out what is happening, but if anyone has had a similar problem in the past, maybe they could offer some advice.
We can and should wrap the persist and flush operations in a try/catch block and then handle the exception, but we'd really like to know why it is happening!
The relevant bits of the SurveyResponse entity look like
class SurveyResponse implements SurveyAwareInterface
{
/**
* id of the response.
*
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* the subQuestion to which the response refers.
*
* #var SurveySubQuestion
*
* #ORM\ManyToOne(targetEntity="\Our\Namspace\Entity\SurveySubQuestion", inversedBy="surveyResponses")
* #ORM\JoinColumn(name="sub_question_id", referencedColumnName="id")
*/
protected $subQuestion;
/**
* the participant.
*
* #var AbstractParticipant
*
* #ORM\ManyToOne(targetEntity="Our\Namespace\Entity\Participant", inversedBy="responses")
* #ORM\JoinColumn(name="participant_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $participant;
And there is this annotation in the Entity;
* #ORM\Table(name="survey_response",
* uniqueConstraints= #ORM\UniqueConstraint(
* name="participant_response", columns={"participant_id", "sub_question_id"})})
*

Related

Symfony Doctrine lazy loading the second child from OneToOne relations

I have a very weird question about tweaking and minimizing the database calls by my Symfony application.
I have 3 related entities, namely (for example) Book, Author and Publisher and they are related by OneToOne relations as shown bellow:
Book
{
/**
* #var Author
*
* #ORM\OneToOne(targetEntity="App\Entity\Author", mappedBy="book", cascade={"persist", "remove"})
*/
private $author;
}
Author
{
/**
* #var Book
*
* #ORM\OneToOne(targetEntity="App\Entity\Book", inversedBy="author", cascade={"persist", "remove"})
*/
private $book;
/**
* #var Publisher|null
*
* #ORM\OneToOne(targetEntity="App\Entity\Publisher", mappedBy="author", cascade={"remove", "persist"}, orphanRemoval=true)
*/
private $publisher;
}
Publisher
{
/**
* #var Author
*
* #ORM\OneToOne(targetEntity="App\Entity\Author", inversedBy="publisher")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id", nullable=false)
*/
private $author;
}
Now, if I make a call to fetch all the database records of books as bellow:
$this->getDoctrine()->getRepository('AppBundle:Author')->findAll();
it makes only 1 call to the database, but when I try to make the same call for the Book entity in the same way:
$this->getDoctrine()->getRepository('AppBundle:Book')->findAll();
it leads to thousand calls.
From my debugging what I have found is, it makes one call to get all the books but then make a several calls (2558 calls in my case) trying to find the Author->Publisher relations. This is a great waste of memory, processor, time and performance.
To dig a bit deeper, it seems like (though I am not 100% sure), it is trying to HydrateAllData and makes the unwanted calls here to the database.
Can anybody help me to figure out what is actually happening and how to stop/tweak it please?

Doctrine2 - Custom persister

I'm busy working on a project and I've ran into a slight issue. I was just wondering whether there is any way to customize the persist action of a specific entity? In my case specifically I want to, on update, remove some fields from other tables before re-saving the entity.
Let's, for arguments sake, say my entity that I want a custom persist action on looks like this:
/**
* #ORM\Table()
* #ORM\Entity
*/
class A {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="B", mappedBy="bar")
* #ORM\Column(name="foo")
*/
private $foo;
//Some additional getters and setters here
}
/**
* #ORM\Table()
* #ORM\Entity
*/
class B {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="A", inversedBy="foo")
* #ORM\JoinColumn(name="bar", referencedColumn="id")
*/
private $bar;
//Getters and setters here.
}
Now I know with a simple example like this doctrine will automatically just update $bar in class B if you update that, but let's just say I'd like to first remove $bar completely (not just update it) and re-save it with the new value? Is this possible?
This could also just be done manually before persisting in my update action, but that feels a bit hacky?
The actual code I want to do this with is much too long to post here, so I'm just opting for a simple proof-of-concept here.
Thanks for any assist!
EDIT
Technically the other entities will be related to the current one, via a OneToMany/ManyToMany/ManyToOne relationship, as in the example above. So isn't there something like preHydrate that I can use to clear current data before hydrating the entity with the submitted data?
You should use event-listeners or -subscribers instead of LifecycleCallbacks (i.e. #PrePersist ) as recommended in Cyrus's answer.
Using LifecycleCallbacks you don't have access to unrelated entities while you can change/remove these with a listener/subscriber where you have direct access to the entity-manager with dependency injection.
Please see the documentation chapter How to Register Event Listeners and Subscribers.
You can use prepersist:
/**
* #ORM\PrePersist
*/
There are preupdate, preremove, etc.
Here you have all the info to do that: http://symfony.com/doc/current/book/doctrine.html

Access control lists in Symfony2 using FOSUserBundle - adding roles to a user

I'm hoping this is more simple than the docs I've been reading.
I have a number of entities with ManyToOne relationships with a standard FOSUserbundle user entity that can login users, register them, logout etc.
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
}
Each of my users can also have a number of pets. I.e. here's the cat entity (simplified):
class Cat
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
**/
private $user_id;
How should I add an access control list user role that defines if the user has any cats or not, and if they do, allows them onto the "cats only" part of my website.
Note (slightly related): I feel mildly retarded when it comes to database design using doctrine, I'll upvote anyone that can point me to a good tutorial / explanation of designing things with different kind of joins, especially if it's a in Symfony/Doctrine environment.
Well, since you haven't gotten anything else yet. I'll throw something out there but my experience is only moderate so far.
In the controller for your cats owners page, you could query if the user has any associated cat entities.
You need to add OneToMany/ManyToOne annotations to your user and cat class, and a variable for the associations. This you could review straight out of the doctrine section of the symfony2 book (If you are working with Symfony2 and haven't taken the time to read the book, that is the minimal level of knowledge to work with the full stack and I suggest you read it all). (Do you really need a entity class just for cat, you could have a pet entity with a "type" option.)
check the security context to ensure logged in, then query db.
$em = $this->getDoctrine()->getManager();
$ownersCats = $em->getRepository('PetsPetSiteBundle:Blog')->findBy(array('type'=>'cat'));
or for one result only
$ownersCats = $em->getRepository('PetsPetSiteBundle:Blog')->findOneBy(array('type'=>'cat'));
Once you have this result if it comes back false, you know he has no cats. Otherwise continue.
Obviously you could make this perform better by only querying for id's or something if you don't plan on using the entities on that page or just performing a count query and seeing if it's greater than 0?

JMSSerializerBundle serialization groups in entities with relations

I have a problem with serializing entity with many relations using groups.
I have a problem with serializing related entities this way.
Let's say I have two entities: Product and related Element.
/**
*
* #Serializer\ExclusionPolicy("none")
*/
class Product {
/**
* Primary key
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*
* #Serializer\Groups({"list","details"})
* #Serializer\Type("integer")
*/
protected $id;
/**
* #Serializer\Groups({"list","details"})
* #Serializer\Type("string")
*/
protected $name;
/**
* #ORM\Column(name="description", type="string", length=4096, nullable=true)
*
* #Serializer\Groups({"details"})
* #Serializer\Type("string")
*/
protected $description;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Madden\ProjectBundle\Entity\ProjectResource", mappedBy="project")
* #Serializer\Groups({"details"})
* #Serializer\Type("ArrayCollection<Element>")
*/
protected $details1;
/**
* Relation to project tasks
* #ORM\OneToMany(targetEntity="Madden\ProjectBundle\Entity\ProjectTask", mappedBy="project")
* #Serializer\Exclude()
* #Serializer\Type("ArrayCollection<Element>")
*/
protected $details2;
...
}
Element entity has a similar structure:
/**
*
* #Serializer\ExclusionPolicy("none")
*/
class Element {
/**
* Primary key
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*
* #Serializer\Groups({"list","details"})
* #Serializer\Type("integer")
*/
protected $id;
/**
* #Serializer\Groups({"list","details"})
* #Serializer\Type("string")
*/
protected $name;
/**
* #ORM\Column(name="description", type="string", length=4096, nullable=true)
*
* #Serializer\Groups({"details"})
* #Serializer\Type("string")
*/
protected $description;
...
}
My problem is that when I'm serializing Product with 'details' group entity I want to serialize only id's of Elements but as you see entity has defined same groups as Product (in case that I would need details of element object) because I want have unified groups on all my entities and prevent making hundreds of groups like 'product_details','element_details', and so on.
Is there a way to eventualy change serialization group when I visit relation or something like that? Handler maybe or something like that?
Regards and thanks for any help
Unfortunately, you can't really (but keep reading ;-)), well at least not without changes to the serializer library. The culprit is that the list of groups is fixed within a GroupExclusionStrategy (which is referenced by the Context) the minute you start the serialization process. There is actually an assertion within the code that prevents modification of the exclusion strategy once the (de-)serialization is running.
But as it happens, I had the exact same problem in a project of mine as well, and I hacked the necessary changes into the serializer code. I have cleaned the code up a bit and uploaded it to Github (https://github.com/andreasferber/serializer/tree/recursion-groups).
It adds new property metadata with which you can add, remove or override the groups when descending into subobjects. With annotations it looks like this:
/**
* #Serializer\RecursionGroups(set={"foo", "bar"}, add={"baz"}, remove={"Default"})
*/
private $myProperty;
You should be able to use XML or Yaml metadata as well, however this is untested since I don't use them and I haven't added test cases yet. Have a look at the reference documentation. Since I haven't done any optimizations yet either, if your entities are really large and deeply nested, it might have a noticable performance impact.
Please let me know if you find this useful, or if you have any suggestions, because if this isn't only needed by me, I will add some tests and try to submit it upstream.
A solution for this is actually described in the official documentation.
That being said the solution proposed by #aferber seems better on many points: easier to maintain, less verbose, more flexible...
You need to use setGroups.
The _group suffix used in the official documentation is not needed.
$context->setGroups([
'Default', //if you want
// use this linked entity but show only its id
'group_of_linked_field',
'group_of_linked_field' => [
'id' // you will need to define this group first
],
// use this linked entity and show fields as described
'group_of_other_linked_field',
'group_of_other_linked_field' => [
// just as an example
'Default',
'details',
],
]);
This does not work with addGroup or addGroups! Both of them won't accept associative arrays. setGroups is your (only?) solution.

Symfony: How do I annotate entity properties that are objects to get Doctrine to store a foreign key?

I'm still getting to grips with Symfony and Doctine and I appreciate this might sound overly simple.
I have at present two basic entities: WebSite (having id and canonicalUrl properties) and Job which has, as one property, a WebSite.
A Job has one WebSite; a WebSite can be referenced by many Jobs. Both are under the same namespace.
Relevant here is the Job entity:
/**
*
* #ORM\Entity
*/
class Job
{
/**
*
* #var integer
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
*
* #var WebSite
*/
protected $website;
}
In database terms, a persisted Job should be storing the id of the relevant WebSite.
Without any changes to the above, calling php app/console doctrine:migrations:diff generates a new migration for a table named Job with a single id field.
How do I annotate Job::website such that Doctrine knows to create an integer field and to get the value as the id of the Website object?
You must explicitly define the relationship. The shortest would be
/**
* #ORM\Entity
*/
class Job
{
/**
* #var WebSite
*
* #ORM\ManyToOne(targetEntity="Website")
*/
protected $website;
}
However, should you find yourself wanting to tweak the relationship to better suit your needs, have a look at the annotation reference (ManyToOne and JoinColumn for this particular case). There's also quite a comprehensive article about association mapping, which you might find interesting.

Resources