Doctrine2 fetching rows that have manyToMany association by QueryBuilder - php

everyone.
I have 2 entities City and POI. Mapping looks like this:
class City {
/**
* #ORM\ManyToMany(targetEntity="POI", mappedBy="cities")
* #ORM\OrderBy({"position" = "ASC"})
*/
protected $pois;
and
class POI {
/**
* #ORM\ManyToMany(targetEntity="City", inversedBy="pois")
* #ORM\JoinTable(name="poi_cities")
*/
protected $cities;
I would like to fetch all POIs that have at least 1 association with some City using QueryBuilder. I should probably use exists() function but I don't quiet know how.

You'd have to Left join them and check if cities is null.
$qb->select('p', 'c')
->from('AcmeDemoBundle:POI', 'p')
->leftJoin('p.cities', 'c')
->where('c IS NOT NULL');
I haven't tested it, but I hope it gives you the general direction. You can read more about the QueryBuilder from here.

Docrine2 was changed in 2013, so the other solution displays error Error: Cannot add having condition on a non result variable. Now we cannot use joined alias just as a condition variable. We should use any of its properties like c.id
So you should fix the code to
$qb->select('p', 'c')
->from('AcmeDemoBundle:POI', 'p')
->leftJoin('p.cities', 'c')
->where('c.id IS NOT NULL');
$results = $qb->getQuery()->execute();
If you want to select entities that does not have any cities, use IS NULL.
$qb->leftJoin('p.cities', 'city')
->where('city.id IS NULL')
->getQuery()
->execute();
Description of a problem and link to the commit that responsible for that - http://www.doctrine-project.org/jira/browse/DDC-2780

Related

Create a group by query with inner join using Doctrine Query Builder

I struggle to create a query to get information from two tables. I want to count rows and group it by category and type.
My normal PostgresSQL query looks like this:
SELECT c.name AS category_name, i.type, count(i) AS number_of_items
FROM item i
INNER JOIN category c
ON i.category_id = c.id
GROUP BY c.name, i.type
How do I build this query using Doctrine Query Builder?
What I have tried:
$qb->select(['c.name', 'i.type', 'count(i)'])
->from('AppBundle:Item', 'i')
->innerJoin('i', 'AppBundle:Category', 'c', 'i.category_id = c.id')
->groupBy('c.name')
->groupBy('i.type');
return $qb->getQuery()->getResult();
But this give me an error:
[Semantical Error] line 0, col 80 near 'i AppBundle:Category':
Error: Class 'i' is not defined.
I'm trying to follow the principle in the documentation found here: http://doctrine-orm.readthedocs.io/projects/doctrine-dbal/en/latest/reference/query-builder.html#join-clauses
Any help would be appreciated.
Using doctrine and without defining any mapping between your related entities is not a good practice you should start from Association Mapping
Once you have defined mappings in your entities you can simply join your main entity using the properties which holds the reference of linked entities, doctrine will automatically detects the join criteria you don't need to specify in query builder, sample mapping for your entities can be defined as
class Item
{
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="items")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
}
-
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
/**
* #ORM\OneToMany(targetEntity="Item", mappedBy="category")
*/
private $items;
public function __construct()
{
$this->items = new ArrayCollection();
}
}
And then your query builder will look like
$qb->select('c.name', 'i.type', 'count(i)'])
->from('AppBundle:Category', 'c')
->innerJoin('c.items','i')
->groupBy('c.name')
->addGroupBy('i.type');
Relationship Mapping Metadata
Or if you still don't want to have mappings and use the other approach you have to use WITH clause in doctrine
$qb->select(['c.name', 'i.type', 'count(i)'])
->from('AppBundle:Item', 'i')
->innerJoin('AppBundle:Category', 'c', 'WITH' , 'i.category_id = c.id')
->groupBy('c.name')
->addGroupBy('i.type');
you don't need to use array inside select try this:
$qb->select('c.name', 'i.type', 'count(i)')
->from('AppBundle:Item', 'i')
->innerJoin('i', 'AppBundle:Category', 'c', 'i.category_id = c.id')
->groupBy('c.name')
->groupBy('i.type');
return $qb->getQuery()->getResult();
Tried to use full expression for your entity Like this :
->from('AppBundle\Entity\Item','i')

