Doctrine Query Builder for entities with Join Table - symfony

User ---[OneToMany]---> AcquiredSkill ---[ManyToOne]---> Skill
I'm having problems with creating a query using query builder in doctrine. Can someone help me convert this mysql query to a doctrine query using query builder?
SELECT u.*, s.*
FROM `user` u
Join `acquired_skill` ac ON ac.user_id = u.user_id
Join `skill` s ON ac.skill_id = s.skill_id
Tables
user
- user_id
- name
skill
- skill_id
- skill_name
acquired_skill
- as_id
- skill_id
- user_id
So far, this is my query but it lacks the join between the skills.
createQueryBuilder('u')
->select('u.user_id', 'u.name')
->getQuery()
->getResult();

Your relation seems like you have many-to-many association between user and skills you can set your entities to setup this relation like user entity will point to skill entity in a many-to-many way
User Entity
/**
* #ORM\ManyToMany(targetEntity="Namespace\YourBundle\Entity\Skill", cascade={"persist"})
* #ORM\JoinTable(name="acquired_skill",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="skill_id", referencedColumnName="id")}
* )
*/
private $skills;
Skill Entity
/**
*
* #ORM\ManyToMany(targetEntity="Namespace\YourBundle\Entity\User", mappedBy="skills")
*/
private $user;
Now in query builder you can join your user entity with skill like below
$this->createQueryBuilder('u')
->select('u')
->innerJoin('u.skills','s')
->getQuery()
->getResult();
for further clarification see docs 22.2.19. #ManyToMany

$qb = $this->entity_manager->createQueryBuilder('u');
$qb->select("*");
$qb->innerJoin('Namespace\YourBundle\Entity\Aq_skill', 'ac', 'WITH', 'ac.user_id = u.user_id');
$qb->innerJoin('Namespace\YourBundle\Entity\skill', 's', 'WITH', 'ac.skill_id = s.skill_id');
$result = $qb->getQuery()->getArrayResult();

Related

Doctrine OneToOne always loaded on querybuilder

I have Partner entity with two relation:
/**
* #var PartnerSettings
* #ORM\OneToOne(targetEntity="PartnerSettings", mappedBy="partner", cascade={"persist", "remove"}, fetch="LAZY")
*/
private $settings;
/**
* #var PartnerRating
* #ORM\OneToOne(targetEntity="PartnerRating", mappedBy="partner", cascade={"persist", "remove"}, fetch="LAZY")
*/
private $rating;
...getRepository(Partner::class)->findAll() work correctly, one query was made,but when I create queryBuilder:
return $this->createQueryBuilder('p')
->getQuery()
->getResult();
doctrine make 31 queries(i have 10 partners)... in debug toolbar i saw select queries to settings and rating for every partner. I don't want it in this case.
Additionally, in every querybuilder where I used join to partners, setting and rating are selected too.
answer
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true);
did the job
This behaviour is known as the N + 1 selects problem. To reduce database queries, you might want to consider the following approach.
First, retrieve all partners:
$partners = $em->createQueryBuilder()
->select("p")
->from("Parent", "p")
->where(/*...*/)
->setParameter(/*...*/)
->indexBy("p.id")
->getQuery()->getResult();
Now load all children at once, in two queries:
$settings = $em->createQueryBuilder()
->select("s")
->from("PartnerSetting", "s")
->where("IDENTITY(s.partner) IN (?1)")
->setParameter(1, array_keys($partners))
->getQuery()->getResult();
$ratings = $em->createQueryBuilder()
->select("r")
->from("PartnerRating", "r")
->where("IDENTITY(r.partner) IN (?1)")
->setParameter(1, array_keys($partners))
->getQuery()->getResult();
Doctrine will now have all of the retrieved entities are stored in memory. So when, for example, you do a $parnter->getRatings(), you don’t trigger a new DB query, instead the entity is filled from memory.
just define the table AND the relation in the select
$qb->select('g', 'gi');
and now it works
Just mapped relation: #ORM\OneToOne(targetEntity="ENTITY", mappedBy="MAPPEDBY", fetch="EAGER")

DQL joining related entities

