Easyadmin issues a lot of querys when filtering and the CRUD class has the #UniqueEntity constraint - symfony

I have a problem when filtering at the index page of a CRUD.
The CRUD class is Product, if a filter by an association (Brand in my case), a lot of queries are issued if the Product class has the #UniqueEntity constraint.
To Reproduce
Product entity class with the constraint on top and its related brand entity
...
* #UniqueEntity(fields={"name"})
*/
class Product
{
/**
* #ORM\ManyToOne(targetEntity=Brand::class, inversedBy="products")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $brand;
Brand class has the inverse side defined too:
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="brand", cascade={"persist"})
*/
private $products;
I have a CRUD controller for the Product entity.
I added an EntityFilter for brands in the filters section:
public function configureFilters(Filters $filters): Filters
{
return $filters
->add(EntityFilter::new('brand', 'Brand'));
}
When i enter a brand in the filter popup and apply, a lot of querys are issued. It is like it is trying to revalidate that the name is unique for each filtered product. Querys are like this (one for each filtered product):
SELECT ... FROM product t0 WHERE t0.name = 'similique culpa non rerum rem in assumenda' LIMIT 2;
"similique culpa non rerum rem in assumenda" being the name of one of the filtered products.
If i remove the #UniqueEntity(fields={"name"}) constraint, the queries are not issued.
Version of Easy Admin bundle is 4.0
Thanks a lot!
UPDATE: Well i think the problem comes when you have the inverse side of the relation defined too. In my example, if i define the OneToMany "brand.products" inverse side of the relation, then, filtering by a brand issues the queries (i guess it is a matter of how the filter form, on the handleRequest call, recursively finds and validates constraints). If i remove the inverse side of the relation in Brand, no queries are issued.
In small applications this maybe no problem but, in my case, i have brands with thousands of products and, if i filter by such brands, a query is issued for each product, so that's no minor problem.

Related

Symfony Doctrine - In a many to many relation why is the patch working in one direction but not the other

I'm on a project where I have a many to many relationship between team and agent. Because my teams can have multiple agents and my agents can have multiple teams.
I'm in a situation where I'm doing a patch so I can add multiple agents to a team (which is working) but I cannot do a working patch to add multiple teams to an agent.
Is it because of mapped by and inversed by?
UPDATE
In my TEAM entity here is the relation
/**
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\Agent", inversedBy="teams")
*/
private $agents;
Here is the relation in my AGENT entity
/**
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\Team", mappedBy="agents")
*/
private $teams;
In my team controller, when I want to give my team some new agents I'm using this piece of code and it works. I can see all the agents associated to the team in the database.
$team->setAgents($theAgents);
But when I want to do the opposite in my agent controller (assigning some teams to a new agent) the agent is created in the database but it's not assigned to any team in the association table. Even if I'm using this:
$agent->setTeams($theTeams);
Hence, is it maybe because it's not possible with Doctrine? Or perhaps I'm missing something.
This is the expected behavior.
For your ManyToMany relation you have the owning side:
/**
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\Agent", inversedBy="teams")
*/
private $agents;
and the inverse side:
/**
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\Team", mappedBy="agents")
*/
private $teams;
which are defined by the settings inversedBy and mappedBy respectively.
For a ManyToMany relation, you can chose which entity is the owning and which the inverse side, either of them can be defined.
Doctrine only checks the owning side for association changes. Check Working with associations, which means on your case, only $agents of Teams is checked for any changes to be persisted in the database.
On Agents, any changes in teams are ignored.
Changes made only to the inverse side of an association are ignored. Make sure to update both sides of a bidirectional association (or at least the owning side, from Doctrine's point of view)
It is your responsibility to include these changes on the owning side also.
So in setTeams, do something like:
public function setTeams($teams) {
$this->teams = $teams;
foreach($teams as $team) {
$team->addAgent($this);
}
}
Note that in addAgent you have to check if the agent already exists in the collection.
In the end the solution I found is similar to Jannes'.
In my Agent entity I added this function:
/**
* #param mixed $team
*/
public function addTeam(Team $team)
{
$this->teams[] = $team;
$team->addAgent($this);
}
and in my Agent controller:
$teams = $request->get('teams');
foreach ($teams as $team){
$myTeam = $em->getRepository('MyBundle:Team')
->find($team["id"]);
$agent->addTeam($myTeam);
}
By doing so, I was I able to have a working post on both sides!
Thank you all again for your help!

Symfony: Ordered one-to-many relationship

I have two separate entities that I want to link by a one-to-many relationship. But I want this relationship to be ordered, meaning every time I call on the first entity, the elements of the second entity come in a pre-ordered way. I cannot use 'order by' calls because this order has nothing to do with the fields of the second entity. I thought about having one field of the first entity be an array of entities, but I'm not sure how to accomplish that either..
EDIT
So far I have something like this:
/**
* #ORM\OneToMany(targetEntity="FusionDesign\BlogBundle\Entity\Element", mappedBy="page")
*/
private $elements;
and
/**
* #ORM\ManyToOne(targetEntity="FusionDesign\BlogBundle\Entity\Page", inversedBy="elements")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*/
private $page;
I'm aware that I can put "ORDER BY whatever ASC" somewhere in there but that orders according to a column in Element, and that's not what I need, because Element entities and Page entities would never be persisted at the same time, nor by the same process. What I want to do is constructing a basic CMS where the user could generate new pages. First choose the kind of elements a page could potentially have (like header image, banner, title, and so on) and persist Element entities with fields describing the html, routing and controller content according to those choices. Then, when a new page is created, give the user the choice to order those potential elements at will, and bind Element entities following an order that reflects the layout desired.
I thought about having something like this
/**
* #var array
*
* #ORM\Column(name="structure", type="array")
*/
private $structure;
Where the array stores Element entities but I have no idea how to do that.
You just need to define the orderBy attribute in doctrine's mapping configuration for the relation.
YAML Mapping Example:
'Example\Entity\Article':
# [..]
oneToMany:
images:
targetEntity: 'Example\Entity\Article\Image\ArticleImage'
mappedBy: 'article'
orderBy: # <--- here
position: 'ASC'

Doctrine ManyToMany, find related and no related results

I have an entity called tournament to which users can register to participate in.
The relationship between the two entities is ManyToMany and need to create a view of Symfony2 in which list all tournaments, with or without registered users so that they can join.
This is my DoctrineQueryBuilder
$em->createQueryBuilder('d')
->select('d, i, u')
->leftJoin('d.item','i')
->leftJoin('d.users','u')
->where('d.active = 1')
->andWhere('d.state = 1')
->orderBy('d.dateStart', 'ASC');
I also need to get the number of users who have joined the tournament.
Preamble
There are various ways to achieve what you want. You can create a sub-query to do the count, however a simpler solution is to let doctrine handle this for you.
The solution described below is based on Doctrine lazy/eager loading capability. When doctrine loads an entity, it will also populate it's associations, either lazily or eagerly (default is lazy).
Solution
Assuming your Tournament entity maps the users association as a ManyToMany relation. You can create a new method which counts your Tournament->users.
ManyToMany associations would populate the entity property (in this cases $users) with an ArrayCollection.
A method called countUsers would do the trick, example implementation below:
...
class Tournament {
...
/**
* #ManyToMany(targetEntity="User")
* #JoinTable(name="tournament_users",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="tournament_id", referencedColumnName="id"}
* )
**/
private $users;
...
public function countUsers(){
return $this->users->count();
}
In the view when iterating over the collection of tournaments, simply call the $tournament->countUsers() method to display the count.
References:
ArrayCollection: http://www.doctrine-project.org/api/common/2.1/class-Doctrine.Common.Collections.ArrayCollection.html

Definitions of one-to-many, many-to-many etc

I'm building a database application using Doctrine2. I'm getting somewhat confused by the foreign key mappings. I'm wondering, have I got these examples correct:
One-To-One: An X has exactly one Y.
One-To-Many: An X can have multiple Ys.
Many-To-One: Multiple Xs can have the same Y.
Many-To-Many: Multiple Xs can have multiple Ys.
This is the specific situation that got me confused:
A User has exactly one HomeTown. Many users can belong to the same home town, so the link for the User is:
/**
* #ORM\ManyToOne(targetEntity="HomeTown", inversedBy="localUsers")
*/
$homeTown;
And, the corresponding HomeTown link is:
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="homeTown")
*/
$localUsers;
OR is it:
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="homeTown")
*/
$localUsers;
Some clarification would be much appreciated!
I've been looking at http://doctrine-orm.readthedocs.org/en/latest/reference/association-mapping.html
When you have OneToMany association, the inverted has to be ManyToOne. Saying that, your second option is correct.
TIP: Using Doctrine CLI command orm:validate-schema might also help to identify this issue.
The full path in Symfony app: php app/console doctrine:schema:validate
If you want one city to have many users the mapping should be as it follows
Entity City
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="homeTown")
*/
private $users;
...
public function __construct()
{
$this->users = new ArrayCollection();
}
...
Entity User
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="users")
* #ORm\JoinColumn(name="home_town", referencedColumnName="id")
*/
private $homeTown;
This mapping shows us that the City which is owning side has On-To-Many relation with User(target Entity). Respectively the User which is inversed side has to be annotated with ManyToOne relation because many users have same city. Of course, here the target entity should be City. Its important to specify which column is pointing the foreignkey with the referencedColumnName attribute in JoinColumn annotation. It shows what column of other table points this key. In this example in table User there is column named "home_town" which is a foreign key pointing to column id of table City
In ManyToOne relation you shod use JoinColumn annotation
This mapping is also Bidirectional.
You can make id Unidirectional as in the User Entity do not use "inversedBy=" attribute and remove OneToMany annotation with $user property from the City entity. It is something like when you have to know the city of a particular user, but you do not need to know all users for a specific city

