DQL many to many and count - php

I'm using Symfony 2 with Doctrine, and I've got two entities joined in a many to many association.
Let's say I have two entities: User and Group, and the related tables on db are users, groups and users_groups.
I'd like to get the top 10 most populated groups in DQL, but I don't know the syntax to perform queries on the join table (users_groups). I already looked on the Doctrine manual but I didn't found the solution, I guess I still have a lot to learn about DQL.
In plain sql that would be:
select distinct group_id, count(*) as cnt from users_groups group by group_id order by cnt desc limit 10
Can you please help me to translate this to DQL?
Update (classes):
/**
* Entity\E_User
*
* #ORM\Table(name="users")
* #ORM\Entity
*/
class E_User
{
/**
* #ORM\ManyToMany(targetEntity="E_Group", cascade={"persist"})
* #ORM\JoinTable(name="users_groups",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="cascade")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_id", referencedColumnName="id", onDelete="cascade")}
* )
*/
protected $groups;
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/* ... other attributes & getters and setters ...*/
}
/**
* Entity\E_Group
*
* #ORM\Table(name="groups")
* #ORM\Entity
*/
class E_Group
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="text", type="string", length=255)
*/
private $name;
/* ... other attributes & getters and setters ...*/
}

It's not easy without seeing the actual classes, but by guessing you have a many-to-many bidirectional relationship:
$dql = "SELECT g.id, count(u.id) as cnt FROM Entity\Group g " .
"JOIN g.users u GROUP BY g.id ORDER BY cnt DESC LIMIT 10;";
$query = $em->createQuery($dql);
$popularGroups = $query->getArrayResult();
UPDATE:
You don't have to use a bidirectional relationship, you can query the other way around:
$dql = "SELECT g.id, count(u.id) as cnt FROM Entity\User u " .
"JOIN u.groups g GROUP BY g.id ORDER BY cnt DESC LIMIT 10;";

For those who want to build the query with Doctrine's QueryBuilder instead of using DQL directly take this solution.
Please note that my problem wasn't to get the top user groups, but technically the problem is pretty similar to mine. I work with posts (like articles/blog posts) and tags that are added to posts. I needed to determine a list of related posts (identified by same tags). That list had to be sorted by relevance (the more same tags another post has the more relevant it is).
This is the method of my PostRepository class:
/**
* Finds all related posts sorted by relavance
* (from most important to least important) using
* the tags of the given post entity.
*
* #param Post $post
*
* #return POST[]
*/
public function findRelatedPosts(Post $post) {
// build query
$q = $this->createQueryBuilder('p')
->addSelect('count(t.id) as relevance')
->innerJoin('p.tags', 't')
->where('t.id IN (:tags)')
->setParameter('tags', $post->getTags())
->andWhere('p.id != :post')
->setParameter('post', $post->getId())
->addGroupBy('p.id')
->addOrderBy('relevance', 'DESC')
->getQuery();
// execute query and retrieve database result
$r = $q->execute();
// result contains arrays, each array contains
// the actual post and the relevance value
// --> let's extract the post entities and
// forget about the relevance, because the
// posts are already sorted by relevance
$r = array_map(function ($entry) {
// first index is the post, second one
// is the relevance, just return the post
return reset($entry);
}, $r);
// array of posts
return $r;
}
Thank you #Tom Imrei for you solution. Also the answer #26549597 was very helpful.

To improve Tom's answer, you could use DQL's HIDDEN keyword. This way, the result doesn't contain the useless cnt column and Arvid's array_map solution isn't needed (which could speed up the result significantly for larger queries).
And the OP's question was to get the top 10 groups, not just their IDs.
It would look something like this:
$query = 'SELECT g, HIDDEN COUNT(u.id) AS cnt FROM Entity\Group g LEFT JOIN g.users u ORDER BY cnt DESC';
$groups = $em->createQuery($query)->setMaxResults(10)->getResult();
Also, note the use of LEFT JOIN that ensures that empty groups are not dismissed.

Related

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

How does inner join work on a many-to-many relationship using Doctrine and Symfony2

