Double association between 2 tables with Doctrine2 PHP - php

I'm trying to do an association of 5 objects with Doctrine2 (PHP).
I'm using PostgreSQL.
Here is the Database schema:
Database schema
A company may have many Hubs, each Hub have one Harbor.
A company may have many Line, each Line have a Linelist.
A Linelist have 2 Harbors.
For example, a Linelist is "Los Angeles-Seattle", and multiple companies may own it thanks to the Line table.
I'm trying to query all the Hub, Harbor, Linelist, and Line for one company.
I have the SQL query:
SELECT *
FROM hub h
JOIN harbor a
ON a.id = h.harbor_id
JOIN linelist l
ON (l.harborstart_id = a.id OR l.harborend_id = a.id)
JOIN line m
ON m.linelist_id = l.id
WHERE h.company_id = 41
AND m.company_id = 41"
I'm trying to do the same using DQL.
I tried this, but it doesn't worked:
$query = $this->getEntityManager()
->createQuery('SELECT h, a, l, m
FROM AmGameBundle:Hub h
JOIN h.harbor a
JOIN a.linelist l
JOIN l.line m
WHERE h.company = :company_id
AND m.company = :company_id')
->setParameter('company_id', $company_id);
As a result, I only have the LineList and Line objects matching harborstart_id, but I want the one matching either harborstart_id or harborend_id.
Do you think this is possible in DQL?
It might be better to change the relation between Harbor and Linelist for a many to many?

I think that's a matter of defining 2 relations from harbor to linelist in your Entities. I imagine you have something like
<?php
/**
* #Entity
* #Table(name="LineList")
*/
class LineList {
/**
* #var object $startHarbor
* #ManyToOne(targetEntity="Harbor", inversedBy="startHarbors")
* #JoinColumn(name="harborstart_id", referencedColumnName="id", nullable=FALSE)
*/
protected $startHarbor;
}
/**
* #Entity
* #Table(name="Harbor")
*/
class Harbor {
/**
* #var object $startHarbors
* #OneToMany(targetEntity="LineList", mappedBy="startHarbor")
*/
protected $startHarbors;
}
That will let you join Harbors to LineLists via harborstart_id (you named the variable linelist, but I think now it's better to change the identifiers as there will be 2 referring to the same foreign table), then if you want to harborend_id
<?php
/**
* #Entity
* #Table(name="LineList")
*/
class LineList {
/**
* #var object $startHarbor
* #ManyToOne(targetEntity="Harbor", inversedBy="startHarbors")
* #JoinColumn(name="harborstart_id", referencedColumnName="id", nullable=FALSE)
*/
protected $startHarbor;
/**
* #var object $endHarbor
* #ManyToOne(targetEntity="Harbor", inversedBy="endHarbors")
* #JoinColumn(name="harborend_id", referencedColumnName="id", nullable=FALSE)
*/
protected $endHarbor;
}
/**
* #Entity
* #Table(name="Harbor")
*/
class Harbor {
/**
* #var object $startHarbors
* #OneToMany(targetEntity="LineList", mappedBy="startHarbor")
*/
protected $startHarbors;
/**
* #var object $endHarbors
* #OneToMany(targetEntity="LineList", mappedBy="endHarbor")
*/
protected $endHarbors;
}
Now you can change the DQL to:
$query = $this->getEntityManager()
->createQuery('SELECT h, a, sh, eh, m
FROM AmGameBundle:Hub h
JOIN h.harbor a
JOIN a.startHarbors sh
JOIN a.endHarbors eh
JOIN l.line m
WHERE h.company = :company_id
AND m.company = :company_id')
->setParameter('company_id', $company_id);
That should get you in the right direction. If it becomes troublesome though, a many-to-many approach as you speculated should be a well documented solution.

Related

Doing a COUNT(*) GROUP BY based on a ManyToMany relation using DQL?

I have two entities Person and Skill, where a Person may have multiple skills.
Person
/**
* #ORM\Entity(repositoryClass="App\Repository\PersonRepository")
*/
class Person
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Skill", inversedBy="people")
*/
private $skills = [];
// other fields and getters/setters
}
Skill
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\SkillRepository")
*/
class Skill
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Person", mappedBy="skills")
*/
private $people = [];
// other fields and getters/setters
}
I have a form where I can filter people by skills, each skill is a checkbox and I want the number of people having that skill along with the checkbox's label.
The result is that:
I got it working using the following native query:
SELECT s.id, COUNT(*) AS c
FROM skill s
JOIN person_skill ps /* table required by the M2M relation between person and skill */
ON s.id = ps.skill_id
GROUP BY s.id
As you can see, I require a JOIN on the ManyToMany table in order to get those counts.
How could I do this using Doctrine's DQL instead of using a native query?
Actually when mapping entities with relations, Doctrine uses a custom object named ArrayCollection.
It comes with many methods, such as filter() and count().
You can add a method to your skill entity if you want that would use the count method of the ArrayCollection (people).
To make sure you use the ArrayCollection properly you'll have to change your Skill class like this:
class Skill
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Person", mappedBy="skills")
*/
private $people; //<-- Removed the default array definition
public function __construct()
{
$this->people = new ArrayCollection(); //Add this line in your constructor
}
public function countPeople()
{
return $this->people->count(); //Will return the number of people joined to the skill
}
// other fields and getters/setters
}
Allright, I found the solution:
$rows = $this->_em->createQueryBuilder('s')
->select('s.id, COUNT(p.id) AS c')
->from(Skill::class, 's')
->join('s.people', 'p')
->groupBy('s.id')
->getQuery()
->getArrayResult();
It generates the following query:
SELECT s0_.id AS id_0, COUNT(p1_.id) AS sclr_1
FROM skill s0_
INNER JOIN person_skill p2_
ON s0_.id = p2_.skill_id
INNER JOIN person p1_
ON p1_.id = p2_.person_id
GROUP BY s0_.id

