Symfony2 ManyToMany in one query - php

I have 2 entities related ManyToMany
class Product
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100, unique=true)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="ProductTransfer", inversedBy="product")
* #ORM\JoinTable(name="products_transfers")
*/
private $transfers;
and second one
class ProductTransfer
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="stock_from_id", type="integer")
*/
private $stockFromId;
/**
* #var int
*
* #ORM\Column(name="stock_to_id", type="integer")
*/
private $stockToId;
/**
* #var int
*
* #ORM\Column(name="product_id", type="integer")
*/
private $productId;
/**
* #ORM\ManyToMany(targetEntity="Product",mappedBy="transfers")
*/
private $product;
All works great, additional table were created by Doctrine.
But when I'm trying to get all rows from ProductTransfers I see in profiler that each row need one single query with 2 joins.
So for 5000 products it will be really many queries.
Is there any way to get them all in one query like in "clean SQL"?
If there is no way to do it with Doctrine, what is best way to achieve that?
Like:
SELECT * FROM product_transfer pt
LEFT JOIN products_transfers pts ON pt.product_id=pts.product_id
LEFT JOIN product p ON pts.product_id=p.id;
edit:
created method in repository
public function loadTransfersByStock($stockId)
{
$q = $this->getEntityManager()
->createQuery(
'SELECT pt,p FROM AppBundle:ProductTransfer pt
LEFT JOIN AppBundle:Product p WITH pt.productId=p.id
WHERE pt.stockToId = :stockId'
);
return $q->setParameter('stockId', $stockId)->getResult();
}
and then result is
ProductsController.php on line 495:
array:3 [▼
0 => ProductTransfer {#1071 ▶}
1 => Product {#1084 ▶}
2 => ProductTransfer {#1099 ▶}
]

By default Doctrine uses lazy loading in object relations - the rows for the relationships are only loaded when you try to access them. If you know you're going to be needing all the related rows, try changing to eager fetching.
/**
* #ORM\ManyToMany(targetEntity="ProductTransfer", inversedBy="product", fetch="EAGER")
* #ORM\JoinTable(name="products_transfers")
*/

When you are writing the query like that in the repository, you are skipping the way doctrine "wants" you to write it. So instead of that, I would do something like this:
//ProductTransferRepository.php
$qb = $this->createQueryBuilder('pt');
$qb
->select('pt, p') // here you can choose the columns you want if you don't need everything from both tables
->leftJoin('pt.product', 'p')
->where('pt.stockToId = :stockId')
->setParameter('stockId', $stockId)
;
return $qb
->getQuery()
->getArrayResult()
;
Use getArrayResult to fetch results as array rather than objects, especially because you have many queries.

Related

Doctrine2 with MS Server pagination and order by not working under ZF2

I have a list of news ordered by the published date and I want to paginate it. I am using Doctrine 2.5 in a Zend Framework 2 project. Here's my entity:
<?php
namespace BuscadorJuridico\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\Form\Annotation as Form;
/**
* Class News
* #package BuscadorJuridico\Entity
*
* #ORM\Table(name="news")
* #ORM\Entity
*/
class News
{
/**
* #var int
*
* #ORM\Column(name="news_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
*/
protected $id;
/**
* #var string
*
* #ORM\Column(name="news_title", type="string", length=200, nullable=false)
*/
public $title;
/**
* #var string
*
* #ORM\Column(name="news_body", type="blob", nullable=false)
*
*/
public $body;
/**
* #var \DateTime
*
* #ORM\Column(name="news_date_published", type="date", nullable=true)
*
*/
protected $datePublished;
/**
* #var bool
*
* #ORM\Column(name="news_status", type="boolean", nullable=false)
*
*/
public $published;
}
I am using the Doctrine\DBAL\Driver\SQLSrv\Driver and the Paginator componenr of ZF2. Here is the pagination code:
$query = $this
->entityManager
->getRepository('BuscadorJuridico\Entity\News')
->createQueryBuilder('news')
->select()
->orderBy('news.datePublished', 'desc')
->andWhere('news.published = true');
$paginator = new Paginator(new DoctrineAdapter(new ORMPaginator($query)));
$paginator->setCurrentPageNumber($page);
$paginator->setItemCountPerPage($count);
But when I go to the list, I get the error:
SQLSTATE [42000, 8120]: [Microsoft][ODBC Driver 11 for SQL Server][SQL Server]Column 'dctrn_result.news_date_published_3' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
When I delete the ->orderBy('news.datePublished', 'desc') line I get the list, but naturally not ordered. This is part of the SQL that Doctrine is generating:
SELECT DISTINCT
news_id_0,
MIN(sclr_5) AS dctrn_minrownum,
ROW_NUMBER() OVER (ORDER BY news_date_published_3 DESC) AS doctrine_rownum
FROM (
SELECT n0_.news_id AS news_id_0,
n0_.news_title AS news_title_1,
n0_.news_body AS news_body_2,
n0_.news_date_published AS news_date_published_3,
n0_.news_status AS news_status_4,
ROW_NUMBER() OVER(ORDER BY n0_.news_date_published DESC) AS sclr_5
FROM news n0_
WHERE n0_.news_status = 1)
dctrn_result
GROUP BY news_id_0
With MySQL the code works. Any help appreciated.
I actually have a PR open to fix this. I'll see if I can poke that along.
https://github.com/doctrine/dbal/pull/818
I used the PR from wschalle and created my own Mssql Driver included the fix, maybe it help you.
https://github.com/kokspflanze/GameBackend/tree/master/src/GameBackend/DBAL

Doctrine query with relationships and count

I am trying to do a complex query in Doctrine but had no luck so far.
I had an Entity called Category.
Category has reports (1 to many)
Category has subcategories (1 to many self referencing)
class Category extends
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #Expose
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
* #Expose
*/
private $name;
/**
* #ORM\OneToMany(targetEntity=".....\Category", mappedBy="parentCategory", cascade={"persist"})
*
* #MaxDepth(1)
*
* #Expose
*
**/
private $subcategories;
/**
* #ORM\ManyToOne(targetEntity="....\Category", inversedBy="subcategories", cascade={"persist"})
* #ORM\JoinColumn(name="idparent", referencedColumnName="id", nullable=true)
*
* #Expose
*/
private $parentCategory;
/**
* #ORM\OneToMany(targetEntity="....\Report", mappedBy="category")
*/
private $reports;
I use these entities in a rest API set up in symfony with FosRestBundle and HATEOASBundle.
So my objective is to return the following:
All parent categories (categories with idparent=null, meaning they are roots) with total_count reports and total_sum_reports of their subcategpories, with their subcategories with total_count reports and total_sum_reports.
Something like that:
name: parentCategory1
total_reports: 20
total_amount 100
subcategories: [
[name: child 1, total_reports: 15, total_amount 80],
[name: child 2, total_reports: 5, total_amount 20],
]
Unfortunately haven't figured this out.
How to take all info in need into a single json object?
Is it possible with Doctrine take all this into a big object or need to create this manually?

Implementing getters for joined doctrine 2 collection

I'm trying to create simple getter methods in original entity for specific item in doctrine collection of joined entity.
Main entity looks something like this:
class Product
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
...
/**
* #ORM\OneToMany(targetEntity="File", mappedBy="product")
*/
private $files;
}
And joined entity:
class PrankFile
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="files")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $product;
/**
* #var string
*
* #ORM\Column(name="type", type="string", length=16)
*/
private $type;
...
My DQL in repository class is very simple:
return $this->getEntityManager()
->createQuery('SELECT p, f FROM AppProductBundle:Product p INNER JOIN p.files f ORDER BY p.created DESC')
->setMaxResults($limit)
->getResult();
In files entity type field tells me what kind of a file is it (image, sound, video, demo etc...)
Problem comes when I wish to print out a list of all products and display image next to product details, I would hate to loop through product files for each product displayed.
Is it possible to create some simple getter on product entity to fetch file of certain type?
Or maybe it would be better to create more complex DQL query for this, again how to do this?
I can't just fetch image record from files in DQL because I need all files for certain products.
Any help with this would be most welcome.
You can filter directly on collections using the filter api. If the collection is not loaded already Doctrine will apply your filter on a SQL level, giving you max performance. In case the collection is already eager loaded Doctrine will filter the ArrayCollection in memory.
use Doctrine\Common\Collections\Criteria;
class Product
{
public function getFilesByType($type)
{
$criteria = Criteria::create()
->where(Criteria::expr()->eq("type", $type))
return $this->files->matching($criteria);
}
}

Where-ing in discriminated tables

How can I select all items from one specific author ? Its possible this way ? Or how can I edit entities if I want many item types and item packages (item has many items) too ?
Item
/**
* #ORM\Table()
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({
* "cd" = "ItemCD",
* "dvd" = "ItemDVD",
* "pack" = "ItemPack",
* })
*/
class Item
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=250, nullable=false)
*/
private $name;
}
ItemCD
/**
* #ORM\Table()
* #ORM\Entity
*/
class ItemCD extends Item
{
/**
* #ORM\ManyToOne(targetEntity="Author", inversedBy="item")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
}
ItemDVD
/**
* #ORM\Table()
* #ORM\Entity
*/
class ItemDVD extends Item
{
/**
* #ORM\ManyToOne(targetEntity="Author", inversedBy="item")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
}
ItemPack
/**
* #ORM\Table()
* #ORM\Entity
*/
class ItemPack extends Item
{
/**
* #ORM\ManyToMany(targetEntity="Item", inversedBy="item")
* #ORM\JoinTable()
*/
private $items;
}
Author
/**
* #ORM\Table()
* #ORM\Entity
*/
class Author
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=250, nullable=false)
*/
private $name;
}
You will have to query for specific elements. This is a known (and wanted) limitation, since DQL is a static typed language: see http://www.doctrine-project.org/jira/browse/DDC-16
Related: how to access fields in inherited table in doctrine2 / dql query
A way of handling this with a workaround is using 2 subqueries in your DQL:
SELECT
i
FROM
Item i
WHERE
i.id IN(
SELECT
i2.id
FROM
ItemDvd i2
WHERE
i2.author = :author
)
OR
i.id IN(
SELECT
i3.id
FROM
ItemCd i3
WHERE
i3.author = :author
)
As you can see you have to extract the identifiers for each possible subtype manually.
Edit: to get all the packs from a given author (along with single DVDs or CDs), the query becomes even worse:
SELECT
i
FROM
Item i
WHERE
i.id IN(
SELECT
i2.id
FROM
ItemDvd i2
WHERE
i2.author = :author
)
OR
i.id IN(
SELECT
i3.id
FROM
ItemCd i3
WHERE
i3.author = :author
)
OR
i.id IN(
SELECT
i4.id
FROM
ItemPack i4
JOIN
i4.items i5
WHERE
i5.id IN (
SELECT
i6.id
FROM
Item i6
WHERE
i6.id IN(
SELECT
i7.id
FROM
ItemDvd i7
WHERE
i7.author = :author
)
OR
i6.id IN(
SELECT
i8.id
FROM
ItemCd i8
WHERE
i8.author = :author
)
)
)
Make $author in Item and have ItemPacks $author value always be null. Then you can do:
$em->findBy("Item", array("author" => $author));
And you always get instances of ItemDVD or ItemCD.
It's tricky and lengthy the answer.
I think the Entities aproach is ok, and by querying the item entity you would get what you want.
Now for forms you'll probably need one FormType per sub-item and then user the aproach for Form Collections (http://symfony.com/doc/2.1/cookbook/form/form_collections.html) and I'm certain that you will need to hook into the pre-bind event to prepare the data.
This is a quick thought, may be it can help you.