I recently worked out an issue with querying ManyToMany relationship join tables, the solution was same as this answer and was wondering how it works.
lets say i have a simple ManyToMany relationship between groups and team, there will be a groups_team tables that will automatically be created here
groups entity
/**
* Groups
*
* #ORM\Table(name="groups")
* #ORM\Entity(repositoryClass="AppBundle\Model\Repository\GroupsRepository")
*/
class Groups {
/**
* #ORM\ManyToMany(targetEntity="Team", inversedBy="group")
*/
protected $team;
public function __construct() {
$this->team = new ArrayCollection();
}
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="groupname", type="string", length=255)
*/
private $groupname;
//obligatory getters and setters :)
team entity
/**
* Team
*
* #ORM\Table(name="team")
* #ORM\Entity(repositoryClass="AppBundle\Model\Repository\TeamRepository")
*/
class Team {
/**
* #ORM\ManyToMany(targetEntity="Groups", mappedBy="team")
*/
protected $group;
public function __construct(){
$this->group = new ArrayCollection();
}
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="teamname", type="string", length=255)
*/
private $team;
//[setters and getters here]
in order to get all the teams in a group i would have to query the groups_team table.i would have directly queried the table in just mysql but in symfony i have to do this
$groups = $em->getRepository("AppBundle\Model\Entity\Groups")->findBy(array('tournament' => $tournament->getId()));
//get all teams with group id in groups_team table
foreach ($groups as $group) {
$teamsingroup = $em->getRepository("AppBundle\Model\Entity\Team")->createQueryBuilder('o')
->innerJoin('o.group', 't')
->where('t.id = :group_id')
->setParameter('group_id', $group->getId())
->getQuery()->getResult();
echo "</b>".$group->getGroupname()."</b></br>";
foreach ($teamsingroup as $teamingroup) {
echo $teamingroup->getTeam()."</br>";
}
}
Can someone explain to me how the innerJoin is working and what is the concept behind this, maybe a few documentation to learn about this. are there better way to do this with symfony and doctrine.
Using ManyToMany between 2 entities involves a third table generally called as a junction table in this type of relation when you build a DQL (doctrine query) doctrine automatically joins junction table depending on the nature of relation you have defined as annotation so considering your query
$teamsingroup = $em->getRepository("AppBundle\Model\Entity\Team")
->createQueryBuilder('o')
->innerJoin('o.group', 't')
You are joining Team entity with Group entity in innerJoin('o.group') part o is the alias for Team entity and o.group refers to property defined in Team entity named as group.
/**
* #ORM\ManyToMany(targetEntity="Groups", mappedBy="team")
*/
protected $group;
Which has a ManyToMany annotation defined for this type of relation doctrine joins your team table first with junction table and then joins your junction table with groups table and the resultant SQL will be something like
SELECT t.*
FROM teams t
INNER JOIN junction_table jt ON(t.id = jt.team_id)
INNER JOIN groups g ON(g.id = jt.group_id)
WHERE g.id = #group_id
Another thing related your way of getting team for each group you can minimize your code by excluding createQueryBuilder part within loop, once you have defined teams property as ArrayCollection i.e $this->team = new ArrayCollection(); on each group object you will get collections of teams associated to that particular group by calling getTeam() function on group object similar to below code.
foreach ($groups as $group) {
$teamsingroup = $group->getTeam();
echo "</b>".$group->getGroupname()."</b></br>";
foreach ($teamsingroup as $teamingroup) {
echo $teamingroup->getTeam()."</br>";
}
}
I guess it's literally select statement with INNER JOIN using key columns defined entity class as mappedBy or inversedBy.
Why don't you have a look of doctrine log and see what the native sql is composed?
How to get Doctrine to log queries in Symfony2 (stackoverflow)
http://vvv.tobiassjosten.net/symfony/logging-doctrine-queries-in-symfony2/ (some code examples)
I don't know your user story behind this, but I also heard that it is recommended to use one to many relationship instead of many to many, unless there is a strong reason to do so, as most of cases can be handled by one to many by reconsidering models.

load related entiites from doctrine collection symfony2

I have category table and make table. Both tables related by third category_make table creating many to many relationship.
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="Ladisi\MotorsBundle\Entity\Make", inversedBy="catogory", fetch="EAGER")
* #ORM\JoinTable(name="catogory_make",
* joinColumns={
* #ORM\JoinColumn(name="catogory_id", referencedColumnName="cat_id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="make_id", referencedColumnName="make_id")
* }
* )
*/
private $make;
I want to get makes that belongs to particular category. I have tried,
$query = $em
->createQuery(
'SELECT c, m FROM LadisiMotorsBundle:Catagory c
JOIN c.make m
WHERE c.catId= :id'
)->setParameter('id', $id);
$result = $query->getResult();
but every time I get only category fields, make entity is not available in result. I also tried to get makes just by calling getMakes method on catagory object, it also returns null (not entity, i guess proxy). How do i solve this. Any help would be great.
You've already created a linking table, so the only thing you have to do (if you have your entities configured correctly) in your controller:
$catagory = $this->getDoctrine()->getManager()->getRepository('LadisiMotorsBundle:Catagory')->findOneBy(['id' => $id]);
$catagory->getMakes();

doctrine query builder join adding table twice

