I'm trying that a self-referenced entity stop from querying the database everytime I fetch the children of one object, and get the whole tree in one query.
This is my entity:
/**
* #ORM\Entity(repositoryClass="ExampleRep")
* #ORM\Table(name="example_table")
*/
class Example {
/**
* #ORM\Id
* #ORM\Column(type="integer", nullable=false);
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Example", inversedBy="children")
* #ORM\JoinColumn(name="parent", referencedColumnName="id", onDelete="SET NULL")
*/
private $parent = null;
/**
* #ORM\OneToMany(targetEntity="Example", mappedBy="parent")
*/
private $children;
}
And i'm calling my date using queryBuilder like:
$query = $this->createQueryBuilder('e');
$query->orderBy('e.parent', 'ASC');
$example_data = $query->getQuery()->getResult();
When I cycle my example_data and call getChildren, another query is made, even if that same object, was already called in the query.
I've followed the example here: Doctrine - self-referencing entity - disable fetching of children but when i do it, my getChildren returns nothing.
Is there a way to fetch my data without overloading the database with multiple requests?
If you know the depth of your tree, you can just do a custom dql query and do:
return $this->createQueryBuilder('example')
->addSelect('children')
->leftJoin('example.children', 'children')
->addSelect('subChildren')
->leftJoin('children.children', 'subChildren')
;
Otherwise, and as stated here, you can generate a flat resultset, and then construct the tree from it.
I made this kind of implementation using materialized paths, but nothing forbids you to do it with foreign keys comparison:
https://github.com/KnpLabs/DoctrineBehaviors/blob/master/src/Knp/DoctrineBehaviors/ORM/Tree/Tree.php#L119
https://github.com/KnpLabs/DoctrineBehaviors/blob/master/src/Knp/DoctrineBehaviors/Model/Tree/Node.php#L219
Related
I have this database design and in some instances Doctrine 2 makes the correct decisions in insert order and sometimes not
I have an automated import process that imports and updates data as the data provider changes it. The field item.something_happened_item_history_id is the new field that causes issues
When starting the import
Step 1. No data in the database and I create one item and one item_history, where item_history.item_id is the item.id and item.something_happened_item_history_id is null. Now I do Flush and Doctrine can figure it out that it needs to insert item before item_history. All is good.
Step 2. Now a new import comes in, and some of the data already exists in the database from the Step 1. But in the new import I actually have a new unique item. So what I do is that I create the item and item_history exactly as in Step 1. But for some reason during the Flush Doctrine thinks that item_history needs to be saved before the item. Which can't be done, because of not nullable foreign key reference on item_history.
I understand that Doctrine makes decisions based on foreign key references and the field something_happened_item_history_id is causing trouble. But it doesn't cause any trouble in the step 1. The problems occur when there is already some data in the database that is loaded into the entityManager.
I haven't been able to figure out how to manipulate Doctrine 2 so that it would always save the item before item_history.
Any ideas how to solve it?
As I really do not want to change my database design, because Doctrine 2 can't figure out the insert order.
Also it is not an option to do more Flushes, as there is a lot of data validation done before the data goes to the database. And I do not want any corrupt data in the database.
Best Regards,
Hendrik
EDIT: Doctrine 2 mapping
/**
* #Table(name="item", uniqueConstraints={#UniqueConstraint(name="uc_something_happened_item_history", columns={"something_happened_item_history_id"})})
**/
class Item
{
/** #Id #Column(name="id", type="integer", options={"unsigned":true}) #GeneratedValue **/
protected $id;
/**
* #OneToMany(targetEntity="ItemHistory", mappedBy="item")
**/
protected $itemHistories;
/**
* #OneToOne(targetEntity="ItemHistory")
* #JoinColumn(name="something_happened_item_history_id", referencedColumnName="id", nullable=true, unique=true)
**/
protected $somethingHappenedItemHistory;
}
/**
* #Table(name="item_history")
**/
class ItemHistory
{
/** #Id #Column(name="id", type="integer", options={"unsigned":true}) #GeneratedValue **/
protected $id;
/**
* #ManyToOne(targetEntity="Item", inversedBy="itemHistories")
* #JoinColumn(name="item_id", referencedColumnName="id", nullable=false)
**/
protected $item;
}
I haven't resolved the problem the way I wanted.
But by making the item_id nullable, then the Doctrine is able to finish the flush.
Data is also correct in the database.
I'm implementing a new application on top of an existing database. The existing database is being used by a mobile application and because the mobile app is being developed by a different team I am not allowed to change the structure of existing tables.
The existing database has a user table and for my own application's users I created my own table and Doctrine entity called PortalUser (table portal_user).
The PortalUser entity is going to have a OneToMany association called $children which refers to the existing User entity. In other words each PortalUser has zero or more child User entities.
The most natural way to implement this is to have something like this (simplified):
User (the existing entity):
class User
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var PortalUser
*
* #ORM\ManyToOne(targetEntity="PortalUser", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
}
PortalUser entity:
class PortalUser
{
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var array
* #ORM\OneToMany(targetEntity="User", mappedBy="parent")
*/
protected $children;
}
This will create a new column "parent_id" in the existing user table which isn't allowed. So would it be possible to get a separate link table with parent_id and child_id columns, equivalent to a regular ManyToMany link table? And if so what annotations would result in such a structure?
Okay this is embarrassing. Turns out it's in the Doctrine documentation:
A unidirectional one-to-many association can be mapped through a join
table. From Doctrine’s point of view, it is simply mapped as a
unidirectional many-to-many whereby a unique constraint on one of the
join columns enforces the one-to-many cardinality.
Doctrine manual
The statement
I'm trying to reproduce the automatic Doctrine mechanism for handling Many-to-Many bidrectional relationships, but introducing a custom join table.
I've already digged into similar questions:
Joining-Table with Metadata Impairs Getters/Setters - Doctrine 2
but it doesn't really help me because it's absolutely unidirectional
doctrine2 many to many self referencing with intermediate details
but this one does not even talk about managing the relations
Doctrine2: Best way to handle many-to-many with extra columns in reference table
is very interesting. However, although the author mentions its bidirectional needs, he doesn't cover the case.
I'm aware that a join table with extra fields is not an association anymore, just a third entity that refers to the two other ones. And from that statement, it's obvious that one cannot expect it to work out-of-the-box as an implicit Many-to-Many association managed by Doctrine.
But i want to have this trio to work as a simple, straight, bidirectional Many-to-Many association, so that means using proxy methods and relying on a Logic class.
The code
There's a Category entity and a Product entity:
/**
* #ORM\Table(name="category")
* #ORM\Entity(repositoryClass="CategoryRepository")
*/
class Category
{
/**
...
*/
protected $id = null;
/**
* #ORM\OneToMany(targetEntity="CategoryProduct", mappedBy="category", fetch="LAZY", cascade={"persist"})
*/
protected $categoryProducts;
}
and
/**
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="ProductRepository")
*/
class Product
{
/**
...
*/
protected $id = null;
/**
* #ORM\OneToMany(targetEntity="CategoryProduct", mappedBy="product", fetch="LAZY", cascade={"persist"})
*/
protected $categoryProducts;
}
and of course a join entity:
/**
* #ORM\Table(name="category_product")
* #ORM\Entity(repositoryClass="CategoryProductRepository")
*/
class CategoryProduct
{
/**
...
*/
protected $id = null;
/**
* #ORM\ManyToOne(targetEntity="Category", fetch="EAGER", inversedBy="categoryProducts")
* #ORM\JoinColumn(onDelete="CASCADE")
*/
protected $category;
/**
* #ORM\ManyToOne(targetEntity="Product", fetch="EAGER", inversedBy="categoryProducts")
* #ORM\JoinColumn(onDelete="CASCADE")
*/
protected $product;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
protected $starred = false;
}
The problem
How to keep an up-to-date list of CategoryProduct entities available to both entities in a pure ORM-style way? In an ORM, everything is managed on the Object layer. Changes to DB are made only on user's request, but it's not compulsory as long as one only works from the ORM point of view. In other words:
$category->addProduct($product);
does not write anything to the DB, and does not even persist any object to the entity manager, but one can still retrieve or remove this product from the list as long as the script runs.
In the case of a custom join table, it's different, because when one wants to add a product, he must create and persist a CategoryProduct entity. So what if we need to retrieve this association from the inverse side?. Here is a code sample that demonstrates my problem:
$product->addCategory($category);
$category->addProduct($product);
In this bidirectional association, how can the $category::addProduct function know about the instance of CategoryProduct entity created by $product::addcategory? The risk is to create two similar join entities for the same association, and i don't know how to avoid it.
I need to query a number of issues in a table of a issue tracking system limiting this query by a complicated condition:
Issues (Entity) are grouped into categories (another Entity). Persons (Entity) are members of multiple roles (fourth Entity), this is one ManyToMany relationship. And finally, a role can have access on one or many categories, this is the second ManyToMany relationship.
<?php
/**
* #Entity
* #Table(name="issue")
*/
class Issue
{
/**
* #ManyToOne(targetEntity="Category", fetch="EAGER")
* #JoinColumn(name="category", referencedColumnName="id", onDelete="RESTRICT", nullable=false)
*/
private $category;
…
}
/**
* #Entity
* #Table(name="category")
*/
class Category
{
/**
* #ManyToMany(targetEntity="Role", mappedBy="categories")
*/
private $roles;
…
}
/**
* #Entity
* #Table(name="role")
*/
class Role
{
/**
* #ManyToMany(targetEntity="Person", mappedBy="roles")
*/
private $persons;
/**
* #ManyToMany(targetEntity="Category", inversedBy="roles")
* #JoinTable(name="role_has_access_on_category",
* joinColumns={#JoinColumn(name="role_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
private $categories;
…
}
/**
* #Entity
* #Table(name="person")
*/
class Person
{
/**
* #ManyToMany(targetEntity="Role", inversedBy="persons")
* #JoinTable(name="person_is_member_of_role",
* joinColumns={#JoinColumn(name="person_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="role_id", referencedColumnName="id")})
*/
private $roles;
…
}
I have left all fields except the relationship ones away, of course there are primary keys and a lot more columns there…
I want to retrieve all issues that belong to categories to which a person with a given primary key has access via the roles it is member of.
At first I just started to experiment how to query ManyToMany relationships, so the code below does not really resemble my target.
I have finally found out how I can get the query to retrieve the other side of one ManyToMany relationship, so I can already get the roles a person belongs to. But this query does not fetch the categories a role has access to.
$qb = $this->em->createQueryBuilder();
$qb->select('person')
->addSelect('role')
->addSelect('category')
->from('Person', 'person')
->innerJoin('person.roles', 'role')
->innerJoin('role.categories', 'category');
$result = $qb->getQuery()->getResult();
$result contains the persons data with all associated roles, but a blank array of categories instead of the entities. The final query would start from the issue side of course, but for now I would only like to get through to the other side…
So now I wonder whether I have to take all the roles and loop through them to fetch all categories. Is there no easier Doctrine way?
By the way, that's the SQL I would use:
SELECT issue.* FROM person AS p, person_is_member_of_role AS pim, role_has_access_on_category AS rha, issue
WHERE
p.id = pim.person_id AND
pim.role_id = rha.role_id AND
rha.category_id = todo.category AND
p.id = ?;
I hope this all makes it clear somehow, otherwise I will revise my question…
You are over-complicating it by trying to put all join conditions in the WHERE clause.
If I understood the question correctly, the query looks like this:
SELECT
i
FROM
Issue i
JOIN
i.category c
JOIN
c.roles r
JOIN
r.persons p
WHERE
p.id = :personId
Translated in QueryBuilder API:
$qb = $entityManager->createQueryBuilder();
$issues = $qb
->select('i')
->from('Issue', 'i')
->innerJoin('i.category', 'c')
->innerJoin('c.roles', 'r')
->innerJoin('r.persons', 'p')
->andWhere($qb->expr()->eq('p.id', ':personId'))
->setParameter('personId', $personId)
->getQuery()
->getResult();
Also, consider avoiding the QueryBuilder if there's no good reason to use it. After all, it's just a string builder.
Say I have an Offer, which can have 1-n Range.
Immediately you think, "put a offer_id inside Range".
But my Offer has a composite primary key (composed of two fields). There is no AUTOINCREMENT id column.
The Doctrine2 documentation doesn't say much about that particular case, here is my entities:
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Offer
{
/**
* #var Site $site
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Site")
* #ORM\JoinColumn(name="site_id", referencedColumnName="id")
*/
private $site;
/**
* #var string $pouet
* #ORM\Id
* #ORM\Column(name="pouet", type="string", length=255)
*/
private $pouet;
}
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="RangeItem")
* #ORM\Entity
*/
class Range
{
/**
* #todo This is test code only do not push me :-)
* #var ArrayCollection
* #ORM\ManyToOne(targetEntity="Offer")
*/
private $offers;
}
I obtaind this error:
[Doctrine\ORM\ORMException]
Column name id referenced for relation from
Pouet\MyBundle\Entity\Range towards Pouet\MyBundle\Entity\Offer does
not exist.
That make sense, but how can I deal with this issue? Is a Table with composite primary key forbidden to have associations on it?
I believe the solution is to mirror the primary key (PK) for the foreign key (FK). I.E. for each column that makes up the PK (site, pouet) you need to have the same columns on the related entity.
You can do this by using the JoinColumns annotation (or the equivalent in YAML/XML) with a JoinColumn for each part of the composite FK:
/**
* #ORM\Table(name="RangeItem")
* #ORM\Entity
*/
class Range
{
/**
* #todo This is test code only do not push me :-)
* #var ArrayCollection
* #ORM\ManyToOne(targetEntity="Offer")
* #ORM\JoinColumns(
* #ORM\JoinColumn(name="site_id", referencedColumnName="site_id"),
* #ORM\JoinColumn(name="pouet", referencedColumnName="pouet")
* )
*/
private $offers;
}
I hope this might help somebody who is still struggling with this issue.
You should be able to use a #JoinColumn annotation in the Range class to specify which Id to use:
/**
* #ORM\ManyToOne(targetEntity="Offer")
* #ORM\JoinColumn(name="offer_pouet", referencedColumnName="pouet")
*/
private $offers;
Because the defaults for #JoinColumn, if you do not specify them, would be offer_id and id, respectively, you need to manually specify (I'm making a bit of an assumption here that pouet is a unique value for your Offer class).
EDIT: based on your comment, I found a tutorial on the Doctrine Project site for Composite Primary Key. The entity relationship has mappedBy for one key and indexBy for the other. Hope that helps.