Doctrine DQL query - getting rows when foreign key is null

I wonder if anyone can help with this Doctrine query.
Basically, My query does not return rows where the foreign key is not set or NULL. And I would like to return all rows.
Here are 2 schemas
Items
class Items{
/**
* #var integer $id
*
* #Column(name="id", type="integer", nullable=false)
* #Id
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
/**
* #var integer $type
*
* #ManyToOne(targetEntity="Types")
*
*/
private $type;
}
Types
class Types{
/**
* #var integer $id
*
* #Column(name="id", type="integer", nullable=false)
* #Id
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
}
And the following DQL query
SELECT i.id, i.name, t.name as type FROM entity\Items i, entity\Types t WHERE i.type=t.id (OTHER CONDITIONS...)
That above query does not return the rows that does not have a value in the type foreign key.
Is it possible to return those rows?
Thank you in advance...
Try a LEFT JOIN:
SELECT i.id, i.name, t.name as type FROM entity\Items i LEFT JOIN i.type t
That will return everything on the left (Items) regardless if there is a matching Type.
Is very simple, you use IS EMPTY
In your entity you have defined a inverse key, then, you call your entity principal where inverseKey IS EMPTY.
SELECT a FROM items a WHERE a.types IS EMPTY;
Then i have a field curriculums is defined like type in the items
/**
* inversed key on principal entity (item)
* #ORM\OneToMany(targetEntity="entity\types", mappedBy="id")
*/
private $types;
Sounds like you do not want this condition in the query;
i.type=t.id
Have you tried removing it or alternative something along the lines
(i.type=t.id OR i.type IS NULL)

Categories