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
Related
Given this code, Doctrine launchs as many querys as rows I have in table2
$qb = $this->getModelManager()->createQuery($er->getClassName(), 't1')->getQueryBuilder();
$qb->select('t1, t2, t3')
->innerJoin('table1.table2', 't2');
->innerJoin('table2.table3', 't3')
->where('t3.id = :foo')
->setParameter('foo', $foo);
each query is like this one:
SELECT t0.id AS id_1,
t0.name AS name_2,
t0.slug AS slug_3,
t0.description AS description_4,
t0.visible AS visible_5
FROM table2 t0
WHERE t0.id = ?
and those are the entities:
TABLE1: The main entity, witch is related to table 2 by manyToOne if I make the innerJoin with table 2, Doctrine act as expected (1 query)
/**
* #ORM\Entity(repositoryClass = "Table1Repo")
* #ORM\Table(name="table1")
*/
class table1 extends BaseTable1 implements table1Interface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
* #Gedmo\Versioned
*/
protected $name;
/**
* #ORM\ManyToOne(targetEntity="table2", inversedBy="tables1")
* #ORM\JoinColumn(name="table2_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $table2;
}
TABLE2, related with table one by OneToMany, and with table 3 by ManyToMany.
/**
* #ORM\Entity(repositoryClass="table2Repository")
* #ORM\Table(name="table2")
*/
class table2 extends Basetable2
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="table3", inversedBy="table2s")
* #ORM\JoinTable(name="table3_table2")
*/
protected $table3;
/**
* #ORM\OneToMany(targetEntity="table1", mappedBy="table2")
* #Accessor(getter="getTables1")
*/
protected $tables1;
}
TABLE3: oly related with table 2 by a ManyToMany relation. When I make the innerJoin with table 2, Doctrine still acts as expected, making only one query
/**
* #ORM\Entity(repositoryClass = "table3Repo")
* #ORM\Table(name="table3")
* #Gedmo\Loggable
*/
class table3 extends Basetable3
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="table2", mappedBy="tables3")
* #ORM\JoinTable(name="table3_table2")
*/
protected $tables2;
}
So, as I add both innerJoins to the query Builder, Doctrine makes only one query, but is when I add the WHERE clause, when Doctrine makes the 279 querys, one per row in table2, witch is related with table1 by oneToMany and with table3 by ManyToMany.
Other relevant point is that the querybuilder is beeing executed under SonataAdmin query_builder fiel option.
I can't find why I'm getting this behaviour, any clue?
When you run a query, you always need to have a root entity which joins to others. In your case, that is table1. After fetching, based on query and entity metadata, Doctrine will attempt to create an object for each instance of root, but unless told otherwise, it will stop there. In fact, for each sub-objects (e.g. table2), it will create dummy "proxy" objects which are barely shallow representations, and are to be resolved from DB whenever to try to dereference them. The process of converting DB results to objects is known as object hydration.
In order to do perform hydration of sub-objects, you need to "select" sub-entity as well:
$qb->select('t1', 't2', 't3')
->innerJoin('t1.table2', 't2');
->innerJoin('t2.table3', 't3')
->where('t3.id = :foo')
->setParameter('foo', $foo);
Pay attention not to go crazy with fetching everything, as it takes longer and consumes reasonably more RAM. Fine-tune your query, until you reach what you want (logic and performance-wise).
Hope this helps...
I have two entities linked by a one-to-many unidirectional with join table association.
use Doctrine\ORM\Mapping as ORM;
/**
* #Entity(repositoryClass="FooRepository")
*/
class Foo
{
/**
* #var Collection
* #ORM\ManyToMany(targetEntity="Bar")
* #ORM\JoinTable(
* name="foo_bar",
* inverseJoinColumns={#ORM\JoinColumn(unique=true)}
* )
*/
private $bars;
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// [...]
}
/**
* #Entity(repositoryClass="BarRepository")
*/
class Bar
{
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// [...]
}
I'd like to create a method into my BarRepository class using the a foo id and a bar id which return one or null Bar object.
Actually my class looks like this:
use Doctrine\ORM\EntityRepository;
class BarRepository extends EntityRepository
{
/**
* Finds a single bar.
* #param int $fooId The foo identifier.
* #param int $barId The bar identifier.
* #return Bar|null
*/
public function findOneByFoo($fooId, $barId)
{
$qb = $this->createQueryBuilder('b');
$qb->innerJoin('Foo', 'f', Expr\Join::WITH, 'f.id = :fooId')
->where('b.id = :barId')
->setParameter('fooId', $fooId)
->setParameter('barId', $barId)
->getQuery()->getOneOrNullResult();
}
}
But this always returns a bar object even if the bar id is not associated to the foo object.
OK, thanks to staskrak, I rewrite my "query" like this, and it works fine.
The Foo and Bar entities are the same.
I kept the same base of the query but added an inner join between the Foo->bars property and the Bar entity.
use Doctrine\ORM\EntityRepository;
class BarRepository extends EntityRepository
{
public function findOneByFoo($fooId, $barId)
{
$parameters = [
':fooId' => $fooId,
':barId' => $barId
];
$qb = $this->createQueryBuilder('b');
return $qb
->innerJoin('Foo', 'f', 'WITH', 'f.id = :fooId')
// below, the added inner join
// it makes the link between the Foo->bars property and the Bar entity
->innerJoin('f.bars', 'fb', 'WITH', 'b.id = fb.id')
->where('b.id = :barId')
->setParameters($parameters)
->getQuery()
->getOneOrNullResult();
}
}
First of all! It's not the required but I suggest you always write
the full mapping in annotations.
Let's look at you entities. We can make the next assertion about your
One-To-Many:
One Foo objects can have many Bar objects, but every Bar object refer to
only one and no more Foo object.
For example One person can have a lot of credit cards, but every credit
card belongs to one single person.
Therefore we can write down:
/**
* #Entity(repositoryClass="FooRepository")
*/
class Foo
{
/**
* Unidirectional One-To-Many
* One Foo has many Bar, however Bar has only one Foo
*
* #ORM\ManyToMany(targetEntity="Bar")
* #ORM\JoinTable(
* name="foo_bar_table",
* joinColumns={#JoinColumn(name="foo_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="bar_id", referencedColumnName="id", unique=true)
*/
private $bars;
/**
* Foo constructor
*/
public function __construct()
{
$this->bars = new \Doctrine\Common\Collections\ArrayCollection();
}
Of course you always will have a bar object with id you have typed.
Why? Let's look at your relations(tables).
Foo - this is a table, which has field id.
Bar - this is a table, which has field id.
foo_bar_table - this table has foo_id, bar_id.
When you make the join - you just add to the one table another one.
These tables don't have association between each other. So you wanted
the Bar object - you got it.
You need to get the Bar object from the Bar repository. It will be
better.
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.
I have an Entity Video related with a Entity Category and I need to run this SQL with Doctrine QueryBuilder, with this I can get the most used categories in all videos (1000+):
SELECT c.*
FROM Video v
INNER JOIN video_category vc ON vc.video_id = v.id
INNER JOIN Category c ON vc.category_id = c.id
GROUP BY c.id
HAVING COUNT(v.id) > 1000
ORDER BY c.name ASC;
My querybuilder:
$queryBuilder = $this->getEntityManager()
->createQueryBuilder()
->select('c')
->from('AcmeVideoBundle:Video', 'v')
// Can Doctrine join itself silently with relational info in the Entities?
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
->groupBy('c.id')
->having('COUNT(v.id) > 1000')
->orderBy('c.name', 'ASC')
->getQuery();
But the SQL query output by queryBuilder is this:
SELECT c0_.id AS id0, c0_.NAME AS name1
FROM Video v1_
INNER JOIN Category c0_ ON (v1_.id = c0_.id)
GROUP BY c0_.id
HAVING COUNT(v1_.id) > 1000
ORDER BY c0_.NAME ASC
Without the relational table (video_category)
The Entities mapping:
/**
* Video
*
* #ORM\Table
* #ORM\Entity(repositoryClass="Acme\VideoBundle\Entity\VideoRepository")
*/
class Video
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Acme\CategoryBundle\Entity\Category", cascade={"persist"})
*/
private $category;
// More fields, getters and setters etc...
}
/**
* Category
*
* #ORM\Table
* #ORM\Entity(repositoryClass="Acme\CategoryBundle\Entity\CategoryRepository")
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
// More fields, getters and setters etc...
}
How can I use the relation table to run the original SQL query with doctrine Querybuilder? I missed something?
INFO: When I findBy{field}, persist, flush, clear on all entities works fine, the Doctrine relations are ok, I have a Video, Category and video_category tables fine, the original SQL query works perfect.
// Can Doctrine join itself silently with relational info in the Entities?
->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
Yes! In fact that is one of the major reasons for using an ORM such as Doctrine 2.
Try:
->leftJoin('v.category','c')
The manual goes into more details though oddly enough is does not seem to have a join example. Hence the common confusion.
http://docs.doctrine-project.org/en/latest/reference/query-builder.html
And you may not be aware of this but the United Nations has passed a resolution outlawing the use of abbreviations for aliases. Just to be safe, try:
$queryBuilder = $this->getEntityManager()
->createQueryBuilder()
->addSelect('category')
->from('AcmeVideoBundle:Video', 'video')
->leftJoin('video.category', 'category')
->groupBy('category.id')
->having('COUNT(video.id) > 1000')
->orderBy('category.name', 'ASC')
->getQuery();
Ok, solved, the problem was that the Entities isn't fully mapped for ManyToMany bidirectional relationship.
The Entities are now:
class Video
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Acme\CategoryBundle\Entity\Category", inversedBy="video") // Note the inversedBy key
*/
private $category;
}
class Category
{
/**
* #var Video
*
* #ORM\ManyToMany(targetEntity="Acme\VideoBundle\Entity\Video", mappedBy="category") // Again the inversed
*/
private $video; // New field for bidirectional ManyToMany
}
And the final QueryBuilder working (now with full alias version :P):
$queryBuilder = $this->getEntityManager()
->createQueryBuilder()
->select('category')
->from('AcmeCategoryBundle:Category', 'category')
->join('category.video', 'video')
->groupBy('category.id')
->having('COUNT(video.id) > 1000')
->orderBy('category.name', 'ASC')
->getQuery();
Best regards
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.