Symfony Error: Invalid PathExpression. Must be a CollectionValuedAssociationField - list

I'm using a query builder to set up a list of documents who have different relationships to other entities. The list also includes a filter form for all of the entities in that query builder.
It worked fine for all of them so far, but as soon as I added the $products entity, it doesn't work any more.
My code:
$qb = $em->createQueryBuilder();
$query = $qb->select('d', 'p')
->from('DocumentBundle:Document', 'd')
->innerJoin('d.products', 'p')
->orderBy('d.id', 'DESC')
->join('d.type', 'dt')
->join('d.status', 's');
if(count($type) > 0){
$query->andwhere($query->expr()->in('d.type', ':type'))
->setParameter('type',$type);
}
if(count($status) > 0) {
$query->andWhere($query->expr()->in('d.status', ':status'))
->setParameter('status', $status);
}
if(count($markets) > 0){
$query->andWhere(':marketIds MEMBER OF d.markets')
->setParameter('marketIds',$markets);
}
else{
$query->andWhere(':marketIds MEMBER OF d.markets')
->setParameter('marketIds',$userMarkets);
}
if(count($airlines) > 0){
$query->andWhere(':airlineIds MEMBER OF d.airlines')
->setParameter('airlineIds',$airlines);
}else{
$query->andWhere(':airlineIds MEMBER OF d.airlines')
->setParameter('airlineIds',$userAirlines);
}
if(count($products) > 0){
$query->andWhere(':productIds MEMBER OF p.id')
->setParameter('productIds',$products);
}
// else{
// $query->andWhere(':productIds MEMBER OF p.id')
// ->setParameter('productIds',$currentuser->getProducts());
// }
$query->andWhere('d.active = ?1')
->setParameter(1, $archive ? 0 : 1);
return $query
->setFirstResult($page * $maxRows - $maxRows)
->setMaxResults($maxRows)
;
So for type and status, I have a ManyToOne relationship, for markets, airlines and (the troublemaker) products, it's a ManyToMany relationship.
The current code throws the exception:
[Semantical Error] line 0, col 237 near 'id AND d.active': Error:
Invalid PathExpression. Must be a CollectionValuedAssociationField.
The strange thing about this, is that I have an other list for another entity which also has a ManyToMany relationship to products, and for that list it is working. Also strange about this: for that other list my query looks like that:
$qb = $em->createQueryBuilder();
$query = $qb->select('a', 'p')
->from('AppBundle:Agency', 'a')
->innerJoin('a.products', 'p')
->orderBy('a.id', 'ASC');
if(count($markets) > 0){
$query->andWhere('a.market IN (:marketIds)')
->setParameter('marketIds',$markets);
}
else{
$query->andWhere('a.market IN (:marketIds)')
->setParameter('marketIds',$currentUser->getMarkets());
}
if(count($products) > 0){
$query->andWhere('p.id IN (:productIds)')
->setParameter('productIds',$products);
}
else{
$query->andWhere('p.id IN (:productIds)')
->setParameter('productIds',$currentUser->getProducts());
}
It's a ManyToOne for markets and a ManyToMany for products here.
I tried to user the same code (p.id in (:productIds)...) for my documents and this is kind of working, so I at least don't get an error any more. But when I then filter on something (like markets, airlines, products etc.) it's not working any more, so the list is just empty. Filtering for that second list is working though, so I don't know where this is coming from.
Code update
$qb = $em->createQueryBuilder();
$query = $qb->select('d', 'p', 'm', 'a')
->from('DocumentBundle:Document', 'd')
->innerJoin('d.products', 'p')
->innerJoin('d.markets', 'm')
->innerJoin('d.airlines', 'a')
->orderBy('d.id', 'DESC')
->join('d.type', 'dt')
->join('d.status', 's');
if(count($type) > 0){
$query->andwhere($query->expr()->in('d.type', ':type'))
->setParameter('type',$type);
}
if(count($status) > 0) {
$query->andWhere($query->expr()->in('d.status', ':status'))
->setParameter('status', $status);
}
if(count($markets) > 0){
$query->andWhere('m.id IN (:marketIds)')
->setParameter('marketIds',$markets);
}
if(count($airlines) > 0){
$query->andWhere('a.id IN (:airlineIds)')
->setParameter('airlineIds',$airlines);
}
if(count($products) > 0){
$query->andWhere('p.id IN (:productIds)')
->setParameter('productIds',$products);
}
$query->andWhere('d.active = :archiveParam')
->setParameter("archiveParam", $archive ? 0 : 1);
dump($query->getQuery());
Filtering is working for status, type and markets but not for airlines and products. Any ideas?
Reproducing issue
So this is my filterform (unnecessary filters are black). they are all multiselect dropdowns. When filtering for markets and status, the list is reducing to only the documents who have that certain status or are assigned to the selected markets. For all of the ManyToMany relationships(markets, airlines and products) I have own database tables and they all contain data.
Sample data would be:
Document Nr. 42 is assigned to airlines LH and LX, to markets CA, MX and US and to product Nr. 1. So when filtering for one of the markets, the document always appear in the list. But when filtering for one of the airlines or the product, the list stays empty.
Edit
I just recently added the product filter and before I had that one, the filtering for airlines actually worked fine. Even though I used the 'wrong' query with MEMBER OF d.airlines.

