DQL to join on property of a property - symfony

I am trying to create a DQL query by testing on the property of a property. The objective is to create a DQL query that will indicate in what role a user is fulfilling a job, returning no rows if the user does not have the proper role.
My Entities are Role, Job, and User. User's have roles, Jobs require a role (to fulfill them), and Roles have 'alternates' that link to another role that can fill in for that role.
Stubbed versions of the entites look like:
class User {
//Annotation not needed for question
protected $id;
/**
* #var SystemBundle\Entity\Role
*
* #ORM\ManyToMany(targetEntity="SystemBundle\Entity\Role")
* #ORM\JoinTable(name="User_User__Role",
* joinColumns={
* #ORM\JoinColumn(name="User_ID", referencedColumnName="ID")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="Role_ID", referencedColumnName="ID")
* }
* )
*/
protected $roles;
}
class Job {
//Annotation not needed for question
protected $id;
/**
* #var SystemBundle\Entity\Role $jobRole
*
* #ORM\ManyToOne(targetEntity="Role")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="Role_ID", referencedColumnName="ID")
* })
*/
protected $jobRole;
}
class Role {
//Annotation not needed for question
protected $id;
/**
* #var SystemBundle\Entity\Role $jobRole
*
* #ORM\OneToOne(targetEntity="Role")
* #ORM\JoinColumn(name="BackupRole_ID", referencedColumnName="ID")
*/
protected $backUpRole;
}
The conceptual logic is to join Roles with Jobs, testing if the current Role IS job.JobRole or job.JobRole.backUpRole. I've tried this, and doctrine really doesnt seem to like the job.jobRole.backUpRole and throws this error at the second period:
[Syntax Error] line 0, col 210: Error: Expected =, <, <=, <>, >, >=, !=, got '.'
My DQL attempt looks like this
SELECT r
FROM SystemBundle:Role r
INNER JOIN ProcessBundle:Job j
WITH j = :j AND j.jobRole = r OR j.jobRole.backUpRole = r
LEFT JOIN UserBundle:User u
WITH r MEMBER OF u.roles
WHERE u = :emp AND u IS NOT NULL
ORDER BY r.id
I can accomplish this task with pure SQL, as well as just using php to walk the associations, but I am looking to stay true to use DQL (because it's vexing me and I want to know if it can be done).
If it helps, here is my pure SQL:
#get all roles
select r.*
from Sys_Role as r
inner join
#get the role assigned to the job
(select r.*
FROM Sys_Role as r
INNER JOIN Sys_Job as j
ON j.JobRole_ID = r.ID
WHERE j.ID = ?) as jr
#join roles based on the job-role's id or backup id
# this should filter the list of roles down to the role and backup role for the job
on ar.ID = aar.ID OR ar.ID = aar.BackUpRole_ID
# and filter by the roles the user is assigned
INNER JOIN User_User__Role as uur
ON r.ID = uur.Role_ID
WHERE uur.User_ID = ?
EDIT: My apologies. While editing the question layout i accidently deleted the DQL. I've added that back the question!
UPDATE: I am exploring making the $backUpRole a bidirectional self-join and attacking from that direction. Doctrine does not like the naive attempt, so it looks like if this tact is used, fresh DQL is needed.
Changing the DQL to WITH j = :j AND j.jobRole = r OR j.jobRole = r.roleIBackUp yeilds a new error: A single-valued association path expression to an inverse side is not supported in DQL queries. Use an explicit join instead.

I figured it out. It IS possible... you need to join the sub-objects before you can use them. I had to switch up the order of my joins and I'm not terribly happy with my filters being in WITH clauses instead of WHERE, but it's hard to argue with results!
SELECT r
FROM SystemBundle:Role r
LEFT JOIN UserBundle:User u
WITH u = :emp
INNER JOIN ProcessBundle:Job j
WITH j = :job
JOIN j.jobRole jr
JOIN jr.backUpRole jrbr
WHERE (jr = ar OR jrbr = ar) AND r MEMBER OF u.role
The last 2 Joins let us alias the objects that belong to the linked entities so we can use them to compare.
The "30,000 foot view" of how this DQL works is such:
Foreach Role
Does this Role belong to the user?
Get the Role for the Job we want
Alias That Role
Alias That ROles Back Up
Is this role the Job's Role or Job's Role's Backup Role?
Kind of weird, but fairly simple once you get it.

Related

How to get the join ID when using Doctrine?

I am working on a project of my company. However, I am not familiar with Doctrine. I am the old-styled query-guy.
Table-A and Table-B is in one-to-many relation, linking up by "a_id" on Table-B. In the Entity-B, $a_name is specified.
Table-A
a_id
a_name
a_attr
Table-B
b_id
a_id
b_name
b_attr
Entity-B
/**
* #ORM\ManyToOne(targetEntity="EntityA")
* #ORM\JoinColumn(name="a_id", referencedColumnName="a_id")
* #var Timezone
*/
protected $a_name;
Now I am writing a method to get a set of records using IN()
/**
* #param array $ids
*
* #return array
*/
public function getByIds($ids) {
$query = $this->getEntityManager()->createQuery('SELECT t FROM Entity-B t INDEX BY t.id WHERE t.id IN (:ids)');
}
The above line "INDEX" and "WHERE" with the Entity-B ID. How can I "INDEX" and "WHERE" with Entity-A's ID (a_id on Table-B)?
Thanks.
Try this or similar for your DQL:
SELECT a.a_id, t.b_id, t.b_name, t.b_attr FROM Entity-B t LEFT JOIN t.a_name a INDEX BY a.id WHERE a.a_id IN (:ids)
We just add a join to Entity-A, include a.id in the select and then index by a.id.
As we working with entities in doctrine, we need to join the entity to get at its id to then index by it. Also, the naming of your properties within each entity could be simpler and more intuitive. So rather than Entity-A (a) having properties a_id, a_attr, etc, just use id, attribute, etc. I assume you just generalised your code for the question and you probably have nicer property names in your project.
Let me know how that DQL works out for you.

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')
// …

limit columns returned in relationnal entity symfony2

Is it possible to filter an entity and display only few columns in symfony2?
I think I can do a custom query for this, but it seems a bit dirty and I am sure there is a better solution.
For example I have my variable $createdBy below, and it contains few data that shouldnt be displayed in this parent entity such as password etc...
/**
* #var Customer
*
* #ORM\ManyToOne(targetEntity="MyCompany\Bundle\CustomerBundle\Entity\Customer")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="created_by", referencedColumnName="id", nullable=false)
* })
*/
protected $createdBy;
So I need to display my Customer entity, but only containing fields like id and name for example.
EDIT :
I already have an instance of Project, the entity with my createdBy field, and I want to grab my customer data 'formatted' for this entity and not returning too much fields like password ...
Thanks
It sounds like expected behavior to me. The doctrine documentation seems to imply that eager fetching is only one level deep.
According to the docs:
Whenever you query for an entity that has persistent associations and
these associations are mapped as EAGER, they will automatically be
loaded together with the entity being queried and is thus immediately
available to your application.
http://doctrine-orm.readthedocs.org/en/latest/reference/working-with-objects.html#by-eager-loading
The entity being queried has eager on createdBy so it will be populated.
to bypass you can create a method in your entity repository as following :
// join entities and load wanted fields
public function findCustom()
{
return $this->getEntityManager()
->createQuery(
'SELECT p FROM AppBundle:Product p ORDER BY p.name ASC'
)
->getResult();
}
hope this helps you
try this and let me know if it works, you should fill the right repository name
'XXXXBundle:CustomerYYYY', 'c'
public function findUser($user_id){
$qb = $this->_em->createQueryBuilder('c')
->select(array('c', 'cb.id', 'cb.name'))
->from('XXXXBundle:Customer', 'c')
->where('c.id <> :id')
->leftJoin('c.createdBy', 'cb')
->setParameter('id', $user_id)->getQuery();
if ($qb != null)
return $qb->getOneOrNullResult();
return null;
}

Best way to reference an entity type as a field on another entity in Symfony2 and Doctrine2

In my SF2/Doctrine2 application i have a entity "User", then another abstract parent entity (which I'll call plant for simplicity's sake) with two subtypes (tree and bush). What I want is for the user to be able to configure which plant subtype they are going to use, then any time they create a new Plant it creates one of this subtype, tree or bush.
My discriminator column looks like this on plant:
/**
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="integer")
* #ORM\DiscriminatorMap({
* 0 = "Tree",
* 1 = "Bush"
* })
*/
My question is how best to create a field on the user entity to select the entity type the User wants to reference. The best I can come up with is something like:
/**
* #ORM\Column(name="plantType", type="integer")
*/
protected $plantType;
Then needing to duplicate this data in another function somewhere on the User class:
public function createNewPlant(){
if ($this->plantType == 0) return new Tree();
elseif ($this->plantType == 1) return new Bush();
}
This seems wrong to me since I'm mapping the integer value to the plant type in two places. Is there a better method?
Thanks a bunch!

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