UPDATED at bottom:
I am trying to do what should be a simple join between two tables. I have a Gig table and a Venue table for a simple band site that I am building using Symfony2 (2.2). It's my first time with Symfony2 and doctrine so it is possible I am going completely in the wrong direction. I have created and populated the tables with DataFixtures and have verified that the ID relationships are correct. The problem I am getting is that the resulting DQL query has the Gig table referenced twice in the FROM section and that is causing me to get back several instances of the same record instead of the x number of records I am expecting. I don't know what I am doing wrong for that to be happening. Also, there may be an easier way of doing this but I am exploring all of my options since I am teaching myself Symfony2 in the process of building the site.
The Gig table contains a venue_id pointing to a Venue table that is defined in the Gig entity as a ManyToOne relationship (shown below). Using a doctrine findAll everything seems to work fine with the Venue class in the Gig Entity being populated correctly. I am trying to create a flat view of a few of the most recent Gigs to be displayed on the front page so I figured I would try to use a Join and include only the fields I need.
Here is the Repository Query:
public function getGigsWithLimit($maxGigs)
{
$qb = $this->createQueryBuilder('b')
->select('
g.gigDate,
g.startTime,
g.endTime,
g.message1 as gig_message1,
g.message2 as gig_message2,
g.url,
v.name,
v.address1,
v.address2,
v.city,
v.state,
v.zip,
v.phone,
v.url as venue_url,
v.message1 as venue_message1,
v.message2 as venue_message2,
v.message3 as venue_message3'
)
->from('WieldingBassBundle:Gig', 'g')
->leftJoin('g.venue', 'v')
->orderBy('g.gigDate', 'DESC')
->setMaxResults($maxGigs);
return $qb->getQuery()->getResult();
}
Here is the DQL it creates:
SELECT
g0_.id AS id0,
g0_.gig_date AS gig_date1,
g0_.start_time AS start_time2,
g0_.end_time AS end_time3,
g0_.message1 AS message14,
g0_.message2 AS message25,
g0_.url AS url6,
v1_.name AS name7,
v1_.address1 AS address18,
v1_.address2 AS address29,
v1_.city AS city10,
v1_.state AS state11,
v1_.zip AS zip12,
v1_.phone AS phone13,
v1_.url AS url14,
v1_.message1 AS message115,
v1_.message2 AS message216,
v1_.message3 AS message317
FROM
Gig g2_,
Gig g0_
LEFT JOIN
Venue v1_ ON g0_.venue_id = v1_.id
LIMIT
6
The Gig g2_ is my problem. If I delete it and execute the query everything is as expected. I don't know what is generating that.
The first table Gigs Entity looks like this (I am leaving out the getters and setters):
/**
* Gig
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Wielding\BassBundle\Entity\GigRepository")
* #ORM\HasLifecycleCallbacks
*/
class Gig
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #ORM\Column(name="gig_date", type="date")
*/
private $gigDate;
/**
* #var \DateTime
*
* #ORM\Column(name="start_time", type="datetime")
*/
private $startTime;
/**
* #var \DateTime
*
* #ORM\Column(name="end_time", type="datetime")
*/
private $endTime;
/**
* #var string
*
* #ORM\Column(name="message1", type="string", length=50, nullable=true)
*/
private $message1;
/**
* #var string
*
* #ORM\Column(name="message2", type="string", length=50, nullable=true)
*/
private $message2;
/**
* #var string
*
* #ORM\Column(name="url", type="string", length=128, nullable=true)
*/
private $url;
/**
* #var integer
*
* #ORM\Column(name="venue_id", type="integer")
*/
private $venueId;
/**
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #ORM\Column(type="datetime")
*/
protected $updated;
/**
* #ORM\ManyToOne(targetEntity="Venue", cascade="persist")
* #ORM\JoinColumn(name="venue_id", referencedColumnName="id")
*/
protected $venue;
The Venue table is simple and does not have any relationships defined so I will leave it out unless it is asked for.
Any ideas? Thanks for any help.
Andrew
I removed everything except what would recreate the problem and here is what I was left with:
I simplified the repository method to:
public function getGigsWithLimit2($maxGigs)
{
$qb = $this->createQueryBuilder('a')
->select('g.id')
->from('WieldingBassBundle:Gig', 'g')
->setMaxResults($maxGigs);
return $qb->getQuery()->getResult();
}
This now generates:
SELECT
g0_.id AS id0
FROM
Gig g1_,
Gig g0_
LIMIT
6
There is that darn Gig g1_ problem again. I got the "Explain Query" from the Symfony profiler and it shows:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE g1_ index IDX_ED7D664240A73EBA 4 9 Using index
1 SIMPLE g0_ index IDX_ED7D664240A73EBA 4 9 Using index; Using join buffer
I don't pretend to know what that means but it shows both table entries with different information about how it was used.
whats the use of "venueId" if you already got "venue" which contains the foreign key?
I found the problem. I am not used to Doctrine and was using the ->from in a repository that did not need it since the entity was automatically related through the annotations. My earlier query that worked was in the controller and not a repository so the ->from was necessary.
You are trying to do SQL. Doctrine is different. Your query fetches every field. Doctrine prefers to fetch entities. I think you probably want this:
public function getGigsWithLimit($maxGigs)
{
$qb = $this->createQueryBuilder('g')
->leftJoin('g.venue', 'v')
->orderBy('g.gigDate', 'DESC')
->setMaxResults($maxGigs);
return $qb->getQuery()->getResult();
}
The return result is a list of entities which you can call all the specific methods directly. You are still welcome to specify fields with doctrine, if you want partial objects, but I've found the normal method of fetching the entire entity covers most of my needs.
This is a fundamentally different paradigm than SQL and will take some getting used to.

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