Doctrine 2 joining table, ManyToOne unidirectional, where tbl2.value = :value

I'm trying to figure out how to join two tables, while querying the second table. I thought it was as simple as:
// Within ServerServiceRepository
return $this->createQueryBuilder('ss')
->join(ServiceType::class, 't')
->where('t.serviceTypeName = :name')
->getQuery()
->execute(['name' => $name]);
But turns out, not so much...
The issue is the query is NOT joining the keys (service_type_id on both tables). What's going on here? I have all the OneToMany relationships setup correctly:
/**
* ServerService
*
* #ORM\Table(name="server_services")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ServerServiceRepository")
*/
class ServerService extends AbstractEntity
{
/**
* #var ServiceType
*
* #ORM\Column(name="service_type_id", type="integer", nullable=false)
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Supportal\ServiceType", fetch="LAZY", inversedBy="serviceTypeId")
* #ORM\JoinColumn(name="service_type_id", referencedColumnName="service_type_id")
*/
private $serviceType;
// [...]
}
/**
* ServiceType
*
* #ORM\Table(name="service_types")
* #ORM\Entity
*/
class ServiceType extends \AppBundle\Entity\AbstractEntity
{
/**
* #var integer
*
* #ORM\Column(name="service_type_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\ServerService", fetch="EXTRA_LAZY", mappedBy="serviceType")
*/
private $serviceTypeId;
/**
* #var string
*
* #ORM\Column(name="service_type_name", type="string", length=255, nullable=true)
*/
private $serviceTypeName;
// [...]
}
I've added / removed the OneToMany relationship from ServiceType to no change. This is really a unidirectional relationship. Per Doctrine's own docs (Chapter 5), ServerType does not require a relationship mapping.
The SQL query is generating a JOIN that's missing the actual keys:
INNER JOIN service_types s1_ ON (s1_.service_type_name = ?)
What am I missing here on Doctrine to get this working right? I've looked at the tutorials. Symofny's example is almost exact what I'm after, except I need to select by "category name" not Product id: https://symfony.com/doc/2.8/doctrine/associations.html
I've got to be missing something so super simple. But I can't for the life of me peg it...
Edit:
I've removed the OneToMany from ServiceType in my code. It's optional. Not needed for this anyway. This is a unidirectional relationship.
I've tried this:
return $this->createQueryBuilder('ss')
->join('ss.serviceType', 't')
->where('t.serviceTypeName = :name')
->getQuery()
->execute(['name' => $name]);
Resulting in this error:
[Semantical Error] line 0, col 85 near 't WHERE t.serviceTypeName': Error: Class AppBundle\Entity\ServerService has no association named serviceType
Solution
The solution was removing the #ORM Column definition. Looks like it's a conflict in the relationship definitions.
First of all, change the ManyToOne docblock definition to:
/**
* #var ServiceType
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Supportal\ServiceType", fetch="LAZY", inversedBy="serverService")
* #ORM\JoinColumn(name="service_type_id", referencedColumnName="service_type_id")
*/
private $serviceType;
Also change the OneToMany, remove line #ORM\OneToMany(targetEntity="AppBundle\Entity\ServerService", fetch="EXTRA_LAZY", mappedBy="serviceType")
Create new field serverService for the OneToMany relation:
/**
*
* #ORM\OneToMany(targetEntity="ServerService", mappedBy="serviceType")
*/
private $serverService;
You should join on the relation field, in this case serviceType. The way you defined the join is like selecting both tables.
Change to:
return $this->createQueryBuilder('ss')
->join('ss.serviceType', 't')
->where('t.serviceTypeName = :name')
->getQuery()
->execute(['name' => $name]);
Since you are applying a condition on the joined result here, using a LEFT JOIN or simply JOIN is the same.
References:
How to Work with Doctrine Associations / Relations
how to do left join in doctrine
Left join ON condition AND other condition syntax in Doctrine

