i got a request with 2 leftJoin on the same table, with different alias :
$qb2->leftJoin('d2.groupeDossiers', 'gd',Join::WITH, 'gd.type = \'assure\'');
$qb2->leftJoin('d2.groupeDossiers', 'gdt',Join::WITH, 'gdt.type = \'tiers\'');
But it seems there's conflicts. How could i do that leftJoin in a single request ?
Thank you !
Without using Doctrine Relationships
If your relationship is like this :
/**
* #ORM\OneToMany(targetEntity="GroupeDossier", mappedBy="test")
*/
private $groupeDossiers;
And
/**
* #ORM\ManyToOne(targetEntity="Test", inversedBy="groupeDossier")
* #ORM\JoinColumn(name="id_test", referencedColumnName="id")
*/
private $test;
The best way to return what you want is :
$qb2->select('MyBundle:Test','t');
$qb2->addSelect('gd');
$qb2->addSelect('gdt');
$qb2->leftJoin('MyBundle:GroupeDossiers', 'gd',Join::WITH, 'gd.type = \'assure\' AND gd.test = t');
$qb2->leftJoin('MyBundle:GroupeDossiers', 'gdt',Join::WITH, 'gdt.type = \'tiers\' AND gdt.test = t');
But then, you should use getArrayResult() to get gd dans gdt
With Doctrine Relationships -- Quick and dirty
In your entity, do this :
/**
* #ORM\OneToMany(targetEntity="GroupeDossier", mappedBy="test")
*/
private $groupeDossiers;
public function getGroupesDossierSeparated() {
$result = ['assure'=>[], 'tiers'=>[]];
foreach ($this->groupeDossiers as $d) {
$result[$d->getType()][] = $d;
}
return $result;
}
Then, just with $qb->leftJoin('d2.groupeDossiers') you can use getGroupesDossierSeparated() to get the data you want.
Related
Let's assume i have "News" entity which has got ManyToMany "Tag" relation
class News
{
/**
* #ORM\ManyToMany(targetEntity="App\Domain\Entity\Vocabulary\Tag")
*/
private Collection $tags;
}
And i have such query:
public function getList(
array $tags = null,
): Query {
if (null !== $tags) {
$qb->andWhere('nt.id IN (:tags)');
$qb->setParameter('tags', $tags);
}
}
The problem is when i pass ["Tag1", "Tag2"] it selects news that have either the first tag or the second, but not both at the same time. How can i rewrite the query to select news which have both tags at the same time?
Some things to notice first:
For doctrine annotations it is possible to use the ::class-constant:
use App\Domain\Entity\Vocabulary\Tag;
class News
{
/**
* #ORM\ManyToMany(targetEntity=Tag::class)
*/
private Collection $tags;
}
If the $tags array is empty doctrine will throw an exception because an empty value set is invalid SQL, at least in mysql:
nt.id IN () # invalid!
Now to the problem:
With the SQL-aggregation functions COUNT and GROUP BY we can count the number of tags for all news. Together with your condition for the allowed tags, the number of tags per news must be equal to the number of tags in the tags array:
/**
* #var EntityManagerInterface
*/
private $manager;
...
/**
* #param list<Tag> $tags - Optional tag filter // "list" is a vimeo psalm annotation.
*
* #return list<News>
*/
public function getNews(array $tags = []): array
{
$qb = $this->manager
->createQueryBuilder()
->from(News::class, 'news')
->select('news')
;
if(!empty($tags)) {
$tagIds = array_unique(
array_map(static function(Tag $tag): int {
return $tag->getId();
}) // For performance reasons, give doctrine ids instead of objects.
); // Make sure duplicate tags are handled.
$qb
->join('news.tags', 'tag')
->where('tag IN (:tags)')
->setParameter('tags', $tagIds)
->addSelect('COUNT(tag) AS HIDDEN numberOfTags')
->groupBy('news')
->having('numberOfTags = :numberOfTags')
->setParameter('numberOfTags', count($tags))
;
}
return $qb
->getQuery()
->getResult()
;
}
Yep, the title suggests: Doctrine is looking for a fieldname that's not there. That's both true and not true at the same time, though I cannot figure out how to fix it.
The full error:
File: D:\path\to\project\vendor\doctrine\dbal\lib\Doctrine\DBAL\Driver\AbstractMySQLDriver.php:71
Message: An exception occurred while executing 'SELECT DISTINCT id_2
FROM (SELECT p0_.name AS name_0, p0_.code AS code_1, p0_.id AS id_2
FROM product_statuses p0_) dctrn_result ORDER BY p0_.language_id ASC, name_0 ASC LIMIT 25
OFFSET 0':
SQLSTATE[42S22]: Column not found: 1054 Unknown column
'p0_.language_id' in 'order clause'
The query the error is caused by (from error above):
SELECT DISTINCT id_2
FROM (
SELECT p0_.name AS name_0, p0_.code AS code_1, p0_.id AS id_2
FROM product_statuses p0_
) dctrn_result
ORDER BY p0_.language_id ASC, name_0 ASC
LIMIT 25 OFFSET 0
Clearly, that query is not going to work. The ORDER BY should be in the sub-query, or else it should replace p0_ in the ORDER BY with dctrn_result and also get the language_id column in the sub-query to be returned.
The query is build using the QueryBuilder in the indexAction of a Controller in Zend Framework. All is very normal and the same function works perfectly fine when using a the addOrderBy() function for a single ORDER BY statement. In this instance I wish to use 2, first by language, then by name. But the above happens.
If someone knows a full solution to this (or maybe it's a bug?), that would be nice. Else a hint in the right direction to help me solve this issue would be greatly appreciated.
Below additional information - Entity and indexAction()
ProductStatus.php - Entity - Note the presence of language_id column
/**
* #ORM\Table(name="product_statuses")
* #ORM\Entity(repositoryClass="Hzw\Product\Repository\ProductStatusRepository")
*/
class ProductStatus extends AbstractEntity
{
/**
* #var string
* #ORM\Column(name="name", type="string", length=255, nullable=false)
*/
protected $name;
/**
* #var string
* #ORM\Column(name="code", type="string", length=255, nullable=false)
*/
protected $code;
/**
* #var Language
* #ORM\ManyToOne(targetEntity="Hzw\Country\Entity\Language")
* #ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* #var ArrayCollection|Product[]
* #ORM\OneToMany(targetEntity="Hzw\Product\Entity\Product", mappedBy="status")
*/
protected $products;
[Getters/Setters]
}
IndexAction - Removed parts not directly related to QueryBuilder. Added in comments showing params as they are.
/** #var QueryBuilder $qb */
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select($asParam) // 'pro'
->from($emEntity, $asParam); // Hzw\Product\Entity\ProductStatus, 'pro'
if (count($queryParams) > 0 && !is_null($query)) {
// [...] creates WHERE statement, unused in this instance
}
if (isset($orderBy)) {
if (is_array($orderBy)) {
// !!! This else is executed !!! <-----
if (is_array($orderDirection)) { // 'ASC'
// [...] other code
} else {
// $orderBy = ['language', 'name'], $orderDirection = 'ASC'
foreach ($orderBy as $orderParam) {
$qb->addOrderBy($asParam . '.' . $orderParam, $orderDirection);
}
}
} else {
// This works fine. A single $orderBy with a single $orderDirection
$qb->addOrderBy($asParam . '.' . $orderBy, $orderDirection);
}
}
================================================
UPDATE: I found the problem
The above issue is not caused by incorrect mapping or a possible bug. It's that the QueryBuilder does not automatically handle associations between entities when creating queries.
My expectation was that when an entity, such as ProductStatus above, contains the id's of the relation (i.e. language_id column), that it would be possible to use those properties in the QueryBuilder without issues.
Please see my own answer below how I fixed my functionality to be able to have a default handling of a single level of nesting (i.e. ProducStatus#language == Language, be able to use language.name as ORDER BY identifier).
Ok, after more searching around and digging into how and where this goes wrong, I found out that Doctrine does not handle relation type properties of entities during the generation of queries; or maybe does not default to using say, the primary key of an entity if nothing is specified.
In the use case of my question above, the language property is of a #ORM\ManyToOne association to the Language entity.
My use case calls for the ability to handle at lease one level of relations for default actions. So after I realized that this is not handled automatically (or with modifications such as language.id or language.name as identifiers) I decided to write a little function for it.
/**
* Adds order by parameters to QueryBuilder.
*
* Supports single level nesting of associations. For example:
*
* Entity Product
* product#name
* product#language.name
*
* Language being associated entity, but must be ordered by name.
*
* #param QueryBuilder $qb
* #param string $tableKey - short alias (e.g. 'tab' with 'table AS tab') used for the starting table
* #param string|array $orderBy - string for single orderBy, array for multiple
* #param string|array $orderDirection - string for single orderDirection (ASC default), array for multiple. Must be same count as $orderBy.
*/
public function createOrderBy(QueryBuilder $qb, $tableKey, $orderBy, $orderDirection = 'ASC')
{
if (!is_array($orderBy)) {
$orderBy = [$orderBy];
}
if (!is_array($orderDirection)) {
$orderDirection = [$orderDirection];
}
// $orderDirection is an array. We check if it's of equal length with $orderBy, else throw an error.
if (count($orderBy) !== count($orderDirection)) {
throw new \InvalidArgumentException(
$this->getTranslator()->translate(
'If you specify both OrderBy and OrderDirection as arrays, they should be of equal length.'
)
);
}
$queryKeys = [$tableKey];
foreach ($orderBy as $key => $orderParam) {
if (strpos($orderParam, '.')) {
if (substr_count($orderParam, '.') === 1) {
list($entity, $property) = explode('.', $orderParam);
$shortName = strtolower(substr($entity, 0, 3)); // Might not be unique...
$shortKey = $shortName . '_' . (count($queryKeys) + 1); // Now it's unique, use $shortKey when continuing
$queryKeys[] = $shortKey;
$shortName = strtolower(substr($entity, 0, 3));
$qb->join($tableKey . '.' . $entity, $shortName, Join::WITH);
$qb->addOrderBy($shortName . '.' . $property, $orderDirection[$key]);
} else {
throw new \InvalidArgumentException(
$this->getTranslator()->translate(
'Only single join statements are supported. Please write a custom function for deeper nesting.'
)
);
}
} else {
$qb->addOrderBy($tableKey . '.' . $orderParam, $orderDirection[$key]);
}
}
}
It by no means supports everything the QueryBuilder offers and is definitely not a final solution. But it gives a starting point and solid "default functionality" for an abstract function.
I want to do a DQL query like:
$dql = "select p
from AcmeDemoBundle:UserTypeA p
where p.UserTypeB = :id
and (
select top 1 r.boolean
from AcmeDemoBundle:Registry r
)
= true";
But it seems that TOP 1 it's not a valid function in doctrine2.
I can't figure out how can I limit the result of the subquery to one row.
DQL does not support limits on subqueries and neither LIMIT nor OFFSET.
See http://www.doctrine-project.org/jira/browse/DDC-885
Although Doctrine doesn't natively support this, you could implement a custom function named FIRST() to achieve this:
<?php
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Subselect;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
/**
* FirstFunction ::=
* "FIRST" "(" Subselect ")"
*/
class FirstFunction extends FunctionNode
{
/**
* #var Subselect
*/
private $subselect;
/**
* {#inheritdoc}
*/
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->subselect = $parser->Subselect();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
/**
* {#inheritdoc}
*/
public function getSql(SqlWalker $sqlWalker)
{
return '(' . $this->subselect->dispatch($sqlWalker) . ' LIMIT 1)';
}
}
(More details: https://www.colinodell.com/blog/201703/limiting-subqueries-doctrine-2-dql)
You should really only use this for read-only purposes since Doctrine will not include other related entities in the result (which may become orphaned or lost if you save it).
I have two entities with a Unidirectional Many-to-One mapping.
Here's Product:
use Doctrine\Common\Collections\ArrayCollection;
/**
* #Entity
* #Table(name="Product")
* #gedmo:TranslationEntity(class="GPos_Model_Translation_ProductTranslation")
*/
class GPos_Model_Product extends GPos_Doctrine_ActiveEntity {
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #ManyToMany(targetEntity="GPos_Model_Category")
* #JoinTable(name="products_categories",
* joinColumns={#JoinColumn(name="product_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="category_id", referencedColumnName="id")}
* )
*/
protected $categories;
public function __construct() {
$this->categories = new ArrayCollection();
}
public function addCategory(GPos_Model_Category $category) {
if (!$this->categories->contains($category))
$this->categories->add($category);
}
}
As you can see, $categories is an ArrayCollection of GPos_Model_Category entities.
Now what?
Well now I'd like to retrive all products that are in a given category and also all products that are NOT in a given category.
I've tried $products = GPos_Model_Product::findByCategories($category->getId());
but that only gave me
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '= '1'' at line 1 and $category's ID is 1 so I guess it's not the way to go. Anyone knows how to deal with that ?
Thank you!
I finally found out how to select all products that are in a category thanks to https://stackoverflow.com/a/9808277/1300454.
I tweaked his solution a bit so I could pass an array of Category entities and it would find all products that are within these categories. If you give more than one entity it will return any product that is in at least one of the given categories.
Here's my tweak (I located this function in my Product entity):
/**
*
* Takes an array of GPos_Model_Category entities as parameter and returns all products in these categories
* #param array $categories
*/
public static function findByCategories($categories) {
$categoryArray = array();
foreach ($categories as $category) {
array_push($categoryArray, $category->getId());
}
$qb = Zend_Registry::get('entityManager')->createQueryBuilder();
$qb ->select('p')
->from('GPos_Model_Product', 'p')
->leftJoin('p.categories', 'c')
->andWhere($qb->expr()->in('c.id', $categoryArray));
return $qb->getQuery()->execute();;
}
Here's how you call it:
$products_cat = GPos_Model_Product::findByCategories(array($category));
In this case $category is an entity alone that's why I put it in an array before giving it to the function.
And here is the way you find products that are not in a given category or list of category:
/**
*
* Takes an array of GPos_Model_Category entities as parameter and returns all products not in these categories
* #param array $categories
*/
public static function findByNotCategories($categories) {
$categoryArray = array();
foreach ($categories as $category) {
array_push($categoryArray, $category->getId());
}
$qb = Zend_Registry::get('entityManager')->createQueryBuilder();
$qb2 = Zend_Registry::get('entityManager')->createQueryBuilder();
$qb->select('p')
->from('GPos_Model_Product', 'p')
->where($qb->expr()->notIn('p.id',
$qb2->select('p2.id')
->from('GPos_Model_Product', 'p2')
->leftJoin('p2.categories', 'c')
->andWhere($qb->expr()->in('c.id', $categoryArray))
->getDQL()
));
return $qb->getQuery()->execute();
}
This is actually using a subselect. I'm selecting all products id that are in the given category (that's the subselect) then I'm selecting all products that are not in the result of the subselect. My job here is done!
I'm trying to make something very simple.. but I do wrong, and I don't know what is the problem. Just I'm trying to insert new item to database with Doctrine 2:
$favouriteBook = new UserFavouriteBook;
$favouriteBook->user_id = 5;
$favouriteBook->book_id = 8;
$favouriteBook->created_at = new DateTime("now");
$this->_em->persist($favouriteBook);
$this->_em->flush();
As you can see.. is very simple, but that, give me next error:
Error: Message: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'user_id' cannot be null
Obviosly, if I make a "dump" before "persist" and "flush" of $favouriteBook, all looks be correct..
This is my "favouriteBook" entity:
/** #Column(type="integer")
* #Id
*/
private $user_id;
/** #Column(type="integer")
* #Id
*/
private $book_id;
/**
* #ManyToOne(targetEntity="Book", inversedBy="usersFavourite")
* #JoinColumn(name="book_id", referencedColumnName="id")
*/
private $book;
/**
* #ManyToOne(targetEntity="User", inversedBy="favouriteBooks")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/** #Column(type="datetime") */
private $created_at;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
Anyone can image what is the problem? .. I don't know what else try.. Thanks
I think what beberlei is saying is that within your favouriteBook entity, you don't need to define the user_id and book_id as class properties, b/c the book and user properties you have set already recognize these as the relevant join columns. Also, your attempt to persist the favouriteBook entity failed because you need to set the book and user entity associations within the favouriteBook entity, not the foreign keys. So it would be:
$favouriteBook = new UserFavouriteBook;
$favouriteBook->book = $book;
$favouriteBook->user = $user;
$favouriteBook->created_at = new DateTime("now");
$this->_em->persist($favouriteBook);
$this->_em->flush();
You are mapping foreign keys and the associations. You have to modify the association not the foreign key field. Its bad-practice to map them both, you should remove $book_id and $user_id completly.