How do I setup up Doctrine's Class Inheritance in Symfony?

My issue is, I'm having trouble grasping DiscriminatorColumn and DiscriminatorMap in Doctrine's Class Inheritance.
I have a products entity that is considered the parent class / table.
There are several child entities that inherit the product entity. (models, parts, and options)
I feel like I should be able to use the primary key to link both tables... But how do I do that with DiscriminatorColumn?
Here is the general idea of what I want to happen...
Fetch all model objects from database while inheriting product parent entity
SELECT object
FROM parts_object parts
LEFT JOIN products_object po
ON parts.product_fk = po.product_id
Or... Fetch all part objects from database while inheriting product parent entity
SELECT object
FROM parts_object parts
LEFT JOIN products_object po
ON parts.product_fk = po.product_id
Ideally I want this done using Doctrine instead of some custom SQL.
Do I need to setup a "type" column for the parent table so each row defines whether it's a part, model, or option?
Doctrine inheritance docs
Okay, I'll try to explain this as simple as possible.
Let's start with DiscriminatorColumn
Discriminator column is basically, as it says, a column in your database. Its used to store, a key, if you like which helps to identify what kind of object you're currently querying, based on your DiscriminatorMap configuration.
DiscriminatorMap is the way you map each of those keys to an entity. You said you have the following
Product [parent]
Model [child of parent]
Part [child of parent]
Option [child of parent]
Then, your discriminator map should look something like this, for example:
#DiscriminatorMap({
"model" = "AppBundle\Entity\Model",
"Part" = "AppBundle\Entity\Part",
"Option" = "AppBundle\Entity\Option"
})
Always pay attention to your last definition in your mapping. The last line must end without a comma!
As of InheritanceType I would suggest you to use #InheritanceType("JOINED") because this will let you have single table for each of your child classes.
Every child class must extend your Product entity class, which is obviously the parent. Each child class must not define $id property, because of the inheritance mapping.
Then querying for records by specific type comes with the following query:
"SELECT product FROM AppBundle\Entity\Product product WHERE product INSTANCE OF AppBundle\Entity\Part"
The query will search only for records mapped to this entity only.
If you have any questions, don't hesitate to ask.
Edit as of new comment
-----------------------
A little bit more explanation. You do not need to create any extra property/column in your entity mappings. The moment you add this annotation #DiscriminatorColumn(name="discr", type="string") doctrine will create that column automatically for you. The column from this example would be named discr with type of VARCHAR.
I still don't understand what is used to join the tables. How does doctrine know to link the ids between the product and model
About this part. If you use #InheritanceType("JOINED") this would mean that your GeneratedValue ID would be set in your main entity - Product. Then each of the child entities that extend Product would automatically get the same ID, which is why you don't need to specify $id property in your child entities.
Lastly, how can you check which entity type you're currently viewing for example. Consider the following scenario, each of your child entities extends Product and we will perform a dummy search for a record:
$product = $entityManager->find('AppBundle:Product', 1); // example
Now, if you actually go and do a var_dump($product) you will notice something interesting. The object would be an instance of either Model,Part or Option because each of these entities are defined in your discriminator map and Doctrine automatically maps your records based on that.
Later, this can come handy in situations like this:
if( $product instanceof \AppBundle\Entity\Part ) {
// do something only if that record belongs to part.
}
If you want to use DiscriminatorMap for Doctrine, so you should use Doctrine, but not SQL.
Basic setup is:
/**
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="MyApp\ProductBundle\Repository\ProductRepository")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="productType", type="string")
* #ORM\DiscriminatorMap({
* "Product" = "Product",
* "Model" = "Model",
* "Part" = "Part",
* "Option" = "Option",
* })
*/
class Product
{
...
}
MyApp\ProductBundle\Entity\Model
/**
* #ORM\Entity(repositoryClass="MyApp\ProductBundle\Repository\ModelRepository")
*/
class Model extends Product
{
}
MyApp\ProductBundle\Entity\Part
/**
* #ORM\Entity(repositoryClass="MyApp\ProductBundle\Repository\PartRepository")
*/
class Part extends Product
{
}
MyApp\ProductBundle\Entity\Option
/**
* #ORM\Entity(repositoryClass="MyApp\ProductBundle\Repository\OptionRepository")
*/
class Option extends Product
{
}
Then if you need to get all products at controller
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository("MyAppProductBundle:Product");
$products = $repo->findAll();
Then if you need select all models, just setup proper repository
$repo = $em->getRepository("MyAppProductBundle:Model");
$models = $repo->findAll();

Resources