Doctrine query with nullable/optional join

I got a method on my repository class to get me Article with their join (category, image ...) and not every Article have categories or image, what don't give me back all expected result, cause my current repository function return only Article who have Categories and Image not null and just ignore what having null value.
My Article entity have the following relationship.
/**
* #ORM\ManyToMany(targetEntity="App\ArticleBundle\Entity\Category", cascade={"persist"})
*/
private $categories;
/**
* #ORM\ManyToOne(targetEntity="App\ArticleBundle\Entity\Image", cascade={"persist"})
*
*/
private $image;
And this is my repository function
public function getArticle($id)
{
$qb = $this->createQueryBuilder('a')
->where('a.id = :theId')
->setParameter('theId', $id)
->join('a.author', 'auth')
->addSelect('auth')
->join('a.categories', 'cat')
->addSelect('cat')
->join('a.image', 'img')
->addSelect('img');
return $qb->getQuery()->getOneOrNullResult();
}
Now I want to know if I can get Article which have categories, image or not in one query with the join. I want to say when using the Doctrine lazy loading (by avoid the join in the query) I get the expected result.
Use ->leftJoin() to get Article which have categories, image or not in one query:
public function getArticle($id)
{
$qb = $this
->createQueryBuilder('a')
->addSelect('auth', 'cat', 'img')
->join('a.author', 'auth')
->leftJoin('a.categories', 'cat')
->leftJoin('a.image', 'img')
->where('a.id = :theId')
->setParameter('theId', $id)
;
return $qb->getQuery()->getOneOrNullResult();
}
Thus, avoids extra queries when Doctrine try to load the related properties in a lazy way.
Explanation:
Using ->join() or ->innerJoin():
This is the simplest, most understood Join and is the most common. This query will return all of the records in the left table (table A) that have a matching record in the right table (table B).
Using ->leftJoin():
This query will return all of the records in the left table (table A) regardless if any of those records have a match in the right table (table B). It will also return any matching records from the right table.
Source: Visual-Representation-of-SQL-Joins explained in detail by C.L. Moffatt

Symfony2/Doctrine2 innerJoin using QueryBuilder