Hi have some entities relateds and need to define a dql query to obtain an entity.
MAIN ENTITY
class proyectosSubsecciones
{
...
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="app\SubseccionesBundle\Entity\Subsecciones")
* #ORM\JoinColumn(name="id_subseccion", referencedColumnName="id")
*/
private $subseccion;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="app\ProyectosBundle\Entity\Proyectos")
* #ORM\JoinColumn(name="id_proyecto", referencedColumnName="id")
*/
private $proyecto;
....
RELATED ENTITY
class subsecciones
{
...
/**
* #ORM\ManyToOne(targetEntity="app\SeccionesBundle\Entity\Secciones")
* #ORM\JoinColumn(name="id_seccion", referencedColumnName="id",nullable=false)
*/
private $seccion;
...
I need to obtain the distinct entities of type "app\SeccionesBundle\Entity\Secciones" from each "app\ProyectosBundle\Entity\Proyectos"
I´m trying a query like:
$consulta=$em->createQuery('
SELECT DISTINCT sc
FROM ProyectosSubseccionesBundle:ProyectosSubsecciones p
JOIN p.subseccion s WITH s.id=p.subseccion
JOIN s.seccion sc WITH sc.id=s.seccion
WHERE p.proyecto= :id
');
$consulta->setParameter('id', $id_proyecto);
$subsecciones=$consulta->getResult();
I get an error that says:
"Cannot select entity through identification variables without choosing at least one root entity alias"
But I only need the data from sc.Any idea??
Use query builder in ProyectosSubseccionesRepository:
return $this->createQueryBuilder('p')
->join('p.subseccion', 's', Join::WITH, 's = p.subseccion')
->join('s.seccion', 'sc', Join::WITH, 'sc = s.seccion')
->where('p.proyecto = :id')
->setParameter('id', $id)
->getQuery()
->execute()
For your problem i assume you have defined bidirectional relationship among your entities.
like
Entity RelationType ReferenceEntity Reference
==========================================================================================
ProyectosSubsecciones ManyToOne Subsecciones $subseccion
ProyectosSubsecciones ManyToOne Proyectos $proyecto
Proyectos OneToMany ProyectosSubsecciones $proyectosSubsecciones
Subsecciones OneToMany ProyectosSubsecciones $proyectosSubsecciones
Subsecciones ManyToOne Secciones $seccion
Secciones OneToMany Subsecciones $subsecciones
Considering above bidirectional definitions you can write your DQL as
SELECT DISTINCT s
FROM Secciones s
JOIN s.subsecciones ss
JOIN ss.proyectosSubsecciones pss
JOIN pss.proyecto
WHERE p.id = :id
The above query will select Secciones entity and join with Subsecciones entity using property $subsecciones defined in Secciones entity.
Then query will join Subsecciones with ProyectosSubsecciones using property $proyectosSubsecciones defined in Subsecciones entity.
Finally it will ProyectosSubsecciones with Proyectos entity using $proyecto property defined in ProyectosSubsecciones entity and lastly it will apply a filter according your WHERE clause.
Note there is no need to use WITH clause to join your entities because, In DQL, joining part will be covered by properties which you define as OneToMany/ManyToOne or ManyToMany WITH is used for if there is no relation mapping defined between entities or if you want to add another filter on joining criteria like ON(a.id = b.some_id AND/WITH some = some)

Doctrine 2: Cache in One-to-Many associations

I'm trying to use doctrine cache from Common package, but I can't get it working with one-to-many, many-to-one accosiations. I'll explain later what I want to do.
My configuration:
'configuration' => array(
'orm_default' => array(
'metadata_cache' => 'filesystem',
'query_cache' => 'filesystem',
'result_cache' => 'filesystem',
'hydration_cache' => 'filesystem',
)
),
My entity
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*
* #var string
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100, nullable=false)
*/
protected $name;
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="Category", inversedBy="childrenId", fetch="EAGER")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
protected $parentId;
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="parentId", fetch="EAGER")
*/
protected $childrenId;
}
My DQL
$result = $this->em->createQueryBuilder()->select('c')
->from('App\Entity\Category', 'c')
->where('c.parentId IS NULL')
->orderBy('c.priority', 'ASC')
->getQuery()
->setFetchMode("App\Entity\Category", "parentId", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
->useResultCache(true, 900, 'categories')
->getResult();
I have 28 categories, 15 of them have parentId.
Above query executes 29 SQL queries, but Doctrine store in cache only 1, So when I run again this query, it executes 28 queries.
Any idea what am I doing wrong? missing some cache configuration? missing some methods in DQL? I would like to cache all queries not only one- main query.
Edit
I would like to use query result in loop, like this:
foreach($result as $row)
{
$categories[]['attr'] = $row->getAttribute()->getName();
$categories[]['value'] = $row->getAttribute()->getValue();
}
but this way cache won't work, so currently I'm using:
foreach($result as $row)
{
$attributes = $this->em->createQueryBuilder()->select('c, a.name, a.value')
->from('App\Entity\Category', 'c')
->innerJoin('App\Entity\Attribute', 'a', 'WITH', 'a.id = c.attribute')
->where('c.id = :catId')
->setParameter('catId', $row['id'])
->getQuery()
->useResultCache(true, 900, $categoryName.'-attributes')
->getArrayResult();
}
But I would rather work on objects then on arrays, but I can't cuz if I use object and it has association then this association will not be cached. So ideally would be some way to cache object + ALL his associations.
Association fetch-modes
The query you present only fetches "parent" Category entities, which get hydrated with an uninitialized collection for the children. When accessing that collection (by iterating over those children for example), Doctrine will load the collection, thus perform another query. It will do that for all parent categories hydrated by the first query.
Setting fetch-mode to EAGER only changes the moment these queries are done. Doctrine will do them right after hydrating the parent categories, it won't wait until you access the collection (like with fetch-mode LAZY). But it will still do those queries.
Fetch-join query
The simplest way to tell Doctrine to query and hydrate the categories with their children is to do a "fetch join" query:
$queryBuilder = $this->em->createQueryBuilder();
$queryBuilder
->select('p', 'c') // [p]arent, [c]hild
->from('App\Entity\Category', 'p')
->leftJoin('p.children', 'c')
->where('p.parent IS NULL')
->orderBy('p.priority', 'ASC');
$query = $queryBuilder->getQuery();
$query
->useResultCache(true, 900, 'categories')
$result = $query->getResult();
Note the select() and leftJoin() calls here.
You also don't need to alter the fetch-mode of the association (by calling setFetchMode()), because the query itself will tell Doctrine to do what you want.
The result of this is that Doctrine will perform 1 query if it isn't cached yet (or the cache is stale), or 0 queries if it is cached (and still fresh).
Assumptions
The property $parentId (in Category) is renamed to $parent. This property will contain the parent Category entity, or null, but never an id.
The property $childrenId is renamed to $children. This property will contain a collection of Category entities (which might be empty), but never a collection (or array) of ids, and certainly never a single id.
The query I suggest above takes these renames into account.
I'm completely ignoring the fact that right after your "edit" a new Attribute entity has sprung into existence. It isn't relevant to your question or this answer IMHO.
More levels
It looks/sounds like your Categories only use 2 levels (parents and children). When you introduce more levels (grandchildren, etc), reading this model can become very inefficient very quickly.
When going for 3 or more levels, you might want to look into the Nested Set model. It's heavier on the writes, but highly optimized for reads.
The DoctrineExtensions library has support for this, and there's also a Symfony Bundle.
According to the docs: http://doctrine-orm.readthedocs.io/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql I think that setFetchMode - EAGER will not work for you as this will produce extra queries that will not be cached.
Why don't you load your categories with attributes?
$result = $this->em->createQueryBuilder()
->select('c, a.name, a.value')
->from('App\Entity\Category', 'c')
->innerJoin('App\Entity\Attribute', 'a', 'WITH', 'a.id = c.attribute')
->where('c.parentId IS NULL')
->orderBy('c.priority', 'ASC')
->getQuery()
->useResultCache(true, 900, 'categories')
->getResult();
And it should work as expected.

Join from inversed side

given the two following intities:
<?php
/**
* User
* #ORM\Entity()
*/
class User implements AdvancedUserInterface, \Serializable, EncoderAwareInterface
{
/**
* #var Vip
* #ORM\OneToOne(targetEntity="Vip", mappedBy="user", fetch="EAGER")
*/
protected $vip;
// …
<?php
/**
* Vip
* #ORM\Entity()
*/
class Vip
{
/**
* #ORM\id #ORM\OneToOne(targetEntity="User", inversedBy="vip", fetch="EAGER")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
protected $user;
// …
SHORT :
How can I do this SQL in DQL given above entities:
SELECT u.firstName, v.foo FROM User join Vip v ON v.user_id = u.id
In other words how can I retrieve 10 first users ( with their VIP infos if it exists), using DQL join in such a way that only one SQL query will be generated by Doctrine. Is that possible ?
Long story:
The owning side is the Vip entity because it holds the reference/foreign key to a User underneath in the database.
I am trying to retrieve all User with their Vip datas.
Using the knplabs/knp-paginator-bundle, I first set up a simple query:
$dql = "SELECT u, p FROM AppBundle:User u;
In spite of enforcing the fetch attribute as « EAGER », Vip infos where not part of the initial query. As a consequence, calling getter getVip() on each iteration from inside the twig for in loop like
{% for user in pagination %}
{% if user.getVip() %}
<span class="label label-warning">V.I.P</span>
{% endif %}
{{% endfor %}}
.. caused a query to be issued on each iteration !
The Symfony dev bar shows 6 queries:
DQL documentation and says that one can use JOIN keyword. So my query became:
$dql = "SELECT u, v FROM AppBundle:User u JOIN u.vip v;
But now I get this error:
Warning: spl_object_hash() expects parameter 1 to be object, null
given
Here I'm stuck, wondering how I could fetch Vip datas (or null) along with User datas, in a single query.
In other words how can I retrieve 10 first users ( with their VIP
infos if it exists), using DQL join in such a way that only one SQL
query will be generated by Doctrine. Is that possible ?
You should initialize all related entities using select clause to avoid additional queries when accessing to the related objects.
$repository = $em->getRepository(User::class);
$users = $repository->createQueryBuilder('u')
->addSelect('v') // Initialize Vip's
->join('u.vip', 'v')
->getQuery()
->setMaxResults(10)
->getResult();
Yes, one may add associated entities to the SELECT statement.
But more precisely, one should only add relations that are really involved in the expected result , in other words, entities fetched as "EAGER".
I realized that the vip entity had another relation (oneToMany with a vehicule entity). I just want to retrieve users with their vip metas. Adding another JOIN to the query would just bring more datas since I would not use vehicules anyway (and issue extra work behind the scenes).
-> So I simply changed the fetch attribute from "EAGER" to "LAZY" in the vip OneToMany declaration.
To conclude:
Ask yourself «what are involved intities ?», should it be part
of the result (do you simply need those infos).
if NO, you might turn fetch attribute to "[EXTRA_]LAZY" in the relation declaration like
/**
* #ORM\OneToMany(targetEntity="Vehicule", mappedBy="vip", fetch="LAZY", …)
*/
protected $vehicules;
if YES you will have to select those entities in your query.
Using DQL:
SELECT u, v, w FROM AppBundle:User u LEFT JOIN u.vip v LEFT JOIN v.vehicules w
Using queryBuilder:
$repository = $em->getRepository(User::class);
$users = $repository->createQueryBuilder('u')
->addSelect('v')
->join('u.vip', 'v')
->addSelect('w')
->join('v.vehicules', 'w')
// …

Mapping and DQL

I have 3 tables - user, area, and contacts. A contact can belong to a user or an area. A user can belong to many areas.
I want to pull all the contacts that belong to a user (as specifically defined in the DB), as well as all contacts that belong to the same area as the user.
Can I get a fresh set of eyes on my Database mapping, and the query I need to write in DQL to get what I want. Am I doing something wrong in my database mapping?
I'm definitely a SQL person, and am able to easily fetch what I want in plain SQL. In plain SQL, here's what I want to do:
select c.* from contact c LEFT JOIN user_area ua ON c.area_id=ua.area_id where (ua.user_id=XXX OR c.user_id=XXX);
USER
/**
* #ORM\ManyToMany(targetEntity="area", inversedBy="areas")
* #ORM\JoinTable(name="user_area",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="area_id", referencedColumnName="id")}
* )
*/
private $areas;
/**
* #ORM\OneToMany(targetEntity="Contact", mappedBy="user")
*/
private $contacts;
CONTACT
/**
* #ORM\ManyToOne(targetEntity="Area")
* #ORM\JoinColumn(name="area_id", referencedColumnName="id")
*/
private $area;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="Contacts")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
AREA
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="users")
*/
private $users;
/**
* #ORM\OneToMany(targetEntity="Contact", mappedBy="area")
*/
private $contacts;
The main problem I'm running into is that DQL really wants you to query an object, and it's just plain easier in SQL to query the user/area relationship table to get what I want. I tried to write an query that pulls areas from contacts, then users from contacts, and then users from areas but I get an error message that "users" isn't a defined index in my areas object. Again, I'm a Doctrine newbie, so I'm probably doing something wrong.
Here's my attempt at a query, from the User object in Symfony:
$qb = $em->createQueryBuilder()
->addSelect('c')
->from('MyBundle:Contact', 'c')
->leftJoin('c.area', 'ca')
->leftJoin('c.user', 'cu')
->leftJoin('ca.users', 'cau')
->add('where', 'c.user = ?1 OR cau.id = ?1')
->add('orderBy', 'c.name')
->setParameter(1, $this->getId());
Someone should have slapped me for providing that previous answer. While it got the job done, I was absolutely right, it was not optimized. Queries using that method were taking 3 seconds to go back and forth to the database (3 seconds!). Clearly, there were plenty of other things going on in my world that took away from performance as a requirement for getting this done, but things have changed. I managed to break down this query into two smaller (Doctrine generated) ones, each taking maybe 0.2 or 0.3s.
$areas = $user->getAreas();
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('c')
->from('MyBundle:Contact', 'c')
->where($qb->expr()->in('c.area', '?1'))
->orWhere('c.user = ?2')
->setParameter(1, $areas->toArray())
->setParameter(2, $user);
$query = $qb->getQuery();
$result = $query->getResult();
return $result;
The fact that I have to call $user->getAreas() adds a database query (if Doctrine doesn't already have that information), but this code, using Query Builder expressions, works much better (0.3s vs. 3s is 10% of the original query time!).
I think the main concept I was missing back then was that the Query Builder wants to work with your objects (Entities), and the properties you've defined in your entities. Coming from a strong SQL background, and knowing the specific SQL query I wanted Doctrine to produce, I wasn't approaching the problem properly.
Hope this update to an 8-month old question helps somebody!
So it turns out you can't fetch objects of objects in DQL. I needed to fetch all "Area"s (an object of "Contact") and then fetch all of that Area's "User"s.
In DQL, you can specify multiple "from()" helper methods, and this was what I needed to get the job done.
$qb = $em->createQueryBuilder()
->addSelect('c')
->from('MyBundle:Contact', 'c')
->from('MyBundle:User', 'u')
->leftJoin('c.area', 'ca')
->leftJoin('c.user', 'cu')
->leftJoin('u.areas', 'ua')
->add('where', 'c.user = ?1 OR (ua.id=ca.id AND u.id = ?1')
->add('orderBy', 'c.name')
->setParameter(1, $this->getId());
The resulting SQL generated from Doctrine doesn't seem particularly optimized, but it gets the job done. If anyone has any thoughts on getting Doctrine to better optimize the following query, I'd love to hear opinions.
SELECT m0_.id AS id0, m0_.name AS name1, m0_.email AS email2, m0_.media_area_id AS media_area_id3, m0_.user_id AS user_id4 FROM contact m0_ LEFT JOIN user u1_ ON m0_.user_id = u1_.id LEFT JOIN area m2_ ON m0_.area_id = m2_.id, user u3_ LEFT JOIN user_area u5_ ON u3_.id = u5_.user_id LEFT JOIN area m4_ ON m4_.id = u5_.area_id WHERE u1_.id = ? OR (m4_.id = m2_.id AND u3_.id = ? );

Resources