I'm having trouble mapping this relationship in Doctrine. I have a UseCase, which has many UseCaseSteps. A UseCaseStep has many sub-steps, which is a OneToMany on UseCaseStep. Here's the pertinent code I have atm:
/**
* UseCase
*
* #ORM\Table(name="use_cases")
* #ORM\Entity(repositoryClass="DesignCase\Bundle\Bundle\Entity\UseCaseRepository")
*/
class UseCase
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Actor", inversedBy="use_cases", cascade={"persist", "remove"})
* #ORM\JoinTable(name="actors_use_cases")
*/
private $actors;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="UseCaseStep", mappedBy="useCase", cascade={"persist", "remove"})
* #ORM\OrderBy({"order" = "ASC"})
*/
private $steps;
}
/**
* UseCaseStep
*
* #ORM\Table(name="use_case_steps")
* #ORM\Entity(repositoryClass="DesignCase\Bundle\Bundle\Entity\UseCaseStepRepository")
*/
class UseCaseStep
{
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="UseCase")
*/
private $useCase;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="UseCaseStep", mappedBy="parent", cascade={"persist", "remove"})
* #ORM\OrderBy({"order" = "ASC"})
*/
private $subSteps;
/**
* #var UseCase
*
* #ORM\ManyToOne(targetEntity="UseCase")
*/
private $useCaseReference;
/**
* #var UseCaseStep
*
* #ORM\ManyToOne(targetEntity="UseCaseStep")
* #ORM\JoinColumn(nullable=true)
*/
private $parent;
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="BusinessRule", cascade={"persist", "remove"})
*/
private $businessRules;
}
That code isn't complete, but I think it has all the relevant information. What I want to do is create a new entity TestCase, which has many TestCaseSteps. A TestCase IS a UseCase with a little more information... same for TestCaseStep and UseCaseStep. IE, a TestCaseStep is a UseCaseStep with data input and expected output fields added to it. A user can create many TestCases from one UseCase.
I tried making UseCase and UseCaseStep #MappedSuperclass, but that doesn't have the desired effect. I get the obvious, "It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass" error. Plus, from the docs, I don't think that's the right approach anyway.
I'm having trouble wrapping my brain around it. Any thoughts? I hope I explained that well enough...
You have an issue there that is much more fundamental than Doctrine. You want to show inheritance in the database. For this approach there is no real correct solution. You could go with making an Entity TestCase that extends UseCase and overwrites the respective properties (need to make them protected) with another relation to TestCaseStep.
You do something similar with UseCaseStep and TestCaseStep. That way you have inheritance in the entities. Now you would have to make sure that you use another table and you end up with completely seperate entities database-wise. They just share the same properties in the entities but are seperate in database.
That approach would be in my opinion the easiest one to follow. Everything else seems to be very complicated as you cannot properly use one table with a different amount of properties for each entity. Each database table has a fixed set of properties that need to be reflected in an entity.
Another approach would surely be to just use the properties in the sub-entity that is used by this entity and create another relationship (many-to-one) between the TestCase and the UseCase or TestCaseStep and UseCaseStep respectively. But the latter approach isn't very easy and can end up being very complicated if you don't have a lot of knowledge about Doctrine, Symfony and databases in general.
Related
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?
I am trying to get my head around Doctrine 2 ORM relationships, I thought I had the hang of it but after reading a few symfony cookbook entries, I suspect I am actually a little confused.
I currently have a system where a template can contain multiple modules (including more than one of each type) and multiple templates can use the same module.
I thought that this would warrant a ManyToMany relationship and indeed looking at my table, it seems to work quite well.
However I realised as I was writing the database query that I needed the modules to load in a certain order, which means my join table needs to have a third 'order_by' column. I've read that a true join table is only ever two columns.
Hence the confusion. What should I set this up as in my entities?
Like #Kris said - You'll go for One to Many towards middle entity. If you go for Many to Many instead then you won't have class file for middle table which is an issue in the most cases.
M-N assumption: ONE Student studies in MANY Courses and ONE COURSE can have MANY Students.
Both of the examples below give you this ERD in database but you want to go for ONE to MANY version.
MANY to MANY:
This will create StudentCourse entity in database but as you see no actual class file for you to deal with.
class Student
{
protected $id;
protected $name;
/**
* #ORM\ManyToMany(targetEntity="Course")
* #ORM\JoinTable(
* name="StudentCourse",
* joinColumns={#ORM\JoinColumn(name="studentId", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="courseId", referencedColumnName="id")}
* )
*/
private $course;
}
class Course
{
protected $id;
protected $name;
}
ONE TO MANY:
This will create StudentCourse entity in database and as you see there is an actual class file for you to deal with when coding e.g. persist() etc.
class Student
{
protected $id;
protected $name;
/**
* #ORM\OneToMany(targetEntity="StudentCourse", mappedBy="studentMap",
* cascade={"persist", "remove"})
*/
protected $studentInverse;
}
class StudentCourse
{
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Course", inversedBy="courseInverse")
* #ORM\JoinColumn(name="course", referencedColumnName="id",
* nullable=false, onDelete="CASCADE")
*/
protected $courseMap;
/**
* #ORM\ManyToOne(targetEntity="Student", inversedBy="studentInverse")
* #ORM\JoinColumn(name="student", referencedColumnName="id",
* nullable=false, onDelete="CASCADE")
*/
protected $studentMap;
}
class Course
{
protected $id;
protected $name;
/**
* #ORM\OneToMany(targetEntity="StudentCourse", mappedBy="courseMap",
* cascade={"persist", "remove"})
*/
protected $courseInverse;
}
EDIT:
onDelete="CASCADE" and cascade={"persist", "remove"} bits are not compulsory. They handle data redundancy. Is it bad to use redundant relationships?
You would need a OneToMany/ManyToOne
If you didn't need to save the order, it would be a manytomany, but since you do, that middle table now must be its own entity unfortunately.
So you would need the following Entities
Template
with a OneToMany to
TemplateModules (probably a better name for this one)
With a ManyToOne to
Modules
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
I having a following issue, I need to make a relationship with two tables, but with no regular id, i need to use strings column. Something like this:
/**
* #ORM\Entity
* #ORM\Table(name="sigtap_tb_procedimento")
*/
class Procedimento
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="ExcecaoCompatibilidade", mappedBy="procedimento_restricao")
* #ORM\JoinColumn(name="co_procedimento_restricao", referencedColumnName="co_procedimento")
*/
private $restricoes;
}
And another Entity
/**
* #ORM\Entity
* #ORM\Table(name="sigtap_rl_excecao_compatibilidade")
*/
class ExcecaoCompatibilidade
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Procedimento", inversedBy="restricoes")
* #ORM\JoinColumn(name="co_procedimento_restricao", referencedColumnName="co_procedimento")
*/
private $procedimento_restricao;
}
co_procedimento_restricao and co_procedimento_restricao are string type, The relation does not working. How can i solve this issue?
Your relation needs to reference a primary key in the other table.
May be I misunderstood your question but cant you reference the id collumn the relationship like this:
/**
* #ORM\OneToMany(targetEntity="ExcecaoCompatibilidade", mappedBy="procedimento_restricao")
*/
private $restricoes;
/**
* #ORM\ManyToOne(targetEntity="Procedimento", inversedBy="restricoes")
* #ORM\JoinColumn(name="co_procedimento_restricao", referencedColumnName="id")
*/
private $procedimento_restricao;
Take a look here:
http://docs.doctrine-project.org/en/2.0.x/reference/association-mapping.html
using one-to-many relations with doctrine
The side using #OneToMany is always the inverse side of a relation from doctrine's pov ( possibly not what you consider being the inverse side ) and never has a join-column definition.
Remove the #JoinColumn annotation from class Procedimento.
#OneToMany has to use mappedBy and #ManyToOne (the owning side) uses inversedBy.
The join-column (or join-table) definition has to be on the owning side together with #ManyToOne.
When using a join-column the name of this column (which will be added to the table of the owning side entity aka the side being "many") will be specified by name="column_name" and the referenced foreign key to store in there is the referencedColumnName="id"definition of the #JoinColum annotation.
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.