I'm trying to build a innerJoin query using Doctrine2/QueryBuilder.
$repo = $this->getDoctrine()
->getRepository('MyBundle:Models');
$query = $repo->createQueryBuilder('m')
->where('m.id = :id')
->setParameter('id', $id);
Doctrine says:
A join always belongs to one part of the from clause. This is why you
have to specify the alias of the FROM part the join belongs to as the
first argument.
As a second and third argument you can then specify the name and alias
of the join-table and the fourth argument contains the ON clause.
Ex.
$queryBuilder
->innerJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
What I can't understand is that 'phonenumbers' table is referencing to Entity Name or DB Table Name.
What I actually want is, is there any way to explicitly refer to entity like
innerJoin('u', 'MyBundle:phonenumbers', 'p', 'u.id = p.user_id')?
It's a bit confusing when it joins just like that. Can please someone explain that to me?
Help!!
You are working on a DQL level with tables, which means that you actually joining a table, so you just need the table name, not the entity name. The table "phonenumbers might not even had an entity to begin with, this is why Doctrine requests a table name and not an entity name.
Edit
It is actually possible to work with entity names as well as follows (taken from my own code which is working as a charm):
$builder = $this->createQueryBuilder('m');
$builder->innerJoin(
'YourBundle:Category',
'c',
Join::WITH,
$builder->expr()->eq('m.id', 'c.mdl_id')
);
To use the constants from Join you should first:
use Doctrine\ORM\Query\Expr\Join;
But this should also work (taken from documentation which means should work like a charm):
$queryBuilder
->select('id', 'name')
->from('users', 'u')
->innerJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
This is taken form: http://doctrine-orm.readthedocs.org/projects/doctrine-dbal/en/latest/reference/query-builder.html#join-clauses
In fact, you're already referencing explicitely your entity phonenumbers in your second argument of your innerJoin function.
You read the doc and i'll retake it point by point with your request :
Argument 1 : alias of the FROM part, here your m (alias of your models)
Argument 2 : The full name of your entity you want join with your models
Argument 3 : an alias to access it (just like the 'm' of models)
Argument 4 : The join condition. (like the ON part of a sql request)
But with symfony and if you have a relation between your two entities like that :
//AppBundle/Entity/Models
class Models {
/**
* ...
**/
private $id;
/**
* ...
* #ORM\ManyToOne(targetEntity="\AppBundle\Entity\Phone", inversedBy="models")
**/
private $phonenumbers;
...
}
you want to join you just can do :
$repo = $this->getDoctrine()
->getRepository('MyBundle:Models');
$query = $repo->createQueryBuilder('m')
->innerJoin('m.phonenumbers', 'p')
->where('m.id = :id')
->setParameter('id', $id);
To explain that : You just have to pass as first argument, the entity property you want to join (here the phone number of your model) and define it as alias (p for phonenumber to access it in a select)

Doctrine 2 - Joined entity is still a proxy and generating two queries after request

$news = \DB::getEntityManager()->createQueryBuilder()
->select('n')
->from('Model\News', 'n')
->join('n.category', 'c')
->join('n.locales', 'l')
->where('l.language = :lang AND n.id = :id AND n.published = 1')
->setParameter('id', $news_id)
->setParameter('lang', $lang)
->getQuery()
->getResult();
$news->category->getName();
This code is generating TWO queries instead of ONE. Why?
After getting my $news entity, the category property is still a Doctrine proxy class (Application\Proxies\__CG__\Model\News\Category). Instance of \Model\News\Category will be loaded only after getting some of the category data (with, for example, $news->category->getName()) and it's generating additional SQL query.
How to make Doctrine to download \Model\News and related to it \Model\News\Category with only one query?
Below is a configuration for category field from \Model\News with annotations:
/**
* #ManyToOne(targetEntity="\Model\News\Category", inversedBy="news", fetch="EAGER")
*/
public $category;
As You can see... EAGER is not working...
Change your first line to ->select('n, c') so that Doctrine will hydrate both your News and Category entities.

Doctrine ManyToMany relation with join condition

I've got a Many-To-Many relation between two tables but I want to add a join condition on the join table.
/**
* #ManyToMany(targetEntity="Company", inversedBy="Accounts")
* #JoinTable(name="account_company",
* joinColumns = { #JoinColumn(name="account_id", referencedColumnName="id") },
* inverseJoinColumns = { #JoinColumn(name="company_id", referencedColumnName="id") }
* )
*/
protected $companies;
I would have a condition like "account_company.is_deleted = 0", how can I make it ?
Yes, you have the choice to hydrate your object and its ManyToMany collection using custom dql:
make a repository method that adds conditions on your join:
// repository class:
public function getAccountWithNonDeletedCompanies($id)
{
return $this->createQueyBuilder('account')
->leftJoin('account.companies', 'companies', 'WITH', 'companies.deleted = 0')
->andWhere('account.id = :id')
->setParameter('id', $account)
->getQuery()
->getOneOrNullResult()
;
}
From what I can understand, you never want to display deleted companies ? (soft delete).
In this case, you may want to use SQL Filters: http://blog.loftdigital.com/doctrine-filters-and-soft-delete
Have dealt with this before and the only solution I got was creating a custom function that will make the query.
So on your Entity create a custom function getCompanies().

Categories