Doctrine many to many select

I have two entities, Group and User:
class Group
{
/**
* #ORM\ManyToMany(targetEntity="Group", inversedBy="groups")
* #ORM\JoinTable(name="admin_group_user",
* joinColumns={#ORM\JoinColumn(name="fk_group", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="fk_user", referencedColumnName="id")}
* )
*/
protected $users;
...
}
class User
{
/**
* #ORM\ManyToMany(targetEntity="Group", inversedBy="users")
* #ORM\JoinTable(name="admin_group_user",
* joinColumns={#ORM\JoinColumn(name="fk_user", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="fk_group", referencedColumnName="id")}
* )
*/
protected $groups;
...
}
I would like to get result like
Group 1 has user A, user B, user C
Group 2 has user D, user E, user F.
Generally something like
SELECT admin_group.id AS group_id, admin_group.name, agu.fk_user, fu.username
FROM admin_group
JOIN admin_group_user agu ON (admin_group.id = agu.fk_group)
JOIN front_user fu ON (agu.fk_user = fu.id);
Does anyone know how to achieve this with Doctrine?
Following documentation about many-to-many bidirectional mapping on doctrine helps you solve your problem:
http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-bidirectional

How to prevent redundant lookups when joining entity relationships?

I'm getting up to speed with Symfony 2 and Doctrine and having difficulties with the number of unnecessary database lookups being performed to hydrate with joined entities.
After performing a joined query with a child object, the child is automatically pulling its other mappings from the database. It's doing this despite that I'm not attempting to access any of its properties. It's as if they're being accessed inside the find query.
My example looks like the below - There are entities called Person and Building that both join an entity called Place:
class Person {
/**
* Where this person lives
* #var Place $Home
*
* #ORM\OneToOne(targetEntity="Place", cascade={"persist"}, inversedBy="Resident" )
* #ORM\JoinColumn(name="place_id", referencedColumnName="id")
*/
private $Home;
}
class Building {
/**
* Where this building stands
* #var Place $Site
*
* #ORM\OneToOne(targetEntity="Place", cascade={"persist"}, inversedBy="Landmark" )
* #ORM\JoinColumn(name="place_id", referencedColumnName="id")
*/
private $Site;
}
class Place {
/**
* Reverse mapping
* #var Person $Resident
*
* #ORM\OneToOne(targetEntity="Person", mappedBy="Home")
*/
private $Resident;
/**
* Reverse mapping
* #var Building $Landmark
*
* #ORM\OneToOne(targetEntity="Building", mappedBy="Site")
*/
private $Landmark;
}
My Person repository join looks like this:
/**
* #override
* #return Person
*/
public function find( $id ){
$query = $this->getEntityManager()
->createQuery('
SELECT p, h
FROM MyBundle:Person p
JOIN p.Home h
WHERE p.id = :id'
)->setParameter('id', $id);
return $query->getSingleResult();
}
How can I prevent the Place fetching its Building relationship separately during the find operation? Is there something I can pass into the Query instance to stop this?
add fetch option to your mapping.
Like so:
class Place {
/**
* Reverse mapping
* #var Person
*
* #ORM\OneToOne(targetEntity="Person", mappedBy="Home", fetch="EXTRA_LAZY")
*/
private $Resident;
}

Doctrine - entities not being fetched

I have some entities with a one-to-many / many-to-one relationship -
Production class -
/**
* #OneToMany(targetEntity="ProductionsKeywords", mappedBy="production")
*/
protected $productionKeywords;
ProductionsKeywords class -
/**
* #ManyToOne(targetEntity="Production", inversedBy="productionKeywords")
* #JoinColumn(name="production_id", referencedColumnName="id", nullable=false)
* #Id
*/
protected $production;
/**
* #ManyToOne(targetEntity="Keyword", inversedBy="keywordProductions")
* #JoinColumn(name="keyword_id", referencedColumnName="id", nullable=false)
* #Id
*/
protected $keyword;
Keyword class -
/**
* #OneToMany(targetEntity="ProductionsKeywords", mappedBy="keyword")
*/
protected $keywordProductions;
If I write a DQL query like
$query = $this->entityManager->createQuery("SELECT p FROM \Entity\Production p");
The productions, productionKeywords and keywords all load fine, however if I try to fetch join the productionKeywords and keywords like
$query = $this->entityManager->createQuery("SELECT p, pk, k FROM \EntityProduction p
LEFT JOIN p.productionKeywords pk
LEFT JOIN pk.keyword k
");
then the entities are not loaded.
Not sure what I'm doing wrong as I have the same relationship setup with some other entities and it works fine with them.
OK, so it seems that if you have a 'pure' join entity (like the ProductionsKeywords class above) then it can't be used in a fetch query. I got around the issue by using a timestamp column in the productions_keywords table as another property in the ProductionsKeywords class and then the fetch joins began to work.

Categories