This part
if(count($products) > 0){
$query->andWhere(':productIds MEMBER OF p.id')
->setParameter('productIds',$products);
}
should look like this
if(count($products) > 0){
$query->andWhere('p.id IN (:productIds) ')
->setParameter('productIds',$products);
}
You are also mixing positional and named parameters.
$query->andWhere('d.active = :archiveParam')
->setParameter("archiveParam", $archive ? 0 : 1);

Related

ASP NET MVC Core 2 with entity framework saving primery key Id to column that isnt a relationship column

Please look at this case:
if (product.ProductId == 0)
{
product.CreateDate = DateTime.UtcNow;
product.EditDate = DateTime.UtcNow;
context.Products.Add(product);
if (!string.IsNullOrEmpty(product.ProductValueProduct)) { SaveProductValueProduct(product); }
}
context.SaveChanges();
When I debug code the ProductId is negative, but afer save the ProductId is corect in database (I know this is normal), but when I want to use it here: SaveProductValueProduct(product) after add to context.Products.Add(product); ProductId behaves strangely. (I'm creating List inside SaveProductValueProduct)
List<ProductValueProductHelper> pvpListToSave = new List<ProductValueProductHelper>();
foreach (var item in dProductValueToSave)
{
ProductValueProductHelper pvp = new ProductValueProductHelper();
pvp.ProductId = (item.Value.HasValue ? item.Value : product.ProductId).GetValueOrDefault();
pvp.ValueId = Convert.ToInt32(item.Key.Remove(item.Key.IndexOf(",")));
pvp.ParentProductId = (item.Value.HasValue ? product.ProductId : item.Value);
pvpListToSave.Add(pvp);
}
I want to use ProductId for relationship. Depends on logic I have to save ProductId at ProductId column OR ProductId at ParentProductId column.
The ProductId 45 save corent at ProductId column (3rd row - this is a relationship), BUT not at ParentProductId (2nd row - this is just null able int column for app logic), although while debug I pass the same negative value from product.ProductId there.
QUESTION:
how can I pass correct ProductId to column that it isn't a relationship column
I just use SaveProductValueProduct(product) after SaveChanges (used SaveChanges twice)
if (product.ProductId == 0)
{
product.CreateDate = DateTime.UtcNow;
product.EditDate = DateTime.UtcNow;
context.Products.Add(product);
}
context.SaveChanges();
if (product.ProductId != 0)
{
if (!string.IsNullOrEmpty(product.ProductValueProduct)) { SaveProductValueProduct(product); }
context.SaveChanges();
}
I set this as an answer because it fixes the matter, but I am very curious if there is any other way to solve this problem. I dont like to use context.SaveChanges(); one after another.

Doctrine QueryBuilder - Struggling with table 'state' variable

I have the following query and the last part of it is to check the state of the item which will be 1 or 0;
My api calls:
http://example.com/api/search?keyword=someword&search_for=item&return_product
The query works as expected, except for one thing. Some of the stone items are disabled and I need to ignore where:
->where('S.state=:state')
->setParameter('state' , 1 )
I am not quite sure where to add this to the current query to get it to work:
$qb = $this->stoneRepository->createQueryBuilder('S');
//Get the image for the item
$qb->addSelect('I')
->leftJoin('S.image' , 'I');
//Check if we want products returned
if ( $return_product )
{
$qb->addSelect('P','PI')
->leftJoin('S.product' , 'P')
->leftJoin('P.image' , 'PI');
}
//Check is we want attributes returned
if ( $return_attribute )
{
$qb->addSelect('A','C')
->leftJoin('S.attribute' , 'A')
->leftJoin('A.category' , 'C');
}
//Check the fields for matches
$qb->add('where' , $qb->expr()->orX(
$qb->expr()->like('S.name' , ':keyword'),
$qb->expr()->like('S.description' , ':keyword')
)
);
//Set the search item
$qb->setParameter('keyword', '%'.$keyword.'%');
$qb->add('orderBy', 'S.name ASC');
Just after the createQueryBuilder call:
$qb = $this->stoneRepository
->createQueryBuilder('S')
->where('S.state = :state')
->setParameter('state', 1);
With the query builder the order is not important: you can add SQL pieces in different order and even override pieces already added.

Doctrine - How to hydrate a collection when using query builder

A previous question I asked was to do with hydrating a result set when using Doctrine and query builder. My issue was how to return an array and their sub-sets:
This was for a single result set and the answer was quite simple:
$qb = $this->stoneRepository->createQueryBuilder('S');
$query = $qb->addSelect('A','P','I','C')
->leftJoin('S.attribute', 'A')
->leftJoin('A.category', 'C')
->innerJoin('S.product' , 'P')
->innerJoin('S.image' , 'I')
->where('S.id = :sid')
->setParameter('sid', (int) $stone_id)
->getQuery();
$resultArray = $query->getOneOrNullResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
return $resultArray;
My next question is how to do this exact same thing for a collection? This is what I have tried:
public function fetchAll()
{
$qb = $this->stoneRepository->createQueryBuilder('S');
$qb->addSelect('A','P','I','C')
->leftJoin('S.attribute', 'A')
->leftJoin('A.category', 'C')
->innerJoin('S.product' , 'P')
->innerJoin('S.image' , 'I')
->where('S.state=:state')
->setParameter('state' , 1 );
$adapter = new DoctrineAdapter( new ORMPaginator( $qb ) );
$collection = new StoneCollection($adapter);
return $collection;
}
The problem I am facing with this solution is that the join tables are not being populated and I am ending up with a collection of empty results.
The StoneCollection class simply extends paginator:
<?php
namespace Api\V1\Rest\Stone;
use Zend\Paginator\Paginator;
class StoneCollection extends Paginator
{
}
I am thinking that perhaps the best mehod is to get an array and to page the array?
EDIT::
I have this working although I am not keen on it as I hit the DB twice. The first time to build the array (Which is the entire result set which could be very big for some applications) and then the second time to page the results which is then returned to HAL in ApiGility for processing...
Ideally this should be done in one go however I am not sure how to hydrate the results in a single instance...
public function fetchAll( $page = 1 )
{
$qb = $this->stoneRepository->createQueryBuilder('S');
$qb->addSelect('A','P','I','C')
->leftJoin('S.attribute', 'A')
->leftJoin('A.category', 'C')
->innerJoin('S.product' , 'P')
->innerJoin('S.image' , 'I')
->where('S.state=:state')
->setParameter('state' , 1 );
$resultArray = $qb->getQuery()->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
$paginator = new \Zend\Paginator\Paginator(new \Zend\Paginator\Adapter\ArrayAdapter($resultArray));
$paginator->setCurrentPageNumber($page);
return $paginator;
}
The Answer to this is as I have above:
I have this working although I am not keen on it as I hit the DB twice. The first time to build the array (Which is the entire result set which could be very big for some applications) and then the second time to page the results which is then returned to HAL in ApiGility for processing...
Ideally this should be done in one go however I am not sure how to hydrate the results in a single instance...
public function fetchAll( $page = 1 )
{
$qb = $this->stoneRepository->createQueryBuilder('S');
$qb->addSelect('A','P','I','C')
->leftJoin('S.attribute', 'A')
->leftJoin('A.category', 'C')
->innerJoin('S.product' , 'P')
->innerJoin('S.image' , 'I')
->where('S.state=:state')
->setParameter('state' , 1 );
$resultArray = $qb->getQuery()->getResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
$paginator = new \Zend\Paginator\Paginator(new \Zend\Paginator\Adapter\ArrayAdapter($resultArray));
$paginator->setCurrentPageNumber($page);
return $paginator;
}
On the Doctrine documentation for Pagination they state to use $fetchJoinCollection = true, which I believe is the same as the HYDRATE you are trying to use.
Doctrine Pagination
On my pagination code for my QueryBuilder I use it like the following:
public function getAllPaginated($page, $limit){
$query = $this->createQueryBuilder('o')
->select('o')
->getQuery();
$paginator = new Paginator($query, $fetchJoinCollection = true);
$paginator->getQuery()
->setFirstResult($limit * ($page - 1)) // Offset
->setMaxResults($limit);
return $paginator;
}

Doctrine 2 date in where-clause (querybuilder)

I've the following problem: I want a where-clause that checks if a user is active AND if he has the right date.
A user contains the following:
State
Startdate
Enddate
So, State should stay on 1, then he should look for state = 1 AND the current date is between the start and enddate. I've the following right now, and it works fine. But the start and enddate is not required. So it could be NULL. How can i get a query like:
SELECT *
FROM user/entity/user
WHERE
state = 1
AND (
(startdate <= CURRENT_DATE AND enddate >= CURRENT_DATE)
OR startdate == NULL
OR enddate == NULL
)
, so i get all my active users, and not only the temporary users.
I've set up the following code right now:
Repository:
public function searchUser($columns, $order_by, $order)
{
//Create a Querybuilder
$qb = $this->_em->createQueryBuilder();
//andx is used to build a WHERE clause like (expression 1 AND expression 2)
$or = $qb->expr()->andx();
//Select all from the User-Entity
$qb->select('u')
->from('User\Entity\User', 'u');
foreach($columns as $column => $name)
{
if($column == 'state')
{
if($columns['state']['value'] == '1') {
$or = $this->betweenDate($qb, $or);
$or = $this->like($columns, $qb, $or);
} elseif($columns['state']['value'] == '2') {
$or = $this->like($columns, $qb, $or);
} elseif($columns['state']['value'] == '3') {
$or = $this->outOfDate($qb, $or);
}
} else {
//Get a where clause from the like function
$or = $this->like($columns, $qb, $or);
}
}
//Set the where-clause
$qb->where($or);
//When there is a order_by, set it with the given parameters
if(isset($order_by)) {
$qb->orderBy("u.$order_by", $order);
}
//Make the query
$query = $qb->getQuery();
/*echo('<pre>');
print_r($query->getResult());
echo('</pre>');*/
//Return the result
return $query->getResult();
}
public function betweenDate($qb, $or)
{
$or->add($qb->expr()->lte("u.startdate", ":currentDate"));
$or->add($qb->expr()->gte("u.enddate", ":currentDate"));
//$or->add($qb->expr()->orx($qb->expr()->eq("u.startdate", null)));
$qb->setParameter('currentDate', new \DateTime('midnight'));
return $or;
}
//This one works perfect
public function like($columns, $qb, $or)
{
//Foreach column, get the value from the inputfield/dropdown and check if the value
//is in the given label.
foreach($columns as $label=>$column){
$value = $column['value'];
$or->add($qb->expr()->like("u.$label", ":$label"));
$qb->setParameter($label, '%' . $value . '%');
}
return $or;
}
I use this "usersearch" also for other fields. So it should pick all the data out of the database, only state is different because "out of date" is not in the database. So it has to check differently. Hope somebody can help.
Problem solved
$startdate = $qb->expr()->gt("u.startdate", ":currentDate");
$enddate = $qb->expr()->lt("u.enddate", ":currentDate");
$or->add($qb->expr()->orX($startdate, $enddate));
That's how I would build the where clause:
//CONDITION 1 -> STATE = 1
$state = $qb->expr()->eq( 'state', ':state' );
//CONDITION 2 -> startdate <= CURRENT_DATE AND enddate >= CURRENT_DATE
$betweenDates = $qb->expr()->andX(
$qb->expr()->lte("u.startdate", ":currentDate"),
$qb->expr()->gte("u.enddate", ":currentDate")
);
//CONDITION 3 -> startdate == NULL
$startDateNull = $qb->expr()->isNull( 'startdate' );
//CONDITION 4 -> enddate == NULL
$endDateNull = $qb->expr()->isNull( 'enddate' );
//CONDITION 5 -> <CONDITION 2> OR <CONDITION 3> OR <CONDITION 4>
$dates = $qb->expr()->orX( $betweenDates, $startDateNull, $endDateNull );
//CONDITION 6 -> <CONDITION 1> AND <CONDITION 5>
$whereClause = $qb->expr()->andX( $state, $dates );

Doctrine 2 query builder vs entity persist performance

Summary: which is quicker: updating / flushing a list of entities, or running a query builder update on each?
We have the following situation in Doctrine ORM (version 2.3).
We have a table that looks like this
cow
wolf
elephant
koala
and we would like to use this table to sort a report of a fictional farm. The problem is that the user wishes to have a customer ordering of the animals (e.g. Koala, Elephant, Wolf, Cow). Now there exist possibilities using CONCAT, or CASE to add a weight to the DQL (example 0002wolf, 0001elephant). In my experience this is either tricky to build and when I got it working the result set was an array and not a collection.
So, to solve this we added a "weight" field to each record and, before running the select, we assign each one with a weight:
$animals = $em->getRepository('AcmeDemoBundle:Animal')->findAll();
foreach ($animals as $animal) {
if ($animal->getName() == 'koala') {
$animal->setWeight(1);
} else if ($animal->getName() == 'elephant') {
$animal->setWeight(2);
}
// etc
$em->persist($animal);
}
$em->flush();
$query = $em->createQuery(
'SELECT c FROM AcmeDemoBundle:Animal c ORDER BY c.weight'
);
This works perfectly. To avoid race conditions we added this inside a transaction block:
$em->getConnection()->beginTransaction();
// code from above
$em->getConnection()->rollback();
This is a lot more robust as it handles multiple users generating the same report. Alternatively the entities can be weighted like this:
$em->getConnection()->beginTransaction();
$qb = $em->createQueryBuilder();
$q = $qb->update('AcmeDemoBundle:Animal', 'c')
->set('c.weight', $qb->expr()->literal(1))
->where('c.name = ?1')
->setParameter(1, 'koala')
->getQuery();
$p = $q->execute();
$qb = $em->createQueryBuilder();
$q = $qb->update('AcmeDemoBundle:Animal', 'c')
->set('c.weight', $qb->expr()->literal(2))
->where('c.name = ?1')
->setParameter(1, 'elephant')
->getQuery();
$p = $q->execute();
// etc
$query = $em->createQuery(
'SELECT c FROM AcmeDemoBundle:Animal c ORDER BY c.weight'
);
$em->getConnection()->rollback();
Questions:
1) which of the two examples would have better performance?
2) Is there a third or better way to do this bearing in mind we need a collection as a result?
Please remember that this is just an example - sorting the result set in memory is not an option, it must be done on the database level - the real statement is a 10 table join with 5 orderbys.
Initially you could make use of a Doctrine implementation named Logging (\Doctrine\DBAL\LoggingProfiler). I know that it is not the better answer, but at least you can implement it in order to get best result for each example that you have.
namespace Doctrine\DBAL\Logging;
class Profiler implements SQLLogger
{
public $start = null;
public function __construct()
{
}
/**
* {#inheritdoc}
*/
public function startQuery($sql, array $params = null, array $types = null)
{
$this->start = microtime(true);
}
/**
* {#inheritdoc}
*/
public function stopQuery()
{
echo "execution time: " . microtime(true) - $this->start;
}
}
In you main Doctrine configuration you can enable as:
$logger = new \Doctrine\DBAL\Logging\Profiler;
$config->setSQLLogger($logger);